Leetcode 128. 가장 긴 연속 시퀀스(해시 + 최적화)

  • Leetcode 128. 가장 긴 연속 시퀀스(해시 + 최적화)
  • 주제
    • 정렬되지 않은 정수 배열 nums가 주어지면 가장 긴 연속 숫자 시퀀스의 길이를 찾습니다(시퀀스의 요소가 원래 배열에서 연속적일 필요는 없음).
    • 이 문제를 해결하기 위해 O(n) 시간 복잡도를 갖는 알고리즘을 설계하고 구현하십시오.
    • 0 <= 숫자.길이 <= 10^5
    • -10^9 <= 숫자[i] <= 10^9
  • 솔루션 1
    • 우선 배열이 정렬되어 있으면 이중 포인터를 사용하여 연속 값을 계산할 수 있다고 생각할 수 있지만 시간 복잡도가 너무 높으므로 연속 방향에서 고려하십시오. 각 요소 num은 해시를 사용하여 num+1을 검색합니다. 숫자+2...
    • Hash: 각 요소를 HashMap에 차례로 넣고, HashMap의 키는 연속 세그먼트의 (다중) 헤드 및 테일 요소를 저장하고(요소가 하나만 있는 경우 현재 요소와 0만 저장됨) 값 세그먼트의 다른 쪽 끝에서 키의 크기를 뺀 값을 저장합니다(num+value와 num은 닫힌 간격을 형성하고 키는 끝 값의 양수이고 키는 끝 값의 음수임).
    • 특정 삽입 방법: 반복되는 요소와 관계없이 각 요소 번호를 저장하기 전에 특정 구간의 마지막 노드의 다음 요소인지 또는 특정 구간의 첫 번째 노드의 이전 요소인지 검색합니다. 다음과 같이 시간:
      • num-1과 num+1은 모두 키에 저장되며 num은 두 개의 연속 세그먼트를 결합할 수 있습니다(num-1은 접미사, num+1은 접두사). 키가 num-1이고 num+1인 요소를 삭제할 수 있습니다. 해시,
        • 키가 num-1+value(num-1)인 값을 num+1+value(num+1) - num-1+value(num-1)로 변경/추가,
        • 키가 num+1+value(num+1)인 값을 -(num+1+value(num+1) - num-1+value(num-1))로 변경/추가하고,
      • 키에 num-1만 존재하고 이전 단락을 병합하고(num-1은 접미사) 해시에서 키가 num-1인 요소를 삭제하고,
        • 키가 num-1+value(num-1)인 값을 num - num-1+value(num-1)로 변경/추가,
        • 그런 다음 num을 접미사 간격으로 추가합니다(num, -(num - num-1+value(num-1))).
      • 키에는 num+1만 존재하며 병합(num+1은 접두사) 후 키가 num+1인 요소가 해시에서 삭제되고,
        • 키가 num+1+value(num+1)인 값을 -(num+1+value(num+1) - num)으로 변경/추가합니다.
        • 그런 다음 num(num, num+1+value(num+1) - num) 접두사가 붙은 간격을 추가합니다.
      • num-1도 num+1도 키에 존재하지 않고 num이 어떤 세그먼트와도 교차하지 않으면 (num,0)을
    • 결과는 매번 삽입 후 value+1의 최대값을 찾게 되는데, 덧셈 과정에서 매번 병합 가능한 구간을 최대한 병합한 후 키의 중간 노드를 삭제하고 키를 왼쪽은 각 구간의 헤드와 테일만(Single 값은 헤드에서 테일까지 동일하므로 하나의 값만 남음),
    • 특수한 경우: 이때 반복되는 요소가 있으면 구간 교차 및 충돌이 발생하는데, 가장 쉬운 방법은 세트 모음을 만들어서 반복되는 요소가 있는 것으로 판단하고 삽입하지 않는 것입니다.
    • 공간 압축: 반복되는 요소를 판단하기 위해 집합 집합을 추가하지 않으면 요소를 삽입하는 것이 번거롭고 특별한 판단이 필요합니다.
      • 끝점에 num이 있으면 키에 num이 있으면 삽입되지 않습니다.
      • num은 끝점 주변에 존재하며, value(num+1)이 음수이거나 value(num-1)이 양수이면 삽입되지 않습니다.
      • num은 엔드포인트 내부에 존재하며 정상적인 처리 로직은 충분합니다 위의 두 가지 규칙으로 인해 큰 간격에서 작은 간격에서만 문제가 발생합니다(작은 간격에 필요한 엔드포인트 값은 큰 간격에 의해 죽임을 당함). , 평가에 영향을 미치지 않습니다.
    • 참고: HashMap은 확장을 피하기 위해 nums.len*4/3으로 초기화됩니다. 최악의 경우 모든 노드가 서로 연속적이지 않기 때문입니다.
    • 시간 복잡도: O(n), 공간 복잡도: O(n)
  • 코드 1
    /**
     * 首先可以想到如果排序好数组、那么可以使用双指针的方式计数连续值,但时间复杂度超了,因此从连续方向考虑:每个元素 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;
    }

  • 솔루션 2(최적화)
    • 해시 최적화: 위의 HashMap은 끝점만 저장하므로 중복 제거가 편리하지 않으며 중간 노드를 삭제해야 합니다.모든 요소가 저장되면 노드를 삭제할 필요가 없으며 중복 제거를 직접 판단할 수 있습니다.
    • HashMap의 키는 각 요소를 나타내며, 요소가 좌/우 끝점이면 왼쪽에서 오른쪽으로 요소의 개수가 값이 되고, 중간 노드인 경우에는 요소의 개수가 값이 된다. 이때 요소 It은 추가 시 사용되지 않습니다(중복 제거에만 해당).
    • 특정 삽입 방법:
      • num이 추가되었는지 확인하고, 추가되었으면 추가하지 않고,
      • 그렇지 않으면 num-1과 num+1의 값을 질의하고 비어 있으면 0을 리턴한다. 비어 있지 않은 경우 왼쪽 엔드포인트여야 합니다(엔드포인트가 아닌 경우 이전 중복 제거와 충돌하는 num을 포함해야 함).
      • 그런 다음 왼쪽 및 오른쪽 끝점의 값을 업데이트하고,
      • 그런 다음 해시에 num을 추가합니다. 값은 임의적입니다(num은 엔드포인트가 아니거나 업데이트됨).
      • 마지막으로 결과 값을 업데이트하고 결과 값과 왼쪽/오른쪽 끝점 값 사이의 최대 값을 취합니다.
    • 시간 복잡도: O(n), 공간 복잡도: O(n)
  • 코드 2(최적화)
    /**
     * 哈希优化:上述 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;
    }
  • 해결 방법 3:
    • Hash+greedy: 연속된 숫자에 따라 사고 방식을 변경합니다. 먼저 폭력에 대해 생각합니다. 모든 요소를 ​​해시에 넣은 다음 각 숫자를 순회하고 각 숫자에 대해 num+1, num+2...를 검색합니다. 결국, 이와 같이 시간복잡도는 O(n^2)인데, 잘 생각해보면 1개 이상의 연속구간이 여러 번 통과되었음을 알 수 있으므로 각 연속구간을 한 번만 탐색하는 방법을 고려한다. , 위의 방법은 메모이제이션 검색을 이용한다고 생각하시면 됩니다,
    • 또한 각각의 연속구간은 가장 작은 num부터 시작하므로 연속구간에서 num보다 큰 요소를 찾을 필요가 없기 때문에 문제는 num이 연속구간의 최소값인지 확인하는 방법,
    • 해결방법 : 순회시 해시에 num-1이 존재하는지 판단하고 존재하지 않는다면 num이 연속구간의 최소값임을 의미하며 이때 num을 이용하여 전체 연속구간의 수를 찾는다. . 있으면 검색할 필요가 없습니다.
    • 시간 복잡도: O(n), 공간 복잡도: O(n)
  • 코드 3:
    /**
     * 哈希 + 贪心:按照 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/

추천

출처blog.csdn.net/qq_33530115/article/details/131213233