需要投入来改善解决方案的性能

时间:2018-04-14 09:13:08

标签: java algorithm

我创建了以下逻辑,以查找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;
    }

5 个答案:

答案 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})中的解决方案,其中digits10,不同位数。 因此,术语2^{digits * 2}本质上是一个常数,它不依赖于输入的大小。

首先,对于每个字符串,计算相应的掩码:从01023的整数(2^{10} - 1),如果字符串包含,则设置位i数字i。 例如,字符串12153具有二进制掩码0000101110,小数为2^5 + 2^3 + 2^2 + 2^1 = 46。 这可以在O (input_length)上完成。 之后,我们将不再需要实际的输入字符串,甚至不需要单独的屏蔽本身。 我们感兴趣的是从01023的每个掩码的计数。

现在,让掩码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的字符串和一个带掩码vu < v的字符串。 如果uv的按位OR为1023,则它们形成一对,即,09的所有位都设置。 因此,如果u | v = 1023,并且f[u]个字符串包含掩码uf[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()适用于子字符串,而不是单个字符。

以下是最容易实现的方式:

  1. 遍历数组,并为每个字符串创建另一个包含BitSet(或更高效,更短)的数组。 BitSet(或short)将包含10位(每个数字一位),如果字符串包含该数字,则为真。
  2. 使用您的算法,但通过检查bitset1或bitset2的基数为10来替换内部检​​查。
  3. 对于第一部分,将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++;
        }
    }
}