Leetcode 128 longest-consecutive-sequence 最长连续序列 Hash法及并查集解法

6 篇文章 0 订阅
5 篇文章 0 订阅

自己实现的LeetCode相关题解代码库:https://github.com/Yuri0314/Leetcode


题目要求

给定一个未排序的整数数组,找出最长连续序列的长度。

要求算法的时间复杂度为 O(n)。

示例:

输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。

看到这道题我首先的思路是对数组先进行排序,然后操作就很简单了,虽然也有想到使用Hash的方法,但没有想出该如何应用。

看完题解之后才发现这是一道典型的应用Hash的O(1)查找效率的题目,下面我总结了使用Hash法和并查集实现的多种实现方法并依次进行说明。

*注:暴力法和排序法的实现及解释可参考官方题解:https://leetcode.com/problems/longest-consecutive-sequence/solution/


1. Hash法1(官方解法)

官方解法不用过多解释,基本步骤就是:

  1. 将所有数字加入Set中进行去重;
  2. 对去重后的所有数字依次执行下列操作:
    1. 判断当前数组是不是一个序列串中的最小值(如果不是最小值,则计算得到的不是其所在串的最大长度)
    2. 如果是最小值,则继续判断比它的值大1的数是否在该集合set中,同时统计序列串的长度
    3. 更新记录的最大序列长度值
class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> num_set = new HashSet<Integer>();
        for (int num : nums)
            num_set.add(num);
        
        int longestStrak = 0;
        for (int num : num_set) {
            if (!num_set.contains(num - 1)) {
                int curNum = num;
                int curStrak = 1;
                while (num_set.contains(++curNum)) ++curStrak;
                longestStrak = Math.max(longestStrak, curStrak);
            }
        }
        return longestStrak;
    }
}

2. Hash法2(累积长度)

该方法的核心思路在于,对遍历到的每一个数字,将其所在串的长度设为“比其值小1的数记录的串长度”+“比其值大1的数记录的串长度”+1,同时在更新数据过程中更新最大序列串长度值。

在该方法中,需要快速找到比当前数字小1或者大1的数字对应记录的串长度,因此使用了一个Hash表来记录。

class Solution {
    public int longestConsecutive(int[] nums) {
        Map<Integer, Integer> map = new HashMap<Integer, Integer>();
        int ans = 0;
        for (int num : nums) {
            if (!map.containsKey(num)) {
                int leftLength = (map.containsKey(num - 1)) ? map.get(num - 1) : 0;
                int rightLength = (map.containsKey(num + 1)) ? map.get(num + 1) : 0;
                int curLength = leftLength + rightLength + 1;
                map.put(num - leftLength, curLength);
                map.put(num + rightLength, curLength);
                map.put(num, curLength);
                ans = Math.max(ans, curLength);
            }
        }
        return ans;
    }
}

3. Hash法3(双向查找动态删除)

由第一种Hash法,即官方给出的Hash解法我们可以想到,是否可以无需判断遍历到的当前数字是所在序列最小值这一步操作呢?其实是可以的,官方题解中进行这一步判断是因为如果这个数字不是所在序列串的最小值,那么它计算得到的所在序列长度一定不是最小的,也就是说,遍历到其他也在该序列上的数字计算得到的长度可能比它大。其关键在于,对在同一个序列上的数字,如果不加判断,可能会出现重复计算。

针对这个问题,可以在遍历每个数字时,分别向其变大变小方法都查找是否在集合中存在该数字,然后在查找计算长度的过程中动态删除掉该数字,使得一个序列串中的所有数字只会被计算一次。

class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> set = new HashSet<Integer>();
        int ans = 0;
        for (int num : nums) set.add(num);
        for (int num : nums) {
            int length = 1;
            int cur = num;
            while (set.contains(--cur)) {
                ++length;
                set.remove(cur);
            }
            cur = num;
            while (set.contains(++cur)) {
                ++length;
                set.remove(cur);
            }
            set.remove(num);
            ans = Math.max(ans, length);
        }
        return ans;
    }
}

4. 并查集法1(使用双Map模拟并查集)

因为本题中要统计数组中数字能组成连续序列串值的最大长度,而每一个这样的串值很明显就是一个集合,可以通过不断归并查找的方式将数字加入对应的集合中。很明显,这可以使用并查集来解决。

在并查集UnionFind中我定义了两个Map,分别表示该数字到所在集合树根的映射和该数字到所在集合存储元素数的映射,同时使用maxCount变量记录了在元素不断归并入对应集合过程中的最大长度。

在使用时,遍历输入数组中的每个数字,不断调用union方法对当前数字和比其值大1的数字进行归并操作即可。

注意:为了达到题目要求的O(n)时间复杂度,必须在查找元素所在集合的根元素的过程中进行路径压缩操作。

class Solution {
    public int longestConsecutive(int[] nums) {
        UnionFind uf = new UnionFind(nums);
        for (int num : nums) {
            uf.union(num, num + 1);
        }
        return uf.getMaxCount();
    }

    class UnionFind {
        private Map<Integer, Integer> unionSet = new HashMap<Integer, Integer>();
        private Map<Integer, Integer> countSet = new HashMap<Integer, Integer>();
        private int maxCount;
        public UnionFind(int[] nums) {
            for (int num : nums) {
                unionSet.put(num, num);
                countSet.put(num, 1);
            }
            maxCount = nums.length > 0 ? 1 : 0;
        }

        public int getRoot(int num) {
            if (num == unionSet.get(num))
                return num;
            else {
                unionSet.put(num, getRoot(unionSet.get(num)));
                return unionSet.get(num);
            }
        }

        public void union(int a, int b) {
            if (!unionSet.containsKey(a) || !unionSet.containsKey(b)) return;
            int rootA = getRoot(a);
            int rootB = getRoot(b);
            if (rootA == rootB) return;
            int newCount = countSet.get(rootA) + countSet.get(rootB);
            if (countSet.get(rootA) < countSet.get(rootB)) {
                unionSet.put(rootA, rootB);
                countSet.put(rootB, newCount);
            }
            else {
                unionSet.put(rootB, rootA);
                countSet.put(rootA, newCount);
            }
            maxCount = Math.max(maxCount, newCount);
        }

        public int getMaxCount() {
            return this.maxCount;
        }
    }
}

5. 并查集法2(双数组模拟并查集,Map记录数值到数组索引的映射)

与上一种并查集方法类似,其基本思路一致,不同之处在于该方法中的UnionFind并查集类使用了两个数组来实现,分别表示数字对应集合树根索引与数字对应集合存储元素数,而由数字值到对应索引位置的这一映射使用一个Map来记录。

注意:这种实现方式下,必须对每个遍历的数字从双向进行union尝试,即union比当前数字大1和小1的数;否则,如果只尝试union大1的数,当数组中的元素为[1, 2, 3, 4]这样某个串中的大数出现在小数前时,将会得到错误答案。

class Solution {
    public int longestConsecutive(int[] nums) {
        UnionFind uf = new UnionFind(nums.length);
        Map<Integer, Integer> map = new HashMap<Integer, Integer>();
        for (int i = 0; i < nums.length; ++i) {
            if (map.containsKey(nums[i])) continue;
            map.put(nums[i], i);
            if (map.containsKey(nums[i] + 1))
                uf.union(i, map.get(nums[i] + 1));
            if (map.containsKey(nums[i] - 1))
                uf.union(i, map.get(nums[i] - 1));
        }
        return uf.getMaxCount();
    }

    class UnionFind {
        private int[] unionSet;
        private int[] countSet;
        private int maxCount;
        public UnionFind(int length) {
            unionSet = new int[length];
            countSet = new int[length];
            for (int i = 0; i < length; ++i) {
                unionSet[i] = i;
                countSet[i] = 1;
            }
            maxCount = length > 0 ? 1 : 0;
        }

        public int getRoot(int i) {
            while (i != unionSet[i]) {
                unionSet[i] = unionSet[unionSet[i]];
                i = unionSet[i];
            }
            return i;
        }

        public void union(int a, int b) {
            int rootA = getRoot(a);
            int rootB = getRoot(b);
            if (rootA == rootB) return;
            if (countSet[rootA] < countSet[rootB]) {
                unionSet[rootA] = unionSet[rootB];
                countSet[rootB] += countSet[rootA];
                maxCount = Math.max(maxCount, countSet[rootB]);
            }
            else {
                unionSet[rootB] = unionSet[rootA];
                countSet[rootA] += countSet[rootB];
                maxCount = Math.max(maxCount, countSet[rootA]);
            }
        }

        public int getMaxCount() {
            return this.maxCount;
        }
    }
}

总结

上述所有方法的均摊时间复杂度均为O(n),空间复杂度为O(n)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值