Leetcode 128. 最長連続シーケンス (ハッシュ + 最適化)

  • Leetcode 128. 最長連続シーケンス (ハッシュ + 最適化)
  • トピック
    • ソートされていない整数配列 nums を指定して、連続する数値の最長シーケンスの長さを見つけます (シーケンスの要素が元の配列で連続している必要はありません)。
    • この問題を解決するには、時間計算量 O(n) のアルゴリズムを設計して実装してください。
    • 0 <= nums.length <= 10^5
    • -10^9 <= nums[i] <= 10^9
  • 解決策 1
    • まず、配列がソートされている場合、ダブルポインタを使用して連続値をカウントできると考えられますが、時間計算量が高すぎるため、連続方向から検討します。各要素 num はハッシュを使用して num+1 を検索し、数値+2...
    • ハッシュ: 各要素が順番に HashMap に入れられます。HashMap のキーには、連続セグメントの (複数の) 先頭要素と末尾要素が格納されます (要素が 1 つだけの場合は、現在の要素と 0 のみが格納されます)。セグメントのもう一方の端からキーのサイズを引いた値を格納します (num+value と num は閉じた間隔を形成し、キーは終了値に対して正の数、キーは終了値に対して負の数です)。
    • 具体的な挿入方法:繰り返し要素に関係なく、各要素番号を格納する前に、それが、あるセクションの最後のノードの次の要素であるか、あるセクションの最初のノードの前の要素であるかを検索する。時間は次のようになります。
      • num-1 と num+1 の両方がキーに格納され、num は 2 つの連続するセグメント (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 の最大値が検索されます。追加プロセスでは、マージできる間隔が毎回可能な限りマージされ、キーの中間ノードが削除され、キーが左は各間隔の先頭と末尾のみです (単一の値は先頭から末尾まで同じなので、1 つの値だけが残ります)、
    • 特殊な場合: 繰り返し要素がある場合、この時点で区間交差や衝突が発生しますので、集合コレクションを作成し、繰り返し要素があると判断して挿入しないのが最も簡単です。
    • スペース圧縮:繰り返し要素を判定するセットを追加しないと要素の挿入が面倒になり、特別な判定が必要になります。
      • エンドポイントに num が存在し、キーにも num が存在する場合、挿入されません。
      • num がエンドポイントの周囲に存在します。value(num+1) が負の場合、または value(num-1) が正の場合、挿入されません。
      • num はエンドポイント内に存在しており、通常の処理ロジックで十分です。上記 2 つのルールにより、大きな間隔内の小さな間隔でのみ問題が発生します (小さな間隔で必要なエンドポイント値は、大きな間隔によって無効になります)。 、評価には影響しません
    • 注: 最悪の場合はすべてのノードが互いに連続していないため、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 のキーは各要素を表します。要素が左/右のエンドポイントの場合、値は左から右への要素の数です。要素が中間ノードの場合、値はその要素の数です。このとき、要素の追加時には使用されません(重複排除のみ)
    • 具体的な挿入方法:
      • numが追加されているかどうかを判断し、追加されている場合は追加されません。
      • それ以外の場合は、num-1 と num+1 の値を照会し、空の場合は 0 を返します。このとき、num-1 が空でない場合は、それが区間の右端である必要があり、num+1 が空の場合は、それが区間の右端である必要があります。空ではなく、左側のエンドポイントである必要があります (エンドポイントでない場合は、前の重複排除と競合する 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:
    • ハッシュ + 貪欲: 連続する num に従って考え方を変更します。まず暴力について考えます。すべての要素をハッシュに入れ、次に各 num を走査し、各 num について num+1、num+2... を検索します。このように、時間計算量は O(n^2) ですが、よく考えてみると、1 要素を超える各連続区間は何度も走査されていることがわかります。したがって、各連続区間を 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