为什么哈希集的性能比列表更快?

时间:2019-01-14 11:01:11

标签: java performance hashmap breadth-first-search

此问题来自leetcode(https://leetcode.com/problems/word-ladder/)!

给出两个单词(beginWord和endWord)以及字典的单词列表,找到从beginWord到endWord的最短转换序列的长度,例如:

一次只能更改一个字母。 每个转换的单词都必须存在于单词列表中。注意beginWord不是转换后的单词。 注意:

如果没有这样的转换序列,则返回0。 所有单词的长度相同。 所有单词仅包含小写字母字符。 您可以假设单词列表中没有重复项。 您可以假设beginWord和endWord为非空并且不相同。

这是我的代码,需要800毫秒才能运行:

class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList){
    if(!wordList.contains(endWord))
        return 0;
    int ret = 1;
    LinkedList<String> queue = new LinkedList<>();
    Set<String> visited = new HashSet<String>();
    queue.offer(beginWord);
    queue.offer(null);
    while(queue.size() != 1 && !queue.isEmpty()) {
        String temp = queue.poll();
        if(temp == null){
            ret++;
            queue.offer(null);
            continue;                
        }
        if(temp.equals(endWord)) {
            //System.out.println("succ ret = " + ret);
            return ret;
        }
        for(String word:wordList) {           
            if(diffOf(temp,word) == 1){
                //System.out.println("offered " + word);
                //System.out.println("ret =" + ret);
                if(!visited.contains(word)){
                visited.add(word);
                queue.offer(word); 
                }
            }
        }
    }
    return 0;
}
private int diffOf(String s1, String s2) {
    if(s1.length() != s2.length())
        return Integer.MAX_VALUE;
    int dif = 0;
    for(int i=0;i < s1.length();i++) {
        if(s1.charAt(i) != s2.charAt(i))
            dif++;
    }
    return dif;    
}
}

这是另一个需要100毫秒才能运行的代码:

class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
    Set<String> set = new HashSet<>(wordList);
    if (!set.contains(endWord)) {
        return 0;
    }

    int distance = 1;
    Set<String> current = new HashSet<>();
    current.add(beginWord);

    while (!current.contains(endWord)) {
        Set<String> next = new HashSet<>();

        for (String str : current) {
            for (int i = 0; i < str.length(); i++) {
                char[] chars = str.toCharArray();

                for (char c = 'a'; c <= 'z'; c++) {
                    chars[i] = c;
                    String s = new String(chars);

                    if (s.equals(endWord)) {
                        return distance + 1;
                    }

                    if (set.contains(s)) {
                        next.add(s);
                        set.remove(s);
                    }
                }
            }
        }
        distance++;

        if (next.size() == 0) {
            return 0;
        }
        current = next;
    }

    return 0;
}
}

我认为第二个代码效率较低,因为它每个单词测试26个字母。为什么这么快?

1 个答案:

答案 0 :(得分:0)

简短的答案:您的“呼吸优先”搜索每“字距单位”(以下称为“迭代”)进行的比较会多出几个数量级。

  • 您将每个候选词与每个剩余词进行比较。每次迭代的时间复杂度T(N×n),
  • 他们将每个候选人与人为构造的“下一个”候选人进行比较。而且由于他们构造了候选者,所以不必“计算”距离。为简单起见,我假设两者(构造或检查)具有相同的运行时间。每次迭代的时间复杂度为T(26×l×n)。

(N =单词列表大小,n =此迭代的候选数,l =单词长度)

当然,26×l×n比N×n小得多,因为单词长度很小,但单词列表很大。

我在("and","has",[List of 2M English words])上尝试了您的例程,但30秒钟后我将其终止是因为我认为它已崩溃。它没有崩溃,只是很慢。我转到另一个50K的单词列表,您的单词列表现在需要8秒,而实现该过程需要0.04秒。

对于我的N = 51306的单词列表,有2167个3个字母的单词。这意味着平均每个单词有3×cbrt(2167)个可能的候选词,即n≈38.82。

  • 他们的预期性能:每次迭代工作T(26×l×n)≈T(3027),
  • 您的预期性能:每次迭代工作T(N×n)≈T(1991784)。

(假设单词列表不会变短;但是有了这么多单词,差异可以忽略不计)


顺便说一句,基于队列的循环缓冲区实现可能比其两个交替集实现更快,因此您可以使混合实现更快。