Leetcode 128. 最长连续序列(哈希 + 优化)

  • Leetcode 128. 最长连续序列(哈希 + 优化)
  • 题目
    • 给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
    • 请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
    • 0 <= nums.length <= 10^5
    • -10^9 <= nums[i] <= 10^9
  • 解法一
    • 首先可以想到如果排序好数组、那么可以使用双指针的方式计数连续值,但时间复杂度超了,因此从连续方向考虑:每个元素 num 使用哈希搜索 num+1、num+2…
    • 哈希:每个元素放入依次 HashMap 中,HashMap 中 key 存放(多个)连续一段数的头与尾元素(如果仅一个元素就只存当前元素与 0)、value 存放该段另一端减去 key 的大小(num+value 与 num 形成闭区间,key 为端头 value 为正数、key 为端尾 value 为负数),
    • 具体插入方式:先不考虑重复元素,每个元素 num 存入前,搜其是否为某段最后一个节点的后一个元素、某段第一个节点的前一个元素,此时有四种情况,如下:
      • num-1 与 num+1 都存在 key 中,num 可将两连续段合并(num-1 为后缀、num+1 为前缀),哈希中删除 key 为 num-1 与 num+1 的元素,
        • 将 key 为 num-1+value(num-1) 的 value 改/添为 num+1+value(num+1) - num-1+value(num-1),
        • 将 key 为 num+1+value(num+1) 的 value 改/添为 -(num+1+value(num+1) - num-1+value(num-1)),
      • 仅 num-1 存在 key 中,合并前一段(num-1 为后缀),哈希中删除 key 为 num-1 的元素,
        • 将 key 为 num-1+value(num-1) 的 value 改/添为 num - num-1+value(num-1),
        • 再添加 num 为后缀的区间(num,-(num - num-1+value(num-1)))
      • 仅 num+1 存在 key 中,合并后一段(num+1 为前缀),哈希中删除 key 为 num+1 的元素,
        • 将 key 为 num+1+value(num+1) 的 value 改/添为 -(num+1+value(num+1) - num),
        • 再添加 num 为前缀的区间(num,num+1+value(num+1) - num)
      • num-1 与 num+1 都未存在 key 中,num 没与任何段有交集,则将 (num,0) 放入
    • 结果是每次插入后求 value+1 的最大值;添加过程中每次均将能合并的区间尽量合并,然后再删除 key 的中间节点,留下的 key 仅为每个区间的头与尾(单值头尾相同、因此仅留下一个值),
    • 特殊情况:如果有重复元素,此时会出现区间相交与碰撞,最简单的办法是在创建一个 set 集合,判断有重复元素就不插入,
    • 空间压缩:如果不添加 set 集合判断重复元素,那么插入元素时比较麻烦,就需要特殊判断:
      • num 存在端点中,则 num 存在 key 中,则不插入
      • num 存在端点周围,value(num+1) 为负数或 value(num-1) 为正数,则不插入
      • num 存在端点内部,正常处理逻辑即可,由于上两条规则,仅会出现大区间内存在小区间的问题(小区间需要的端点值会被大区间干掉),不影响求值
    • 注意:HashMap 初始化为 nums.len*4/3 避免扩容,因为最坏情况是所有节点互不连续
    • 时间复杂度:O(n),空间复杂度:O(n)
  • 代码一
    /**
     * 首先可以想到如果排序好数组、那么可以使用双指针的方式计数连续值,但时间复杂度超了,因此从连续方向考虑:每个元素 num 使用哈希搜索 num+1、num+2...
     * 哈希:每个元素放入依次 HashMap 中,HashMap 中 key 存放(多个)连续一段数的头与尾元素(如果仅一个元素就只存当前元素与 0)、
     * value 存放该段另一端减去 key 的大小(num+value 与 num 形成闭区间,key 为端头 value 为正数、key 为端尾 value 为负数),
     * 具体插入方式:先不考虑重复元素,每个元素 num 存入前,搜其是否为某段最后一个节点的后一个元素、某段第一个节点的前一个元素,此时有四种情况,如下:
     *     num-1 与 num+1 都存在 key 中,num 可将两连续段合并(num-1 为后缀、num+1 为前缀),哈希中删除 key 为 num-1 与 num+1 的元素,
     *         将 key 为 num-1+value(num-1) 的 value 改/添为 num+1+value(num+1) - num-1+value(num-1),
     *         将 key 为 num+1+value(num+1) 的 value 改/添为 -(num+1+value(num+1) - num-1+value(num-1)),
     *     仅 num-1 存在 key 中,合并前一段(num-1 为后缀),哈希中删除 key 为 num-1 的元素,
     *         将 key 为 num-1+value(num-1) 的 value 改/添为 num - num-1+value(num-1),
     *         再添加 num 为后缀的区间(num,-(num - num-1+value(num-1)))
     *     仅 num+1 存在 key 中,合并后一段(num+1 为前缀),哈希中删除 key 为 num+1 的元素,
     *         将 key 为 num+1+value(num+1) 的 value 改/添为 -(num+1+value(num+1) - num),
     *         再添加 num 为前缀的区间(num,num+1+value(num+1) - num)
     *     num-1 与 num+1 都未存在 key 中,num 没与任何段有交集,则将 (num,0) 放入
     * 结果是每次插入后求 value+1 的最大值;添加过程中每次均将能合并的区间尽量合并,然后再删除 key 的中间节点,留下的 key 仅为每个区间的头与尾(单值头尾相同、因此仅留下一个值),
     * 特殊情况:如果有重复元素,此时会出现区间相交与碰撞,最简单的办法是在创建一个 set 集合,判断有重复元素就不插入,
     * 空间压缩:如果不添加 set 集合判断重复元素,那么插入元素时比较麻烦,就需要特殊判断:
     *     num 存在端点中,则 num 存在 key 中,则不插入
     *     num 存在端点周围,value(num+1) 为负数或 value(num-1) 为正数,则不插入
     *     num 存在端点内部,正常处理逻辑即可,由于上两条规则,仅会出现大区间内存在小区间的问题(小区间需要的端点值会被大区间干掉),不影响求值
     * 注意:HashMap 初始化为 nums.len*4/3 避免扩容,因为最坏情况是所有节点互不连续
     * 时间复杂度:O(n),空间复杂度:O(n)
     */
    public int solution(int[] nums) {
        // 判空
        if (nums == null || nums.length <= 0) {
            return 0;
        }

        // 初始化 HashMap
        int len = nums.length;
        Map<Integer, Integer> consecutiveMap = new HashMap<>(((len / 3) << 2) + 1);

        // 依次插入每个元素 num 到 HashMap,返回计算结果
        int res = doLongestConsecutive(nums, len, consecutiveMap);
        // System.out.println(res + "\r\n");

        return res;
    }

    /**
     * 依次插入每个元素 num 到 HashMap,HashMap 中 key 存放(多个)连续一段数的头与尾元素(如果仅一个元素就只存当前元素与 0)、value 存放该段尾减去头的大小(元素个数 - 1)
     * 返回计算结果
     */
    private int doLongestConsecutive(int[] nums, int len, Map<Integer,Integer> consecutiveMap) {
        // 最少一个元素
        int res = 1;
        for (int num : nums) {
//            System.out.print(num + " : ");
            // 则 num 存在 key 中或 value(num+1) 为负数或 value(num-1) 为正数,不插入
            if (consecutiveMap.containsKey(num)) {
                continue;
            }
            Integer valNext = consecutiveMap.get(num + 1);
            if (valNext != null && valNext < 0) {
                continue;
            }
            Integer valPrev = consecutiveMap.get(num - 1);
            if (valPrev != null && valPrev > 0) {
                continue;
            }

            // num-1 与 num+1 都存在 key 中,num 可将两连续段合并
            if (valNext != null && valPrev != null) {
                consecutiveMap.remove(num - 1);
                consecutiveMap.remove(num + 1);

                int valPositive = num + 1 + valNext - (num - 1 + valPrev);
                consecutiveMap.put(num - 1 + valPrev, valPositive);
                consecutiveMap.put(num + 1 + valNext, -valPositive);

                res = Math.max(res, valPositive + 1);

            // 仅 num-1 存在 key 中,合并前一段
            } else if (valPrev != null) {
                consecutiveMap.remove(num - 1);

                int valPositive = num - (num - 1 + valPrev);
                consecutiveMap.put(num - 1 + valPrev, valPositive);
                consecutiveMap.put(num, -valPositive);

                res = Math.max(res, valPositive + 1);

            // 仅 num+1 存在 key 中,合并后一段
            } else if (valNext != null) {
                consecutiveMap.remove(num + 1);

                int valPositive = num + 1 + valNext - num;
                consecutiveMap.put(num + 1 + valNext, -valPositive);
                consecutiveMap.put(num, valPositive);

                res = Math.max(res, valPositive + 1);

            // num-1 与 num+1 都未存在 key 中,num 没与任何段有交集,则将 (num,0) 放入
            } else {
                consecutiveMap.put(num, 0);
            }
//            System.out.println(consecutiveMap);
        }

        return res;
    }

  • 解法二(优化)
    • 哈希优化:上述 HashMap 仅存储端点、因此不方便去重还需要删除中间节点,如果存储所有元素、那么就不需要删除节点同时直接可以判断去重,
    • HashMap 中 key 代表每个元素,如果该元素为左/右端点、value 为从左到右的元素个数,如果该元素为中间节点、value 为它作为端点时的元素个数、此时该元素在新增时并不会被用到(仅用于去重)
    • 具体插入方式:
      • 判断 num 是否添加过,添加过则不再添加,
      • 否则查询 num-1 与 num+1 的 value 值、空则返回 0,此时 num-1 如果非空则一定是区间右端点、num+1 非空则一定是左端点(不是端点则代表一定包含 num,这与前面的去重冲突),
      • 接着更新左右端点的值,
      • 然后将 num 加入哈希、value 任意(要么 num 不是端点、要么已更新了),
      • 最后更新结果值、在其与左/右端点 value 取最大值
    • 时间复杂度:O(n),空间复杂度:O(n)
  • 代码二(优化)
    /**
     * 哈希优化:上述 HashMap 仅存储端点、因此不方便去重还需要删除中间节点,如果存储所有元素、那么就不需要删除节点同时直接可以判断去重,
     * HashMap 中 key 代表每个元素,如果该元素为左/右端点、value 为从左到右的元素个数,如果该元素为中间节点、value 为它作为端点时的元素个数、此时该元素在新增时并不会被用到(仅用于去重)
     * 具体插入方式:
     *     判断 num 是否添加过,添加过则不再添加,
     *     否则查询 num-1 与 num+1 的 value 值、空则返回 0,此时 num-1 如果非空则一定是区间右端点、num+1 非空则一定是左端点(不是端点则代表一定包含 num,这与前面的去重冲突),
     *     接着更新左右端点的值,
     *     然后将 num 加入哈希、value 任意(要么 num 不是端点、要么已更新了),
     *     最后更新结果值、在其与左/右端点 value 取最大值
     * 时间复杂度:O(n),空间复杂度:O(n)
     * @param nums
     * @return
     */
    public int solution2(int[] nums) {
        // 判空
        if (nums == null || nums.length <= 0) {
            return 0;
        }

        // 初始化 HashMap,所有元素最多添加一次、避免扩容
        int len = nums.length;
        Map<Integer, Integer> consecutiveMap = new HashMap<>((len / 3) << 2);

        // 依次加入元素
        int res = doLongestConsecutive2(nums, len, consecutiveMap);

        return res;
    }

    /**
     * 具体插入方式:
     *     判断 num 是否添加过,添加过则不再添加,
     *     否则查询 num-1 与 num+1 的 value 值、空则返回 0,此时 num-1 如果非空则一定是区间右端点、num+1 非空则一定是左端点(不是端点则代表一定包含 num,这与前面的去重冲突),
     *     接着将 num 加入哈希、value 任意(要么 num 不是端点、要么是端点但后面会更新)
     *     然后更新左右端点的值,
     *     最后更新结果值、在其与左/右端点 value 取最大值
     */
    private int doLongestConsecutive2(int[] nums, int len, Map<Integer,Integer> consecutiveMap) {
        int res = 0;
        for (int num : nums) {
            // 判断 num 是否添加过,添加过则不再添加
            if (consecutiveMap.containsKey(num)) {
                continue;
            }

            // 查询 num-1 与 num+1 的 value 值、空则返回 0
            int previous = consecutiveMap.getOrDefault(num - 1, 0);
            int next = consecutiveMap.getOrDefault(num + 1, 0);

            // 将 num 加入哈希、value 任意(要么 num 不是端点、要么是端点但后面会更新)
            consecutiveMap.put(num, -1);

            // 更新左右端点的值
            int current = previous + next + 1;
            consecutiveMap.put(num - previous, current);
            consecutiveMap.put(num + next, current);

            // 更新结果值、在其与左/右端点 value 取最大值
            res = Math.max(res, current);
        }

        return res;
    }
  • 解法三:
    • 哈希 + 贪心:按照 num 连续的方式换一种思路,首先我们思考暴力:将元素全部放入哈希中,接着遍历每一个 num,每个 num 搜索 num+1、num+2… 直到结束,这样时间复杂度为O(n^2),但是仔细思考可知:每个大于 1 个元素的连续区间,我们重复遍历了多次;因此考虑如何让每个连续区间只搜索一次,上面的方式可看做使用了记忆化搜索,
    • 除此之外每个连续区间均从最小的 num 开始,这样就不需要让该连续区间大于 num 的元素搜索一遍了,问题转化为:如何在遍历时、O(1)复杂度确认 num 是否为该连续区间最小值,
    • 解法:遍历时判断 num-1 是否存在哈希中,如果不存在则代表 num 是该连续区间的最小值、此时使用 num 搜索整个连续区间的个数,如果存在则不需要搜索
    • 时间复杂度:O(n),空间复杂度:O(n)
  • 代码三:
    /**
     * 哈希 + 贪心:按照 num 连续的方式换一种思路,首先我们思考暴力:将元素全部放入哈希中,接着遍历每一个 num,每个 num 搜索 num+1、num+2... 直到结束,
     * 这样时间复杂度为O(n^2),但是仔细思考可知:每个大于 1 个元素的连续区间,我们重复遍历了多次;因此考虑如何让每个连续区间只搜索一次,上面的方式可看做使用了记忆化搜索,
     * 除此之外每个连续区间均从最小的 num 开始,这样就不需要让该连续区间大于 num 的元素搜索一遍了,问题转化为:如何在遍历时、O(1)复杂度确认 num 是否为该连续区间最小值,
     * 解法:遍历时判断 num-1 是否存在哈希中,如果不存在则代表 num 是该连续区间的最小值、此时使用 num 搜索整个连续区间的个数,如果存在则不需要搜索
     * 时间复杂度:O(n),空间复杂度:O(n)
     * @param nums
     * @return
     */
    public int solution3(int[] nums) {
        // 判空
        if (nums == null || nums.length <= 0) {
            return 0;
        }

        // 元素存入哈希,哈希仅用于校验是否存在
        Set<Integer> numsSet = putNumsIntoSet(nums);
//        System.out.println(numsSet);

        // 遍历元素,并保证每个连续区间均从最小的 num 开始搜索
        int res = doLongestConsecutive3(nums, numsSet);

        return res;
    }

    /**
     * 元素存入哈希
     */
    private Set<Integer> putNumsIntoSet(int[] nums) {
        // 初始化 HashSet,所有元素最多添加一次、避免扩容
        int len = nums.length;
        Set<Integer> consecutiveSet = new HashSet<>((len / 3) << 2);

        for (int num : nums) {
            consecutiveSet.add(num);
        }

        return consecutiveSet;
    }

    /**
     * 遍历元素,并保证每个连续区间均从最小的 num 开始搜索
     */
    private int doLongestConsecutive3(int[] nums, Set<Integer> numsSet) {
        // 最少一个元素
        int res = 1;

        // 遍历去重后的集合
        for (int num : numsSet) {
            // 判断 num-1 存在哈希,直接往后遍历
            if (numsSet.contains(num - 1)) {
                continue;
            }

//            System.out.println(num);

            // 判断 num-1 不存在哈希,代表连续区间头元素,搜索整个区间
            int next = num + 1;
            while (numsSet.contains(next)) {
                next++;
            }

            // 校验结果
            res = Math.max(res, next - num);
        }

        return res;
    }

参考:https://leetcode.cn/problems/longest-consecutive-sequence/solution/xiao-bai-lang-ha-xi-ji-he-ha-xi-biao-don-j5a2/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值