有人可以帮助分析我的代码的时间复杂度吗?

时间:2017-02-06 19:38:25

标签: java algorithm performance

情境:

我试图在LintCode上解决一个问题,"最长的连续序列",给定一个未排序的整数数组,找到最长的连续元素序列的长度。

链接到原始问题:https://www.lintcode.com/en/problem/longest-consecutive-sequence/

所以这是我直观的解决方案:

public int longestConsecutive(int[] num) {
    // write you code here
    if (num == null || num.length == 0) {
        return 0;
    }

    HashSet<Integer> hash = new HashSet<Integer>();
    int count = 1, offset = 1, max = 1, loops;

    for (int i : num) {
        hash.add(i);
    }

    for (Object i : hash.toArray()) {
        Integer elem = (Integer)i;

        hash.remove(elem);
        loops = hash.size();

        while (offset <= loops) {
            if (hash.contains(elem + offset)) {
                count++;
                hash.remove(elem + offset);
            } else {
                break;
            }

            offset++;
        }

        offset = 1;

        while (offset <= loops) {
            if (hash.contains(elem - offset)) {
                count++;
                hash.remove(elem - offset);
            } else {
                break;
            }

            offset++;
        }

        max = Math.max(max, count);
        count = 1;
        offset = 1;
    }

    return max;
}

解释

我试图遍历HashSet中的每个元素。

对于每个元素,首先检查其增量连续邻居,直到删除自身后HashSet的大小(因为最好的情况是HashSet中的所有剩余元素都是它的连续邻居)。

对于每个有效的邻居,我们增加计数并从HashSet中删除邻居。如果我们遇到了一个小姐,那么我们就会以相同的方式打破并开始检查其递减的邻居。

重复直到HashSet为空并返回最大长度。

问题:

  1. 此代码的时间复杂度是多少?

  2. 说每个外循环迭代中的内循环迭代次数取决于前一次外循环迭代中内循环迭代的次数是否正确?

  3. 我的想法是尽管for循环中有while循环,但这些循环的迭代次数是相关的。例如,如果输入中的所有元素都是连续的,那么外部for循环将只执行一次,因为所有操作都是由两个内部while循环完成的。在两个while循环退出之后,HashSet中将没有元素。

    另一方面,如果没有元素是连续的,则所有操作都由外部for循环完成。

    1. 我们是否也考虑hash.toArray()的费用?

    2. 最后,有没有办法将两个内部while循环结合在一起?

    3. Java是否支持类似&#34; dynamic for loop&#34;,我的意思是在for循环执行时迭代次数可以改变?我问这个是因为我在使用ConcurrentModificationException时无法绕过for (Object i: hash),同时在内部循环中通过hash.remove删除元素。所以我使用for (Object i : hash.toArray())作为解决方法,因此增加了不必要的开销。

2 个答案:

答案 0 :(得分:-1)

时间复杂度是最坏的情况复杂性。 因此看起来像O(n ^ 2),其中n =元素中的元素数。

事实上:

     for (Object i : hash.toArray()) {

是O(n),很容易假设hash.toArray()只执行一次(否则它将添加另一个O(n)循环)。

然后

     while (offset <= loops) { 

也是O(n)as loops = hash.size()

因此,因为循环是一个在另一个内部,所以时间复杂度为O(n ^ 2)。

NB第二个

    while (offset <= loops) { 

不影响整体复杂性,因为O(n + n)= O(n)。然后O(n *(n + n))= O(n ^ 2)。

但是,您操纵hashset,但不操作在外部循环中迭代的数组。因此,该数组永远不会为空,并且无论如何都会迭代它。

最后,我不确定您的算法是否完全解决了问题。如果您拥有一个序列中所有元素但没有排序且外部循环中的第一个元素位于序列中间的集合会发生什么?我相信你会发现2个序列中有一半的元素,但不是整个集合中最长的序列。

答案 1 :(得分:-1)

根据您的问题:

  

此代码的时间复杂度是多少?

在最坏的情况下,您的代码复杂性已经是O(N)。

for (int i : num) { - 一个N元素的循环。 hash.toArray() - 最坏情况下的另一个N元素循环(即没有重复) while (offset <= loops) { - 在最坏的情况下,这些循环中的任何一个都是N个元素。由于每次找到匹配时都会减小hash的大小,如果找不到匹配的元素就会中断循环 - 这种最坏的情况只会发生一次,并且仅针对其中一个循环({{1} }或hash.remove(elem - offset))。因此,对于这两个循环,它不会超过hash.remove(elem + offset)个元素。

总共得到O(3 x N),但由于在最坏情况分析中常数可以省略,因此你的算法是O(N)。

  

我的想法是否正确?

是的,你的想法是正确的。

  

最后,有没有办法将两个内部while循环结合在一起?

实际上在2个循环中没有任何意义,因为最终你将遍历所有元素。但是你需要以某种方式将序列映射到元素,以便保持序列开始的位置和结束的位置。 在你的情况下,你保持序列在循环体中开始和结束 - 这意味着你必须向左和向右看,以免错过任何候选人。如果您将数字转换为具有开始和长度的实际序列,并且只是尝试将它们组合在一起 - 您只需要遍历一个方向(Nelem + offset)。您可以查看elem - offset

中的elem + offset实现

理论符合实践

回复longestConsecutiveN建议:添加任何排序(通过树或add sorting)将使其成为O(N x logN),因为即使是最好的排序算法,例如Array.sort时间复杂度为O(N x logN)。

虽然实际,但理论 O(N x logN)算法可能比O(N)算法运行得更快,因为通常有一些因子(操作被认为是O(1) ))。 它有点超出了范围,但在目前的背景下分享仍然很有趣。

这里有几个实现要结合:

  • timsort - 原始代码 - longestConsecutiveBefore
  • time-complexity O(3 x N) - 略有改进的原文,主要删除了额外的longestConsecutiveAfter和一些不必要的hash.toArray次访问 - HashSet
  • time-complexity O(2 x N) - 一种不同的方法,尝试将元素数组减少到longestConsecutiveN个序列,然后将它们组合起来 - HashMap
  • time-complexity O(2 x N) - longestConsecutiveTree方法,首先对数组进行排序,然后一次性检查序列 - TreeSet
  • time-complexity O(N x logN + N) - longestConsecutiveSorted方法,首先对数组进行排序,然后一次性检查序列 - Array.sort

以下是所有代码(原始time-complexity O(N x logN + N)除外 - 它在问题中):

longestConsecutiveBefore

Heres是基准测试结果(source available via a gist):

public int longestConsecutiveAfter(int[] num) {
    // write you code here
    if (num == null || num.length == 0) {
        return 0;
    }



    HashSet<Integer> hash = new HashSet<Integer>();
    int count = 1, currentSequence = 1;

    for (Integer i : num) {
        hash.add(i);
    }

    if (hash.size() == 1) return 1; //all elements are the same

    int i = 0;
    while (hash.size() > 0) {
        int elem = num[i++];
        if (hash.remove(elem)) {
            int lcursor = elem;
            int rcursor = elem;
            currentSequence = 1;

            while (hash.remove(++rcursor)) {
                currentSequence++;
            }

            while (hash.remove(--lcursor)) {
                currentSequence++;
            }

            count = Math.max(count, currentSequence);
        }
    }

    return count;
}

public static int longestConsecutiveN(int[] num) {
    // write you code here
    if (num == null || num.length == 0) {
        return 0;
    }

    HashMap<Integer, Integer> hash = new HashMap<>();
    for (Integer i : num) {
        hash.put(i, 1);
    }

    Integer max = 1;
    Integer curSeqLength = null;
    for (Integer i: num) {
        if ((curSeqLength = hash.get(i)) != null) {
            Integer mergeSeqLength = null;
            while ((mergeSeqLength = hash.remove(i + curSeqLength)) != null) {
                curSeqLength = curSeqLength + mergeSeqLength;
                hash.put(i, curSeqLength);
            }
            if (curSeqLength > max) max = curSeqLength;
        }
    }

    return max;
}

public int longestConsecutiveTree(int[] num) {
    // write you code here
    if (num == null || num.length == 0) {
        return 0;
    }

    Set<Integer> hash = new TreeSet<>();
    int count = 1, currentSequence = 1;

    for (int i : num) {
        hash.add(i);
    }

    if (hash.size() == 1) return 1; //all elements are the same

    Iterator<Integer> iterator = hash.iterator();
    int elem = iterator.next();
    while (iterator.hasNext()) {
        int cursor = iterator.next();
        if (cursor - elem == 1) {
            currentSequence++;
        } else {
            count = Math.max(count, currentSequence);
            currentSequence = 1;
        }
        elem = cursor;
    }

    count = Math.max(count, currentSequence);
    return count;
}



public int longestConsecutiveSorted(int[] num) {
    // write you code here
    if (num == null || num.length == 0) {
        return 0;
    }

    if (num.length == 1) {
        return 1;
    }


    int count = 1, currentSequence = 1;

    int[] copy = new int[num.length];
    System.arraycopy(num, 0, copy, 0, num.length);
    Arrays.sort(copy);
    int i = 1;
    while (++i < copy.length) {
        int diff = copy[i] - copy[i - 1];
        if (diff == 1) {
            currentSequence++;
        } else if (diff != 0) {
            count = Math.max(count, currentSequence);
            currentSequence = 1;
        }
    }

    count = Math.max(count, currentSequence);
    return count;
}

因此理论上较慢的100 elements Benchmark Mode Cnt Score Error Units LongestConsecutiveBench.sorted sample 4851102 1600,768 ± 78,974 ns/op LongestConsecutiveBench.n sample 6464973 4008,925 ± 13,191 ns/op LongestConsecutiveBench.after sample 5847310 4452,501 ± 15,122 ns/op LongestConsecutiveBench.tree sample 5934834 4489,135 ± 72,672 ns/op LongestConsecutiveBench.before sample 4684143 5704,490 ± 88,950 ns/op 1000 elements Benchmark Mode Cnt Score Error Units LongestConsecutiveBench.sorted sample 6629830 2002,731 ± 28,507 ns/op LongestConsecutiveBench.after sample 3379354 52919,011 ± 54,184 ns/op LongestConsecutiveBench.n sample 3415801 56399,068 ± 53,115 ns/op LongestConsecutiveBench.before sample 2717184 73837,520 ± 192,104 ns/op LongestConsecutiveBench.tree sample 1360498 147322,523 ± 565,219 ns/op 10000 elements Benchmark Mode Cnt Score Error Units LongestConsecutiveBench.sorted sample 5319312 18974,711 ± 84,455 ns/op LongestConsecutiveBench.after sample 233353 859290,201 ± 1229,219 ns/op LongestConsecutiveBench.n sample 210452 952588,627 ± 1328,927 ns/op LongestConsecutiveBench.before sample 161523 1241214,958 ± 5034,863 ns/op LongestConsecutiveBench.tree sample 78707 2552443,734 ± 10685,110 ns/op O(N x logN + N)实现几乎比其他O(N)更好。这种情况正在发生,因为我们正在对基元进行操作,并且必须进行大量的自动装箱(int - &gt; Integer,反之亦然)。原始类型的LongestConsecutiveBench.sorted可以解决问题。

值得一提的是,在相同的情况下,理论上较慢HashMap O(N x logN + N)实现较慢几乎然后其他O (N)应该实现。