【问题标题】:My program times out on large Strings, how could I make it faster?我的程序在大字符串上超时,我怎样才能让它更快?
【发布时间】:2017-02-01 01:03:22
【问题描述】:

我正在尝试反转字符串的元音。我的程序可以工作,但它很慢并且在大字符串上超时。我怎样才能让它更快? 编写一个函数,将字符串作为输入并仅反转字符串的元音。

示例 1:给定 s = "hello"return "holle"

示例 2:给定 s = "leetcode"return "leotcede"

注意: 元音不包括字母“y”。

public class Solution {
    public String reverseVowels(String s){

        Set<Character> vowel = new HashSet<>();

        char [] sArray = s.toCharArray();
        vowel.add('a');
        vowel.add('e');
        vowel.add('o');
        vowel.add('i');
        vowel.add('u');
        vowel.add('A');
        vowel.add('E');
        vowel.add('I');
        vowel.add('O');
        vowel.add('U');

        Stack<Character> v = new Stack<>();
        String temp = "";

        int i = 0;
        for (Character c: sArray){
            if (vowel.contains(c)){
                v.push(c);

            }
            i++;
        }
        for (Character c: sArray){
            if (vowel.contains(c)){
               if (!v.empty())
                c = v.pop();
            }
            temp+= c;

        }
        return temp;

    }
}

编辑:我已将代码更改为使用 StringBuilder,现在它通过了测试用例,但它仍然比 95% 的 Java 解决方案慢,因此我将进行其他更改以使其更快。我的程序运行缓慢并不完全是因为没有使用 StringBuilder。即使进行了这种更改,我的程序仍然很慢。这不是其他问题的重复。

【问题讨论】:

标签: java algorithm


【解决方案1】:

这里发生了一些事情:

  1. 每次使用该方法时都会重新创建元音的Set,并且每次都将元音添加到其中(这也可能涉及扩展集合的大小);因为它们不会改变,所以没有必要。 (这并没有太大的区别,但我想我会提到它。)

  2. 每个 char 原语都被不必要地自动装箱到 Character 对象中 - 在您的两个循环中以及添加到元音的 Stack 时。

  3. 正如 Tom 链接的可能重复项所暗示的那样,您可能会从 StringBuilder.append(char) 而不是 + 运算符获得更好的性能。

在我看来,避免上述缺陷的更好实现可能如下所示:

private static boolean isVowel(char c) {
    switch (c) {
        case 'a':
        case 'e':
        case 'i':
        case 'o':
        case 'u':
        case 'A':
        case 'E':
        case 'I':
        case 'O':
        case 'U':
            return true;
        default: return false;
    }
}

public static String reverseVowels(String s) {
    final char [] sArray = s.toCharArray();
    final StringBuilder reversedString = new StringBuilder();
    final StringBuilder vowels = new StringBuilder();

    int vowelIndex = -1;
    for (char c : sArray) {
        if (isVowel(c)) {
            vowels.append(c);
            ++vowelIndex;
        }
    }

    for (char c : sArray) {
        if (isVowel(c))
            c = vowels.charAt(vowelIndex--);

        reversedString.append(c);
    }
    return reversedString.toString();
}

【讨论】:

  • 谢谢。这大大改善了事情。我首先改变了这种情况,结果只提高了 2%,但仍然比使用 Java 的 95% 的其他解决方案慢。然后我完全改变了你的解决方案,它击败了 66% 的 Java 提交。非常感谢。我真的很感激这些解释。他们使它很容易理解。
  • @m-brynczka 你认为是否值得尝试在第一次运行时注意元音位置的方法,然后在第二次运行时只使用它们而不是再次遍历整个字符串通过?对于小琴弦或元音较多的琴弦,它可能无济于事,但我可以相信,对于带有少量元音的长琴弦,它可能会产生明显的不同。顺便说一句,答案很好:)
【解决方案2】:

这是我的示例(确保为此为 JVM 提供了足够的堆)。

在我的机器上反转 3.2 亿个字符的元音需要 719 毫秒。 此解决方案使用 char 数组中的两个索引就地交换元音。

内存效率不是最高的,因为 new String(c) 会像 String.toCharArray 一样复制数组 但是,这个速度很快。

public class Main {

    public static void main(String[] args) {
        Main m = new Main();
        System.out.println(m.reverseVowels("hello"));
        System.out.println(m.reverseVowels("leetcode"));
        char[] longString = new char[320_000_000];
        Arrays.fill(longString, 'e');
        String loooooongString = new String(longString);
        long t = System.currentTimeMillis();
        m.reverseVowels(loooooongString);
        System.out.println("Duration:" + (System.currentTimeMillis() - t));
    }

    String reverseVowels(String s) {
        char[] c = s.toCharArray();
        int i = 0;
        int j = c.length - 1;
        char tmp;
        while (i < j) {
            while (i < j && !isVowel(c[i])) i++;
            while (i < j && !isVowel(c[j])) j--;
            if (i >= j) break;
            tmp = c[i];
            c[i] = c[j];
            c[j] = tmp;
            i++;
            j--;
        }
        return new String(c);
    }

    boolean isVowel(char c) {
        switch (c) {
            case 'a':
            case 'e':
            case 'i':
            case 'o':
            case 'u':
            case 'A':
            case 'E':
            case 'O':
            case 'U':
                return true;
            default:
                return false;
        }
    }
}

【讨论】:

  • 一般来说,如果您需要优化处理大量字符串的速度和内存。如果您知道如何即时从/转换为 UTF-8,请查看是否可以使用 char[] 甚至 byte[]。字符串在几个方面很昂贵: - 内部无法访问的 char[] 被复制得太频繁 - char 是 2 个字节宽 - 字符串是 final 并且您不能轻易替换或扩展它的方法。所有这些都有正当的理由,但我希望 JDK 9 会在这方面带来一些优化
  • 这是一个击败 95% 的 Java 解决方案的非常好的解决方案。我非常感谢如何就地执行它以及使用 char[] 的解释。我将研究 byte[] 并即时从/转换为 UTF-u。
  • 很遗憾,如果不将所需空间增加三倍,几乎不可能做到这一点。通过toCharArray() 或使用StringBuilder 创建一个新的char[],您最终会在内存中获得3 个数据副本:原始字符串、直接操作的char[] 和新字符串。
  • 我忘记了:你也可以尝试坚持StringBuilder 并且永远不要转换为字符串。 StringBuilder 实现 CharSequence 如果恰好是您在处理链中进一步需要的全部。
  • 感谢 Jochen 的额外提示!
【解决方案3】:

我不确定性能,但试试这个:

    Set<Character> vowels = new HashSet<>();
    vowels.add('a');
    vowels.add('e');
    vowels.add('o');
    vowels.add('i');
    vowels.add('u');
    vowels.add('A');
    vowels.add('E');
    vowels.add('I');
    vowels.add('O');
    vowels.add('U');

    String str = "KAKEKIKOKUKaKeKiKoKuK";
    char[] strArray1 = str.toCharArray();
    char[] strArray2 = str.toCharArray();
    int pos1 = -1;
    int pos2 = strArray1.length - 1;
    int size = strArray1.length;
    Stack<Character> vowelsStack = new Stack<>();
    boolean isPos1Vowel = false;

    while (true) {
        if (pos2 > -1) {
            if (vowels.contains(strArray2[pos2])) {
                vowelsStack.add(strArray2[pos2]);
            }
            pos2--;
        }
        if (isPos1Vowel) {
            if (!vowelsStack.isEmpty()) {
                strArray1[pos1] = vowelsStack.remove(0);
                pos1++;
                if (pos1 < size) {
                    isPos1Vowel = vowels.contains(strArray1[pos1]);
                } else {
                    break;
                }
            }
        } else {
            pos1++;
            if (pos1 < size) {
                isPos1Vowel = vowels.contains(strArray1[pos1]);
            } else {
                break;
            }
        }
    }

    System.out.println(new String(strArray1));

请告诉我它是否如你所愿。

【讨论】:

    【解决方案4】:

    正如 Tom 在他的评论中所建议的,主要问题可能是您使用的是 String 而不是 StringBuilder,例如参见 here。尝试将 temp 更改为新的 StringBuilder,并以这种方式构建您的字符串。运气好的话,一切都会变得不同。或者,使用 David 的方法,只需更改 char 数组中的字符,然后对其执行最终的 toString。

    否则,如果它仍然不够快,而不是遍历整个字符串两次,您也许可以在第一次通过时记下字符串中每个元音的位置(也许使用整数队列? ),然后快速迭代元音位置,用堆栈中的下一个元音替换每个位置的字母。请注意,我不确定这一定会更快,但可能值得一试。这种方法的一个潜在优势可能是,一旦用完所有元音,就可以停止处理字符串。例如,如果你有一个像“aeiouzx...”这样的字符串,其中唯一的元音出现在前面,一旦你浏览了你的元音列表,你就会知道你已经完成并且可以停在那里,可能避免大量浪费时间。可能是对特定字符串的改进。

    希望其中之一可以将您的表现提高到所需的水平。

    【讨论】:

    • 谢谢。我改成 StringBuilder,它通过了测试用例,虽然我的代码仍然很慢,而且 95% 的 Java 答案都比我的快。我还将尝试您的其他建议,看看它们是否更快。谢谢!
    猜你喜欢
    • 2012-03-23
    • 2011-06-14
    • 1970-01-01
    • 2015-07-04
    • 2014-07-05
    • 1970-01-01
    • 2012-01-15
    • 2020-12-27
    • 1970-01-01
    相关资源
    最近更新 更多