GuilinDev

Lc0128

05 August 2008

128 Longest Consecutive Sequence

原题概述

Given an unsorted array of integers, find the length of the longest consecutive elements sequence.

Your algorithm should run in O(n) complexity.

Example:

1
2
3
Input: [100, 4, 200, 1, 3, 2]
Output: 4
Explanation: The longest consecutive elements sequence is [1, 2, 3, 4]. Therefore its length is 4.

题意和分析

没有排序的数组里面寻找最长的子序列,要求时间复杂度是O(n)。

1) 排序后,检查当前元素是否比上一个元素大1,找到最大连续的长度,O(nlogn),不行;

2) 没有空间复杂度的要求,于是可以额外用一个HashSet,把数组里面所有的元素放入到set里面,然后遍历数组,对每个元素都进行移除操作,同时用两个指针prev和next求出当前元素的构成连续数列的前面和后面一个数,继续检查prev和next是否在set中存在,如果存在就继续移除,最后用next - prev - 1(因为两个指针指向的元素在set中不存在的时候才停止移除,所以有-1),对每个元素都进行这样的操作后求出连续序列最大的。

3) 也可以采用HashMap来做,刚开始map为空,然后遍历所有数组中的元素,如果该数字不在map中,那么分别检查前后两个数字是否在map中,如果在,则返回其哈希表中映射值,若不在,则返回0,将prev+next+1作为当前数字的映射,并更新result结果,然后更新num-left和num-right的映射值。

这其实是动态规划的思想,时间复杂度O(n)。

4) Union Find

  • 初始:所有元素各自为战
  • 首次遍历:所有元素 x 向各自邻居 x + 1,发起结盟,并「以大者为领队」
    • 若有邻居,才结盟成功
    • 领队,即 区间右边界
    • 不只是元素 x 与邻居 x + 1 结盟,而是整个 x 所在队伍与整个 x + 1 所在队伍结盟
      • 如 [1, 2, 3] 与 [4, 5] 两个队伍结盟
  • 二次遍历:记录所有人与其领队距离 距离,即 区间右边界 - 当前元素 + 1

代码

HashSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
   public int longestConsecutive(int[] nums) {
      if (nums == null || nums.length == 0) return 0;

      HashSet<Integer> set = new HashSet<>();
      int result = 0;

      //将数组里的所有元素放到HashSet里面
      for (int num : nums) set.add(num);

      for (int num : nums) {
         if (set.remove(num)) {//Java的remove方法是有返回值的,同样add也有
            int prev = num - 1, next = num + 1;
            while (set.remove(prev)) prev--;
            while (set.remove(next)) next++;

            result = Math.max(result, next - prev - 1);
         }
      }
      return result;
   }
}

HashMap动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
   public int longestConsecutive(int[] nums) {
      if (nums == null || nums.length == 0) return 0;

      HashMap<Integer, Integer> map = new HashMap<>();
      int result = 0;
      for (int num : nums) {
         if (!map.containsKey(num)) {
            //注意这里的prev和next是元素在HashMap中的索引值
            int prev = map.containsKey(num - 1) ? map.get(num - 1) : 0;
            int next = map.containsKey(num + 1) ? map.get(num + 1) : 0;

            int sum = prev + next + 1;
            map.put(num, sum);

            result = Math.max(result, sum);

            map.put(num - prev, sum);
            map.put(num + next, sum);
         }
      }

      return result;
   }
}

备忘录法,dfs形成的递归树中有重复子结构。 比如[100, 4, 200, 1, 3, 2] ,遍历4时,已经将dfs(3),dfs(2),dfs(1)的情况给遍历过了,因此,再往后递归的时候,我们应该剪枝。 于是添加备忘录Map。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
    Set<Integer> checkInNums = new HashSet<>(); //初始化Set用来判断数字是否在nums中
    Map<Integer, Integer> m = new HashMap<>();  //备忘录
    public int longestConsecutive(int[] nums) {
        if(nums.length == 0)
            return 0;
        for(int i = 0; i < nums.length; i++)
            checkInNums.add(nums[i]);
        int res = 0;
        for(int i = 0; i < nums.length; i++)
            res = Math.max(res, dfs(nums[i]));
        return res;
    }
    //返回小于等于当前数的最大连续序列的长度
    public int dfs(int cur) {
        if(m.containsKey(cur))
            return m.get(cur);
        int len = 1;    //当前长度为1
        if(checkInNums.contains(cur-1))
            len += dfs(cur-1);
        m.put(cur, len);
        return len;
    }
}

Union Find模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Solution {
    public int longestConsecutive(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        HashMap<Integer, Integer> map = new HashMap<>(); //k-v: 元素 - 元素位置
        int[] parent = new int[nums.length];
        
        for (int i = 0; i < nums.length; i++) {
            parent[i] = i;
            if (map.containsKey(nums[i])) {// 先把所有元素设为自己的leader,顺便除重
                continue;
            }
            map.put(nums[i], i);
        }
        
        for (int i = 0; i < nums.length; i++) {
            // 遇到相邻的元素,并且leader不一样,做union操作
            if (map.containsKey(nums[i] - 1) && find(i, parent) != find(map.get(nums[i] - 1), parent)) {
                union(i, map.get(nums[i] - 1), parent);
            }
            if (map.containsKey(nums[i] + 1) && find(i, parent) != find(map.get(nums[i] + 1), parent)) {
                union(i, map.get(nums[i] + 1), parent);
            }
        }
        
        // 找到parent中value最多的元素,代表某老大出现的次数最多,因此该集团的成员最多
        int maxLen = 0;
        int[] count = new int[parent.length];
        for (int val : map.values()) {
            int currParent = find(val, parent);
            count[currParent]++;
            maxLen = Math.max(maxLen, count[currParent]);
        }
        return maxLen;
    }
    
    // 以下为Union Find代码,
    private int find(int p, int[] parent) {
        if (p == parent[p]) {
            return parent[p];
        }
        parent[p] = find(parent[p], parent); // 路径压缩,到最终leader
        /**路径压缩的迭代写法
        while (p != parent[p]) {
            parent[p] = parent[parent[p]];
            p = parent[p];
        }
        */
        return parent[p];
    }
    private void union(int p, int q, int[] parent) {
        int f1 = find(p, parent);
        int f2 = find(q, parent);
        
        if (f1 != f2) {
            parent[f1] = f2;
        }
    }
}

Union Find 做成class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class Solution {
    public int longestConsecutive(int[] nums) {
        if (nums.length == 0) return 0;

        // 首次遍历,与邻居结盟
        UnionFind uf = new UnionFind(nums);
        for (int v : nums)
            uf.union(v, v + 1); // uf.union() 结盟

        // 二次遍历,记录领队距离
        int max = 1;
        for (int v : nums) {
            max = Math.max(max, uf.find(v) - v + 1); // uf.find() 查找领队
        }
        return max;
    }
    
    // Union Find类代码
    class UnionFind {
        private int count;
        private Map<Integer, Integer> parent; // (curr, leader)

        UnionFind(int[] arr) {
            count = arr.length;
            parent = new HashMap<>();
            for (int v : arr)
                parent.put(v, v); // 初始时,各自为战,自己是自己的领队
        }

        // 结盟
        void union(int p, int q) {
            // 不只是 p 与 q 结盟,而是整个 p 所在队伍 与 q 所在队伍结盟
            // 结盟需各领队出面,而不是小弟出面
            Integer rootP = find(p), rootQ = find(q);
            if (rootP == rootQ) return;
            if (rootP == null || rootQ == null) return;

            // 结盟
            parent.put(rootP, rootQ); // 谁大听谁
            // 应取 max,而本题已明确 p < q 才可这么写
            // 当前写法有损封装性,算法题可不纠结

            count--;
        }

        // 查找领队
        Integer find(int p) {
            if (!parent.containsKey(p))
                return null;

            // 递归向上找领队
            int root = p;
            while (root != parent.get(root))
                root = parent.get(root);

            // 路径压缩:扁平化管理,避免日后找领队层级过深
            while (p != parent.get(p)) {
                int curr = p;
                p = parent.get(p);
                parent.put(curr, root);
            }

            return root;
        }
    }
}