想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:https://github.com/tingaicompass/AI-Compass
仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。
📖 第3课:最长连续序列
模块 :哈希表 | 难度 :Medium ⭐⭐
LeetCode 链接 :https://leetcode.cn/problems/longest-consecutive-sequence/
前置知识 :第1课(两数之和)、第2课(字母异位词分组)
预计学习时间:30分钟
🎯 题目描述
给你一个未排序的整数数组,请你找出其中最长连续序列 的长度。要求算法的时间复杂度为 O(n)。
什么是连续序列?比如 [1, 2, 3, 4] 就是连续的,[100, 4, 200, 1, 3, 2] 里最长的连续序列是 [1, 2, 3, 4],长度为 4。
示例:
输入:nums = [100, 4, 200, 1, 3, 2]
输出:4
解释:最长连续序列是 [1, 2, 3, 4],长度为 4
输入:nums = [0, 3, 7, 2, 5, 8, 4, 6, 0, 1]
输出:9
解释:最长连续序列是 [0, 1, 2, 3, 4, 5, 6, 7, 8],长度为 9
约束条件:
0 <= nums.length <= 10^5(数组可以为空)-10^9 <= nums[i] <= 10^9(元素可以为负数)
🧪 边界用例(面试必考)
| 用例类型 | 输入 | 期望输出 | 考察点 |
|---|---|---|---|
| 空数组 | nums=[] |
0 |
处理空输入 |
| 单元素 | nums=[1] |
1 |
最小有效输入 |
| 无连续 | nums=[1, 3, 5, 7] |
1 |
每个元素都是独立序列 |
| 全连续 | nums=[1, 2, 3, 4] |
4 |
整个数组就是答案 |
| 含重复 | nums=[1, 2, 0, 1] |
3 |
重复元素不影响连续性,序列是 [0,1,2] |
| 含负数 | nums=[-1, -2, 0, 1] |
4 |
负数序列 [-2,-1,0,1] |
| 大规模 | n=10^5 |
--- | 排序法 O(nlogn) 能过,但要求 O(n) |
💡 思路引导
生活化比喻
想象你在整理一副乱糟糟的扑克牌 ,要找出里面最长的顺子(连续的牌)。
🐌 笨办法 :先把牌排个序(洗牌),然后从头到尾扫一遍,数数哪段连续的最长。这个办法稳,但是排序太费时间------如果有 10 万张牌,光排序就得花不少功夫(O(nlogn))。
🚀 聪明办法 :你把所有牌摊在桌上 (扔进一个集合 set),然后用眼睛快速扫:"这张 5 是不是某个顺子的开头 ?怎么判断?看看桌上有没有 4------如果有 4,那 5 肯定不是开头,跳过!如果没有 4,那 5 就是开头,我就从 5 开始往后数:6、7、8...一直数到断为止。" 这样每张牌最多只看两次(一次判断是否开头,一次计入某个序列),总时间 O(n)------而且不用排序!
关键魔法:用 set 的 O(1) 查找能力,只对"序列起点"开始计数,避免重复劳动。
关键洞察
与其排序后扫描,不如用 set 存储所有数,对每个潜在的"序列起点"(即 num-1 不在 set 中的数)向后扩展,统计连续长度。这样每个数最多被访问常数次,总体 O(n)。
🧠 解题思维链
这一节模拟你在面试中"从零开始思考"的过程。
Step 1:理解题目 → 锁定输入输出
- 输入 :一个未排序的整数数组
nums(长度 0~10^5) - 输出:一个整数,表示最长连续序列的长度(不是序列本身!)
- 限制 :时间复杂度必须是 O(n),这意味着不能排序(排序至少 O(nlogn))
Step 2:先想笨办法(排序法)
最直接的想法------先排序,再线性扫描。排序后相同的数挨在一起,连续的数也挨在一起,扫一遍就能找到最长连续段。
- 时间复杂度:O(nlogn) ← 排序的代价
- 瓶颈在哪:排序这一步太慢了,题目要求 O(n)
Step 3:瓶颈分析 → 优化方向
排序法的问题是"为了知道哪些数是连续的,我必须先排序"。但排序本质上是在做"全局比较",我们其实不需要知道所有数的顺序------只需要快速判断"某个数的前一个/后一个存在吗?"
- 核心问题:"判断某个数是否在数组中"这个查找操作,如果用线性搜索是 O(n),太慢
- 优化思路:能不能把查找从 O(n) 降到 O(1)? → 能!用 set(集合)
Step 4:选择武器
- 选用:HashSet(Python 中的 set)
- 理由:set 支持 O(1) 的成员查找(
num in num_set),我们可以:- 先把所有数扔进 set(O(n))
- 对每个数,判断
num-1是否在 set 中:- 如果在,说明
num不是序列起点,跳过 - 如果不在,说明
num是起点,从它开始向后数num+1, num+2, ...直到断
- 如果在,说明
🔑 模式识别提示 :当题目要求 O(n) 时间且需要频繁查找元素是否存在 ,优先考虑"HashSet 去重 + O(1) 查找"模式
🔑 解法一:排序扫描法(直觉法)
思路
先对数组排序,然后一遍扫描:遇到连续的数就计数,遇到断点就重置计数器,记录过程中的最大值。注意要跳过重复的数(比如 [1, 1, 2] 中两个 1 不影响连续性)。
图解过程
示例:nums = [100, 4, 200, 1, 3, 2]
Step 1:排序
排序后:nums_sorted = [1, 2, 3, 4, 100, 200]
Step 2:线性扫描,统计连续段
i=0, num=1, 当前序列长度 current=1
[1] 2 3 4 100 200
^
i=1, num=2, 2=1+1,连续! current=2
[1, 2] 3 4 100 200
^
i=2, num=3, 3=2+1,连续! current=3
[1, 2, 3] 4 100 200
^
i=3, num=4, 4=3+1,连续! current=4
[1, 2, 3, 4] 100 200
^
i=4, num=100, 100≠4+1,断了! 记录 max_len=4, current=1
[1, 2, 3, 4] [100] 200
^
i=5, num=200, 200≠100+1,断了! max_len=4, current=1
[1, 2, 3, 4] [100] [200]
^
最终答案:max_len = 4 ✅
示例:nums = [1, 2, 0, 1] (含重复)
Step 1:排序
排序后:nums_sorted = [0, 1, 1, 2]
Step 2:扫描(跳过重复)
i=0, num=0, current=1
[0] 1 1 2
^
i=1, num=1, 1=0+1,连续! current=2
[0, 1] 1 2
^
i=2, num=1, 重复!跳过(不更新 prev)
[0, 1] [1] 2
^
i=3, num=2, 2=1+1,连续! current=3
[0, 1, 2]
^
最终答案:max_len = 3 ✅
Python代码
python
from typing import List
def longest_consecutive_sort(nums: List[int]) -> int:
"""
解法一:排序扫描法
思路:先排序,再线性扫描统计连续段长度
"""
if not nums: # 边界:空数组返回 0
return 0
nums.sort() # O(nlogn) 排序
max_len = 1 # 最长序列长度
current_len = 1 # 当前序列长度
for i in range(1, len(nums)):
if nums[i] == nums[i - 1]: # 跳过重复元素
continue
elif nums[i] == nums[i - 1] + 1: # 连续,计数+1
current_len += 1
max_len = max(max_len, current_len)
else: # 断了,重置计数
current_len = 1
return max_len
# ✅ 测试
print(longest_consecutive_sort([100, 4, 200, 1, 3, 2])) # 期望输出:4
print(longest_consecutive_sort([0, 3, 7, 2, 5, 8, 4, 6, 0, 1])) # 期望输出:9
print(longest_consecutive_sort([])) # 期望输出:0 (空数组)
print(longest_consecutive_sort([1])) # 期望输出:1 (单元素)
print(longest_consecutive_sort([1, 2, 0, 1])) # 期望输出:3 (含重复)
复杂度分析
- 时间复杂度 😮(nlogn) --- 排序的代价
- 具体地说:如果 n=10^5,需要约 10^5 × log(10^5) ≈ 1.7×10^6 次操作
- 空间复杂度😮(1) 或 O(n) --- 取决于排序算法(Timsort 最坏 O(n))
优缺点
- ✅ 思路清晰,代码简单,容易理解
- ✅ 处理重复元素很自然(排序后相同值挨在一起)
- ❌ 时间复杂度 O(nlogn),不满足题目要求的 O(n)------能不能不排序?
⚡ 解法二:HashSet 智能扫描(最优解 O(n))
优化思路
排序的代价是 O(nlogn),我们能不能不排序?关键洞察:用 set 存储所有数,对每个数判断它是否是"序列起点"(即 num-1 不在 set 中)------如果是,就从这个起点向后扩展 num+1, num+2, ...,统计长度。
这样做的妙处:每个数最多只会被作为"序列起点"访问一次,后续扩展中被访问一次,总共常数次,所以总体 O(n)。
💡 关键想法 :不要对每个数都尝试向后扩展,只对"起点"扩展。如何识别起点?看
num-1在不在 set 里------不在就是起点,在就跳过(因为会被更前面的起点覆盖)。
图解过程
示例:nums = [100, 4, 200, 1, 3, 2]
Step 1:构建 set
num_set = {100, 4, 200, 1, 3, 2} (O(n))
Step 2:遍历数组,只对"起点"扩展
╔════════════════════════════════════════════════════════════╗
║ num=100 ║
║ 100-1=99 在 set 里吗?set 中没有 99 → 99 不在 ║
║ → 100 是起点!开始扩展: ║
║ 100 在? ✅ length=1 ║
║ 101 在? ❌ 停止,当前序列长度 = 1 ║
║ max_len = 1 ║
╠════════════════════════════════════════════════════════════╣
║ num=4 ║
║ 4-1=3 在 set 里吗?set 中有 3 → ✅ 在 ║
║ → 4 不是起点,跳过!(会被 1 的扩展覆盖) ║
╠════════════════════════════════════════════════════════════╣
║ num=200 ║
║ 200-1=199 在 set 里吗?❌ 不在 ║
║ → 200 是起点!开始扩展: ║
║ 200 在? ✅ length=1 ║
║ 201 在? ❌ 停止,当前序列长度 = 1 ║
║ max_len = 1 ║
╠════════════════════════════════════════════════════════════╣
║ num=1 ║
║ 1-1=0 在 set 里吗?❌ 不在 ║
║ → 1 是起点!开始扩展: ║
║ 1 在? ✅ length=1 ║
║ 2 在? ✅ length=2 ║
║ 3 在? ✅ length=3 ║
║ 4 在? ✅ length=4 ║
║ 5 在? ❌ 停止,当前序列长度 = 4 ║
║ max_len = 4 ✅ ║
╠════════════════════════════════════════════════════════════╣
║ num=3 ║
║ 3-1=2 在 set 里吗?✅ 在 ║
║ → 3 不是起点,跳过! ║
╠════════════════════════════════════════════════════════════╣
║ num=2 ║
║ 2-1=1 在 set 里吗?✅ 在 ║
║ → 2 不是起点,跳过! ║
╚════════════════════════════════════════════════════════════╝
最终答案:max_len = 4 ✅
关键观察:虽然遍历了 6 个数,但只对 3 个"起点"(100, 200, 1)做了扩展,
而且 1 的扩展覆盖了 2, 3, 4,它们不会被重复计算!
示例:nums = [0, 3, 7, 2, 5, 8, 4, 6, 0, 1] (含重复)
Step 1:构建 set(自动去重)
num_set = {0, 1, 2, 3, 4, 5, 6, 7, 8} (长度 9)
Step 2:遍历(这里只展示关键的"起点")
num=0, 0-1=-1 不在? ✅ → 起点!
扩展:0→1→2→3→4→5→6→7→8→9(不在)
length = 9 ✅
num=3, 3-1=2 在? ✅ → 跳过
num=7, 7-1=6 在? ✅ → 跳过
... (其他数都不是起点,全部跳过)
最终答案:max_len = 9 ✅
Python代码
python
from typing import List
def longest_consecutive_hash(nums: List[int]) -> int:
"""
解法二:HashSet 智能扫描(最优解)
思路:用 set 存储所有数,只对"序列起点"向后扩展
"""
if not nums: # 边界:空数组返回 0
return 0
num_set = set(nums) # O(n) 构建 set,自动去重
max_len = 0
for num in num_set: # 遍历 set(已去重,比原数组可能更短)
# 关键判断:num 是序列起点吗?(即 num-1 不在 set 中)
if num - 1 not in num_set: # O(1) 查找
current_num = num # 从起点开始
current_len = 1 # 当前序列长度
# 向后扩展:num+1, num+2, ...
while current_num + 1 in num_set: # O(1) 查找
current_num += 1
current_len += 1
max_len = max(max_len, current_len) # 更新最大值
return max_len
# ✅ 测试
print(longest_consecutive_hash([100, 4, 200, 1, 3, 2])) # 期望输出:4
print(longest_consecutive_hash([0, 3, 7, 2, 5, 8, 4, 6, 0, 1])) # 期望输出:9
print(longest_consecutive_hash([])) # 期望输出:0 (空数组)
print(longest_consecutive_hash([1])) # 期望输出:1 (单元素)
print(longest_consecutive_hash([1, 2, 0, 1])) # 期望输出:3 (含重复)
print(longest_consecutive_hash([1, 3, 5, 7])) # 期望输出:1 (无连续)
复杂度分析
- 时间复杂度 😮(n) --- 构建 set O(n),遍历 O(n),每个数最多被扩展访问一次
- 具体地说:虽然有嵌套的
while,但每个数只会被while访问一次(作为某个序列的成员),所以总体仍是 O(n) - 如果 n=10^5,只需要约 2×10^5 次操作,比排序法快 8 倍!
- 具体地说:虽然有嵌套的
- 空间复杂度 😮(n) --- set 存储所有不重复的数
- 典型的"空间换时间"策略
🐍 Pythonic 写法
利用 Python 的 max 函数和生成器表达式:
python
def longest_consecutive_pythonic(nums: List[int]) -> int:
"""
Pythonic 简洁版:用生成器表达式一行求最大序列长度
"""
if not nums:
return 0
num_set = set(nums)
def get_length(num):
"""获取以 num 为起点的序列长度"""
length = 1
while num + length in num_set:
length += 1
return length
# 只对"起点"(num-1 不在 set 中)计算长度,取最大值
return max(get_length(num) for num in num_set if num - 1 not in num_set)
这个写法用到了 Python 的生成器表达式 和条件过滤,更简洁,但可读性稍差。
⚠️ 面试建议 :先写清晰版本(解法二)展示思路,如果面试官问"能不能更简洁",再提 Pythonic 写法。面试官更看重你的思考过程和边界处理,而非代码行数。
📊 解法对比
| 维度 | 解法一:排序扫描 | 解法二:HashSet 智能扫描 |
|---|---|---|
| 时间复杂度 | O(nlogn) | O(n) ⭐ |
| 空间复杂度 | O(1) ~ O(n) | O(n) |
| 代码难度 | 简单 | 中等(需要理解"起点"概念) |
| 面试推荐 | ⭐⭐ | ⭐⭐⭐ |
| 适用场景 | 题目没要求 O(n),或者内存极度紧张 | 题目明确要求 O(n)(首选!) |
面试建议 :先讲排序法展示基本思路,然后指出"题目要求 O(n),排序不满足",再引出 HashSet 优化。这样展示了从暴力到优化的思维过程,比直接说最优解更有说服力!
🎤 面试现场
模拟面试中的完整对话流程,帮你练习"边想边说"。
面试官:请你解决一下这道题,要求时间复杂度 O(n)。
你:(审题30秒)好的,这道题要求在未排序数组中找最长连续序列。我的第一个想法是先排序,然后线性扫描统计连续段,时间复杂度 O(nlogn)。但题目明确要求 O(n),所以排序行不通。
让我换个思路:我可以用 HashSet 存储所有数,这样判断某个数是否存在只需要 O(1)。关键优化在于------不对每个数都尝试扩展,只对"序列起点"扩展 。怎么判断起点?如果 num-1 不在 set 里,说明 num 是某个连续序列的开头。
面试官:很好,请写一下代码。
你 :(边写边说)首先构建一个 set 存储所有数...(写代码)...然后遍历 set,对每个数判断 num-1 是否在 set 中,如果不在,说明它是起点,就从这里开始向后扩展 num+1, num+2... 直到断。
面试官:为什么这样做是 O(n)?不是有嵌套的 while 循环吗?
你 :好问题!关键在于每个数最多只会被 while 访问一次 。比如序列 [1,2,3,4],只有 1 是起点,while 会扩展到 2、3、4,但当后面遍历到 2、3、4 时,它们的 num-1 都在 set 里,会被跳过,不会再进入 while。所以虽然看起来是嵌套,但总访问次数仍是 O(n)。
面试官:测试一下?
你 :用示例 [100, 4, 200, 1, 3, 2] 走一遍...(手动模拟):构建 set 后,100 是起点扩展到 1,200 是起点扩展到 1,1 是起点扩展到 4 (覆盖了 2、3、4),最终答案 4。再测一个边界 [1, 2, 0, 1] (含重复):set 去重后是 {0,1,2},0 是起点扩展到 3,结果正确。
高频追问
| 追问 | 应答策略 |
|---|---|
| "还有更优解吗?" | 时间已经是 O(n) 最优,空间也无法优化(必须存储所有数才能 O(1) 查找)。如果内存极度紧张,可以用排序法 O(nlogn) 时间换 O(1) 空间,但不满足题目要求 |
| "如果数组特别大放不进内存怎么办?" | 可以分块处理 :把数据分段读入,每段内部用 HashSet,然后合并结果。或者用外排序先排序,再流式扫描,时间 O(nlogn) 但空间可控 |
| "能不能原地修改数组,不用额外空间?" | 不行。哈希查找必须要 O(n) 额外空间存储 set,否则每次查找退化为 O(n),总体变成 O(n²)。除非允许排序(破坏原数组),但时间会变 O(nlogn) |
| "如果要返回最长序列本身,不只是长度?" | 在 while 扩展时用一个列表记录 [num, num+1, ...],更新 max_len 时同步更新 result 列表。时间空间复杂度不变 |
🎓 知识点总结
Python技巧卡片 🐍
python
# 技巧1:set 去重 + O(1) 成员查找
num_set = set([1, 2, 2, 3]) # → {1, 2, 3} 自动去重
print(2 in num_set) # → True, O(1) 时间
# 技巧2:用 set 而不是 list 做查找
# ❌ 错误:if num in nums_list # O(n) 查找,慢!
# ✅ 正确:if num in num_set # O(1) 查找,快!
# 技巧3:生成器表达式节省内存
max_len = max((f(x) for x in data), default=0) # 比列表推导式省内存
💡 底层原理(选读)
为什么 set 查找是 O(1)?
Python 的
set底层用哈希表 (Hash Table)实现。当你执行num in num_set时:
- 计算
num的哈希值(一个整数),比如hash(5) = 12345- 用哈希值模表大小,得到桶的位置:
12345 % table_size- 直接跳到这个桶,检查
num是否在里面平均情况下,每个桶只有常数个元素,所以是 O(1)。最坏情况(所有元素哈希冲突)是 O(n),但实际中几乎不会发生。
set vs list 查找对比:
num in list: 必须从头扫到尾,O(n)num in set: 直接跳到对应桶,O(1)所以当你需要频繁判断"某个元素是否存在",一定要先转成 set!
算法模式卡片 📐
- 模式名称:HashSet 去重 + 智能遍历(只访问关键点)
- 适用条件:需要 O(n) 时间查找连续性、重复性、存在性等问题
- 识别关键词:"连续序列"、"O(n) 时间"、"未排序数组"、"判断存在"
- 模板代码:
python
# 核心模式:先去重存 set,再只对"关键点"(起点/边界)处理
num_set = set(nums)
for num in num_set:
if is_critical_point(num): # 只对关键点处理,避免重复劳动
process(num)
易错点 ⚠️
- 忘记判断空数组 :
if not nums: return 0是必须的,否则max()空序列会报错 - 混淆"去重"和"统计重复" : 本题重复元素不影响连续性,所以直接用 set 去重。如果题目要求统计重复次数,就要用
Counter - 遍历原数组而不是 set :
for num in nums会遍历重复元素,浪费时间。应该for num in num_set只遍历不重复的值 - for 和 while 嵌套时误判复杂度: 虽然有嵌套,但每个元素只被 while 访问一次,总体仍是 O(n),不是 O(n²)
- 边界用例 : 记得测试空数组
[]、单元素[1]、无连续[1,3,5]、含重复[1,1,2]
🏗️ 工程实战(选读)
这个算法思想在真实项目中的应用,让你知道"学了有什么用"。
- 场景1:日志分析 - 在服务器日志中找"最长连续无错误时段"。把时间戳转成序列号,用本题算法找最长连续段,对应最长稳定运行时间。
- 场景2:游戏开发 - 检测玩家连续签到天数、连续胜场等。把日期转成天数序列,用 HashSet 快速判断某一天是否签到,找最长连续段。
- 场景3:基因序列分析 - 在 DNA 序列中找最长的连续重复片段,或者在蛋白质编码中找连续的氨基酸序列。
🏋️ 举一反三
完成本课后,试试这些同类题目来巩固知识:
| 题目 | 难度 | 相关知识点 | 提示 |
|---|---|---|---|
| LeetCode 448. 找到所有数组中消失的数字 | Easy | HashSet / 原地标记 | 用 set 存储出现过的数,然后 1~n 中不在 set 里的就是消失的 |
| LeetCode 41. 缺失的第一个正数 | Hard | HashSet 或原地哈希 | 本题进阶版:要求 O(n) 时间 + O(1) 空间,需要原地哈希技巧(把元素放到对应下标) |
| LeetCode 674. 最长连续递增序列 | Easy | 一次遍历 | 比本题简单,不需要 set,直接扫描即可(因为要求原数组中的连续,不是值连续) |
| LeetCode 298. 二叉树最长连续序列 | Medium | 树的 DFS | 把"数组连续序列"推广到"树路径连续序列",用 DFS 遍历 |
📝 课后小测
试试这道变体题,不要看答案,自己先想5分钟!
题目 :给定一个未排序数组,返回最长连续递增子序列 的长度。注意是子序列(元素必须在原数组中连续),不是本题的"值连续"。
例如:[1, 3, 5, 4, 7] 最长连续递增子序列是 [1, 3, 5] 或 [3, 5] 或 [4, 7],长度都是... 等等,题目问的是最长,所以是 3。
💡 提示(实在想不出来再点开)
这道题比"最长连续序列"简单,因为要求原数组中连续,所以不需要 set ,一次遍历即可。维护一个 current_len,如果 nums[i] > nums[i-1] 就 current_len++,否则重置为 1。
✅ 参考答案
python
def find_length_of_lcis(nums: List[int]) -> int:
"""最长连续递增子序列(数组中连续)"""
if not nums:
return 0
max_len = 1
current_len = 1
for i in range(1, len(nums)):
if nums[i] > nums[i - 1]: # 递增,计数+1
current_len += 1
max_len = max(max_len, current_len)
else: # 断了,重置
current_len = 1
return max_len
# 测试
print(find_length_of_lcis([1, 3, 5, 4, 7])) # → 3 ([1,3,5])
核心思路:因为要求原数组中连续,所以直接一次遍历,遇到递增就计数,遇到断点就重置。O(n) 时间,O(1) 空间,比本题的 HashSet 方法更简单!
如果这篇内容对你有帮助,推荐收藏 AI Compass:https://github.com/tingaicompass/AI-Compass
更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。