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

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

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


文章目录

  • LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到
    • @[TOC](文章目录)
  • 题目
  • 一、审题
  • 最巧妙的方法,就是用set去找那个最小的开头,然后一串撸下去,把长度收集起来,6得很
  • 还有一个比较中规中矩的办法,用俩哈希表记录每个数x所在的段首,段尾,这样把最长长度更新出来
  • 总结

题目

给定一个未排序的整数数组 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会那个最长的序列更长
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第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开始的序列,是最长的连续序列
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第2张图片

这么一搞的话,你也不会多遍历,因为下一次要再开头找得是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;
        }

绝对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();
    }

结果:
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第3张图片
LeetCode测试
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第4张图片
额,虽然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
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第5张图片

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
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第6张图片

再来一个4,会是啥情况呢?
查x+1=5,没有,不管
查x-1=3,有的,显然4不能做开头了,3也不可能做结尾了,更新长度
4做结尾的最长连续子序列长为4,同时1开头的长也为4
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第7张图片
咦?????

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

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

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

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

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

LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第8张图片
如果前一段目前的结尾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所在段的长度,拿过来往前倒腾,就是咱要的头呗!
你细品
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第9张图片

就像上面的x=4,preEnd=x-1=3
mapHead.get(preEnd)=3,因此,我们知道真的3所在的这段开头应该是preStart=3-3+1=1
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第10张图片

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

同样的道理!

如果preEnd是x【就是前一段尾部】,则curStart就是x+1【后一段开头】
这里能拼,能合并成一段最长连续序列
意味着,x是不能做尾部的,x+1不能做头的,
因此我需要更新x这一段头部preStart的长度,也要知道x+1那段长度所在尾部curEnd的长度,
这俩既然都合并在一段,那就是相同的长度
curEnd怎么求呢?
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第11张图片
curEnd=curStart+mapTail.get(curStart)-1;
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第12张图片
比如上面的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代表我们是同一段连续子序列,因此要同时更新
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第13张图片

长度2怎么来?
len=curEnd-preStart+1
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第14张图片

这个过程很复杂,但是事情就是,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;
    }

有了这个合并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;
    }

你看主函数的代码,和那个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();
    }

结果:

4
3
复习:
4

3是arr没有去重导致map记录混乱了
LeetCode测试:
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第15张图片
LeetCode高频题128. 最长连续序列,经常被互联网大厂面试考到_第16张图片
看来还有办法加速的,我这里啊方法,都是太骚,有点慢
但是做法是对的

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

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


总结

提示:重要经验:

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

你可能感兴趣的:(大厂面试高频题之数据结构与算法,leetcode,最长连续子序列,连续子序列,子序列长度,数组)