【问题标题】:What are good practices of making code faster?使代码更快的好的做法是什么?
【发布时间】:2020-04-27 17:48:12
【问题描述】:

问题说明:Sherlock and the Valid String

此代码通过了所有正确性测试,15/20,但是,由于时间限制,一些测试失败了。 什么是让代码更快的好习惯?如何避免for 循环?

static String isValid(String s) {

    String yesOrNo;
    //Step 1: count the frequency of each char and out in a map <char, number>
    Map<Character, Integer> map = new HashMap<>();
    for (int i = 0; i < s.length(); i++) {
        int count = 0;
        for (int c = 0; c < s.length(); c++) {
            if (s.charAt(c) == s.charAt(i))
                count++;
        }
        map.put(s.charAt(i), count);
    }
    //Step2: add all the numbers of occurrences of each char into a list
    List<Integer> values = new ArrayList<>();
    for (Map.Entry<Character, Integer> kv : map.entrySet()
    ) {
        values.add(kv.getValue());
    }
    //Step 3: find the benchmark number
    Map<Integer, Integer> occurPairs = new TreeMap<>();
    for (int i = 0; i < values.size(); i++) {
        occurPairs.put(values.get(i), Collections.frequency(values, values.get(i)));
    }
    Map.Entry<Integer, Integer> popVal = Collections.max(occurPairs.entrySet(), Map.Entry.comparingByValue());
    Map.Entry<Integer, Integer> smallest = Collections.min(occurPairs.entrySet(), Map.Entry.comparingByValue());

    //Step 4: compare each value with the benchmark
    int numOfWrong = 0;
    for (Integer value : values) {
        if (!value.equals(popVal.getKey()))
            numOfWrong += Math.abs(popVal.getKey() - value);
    }
    if (occurPairs.size() == 2 && smallest.getValue() == 1 && smallest.getKey() == 1)
        yesOrNo = "YES";
    else if (numOfWrong > 1)
        yesOrNo = "NO";
    else
        yesOrNo = "YES";
    System.out.println(yesOrNo);
    return yesOrNo;
}

【问题讨论】:

  • 我认为您正在做很多额外的工作来检查字符串是否无效。如果任何其他频率相差超过 1,则您可以立即返回 false,无需额外比较。
  • 你知道什么是 Big-O 复杂度吗?如果不是,我们可以在答案中解释。
  • @NomadMaker HR 希望我返回一个字符串。 my 代码没有真正的其余部分,因为这是我输入的唯一方法,然后 HR 完成其余部分。
  • @JohnKugelman 这将非常有帮助。如果您有时间解释,我想学习一些新内容。
  • @Tyberius 我不同意“任何”,如果您阅读要求,它表示如果字符串的所有字符出现相同的次数,Sherlock 认为该字符串是有效的。如果他可以仅删除字符串中 1 个索引处的 1 个字符,并且其余字符将出现相同的次数,这也是有效的。给定一个字符串,判断它是否有效。如果是,则返回 YES,否则返回 NO。

标签: java performance optimization


【解决方案1】:

我不会直接告诉你出了什么问题,但我会给你一个概念框架,你可以通过它来分析你的代码。

想想你在输入字符串中每个字母执行的步骤数。

  • 如果您执行固定数量的步骤,那么您将获得 线性 缩放。假设您对每个字母执行三个步骤。如果有 n 个字母并且您执行 3n 个操作,那么您的状态很好。

  • 但是,如果您对每个字母执行 n 次操作,那么您将获得 3n² 的 二次 缩放 或总计操作。 (前面的常数并不重要。重要的是指数。)假设您有 1,000 个字母。 3n 缩放意味着 3000 次操作。 3n² 缩放意味着 3 百万

二次基本上意味着“不缩放”。当输入变大时,二次算法不是按输入长度成比例缩放,而是崩溃。它们在正常工作量下工作正常,但在压力下会分崩离析。 Hacker Rank 很可能会在您的算法中抛出非常长的输入字符串来检测二次爆炸。

我在上面谈到了n。在 Java 术语中 ns.length()。您能在代码中找出执行s.length() * s.length() 操作的二次步骤吗?

是的,在第 1 步中,我对 s 进行了两次迭代以计算每个字符的频率。

没错。好的。现在,你怎么能一次完成第 1 步?

想想你会如何在纸上做到这一点。你不会一遍又一遍地扫描字符串,对吧?您只需查看每个字母一次,并随时计算所有字母。您可能会有一张带有字母和计数标记的表格,例如:

A   ||||
B   
C   |
D   ||
E   |||||||
F   
...

在代码中做同样的事情,它会将 减少到 n

【讨论】:

  • 非常感谢!我在 SO 上找到了这个,@JohnKugelman,通过了一切。 s.chars().forEach(e->map.put((char)e, map.getOrDefault((char)e, 0) + 1));
  • 是的,这行得通。我会挑战你自己写,所以它不是“魔法”。只需对现有代码稍作修改即可。
  • 好的,我会再花一分钟时间查看它以获得更简单的解决方案。再次,非常感谢。
【解决方案2】:

除了@JohnKugelman 的回答,您还可以这样做。

首先,您将通过两次字符串(使用内循环)来计算 O(n^2) 的出现次数。你已经找到了一个 O(n) 的解决方案

Map<Character, Integer> occurences = new HashMap<>();

s.chars()
 .forEach(e-> occurences.put((char)e, occurences.getOrDefault((char)e, 0) + 1));

现在我们需要找到一个简单的迭代来找出答案。

以下是我们对"YES" 案例的了解。

  • 所有字母的频率相同,例如:aabbccddeeff
  • 所有字母的频率相同,但单个字母出现 1 次以上。例如:aabbccddd
  • 所有字母的频率相同,但一个字母出现 1 次。例如:aaaabbbbcccce

所以我们需要做的是遍历地图的值并计算出现次数。

首先,让我们得到我们的迭代器

Iterator<Map.Entry<Character, Integer>> iterator = occurences
                .entrySet()
                .iterator();

然后选择第一个数字作为“基准”并为第一个不同的值定义一个变量和一个计数

int benchmark = iterator.next().getValue();
int benchmarkCount = 1;
int firstDifferent = -1;
int differentCount = 0;

遍历数字

while(iterator.hasNext()) {
    int next = iterator.next().getValue();
    if (next == benchmark) { // if the next number is same
        benchmarkCount++; // just update our count
    } else { // if it is different
        // if we haven't found a different one yet or it is the same different value from earlier
        if (firstDifferent == -1 || firstDifferent == next) {
            firstDifferent = next;
            differentCount++;
        }
    }
}

现在我们需要做的就是分析我们的数字

int size = occurences.size();

if (benchmarkCount == size) return "YES"; // if all of the numbers are the same
if (benchmarkCount == size - 1) { // if we hit only single different
    // either the diffent number is 1 or it is greater than our benchmark by value of 1
    if (firstDifferent == 1 || firstDifferent - benchmark == 1) {
        return "YES";
    }
}
// same case with above
if (differentCount == size - 1) {
    if (benchmark == 1 || benchmark - firstDifferent == 1) {
        return "YES";
    }
}

完整解决方案

static String isValid(String s) {
    Map<Character, Integer> occurences = new HashMap<>();

    s.chars().forEach(e-> occurences.put((char)e, occurences.getOrDefault((char)e, 0) + 1));

    Iterator<Map.Entry<Character, Integer>> iterator = occurences
            .entrySet()
            .iterator();

    int benchmark = iterator.next().getValue();
    int benchmarkCount = 1;
    int firstDifferent = -1;
    int differentCount = 0;

    while(iterator.hasNext()) {
        int next = iterator.next().getValue();
        if (next == benchmark) {
            benchmarkCount++;
        } else {
            if (firstDifferent == -1 || firstDifferent == next) {
                firstDifferent = next;
                differentCount++;
            }
        }
    }
    int size = occurences.size();

    if (benchmarkCount == size) return "YES";
    if (benchmarkCount == size - 1) {
        if (firstDifferent == 1 || firstDifferent - benchmark == 1) {
            return "YES";
        }
    }
    if (differentCount == size - 1) {
        if (benchmark == 1 || benchmark - firstDifferent== 1) {
            return "YES";
        }
    }

    return "NO";
}

【讨论】:

  • 这很复杂,看我的回答。
【解决方案3】:

我发现其他解决方案太复杂了。使用不同字符的数量(以下counts.length)和它们的最小频率(以下min),我们知道字符串长度至少为counts.length * min

当单个字符出现一次以上min 时,我们的字符串长了一个。

当有更多这样的字符时,字符串会比counts.length * min + 1 长。当任何字符比min + 1 出现的频率更高时,字符串也会变得比counts.length * min + 1 更长。

正如 Bunyamin Coskuner 所指出的,我的解决方案遗漏了“aabbc”之类的情况。这个可行,但它不再像想要的那么简单:

static String isValid(String s) {
    if (s.isEmpty()) return "YES";
    final int[] counts = s.chars()
            .mapToObj(c -> c)
            .collect(Collectors.groupingBy(c -> c, Collectors.counting()))
            .values()
            .stream()
            .mapToInt(n -> n.intValue())
            .toArray();
    final int min = Arrays.stream(counts).min().getAsInt();
    // Accounts for strings like "aabb" and "aabbb".
    if (s.length() <= counts.length * min + 1) return "YES";
    // Here, strings can be only valid when the minimum is one, like for "aabbc".
    if (min != 1) return "NO";
    final int minButOne = Arrays.stream(counts).filter(n -> n>1).min().getAsInt();
    return s.length() == (counts.length - 1) * minButOne + 1 ? "YES" : "NO";
}

【讨论】:

  • 很抱歉,您的“简单”解决方案不适用于所有情况。另外,你能告诉我你的解决方案的复杂性是什么,因为这是问题的重点吗?
  • @BunyaminCoskuner 抱歉,我还没有测试过。你能给我一个失败的测试吗? +++ 复杂度和你的一样,在s.length() 中是线性的。 +++ 我的解决方案中唯一有点复杂的部分是收集线(google for "java stream grouping count")。
  • 它不适用于像 "aabbc" 这样您可以删除 "c" 以使其成为有效字符串的情况。但是,您的函数返回“NO”,因为 s.length = 5、counts.length = 3 和 min = 1
  • @BunyaminCoskuner 谢谢....我今天会修复它。我仍然认为我的方法很好,只是我应该稍微测试一下。我的错。
  • 我并没有说你的方法不好,但是只使用流 API 并不能使代码“好”或“简单”。此外,添加对边缘情况的检查使其与我的一样复杂,最后您必须再检查一次计数数组。我指的是您对我的回答的初步评论。另外,我也不建议我的完美。可以使用您的方法的某些部分对其进行改进。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-17
  • 1970-01-01
  • 2016-12-13
  • 1970-01-01
  • 1970-01-01
  • 2010-10-16
  • 1970-01-01
相关资源
最近更新 更多