我创建了以下逻辑,以查找2个字符串的组合是否具有0-9中的所有数字至少一次。但我认为这非常幼稚,需要改进性能。你能否提出更好的解决方案以及我的解决方案有什么问题。感谢。
输入:带数字的字符串数组(例如:012345,6789,34567)。我试图找到多少对字符串将至少有一次所有数字0-9。(在例如:1对-1和第2)。
static long getNumberOfValidPairs(String[] tickets) {
long count=0;
for(int i=0;i<tickets.length-1;i++){
for(int j=i+1;j<tickets.length;j++){
String concat = tickets[i]+tickets[j];
if(concat.length() <10){
continue;
}
if(concat.contains("0") && concat.contains("1") && concat.contains("2") && concat.contains("3") && concat.contains("4") && concat.contains("5") && concat.contains("6") && concat.contains("7") && concat.contains("8") && concat.contains("9")){
count++;
}
}
}
return count;
}
改进的解决方案:
static long getNumberOfValidPairs(String[] tickets) {
long count=0;
short[] masks = new short[tickets.length];
char[] chs = null;
short mask = 0;
short mask_full = (short) 0b1111111111;
for(int i=0;i<tickets.length;i++){
chs = tickets[i].toCharArray();
mask = 0;
for(char ch:chs){
if (ch >= '0' && ch <= '9') {
int digit = ch - '0';
mask |= (1 << digit);
}
}
masks[i] = mask;
}
for(int i=0;i<tickets.length-1;i++){
short mask_i = masks[i];
for(int j=i+1;j<tickets.length;j++){
short mask_j = masks[j];
short mask_i_j_concatenated = (short) (mask_i | mask_j);
if (mask_i_j_concatenated == mask_full) {
// System.out.println("Strings [" + string_i + "] and [" + string_j + "] form a pair.");
count++;
}
}
}
return count;
}
答案 0 :(得分:3)
我认为这里的时间复杂度为O(n^2)
,因为您需要尝试所有对。所以你做的两个for
周期都可以。
所以基本上你唯一能改进的就是检查两个字符串是否形成一对。目前,您通过连接然后搜索每个0-9
数字来执行此操作。这不是最佳的,因为你创建了不必要的字符串并且还搜索了每个数字,基本上扫描了10次字符串。
您可以做的是为每个字符串创建一个位掩码,位置i
的位显示字符串中是否存在i
。然后,您可以通过简单地对两个位掩码进行检查来检查串联是否包含所有数字,并检查结果是否为2 ^ 10-1,即1023.因为您只需要计算一次位掩码并且|
操作很快,这比连接和扫描数字要好。
一些代码。假设我们有一个字符串列表如下:
List<String> strings = Arrays.asList("012345","6789","34567");
这是创建位掩码的方法:
short[] masks = new short[strings.size()];
for (int i = 0; i < strings.size(); i++) {
String str = strings.get(i);
char[] chs = str.toCharArray();
short mask = 0;
for (int index = 0; index < chs.length; index++) {
char ch = chs[index];
if (ch >= '0' && ch <= '9') {
int digit = ch - '0';
mask |= (1 << digit);
}
}
masks[i] = mask;
}
这是你检查对的方法:
short mask_full = (short) 0b1111111111;
for (int i = 0; i < strings.size() - 1; i++) {
String string_i = strings.get(i);
short mask_i = masks[i];
for (int j = i; j < strings.size(); j++) {
String string_j = strings.get(j);
short mask_j = masks[j];
short mask_i_j_concatenated = (short) (mask_i | mask_j);
if (mask_i_j_concatenated == mask_full) {
System.out.println("Strings [" + string_i + "] and [" + string_j + "] form a pair.");
}
}
}
我只是在没有太多验证的情况下草拟了代码,所以要小心。
答案 1 :(得分:3)
这确实可以比O (input_length^2)
更快地解决,其中input_length
是所有给定字符串的总长度。
以下是O (input_length + 2^{digits * 2})
中的解决方案,其中digits
为10
,不同位数。
因此,术语2^{digits * 2}
本质上是一个常数,它不依赖于输入的大小。
首先,对于每个字符串,计算相应的掩码:从0
到1023
的整数(2^{10} - 1
),如果字符串包含,则设置位i
数字i
。
例如,字符串12153
具有二进制掩码0000101110
,小数为2^5 + 2^3 + 2^2 + 2^1 = 46
。
这可以在O (input_length)
上完成。
之后,我们将不再需要实际的输入字符串,甚至不需要单独的屏蔽本身。
我们感兴趣的是从0
到1023
的每个掩码的计数。
现在,让掩码m
的字符串数量为f[m]
。
现在可以找到答案如下:
answer = f[1023] * (f[1023] - 1) / 2
for u = 0, 1, 2, ..., 1022:
for v = u+1, u+2, ..., 1023:
if u | v == 1023:
answer += f[u] * f[v]
实际上,单独包含所有数字的f[1023]
字符串可以任意配对。
如果有5
个这样的字符串,则有choose (5, 2) = 5 * (5 - 1) / 2 = 10
种方法可以将其中的一对用于其中。
现在一般情况。
考虑一个带掩码u
的字符串和一个带掩码v
和u < v
的字符串。
如果u
和v
的按位OR为1023
,则它们形成一对,即,0
到9
的所有位都设置。
因此,如果u | v = 1023
,并且f[u]
个字符串包含掩码u
和f[v]
字符串,其中包含掩码v
,则会有f[u] * f[v]
个这样的对通过这两个面具。
此解决方案可以进一步优化,从O (input_length + 2^{digits * 2})
到O (input_length + 2^{digits} * digits)
,首先将g[v]
计算为f[w]
的所有超集w
的总和{ {1}}使用动态编程。
答案 2 :(得分:1)
您可以使用StringBuilder
在字符串和分配内存方面获得更好的性能(在这种情况下可能会产生轻微影响)
StringBuilder sb = new StringBuilder();
sb.append(tickets[i]).append(tickets[i]);
您可以使用sb.toString()
转换为字符串并使用它执行所有字符串操作
注意:每次都不要创建StringBuilder
实例,使用delete
清除数组
你可以做的另一种方法是使用Set
检查所有0到9的数字,只需检查长度
Set<Character> set = new HashSet<Character>();
for(Character c : "8654231097777".toCharArray()){
set.add(c);
}
System.out.println(set.size());
这样你只需要对字符串进行一次传递,而不是多次传递(每次contains
调用)
但它仍然是O(1):而不是10个循环,你会做1 ...
答案 3 :(得分:1)
首先,如果没有必要,你不应该尝试优化。除非您的阵列非常大,或者您正在使用大量类型,否则这很可能会导致性能问题。
无论如何,这里有一些关于解决方案速度慢的想法,以及什么可以使它更快:
contains()
10次,用于ech对。并且每个contains()
都需要遍历字符串,直到找到搜索到的子字符串。 contains()
适用于子字符串,而不是单个字符。以下是最容易实现的方式:
对于第一部分,将String转换为BitSet(或short)应该只需通过迭代字符串的字符,并将对应于该数字的位设置为true来完成。
答案 4 :(得分:0)
准备优化
首先,我们将重构内循环
for (int j = i + 1; j < tickets.length; j++) {
String pair = tickets[i] + tickets[j];
if (pair.length() < 10) {
continue;
}
if (containsAllDigit(pair)) {
count++;
}
}
所以,我们刚刚创建了新函数containsAllDigit
private static boolean containsAllDigit(String pair) {
return pair.contains("0")
&& pair.contains("1") && pair.contains("2")
&& pair.contains("3") && pair.contains("4")
&& pair.contains("5") && pair.contains("6")
&& pair.contains("7") && pair.contains("8")
&& pair.contains("9");
}
现在,让containsAllDigit
方法也重构
private static boolean containsAllDigit(String pair) {
String[] digits =
new String[] { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
for (String digit : digits) {
if (!pair.contains(digit)) {
return false;
}
}
return true;
}
基本上,此检查与先前的检查相同。 &&
是短路运算符,第一次方法contains
返回false
时,它将停止评估布尔表达式。
最后,我们将数字列表作为参数传递。因此,方法containsAllDigit
将是
private static boolean containsAllDigit(String pair, String[] digits)
<强>优化强>
让我们分析每对的结构。每对包含左侧和右侧。如果左侧包含所有数字,则没有理由检查右侧
例如,此数组{"123456789", "1", "2", "3"}
。这是答案3.
现在,让我们从数组的第一个元素中删除数字1
。现在,“完整”对的答案是1.当我们检查对时,我们只检查右侧是否包含数字1
。
如果我们推广这种方法,我们只需要检查该对的右侧部分是否包含缺失的数字
现在我们创建一个查找缺失数字的方法,称之为findMissingDigits
。作为参数,我们传递一对的左侧部分,第二个参数是数字列表 - String[] digits
。
private static String[] findMissingDigits(String left, String[] digits) {
List<String> ret = new ArrayList<>();
for (String digit : digits) {
if (!left.contains(digit)) {
ret.add(digit);
}
}
return ret.toArray(new String[0]);
}
最后一步是改变内循环。方法containsAllDigit
现在获取缺失数字列表而不是所有数字列表。
for (int i = 0; i < tickets.length - 1; i++) {
String left = tickets[i];
String[] missingDigits = findMissingDigits(left, digits);
for (int j = i + 1; j < tickets.length; j++) {
String pair = left + tickets[j];
if (pair.length() < 10) {
continue;
}
if (containsAllDigit(pair, missingDigits)) {
count++;
}
}
}