Leetcode 128. Longest Consecutive Sequence (Hash + Optimization)

  • Leetcode 128. Longest Consecutive Sequence (Hash + Optimization)
  • topic
    • Given an unsorted integer array nums, find the length of the longest sequence of consecutive numbers (it is not required that the elements of the sequence are consecutive in the original array).
    • Please design and implement an algorithm with O(n) time complexity to solve this problem.
    • 0 <= nums.length <= 10^5
    • -10^9 <= nums[i] <= 10^9
  • Solution one
    • First of all, you can think that if the array is sorted, you can use double pointers to count continuous values, but the time complexity is too high, so consider from the continuous direction: each element num uses hash to search num+1, num+2...
    • Hash: Each element is put into the HashMap in turn, the key in the HashMap stores (multiple) head and tail elements of a continuous segment (if there is only one element, only the current element and 0 are stored), and the value stores the other end of the segment minus The size of the key (num+value and num form a closed interval, the key is a positive number for the end value, and the key is a negative number for the end value),
    • Specific insertion method: regardless of repeated elements, before storing each element num, search whether it is the next element of the last node of a certain section, or the previous element of the first node of a certain section. There are four situations at this time, as follows:
      • Both num-1 and num+1 are stored in the key, num can combine two consecutive segments (num-1 is the suffix, num+1 is the prefix), delete the element whose key is num-1 and num+1 in the hash,
        • Change/add the value whose key is num-1+value(num-1) to num+1+value(num+1) - num-1+value(num-1),
        • Change/add the value whose key is num+1+value(num+1) to -(num+1+value(num+1) - num-1+value(num-1)),
      • Only num-1 exists in the key, merge the previous paragraph (num-1 is the suffix), delete the element whose key is num-1 in the hash,
        • Change/add the value whose key is num-1+value(num-1) to num - num-1+value(num-1),
        • Then add num as a suffix interval (num, -(num - num-1+value(num-1)))
      • Only num+1 exists in the key, after the merge (num+1 is the prefix), the element whose key is num+1 is deleted from the hash,
        • Change/add the value whose key is num+1+value(num+1) to -(num+1+value(num+1) - num),
        • Then add the interval prefixed with num (num, num+1+value(num+1) - num)
      • Neither num-1 nor num+1 exists in the key, and num does not intersect with any segment, then put (num,0) into
    • The result is to find the maximum value of value+1 after each insertion; during the addition process, the intervals that can be merged are merged as much as possible each time, and then the middle node of the key is deleted, and the key left is only the head and tail of each interval ( Single values ​​are the same head to tail, so only one value is left),
    • Special case: If there are repeated elements, interval intersection and collision will occur at this time. The easiest way is to create a set collection and judge that there are repeated elements and not insert them.
    • Space compression: If you do not add a set set to judge repeated elements, it will be troublesome to insert elements, and special judgment is required:
      • If num exists in the endpoint, then num exists in the key, then it will not be inserted
      • num exists around the endpoint, if value(num+1) is negative or value(num-1) is positive, it will not be inserted
      • num exists inside the endpoint, and the normal processing logic is enough. Due to the above two rules, there will only be problems with small intervals in the large interval (the endpoint value required by the small interval will be killed by the large interval), and will not affect the evaluation
    • Note: HashMap is initialized to nums.len*4/3 to avoid expansion, because the worst case is that all nodes are not continuous with each other
    • Time complexity: O(n), space complexity: O(n)
  • code one
    /**
     * 首先可以想到如果排序好数组、那么可以使用双指针的方式计数连续值,但时间复杂度超了,因此从连续方向考虑:每个元素 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;
    }

  • Solution 2 (optimization)
    • Hash optimization: The above HashMap only stores endpoints, so it is not convenient to deduplicate and intermediate nodes need to be deleted. If all elements are stored, then there is no need to delete nodes and the deduplication can be directly judged.
    • The key in the HashMap represents each element. If the element is the left/right endpoint, the value is the number of elements from left to right. If the element is an intermediate node, the value is the number of elements when it is the endpoint. At this time, the element It will not be used when adding (only for deduplication)
    • Specific insertion method:
      • Determine whether num has been added, if it has been added, it will not be added,
      • Otherwise, query the value of num-1 and num+1, and return 0 if it is empty. At this time, if num-1 is not empty, it must be the right endpoint of the interval, and if num+1 is not empty, it must be the left endpoint (if it is not an endpoint, it must contain num, which conflicts with the previous deduplication),
      • Then update the values ​​​​of the left and right endpoints,
      • Then add num to the hash, value is arbitrary (either num is not an endpoint, or has been updated),
      • Finally, update the result value and take the maximum value between it and the left/right endpoint value
    • Time complexity: O(n), space complexity: O(n)
  • Code 2 (optimization)
    /**
     * 哈希优化:上述 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;
    }
  • Solution three:
    • Hash+greedy: Change the way of thinking according to the continuous num, first we think about violence: put all the elements into the hash, then traverse each num, search num+1, num+2... for each num until the end, In this way, the time complexity is O(n^2), but after careful consideration, we can see that each continuous interval greater than 1 element has been traversed many times; therefore, considering how to search each continuous interval only once, the above method can be Think of it as using memoized search,
    • In addition, each continuous interval starts from the smallest num, so that there is no need to search for elements larger than num in the continuous interval. The problem is transformed into: how to confirm whether num is The minimum value of the continuous interval,
    • Solution: When traversing, judge whether num-1 exists in the hash. If it does not exist, it means that num is the minimum value of the continuous interval. At this time, use num to search for the number of the entire continuous interval. If it exists, there is no need to search
    • Time complexity: O(n), space complexity: O(n)
  • Code three:
    /**
     * 哈希 + 贪心:按照 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;
    }

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

Guess you like

Origin blog.csdn.net/qq_33530115/article/details/131213233
Recommended