• LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到


    LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到

    提示:本题是系列LeetCode的150道高频题,你未来遇到的互联网大厂的笔试和面试考题,基本都是从这上面改编而来的题目
    互联网大厂们在公司养了一大批ACM竞赛的大佬们,吃完饭就是设计考题,然后去考应聘人员,你要做的就是学基础树结构与算法,然后打通任督二脉,以应对波云诡谲的大厂笔试面试题!
    你要是不扎实学习数据结构与算法,好好动手手撕代码,锻炼解题能力,你可能会在笔试面试过程中,连题目都看不懂!比如华为,字节啥的,足够让你读不懂题
    在这里插入图片描述


    题目

    给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

    请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

    来源:力扣(LeetCode)
    链接:https://leetcode.cn/problems/longest-consecutive-sequence
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


    一、审题

    示例 1:

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

    输入:nums = [0,3,7,2,5,8,4,6,0,1]
    输出:9

    提示:

    0 <= nums.length <= 105
    -109 <= nums[i] <= 109


    基本上都是o(n)复杂度一遍搞定

    最巧妙的方法,就是用set去找那个最小的开头,然后一串撸下去,把长度收集起来,6得很

    这个方法啥意思呢?

    就是说,你把arr所有元素放入set中

    从arr的i=0–N-1遍历一遍
    核查每一个元素x
    (1)如果x-1在set中,显然x不能做一个最长连续序列的开头,你看看是不是,因为x开头不可能最长啊,x-1还在呢,x-1会那个最长的序列更长
    在这里插入图片描述
    你看看上面的x=2
    x-1=1,1在set中,显然2不能作为一个最长连续序列的开头,因为它开头的话,长度3
    咋着也没有1开头的长度4长

    (2)如果发你发现x-1竟然,没有在set中,x就可以做一个最小的开头
    比如
    x=1,x-1=0,0不在set中,显然,可能从1开始的序列,是最长的连续序列
    在这里插入图片描述

    这么一搞的话,你也不会多遍历,因为下一次要再开头找得是x=6了,这样x-5不在set中
    不过下次x=100,很短的,再撸一条新的序列也没用,干不过4这个长度

    okay,思想你应该OK了
    撸代码就是控制x能作为开头的情况下,去找,更新最大长度
    一遍o(n)遍历问完就搞定

    手撕代码:

    public int longestConsecutive(int[] nums) {
                if (nums == null || nums.length == 0) return 0;
                if (nums.length == 1) return 1;
    
                int N = nums.length;
                int max = 1;
                HashSet<Integer> set = new HashSet<>();
                for (int i = 0; i < N; i++) {
                    set.add(nums[i]);
                }
                //(1)如果x-1在set中,显然x不能做一个最长连续序列的开头,
                // 你看看是不是,因为x开头不可能最长啊,x-1还在呢,x-1会那个最长的序列更长
    
                //(2)如果发你发现x-1竟然,没有在set中,x就可以做一个最小的开头
                for(Integer x: nums){
                    int len = 1;//如果开头OK,就是x自己一个长度
                    if (!set.contains(x - 1)){
                        //x就是一个合适的开头
                        int num = x;//从x开头算
                        while (set.contains(num + 1)){
                            len++;//x+1也在,OK连续了
                            num++;
                        }
                        //啥时候断开,OK更新max
                        max = Math.max(max, len);
                    }
                    //统计过的一定不会来了,因此o(n)
                }
    
                return max;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    绝对OK
    测试一把:

        public static void test(){
            int[] arr = {100, 200, 3, 2, 1, 4};
            Solution solution = new Solution();
            System.out.println(solution.longestConsecutive(arr));
        }
    
        public static void main(String[] args) {
            test();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    结果:
    在这里插入图片描述
    LeetCode测试
    在这里插入图片描述
    额,虽然o(n),但是,应该说,set那个地方,加入,包括set的查询,实际上有点费时间了
    下面看看还能否提速


    还有一个比较中规中矩的办法,用俩哈希表记录每个数x所在的段首,段尾,这样把最长长度更新出来

    咋说呢?
    俩哈希表mapHead,mapTail
    一个记录arr中任意数字x,x做头的这段最长连续子序列的长度是?x做尾部时的这段最长连续子序列的长度是?
    比如100, 200, 3, 2, 1, 4,其实4这段做头的话,它最长长度是1,而4做尾的那段连续子序列长度是4

    懂?

    有了mapH,和mapT
    遍历arr中的每一个数x
    (0)最开始x来了,直接把x当做单独一段,则x做头长度或者做尾的这段长度自然是1,默认就自己单独做一段呗

    (1)然后,立马查询:一定要查一下x-1是不是在mapHead中了?
    如果x-1在mapHead中有了,则x的就不能做头,必须从mapHead中删除,同时你还要更新x-1的这段长度
    目的就是合并x和x-1所在的两段,构成一个更长的连续子序列

    (2)同时查一下x+1是不是在mapTail中了?
    如果x+1在mapTail中,则x就不能成尾部,需要删除,更新x+1这段的长度,
    目的就是合并x和x+1所在的两段,构成一个更长的连续子序列

    懂?这么做就是要保证我们一直更新合并那个最长连续序列,记录他们的首尾位置和长度

    比如
    100, 200, 3, 2, 1, 4
    x从左往右遍历,直到x=1时,咱们map都记录了这些:
    100,200,各自都是独立成段,长度都是1
    在这里插入图片描述

    3进来独立成首尾
    然后2来独立成首尾,查x-1=1没有,不管,但是x+1=3可是有了的,所以2不可能做尾,3也不可能做头,所以x=3做尾部,最长连续子序列长度暂时是2,2做开头,长度也是2

    然后1进来独立成首尾,查x-1没有,不管,但是x+1=2可是有了的,所以1不可能做尾,2也不可能做开头,删除,所以x=1做开头的最长连续子序列长度是3
    在这里插入图片描述

    再来一个4,会是啥情况呢?
    查x+1=5,没有,不管
    查x-1=3,有的,显然4不能做开头了,3也不可能做结尾了,更新长度
    4做结尾的最长连续子序列长为4,同时1开头的长也为4
    在这里插入图片描述
    咦?????

    你一定纳闷,我咋知道4更新,1也要更新呢?
    你咋知道1要更新呢?

    难不成是一个一个从4,3,2,1遍历来的?

    不,其实这里根据4所在区间的长度去算出来了的

    我们会单独整一个函数,去更新mapHead和mapTail
    下面定义一波merge函数,带着map,然后preEnd【这个就是前面一段的首位置】,和curStart【这个是后面一段的尾位置】

    咱们的目标就是每当遇到前后两段能连起来,构成一个更长的连续子序列时,
    就要合并他们俩,然后记录合并后的头位置,长度,尾位置,长度,俩长度一样

    在这里插入图片描述
    如果前一段目前的结尾preEnd是x-1,则后一段目前的开头curStart就是x
    如果前一段目前的结尾preEnd是x,则后一段目前的开头curStart就是x+1

    preEnd是x-1,则curStart就是x,一旦调用这个函数,意味着x-1在mapHead中,我curStart=x不能做头了,x-1=preEnd也不能做尾了
    所以map中最终要删除他们俩
    ——为嘛呢?就是因为我要将前面的尾部拉过来接后面的头部
    这样子的话,整体我们就需要整合并之后这段的新的头部preStart,新的尾部curEnd

    而新的一段起点preStart是啥呢?preStart实际上是preEnd-mapHead.get(preEnd)+1
    mapHead记录的就是x-1所在段的长度,拿过来往前倒腾,就是咱要的头呗!
    你细品
    在这里插入图片描述

    就像上面的x=4,preEnd=x-1=3
    mapHead.get(preEnd)=3,因此,我们知道真的3所在的这段开头应该是preStart=3-3+1=1
    在这里插入图片描述

    这就是为啥,我来到4这个地方,我知道1那所在段的长度是4,也要更新了,懂了吧???

    同样的道理!

    如果preEnd是x【就是前一段尾部】,则curStart就是x+1【后一段开头】
    这里能拼,能合并成一段最长连续序列
    意味着,x是不能做尾部的,x+1不能做头的,
    因此我需要更新x这一段头部preStart的长度,也要知道x+1那段长度所在尾部curEnd的长度,
    这俩既然都合并在一段,那就是相同的长度
    curEnd怎么求呢?
    在这里插入图片描述
    curEnd=curStart+mapTail.get(curStart)-1;
    在这里插入图片描述
    比如上面的preEnd=x=2时,curStart=x+1=3
    mapTail.get(curStart)=1,因此
    最后3作为尾时,除了更新2作为开头的长度为2之外
    还要更新curEnd=curStart+mapTail.get(curStart)-1;作为尾的长度也是2,他俩是同一段连续子序列
    curEnd=3+1-1=3
    因此绿色那俩2代表我们是同一段连续子序列,因此要同时更新
    在这里插入图片描述

    长度2怎么来?
    len=curEnd-preStart+1
    在这里插入图片描述

    这个过程很复杂,但是事情就是,mapH,mapT记录着x开头的那段最长子序列长度,x结尾的那段最长子序列长度
    随着更多的数来,
    咱们拿着preEnd,curStart这两段的长度,合并起来,
    找到合并之后这段连续子序列开头preStart和这段的尾部curEnd,
    也计算最新的长度,把preStart和curEnd记录到mapHead和mapTail中

    过程比较繁杂,但是思想就是这么个思想

    手撕代码主要是理解这个更新长度的过程:

        //复习这个合并过程,今天终于自己理清了,有了前一段preEnd,后一段curStart
        //咱们合并这俩,新段的开头preStart搞出来,新段的结尾curEnd搞出来
        //重新计算len,再记录,删除preEnd和curStart
        //返回合并后新段的长度,方便最后更新给max
        public static int mergeReview(HashMap<Integer, Integer> mapH,
                                       HashMap<Integer, Integer> mapT,
                                       int preEnd, int curStart){
            //先推合并之后这段的头尾
            int preStart = preEnd - mapT.get(preEnd) + 1;
            int curEnd = curStart + mapH.get(curStart) - 1;
            //推理新段长度
            int len = curEnd - preStart + 1;
    
            //然后删除preEnd和curStart,他们被合并了
            mapH.remove(curStart);
            mapT.remove(preEnd);
    
            //然后加最新这段的头尾和长度
            mapH.put(preStart, len);
            mapT.put(curEnd, len);//OK
    
            return len;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    有了这个合并mapH和mapT的函数

    其实主函数,就是最开始我们说的,一进来x独立成段,长度都是1,
    立马查x-1是否在mapHead中,
    立马查x=1是否在mapTail中

    查x-1时,preEnd=x-1,curStart=x,合并前面x-1这段,和我x这段——返回最新段长度,看看有没有更长,更新给max
    查x+1时,preEnd=x,curStart=x+1,合并前面我x这段和后面x+1这段——返回最新段长度,看看有没有更长,更新给max

    然后手撕代码:

        //其实主函数,就是最开始我们说的,一进来x独立成段,长度都是1,
        //立马查x-1是否在mapHead中,
        //立马查x=1是否在mapTail中
        //
        //查x-1时,preEnd=x-1,curStart=x,合并前面x-1这段,和我x这段——返回最新段长度,看看有没有更长,更新给max
        //查x+1时,preEnd=x,curStart=x+1,合并前面我x这段和后面x+1这段——返回最新段长度,看看有没有更长,更新给max
        public static int longestConsecutiveReview(int[] nums) {
            if (nums == null || nums.length == 0) return 0;
    
            HashMap<Integer, Integer> mapH = new HashMap<>();
            HashMap<Integer, Integer> mapT = new HashMap<>();
    
            //先把重复的去掉
            HashSet<Integer> set = new HashSet<>();
            for (int i = 0; i < nums.length; i++) {
                set.add(nums[i]);
            }
    
            int max = 1;//最次1
            //遍历,加map,然后查
            for(Integer x : set){
                mapH.put(x, 1);//一进来x独立成段,长度都是1,
                mapT.put(x, 1);
    
                //查x-1时,preEnd=x-1,curStart=x,合并前面x-1这段
                if(mapT.containsKey(x - 1)){
                    int len = mergeReview(mapH, mapT, x - 1, x);
                    max = Math.max(max, len);
                }
                //查x+1时,preEnd=x,curStart=x+1,合并前面我x这段和后面x+1这段
                if (mapH.containsKey(x + 1)){
                    int len = mergeReview(mapH, mapT, x, x + 1);
                    max = Math.max(max, len);
                }
            }
    
            return max;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    你看主函数的代码,和那个merge的代码,再返回去看我的图
    仔细理解这个合并过程

    确实复杂,但是速度应该快点

    今天我先不验证了,太晚,我回头重新自己手撕代码,然后再来验证

    今天我重新写了一下
    中间注意把arr重复的元素干掉【arr放入哈希集那】
    这里其实会耗时间
    测试:

        public static void test(){
    //        int[] arr = {100,4,200,1,3,2};//4
            int[] arr = {-7,-1,3,-9,-4,7,-3,2,4,9,4,-9,8,-7,5,-1,-7};//4
            System.out.println(longestConsecutiveLen(arr));
            System.out.println(longestConsecutiveLen1(arr));
            System.out.println("复习:");
            System.out.println(longestConsecutiveReview(arr));
        }
    
        public static void main(String[] args) {
            test();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    结果:

    4
    3
    复习:
    4
    
    • 1
    • 2
    • 3
    • 4

    3是arr没有去重导致map记录混乱了
    LeetCode测试:
    在这里插入图片描述
    在这里插入图片描述
    看来还有办法加速的,我这里啊方法,都是太骚,有点慢
    但是做法是对的

    等过几天我再想一个更好的招

    笔试你先写set那个方法
    面试的话,可以跟面试官聊一下这个方法


    总结

    提示:重要经验:

    1)set可以控制最小的开头
    2)哈希表俩记录最长连续子序列的长度,那个过程复杂,关键就是合并过程:咱们的目标就是每当遇到前后两段能连起来,构成一个更长的连续子序列时,就要合并他们俩,然后记录合并后的头位置,长度,尾位置,长度,俩长度一样
    3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

  • 相关阅读:
    电源差异。
    Spring原理-IOC和AOP
    vue环境中bpmn工具实现翻译汉化
    Windows部署Docker
    高通平台Android 蓝牙调试和配置手册-- SnS Issues
    初识C语言,新人介绍
    Dubbo3.0入门-Java版
    OpenMV与JSON编码
    【C语言 模拟实现strcpy函数】
    阿里云oss迁移到AWS S3
  • 原文地址:https://blog.csdn.net/weixin_46838716/article/details/126024129