目录
- [1 问题描述](#1 问题描述)
- 2.问题分析
- 3.算法设计与实现
-
- [3.1 哈希集合法(最优解法)](#3.1 哈希集合法(最优解法))
- [4.2 哈希映射法](#4.2 哈希映射法)
- [4.3 排序法(对比参考)](#4.3 排序法(对比参考))
- [4.4 并查集法](#4.4 并查集法)
- [4.5 区间DP法](#4.5 区间DP法)
- [4.6 图+BFS法(图论思路)](#4.6 图+BFS法(图论思路))
- 4.性能分析对比
-
- [4.1 复杂度对比](#4.1 复杂度对比)
- [4.2 性能分析](#4.2 性能分析)
- [4.3 选择建议](#4.3 选择建议)
- 5.边界情况处理
-
- [5.1 常见边界情况](#5.1 常见边界情况)
- [5.2 边界测试用例](#5.2 边界测试用例)
- [5.3 边界处理技巧](#5.3 边界处理技巧)
- 6.扩展与变体
-
- [6.1 返回最长连续序列本身](#6.1 返回最长连续序列本身)
- [6.2 允许间隔k的"连续"序列](#6.2 允许间隔k的"连续"序列)
- [6.3 动态添加元素的最长连续序列](#6.3 动态添加元素的最长连续序列)
- [6.4 二维矩阵中的最长连续序列](#6.4 二维矩阵中的最长连续序列)
- 7.总结
-
- [7.1 核心知识点总结](#7.1 核心知识点总结)
- [7.2 算法思维提升](#7.2 算法思维提升)
- [7.3 实际应用场景](#7.3 实际应用场景)
- [7.4 面试建议](#7.4 面试建议)
1 问题描述
问题描述
给定一个未排序的整数数组 nums,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。要求设计并实现时间复杂度为 O(n) 的算法。
示例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
示例3
输入:nums = [1,0,1,2]
输出:3
提示:
- 0 <= nums.length <= 10^5
- -10^9 <= nums[i] <= 10^9
2.问题分析
理解连续序列
- 序列中的数字按值递增1排列
- 序列元素在原数组中可以不连续
- 序列必须是严格递增的,差值必须为1
- 序列长度至少为1(单个元素也是连续序列)
关键挑战
- 时间复杂度要求O(n):不能使用排序(O(n log n))
- 数组可能非常大:最多10^5个元素
- 数值范围大 :-109到109
- 可能包含重复元素:需要正确处理重复值
- 序列不要求在原数组中连续:需要全局视角
核心洞察
要找到最长连续序列,我们需要快速判断一个数字的前驱(num-1)和后继(num+1)是否存在。哈希表/集合提供了O(1)的查找能力。
破题关键
- 连续序列的起点特征:数字num是某个连续序列的起点,当且仅当num-1不在数组中
- 我们可以从每个起点开始,向后扩展,直到无法继续
- 每个数字只属于一个连续序列
3.算法设计与实现
3.1 哈希集合法(最优解法)
核心思想
利用HashSet的O(1)查找能力,识别连续序列的起点,只从起点开始向后扩展统计序列长度,避免重复计算。
算法思路
- 去重存储:将所有数字存入HashSet,自动去除重复元素
- 识别起点 :遍历HashSet中的每个数字,检查num-1是否在集合中
- 如果num-1不在集合中,说明num是某个连续序列的起点
- 如果num-1在集合中,说明num不是起点,跳过处理
- 扩展统计:对于每个起点,向后扩展(检查num+1、num+2...),统计连续序列长度
- 更新结果:记录并更新遇到的最大长度
时间复杂度分析
- 每个数字最多被访问两次:
- 一次在外层循环中检查是否为起点
- 一次在内层循环中作为连续序列的一部分被扩展
- 总时间复杂度:O(n),其中n为数组长度
空间复杂度分析
- 需要HashSet存储所有数字:O(n)
代码实现
java
import java.util.HashSet;
import java.util.Set;
public class LongestConsecutiveSequence {
public int longestConsecutive(int[] nums) {
// 边界条件处理
if (nums == null || nums.length == 0) {
return 0;
}
// 步骤1:将所有数字存入HashSet(自动去重)
Set<Integer> numSet = new HashSet<>();
for (int num : nums) {
numSet.add(num);
}
int longestStreak = 0;
// 步骤2:遍历集合中的每个数字
for (int num : numSet) {
// 步骤3:检查是否为序列起点
// 关键:只有当num-1不在集合中时,num才是序列起点
if (!numSet.contains(num - 1)) {
int currentNum = num;
int currentStreak = 1;
// 步骤4:从起点向后扩展
while (numSet.contains(currentNum + 1)) {
currentNum++;
currentStreak++;
}
// 步骤5:更新最长序列长度
longestStreak = Math.max(longestStreak, currentStreak);
}
}
return longestStreak;
}
}
4.2 哈希映射法
核心思想
使用HashMap记录每个数字所在的连续序列长度。当插入新数字时,检查其左右相邻数字,更新序列长度,并同步更新序列边界的长度信息。
算法思路
- 初始化映射:创建HashMap,键为数字,值为该数字所在连续序列的长度
- 遍历处理 :对于数组中的每个数字num:
- 如果num已存在Map中,跳过(避免重复处理)
- 获取左邻居长度left(num-1在Map中的值,默认为0)
- 获取右邻居长度right(num+1在Map中的值,默认为0)
- 计算当前序列长度:length = left + right + 1
- 更新当前数字的映射:map.put(num, length)
- 更新左边界:如果left>0,则map.put(num-left, length)
- 更新右边界:如果right>0,则map.put(num+right, length)
- 更新最大长度
- 返回结果:遍历完成后返回记录的最大长度
时间复杂度分析
- 每个数字只被处理一次:O(n)
- HashMap的插入和查询操作平均为O(1)
- 总时间复杂度:O(n)
空间复杂度分析
- 需要HashMap存储所有数字及其序列长度:O(n)
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class LongestConsecutiveSequenceMap {
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
// Map存储:数字 -> 该数字所在连续序列的长度
Map<Integer, Integer> map = new HashMap<>();
int maxLength = 0;
for (int num : nums) {
// 跳过已处理的数字(去重)
if (map.containsKey(num)) {
continue;
}
// 获取左右相邻数字的序列长度
int left = map.getOrDefault(num - 1, 0);
int right = map.getOrDefault(num + 1, 0);
// 计算当前序列的总长度
int currentLength = left + right + 1;
maxLength = Math.max(maxLength, currentLength);
// 更新当前数字(值可以任意,主要标记已处理)
map.put(num, currentLength);
// 更新左边界(如果存在左邻居)
if (left > 0) {
// num-left是左边界的起点
map.put(num - left, currentLength);
}
// 更新右边界(如果存在右邻居)
if (right > 0) {
// num+right是右边界的终点
map.put(num + right, currentLength);
}
}
return maxLength;
}
}
4.3 排序法(对比参考)
核心思想
对数组排序后,遍历统计连续序列的长度。这种方法虽然简单直观,但不满足O(n)时间复杂度要求。
算法思路
- 排序数组:使用Arrays.sort()对数组进行排序
- 遍历统计 :
- 初始化最长长度和当前长度都为1
- 遍历排序后的数组(从第二个元素开始)
- 如果当前元素等于前一个元素,跳过(处理重复)
- 如果当前元素等于前一个元素加1,当前长度加1
- 否则,更新最长长度,重置当前长度为1
- 返回结果:返回最长长度和当前长度的最大值
时间复杂度分析
- 排序操作:O(n log n)
- 遍历操作:O(n)
- 总时间复杂度:O(n log n)
空间复杂度分析
- Arrays.sort()在Java中对于基本类型使用双轴快速排序,空间复杂度为O(log n)
- 如果排序需要额外空间,则为O(n)
代码实现
java
import java.util.Arrays;
public class LongestConsecutiveSequenceSorting {
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
// 步骤1:排序
Arrays.sort(nums);
int longestStreak = 1;
int currentStreak = 1;
// 步骤2:遍历统计
for (int i = 1; i < nums.length; i++) {
// 处理重复元素:跳过但不重置序列
if (nums[i] == nums[i - 1]) {
continue;
}
// 如果连续,增加当前长度
if (nums[i] == nums[i - 1] + 1) {
currentStreak++;
} else {
// 序列中断,重置当前长度
longestStreak = Math.max(longestStreak, currentStreak);
currentStreak = 1;
}
}
// 步骤3:返回最大值(处理以最后一个元素结尾的序列)
return Math.max(longestStreak, currentStreak);
}
}
4.4 并查集法
核心思想
将每个数字看作一个节点,如果两个数字连续(差值为1),则将它们所在的集合合并。最后找到最大的连通分量大小即为最长连续序列长度。
算法思路
- 初始化并查集:为每个数字创建独立的集合
- 构建连接关系:对于每个数字num,检查num-1和num+1是否存在,如果存在则合并对应集合
- 查找最大集合:遍历所有集合,找到大小最大的集合
- 返回结果:最大集合的大小即为最长连续序列长度
时间复杂度分析
- 并查集操作(查找、合并)接近O(1)(使用路径压缩和按秩合并)
- 需要处理每个数字和它的邻居:O(n)
- 总时间复杂度:O(nα(n)),其中α(n)是反阿克曼函数,增长极慢
空间复杂度分析
- 需要存储每个数字的父节点和集合大小:O(n)
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class LongestConsecutiveSequenceUnionFind {
// 并查集类
class UnionFind {
private Map<Integer, Integer> parent;
private Map<Integer, Integer> size;
public UnionFind(int[] nums) {
parent = new HashMap<>();
size = new HashMap<>();
// 初始化,每个元素自成一组
for (int num : nums) {
parent.put(num, num);
size.put(num, 1);
}
}
// 查找根节点(带路径压缩)
public int find(int x) {
if (parent.get(x) != x) {
parent.put(x, find(parent.get(x)));
}
return parent.get(x);
}
// 合并两个集合
public void union(int x, int y) {
if (!parent.containsKey(x) || !parent.containsKey(y)) {
return;
}
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
// 按大小合并(小树接在大树下)
if (size.get(rootX) < size.get(rootY)) {
parent.put(rootX, rootY);
size.put(rootY, size.get(rootY) + size.get(rootX));
} else {
parent.put(rootY, rootX);
size.put(rootX, size.get(rootX) + size.get(rootY));
}
}
// 获取最大集合大小
public int getMaxSize() {
int max = 0;
for (int s : size.values()) {
max = Math.max(max, s);
}
return max;
}
}
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
UnionFind uf = new UnionFind(nums);
// 合并连续的数字
for (int num : nums) {
// 如果num-1存在,合并num和num-1
if (uf.parent.containsKey(num - 1)) {
uf.union(num, num - 1);
}
// 如果num+1存在,合并num和num+1
if (uf.parent.containsKey(num + 1)) {
uf.union(num, num + 1);
}
}
return uf.getMaxSize();
}
}
4.5 区间DP法
核心思想
将问题转化为区间动态规划问题。虽然这不是最优解,但它提供了不同的解题视角。我们使用动态规划记录每个数字所在的连续区间边界。
算法思路
- 数据预处理:使用HashSet去重并快速查找
- DP状态定义 :
- leftMap:记录以某个数字为左端点的区间长度
- rightMap:记录以某个数字为右端点的区间长度
- 状态转移 :遍历每个数字,尝试将其与左右邻居连接
- 如果num-1存在,则num可以接在num-1的右侧
- 如果num+1存在,则num可以接在num+1的左侧
- 更新合并后区间的左右边界
- 结果统计:记录过程中遇到的最大区间长度
时间复杂度分析
- 每个数字处理一次:O(n)
- 每次更新操作O(1)
- 总时间复杂度:O(n)
空间复杂度分析
- 需要两个HashMap存储区间边界信息:O(n)
代码实现
java
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class LongestConsecutiveSequenceDP {
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
// 去重
Set<Integer> numSet = new HashSet<>();
for (int num : nums) {
numSet.add(num);
}
// DP状态:记录区间边界
Map<Integer, Integer> leftMap = new HashMap<>(); // 右边界 -> 区间长度
Map<Integer, Integer> rightMap = new HashMap<>(); // 左边界 -> 区间长度
int maxLength = 0;
for (int num : numSet) {
// 尝试将num与左右区间合并
int leftLen = rightMap.getOrDefault(num - 1, 0);
int rightLen = leftMap.getOrDefault(num + 1, 0);
// 新区间长度
int newLen = leftLen + rightLen + 1;
maxLength = Math.max(maxLength, newLen);
// 更新区间边界
// 左边界
int leftBound = num - leftLen;
// 右边界
int rightBound = num + rightLen;
// 更新左右边界的映射
leftMap.put(leftBound, newLen);
rightMap.put(rightBound, newLen);
// 如果左右区间存在,还需要更新中间点(如果存在)
if (leftLen > 0) {
rightMap.put(num - 1, newLen); // 原来左区间的右边界
}
if (rightLen > 0) {
leftMap.put(num + 1, newLen); // 原来右区间的左边界
}
}
return maxLength;
}
}
优化版本
java
public int longestConsecutiveDPOptimized(int[] nums) {
if (nums == null || nums.length == 0) return 0;
Set<Integer> set = new HashSet<>();
for (int num : nums) set.add(num);
// 只需要一个map,记录每个数字所在的区间长度
Map<Integer, Integer> map = new HashMap<>();
int maxLen = 0;
for (int num : set) {
if (map.containsKey(num)) continue;
// 找到左边界的连续数字
int left = num;
while (set.contains(left - 1) && !map.containsKey(left - 1)) {
left--;
}
// 找到右边界的连续数字
int right = num;
while (set.contains(right + 1) && !map.containsKey(right + 1)) {
right++;
}
// 计算区间长度
int len = right - left + 1;
maxLen = Math.max(maxLen, len);
// 标记区间内的所有数字
for (int i = left; i <= right; i++) {
map.put(i, len);
}
}
return maxLen;
}
4.6 图+BFS法(图论思路)
核心思想
将问题建模为图论问题:每个数字是一个节点,如果两个数字相差1,则在它们之间添加一条边。问题转化为在无向图中寻找最大的连通分量。
算法思路
- 建图 :
- 使用HashSet存储所有数字(去重)
- 对于每个数字,它的邻居是num-1和num+1(如果存在)
- BFS遍历 :
- 使用visited集合记录已访问的节点
- 对于每个未访问的数字,执行BFS搜索其连通分量
- 在BFS过程中,统计连通分量的大小
- 结果更新:记录最大的连通分量大小
时间复杂度分析
- 每个节点被访问一次:O(n)
- 每个节点的邻居查找O(1)
- 总时间复杂度:O(n)
空间复杂度分析
- 需要HashSet存储数字:O(n)
- 需要队列和visited集合:O(n)
- 总空间复杂度:O(n)
代码实现
java
import java.util.*;
public class LongestConsecutiveSequenceGraph {
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
// 步骤1:构建数字集合(去重)
Set<Integer> numSet = new HashSet<>();
for (int num : nums) {
numSet.add(num);
}
// 步骤2:BFS遍历寻找最大连通分量
Set<Integer> visited = new HashSet<>();
int maxLength = 0;
for (int num : numSet) {
// 如果已经访问过,跳过
if (visited.contains(num)) {
continue;
}
// BFS搜索当前连通分量
int currentLength = 0;
Queue<Integer> queue = new LinkedList<>();
queue.offer(num);
visited.add(num);
while (!queue.isEmpty()) {
int current = queue.poll();
currentLength++;
// 检查左邻居
if (numSet.contains(current - 1) && !visited.contains(current - 1)) {
queue.offer(current - 1);
visited.add(current - 1);
}
// 检查右邻居
if (numSet.contains(current + 1) && !visited.contains(current + 1)) {
queue.offer(current + 1);
visited.add(current + 1);
}
}
// 更新最大长度
maxLength = Math.max(maxLength, currentLength);
}
return maxLength;
}
}
DFS版本
java
public class LongestConsecutiveSequenceDFS {
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) return 0;
Set<Integer> set = new HashSet<>();
for (int num : nums) set.add(num);
Set<Integer> visited = new HashSet<>();
int maxLen = 0;
for (int num : set) {
if (visited.contains(num)) continue;
int len = dfs(set, visited, num);
maxLen = Math.max(maxLen, len);
}
return maxLen;
}
private int dfs(Set<Integer> set, Set<Integer> visited, int num) {
if (!set.contains(num) || visited.contains(num)) {
return 0;
}
visited.add(num);
int len = 1;
// 向左右两个方向递归搜索
len += dfs(set, visited, num - 1);
len += dfs(set, visited, num + 1);
return len;
}
}
图邻接表版本
java
import java.util.*;
public class LongestConsecutiveSequenceAdjList {
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) return 0;
// 构建邻接表
Map<Integer, List<Integer>> graph = new HashMap<>();
Set<Integer> nodeSet = new HashSet<>();
// 添加所有节点
for (int num : nums) {
nodeSet.add(num);
graph.putIfAbsent(num, new ArrayList<>());
}
// 添加边(与相邻数字的连接)
for (int num : nodeSet) {
if (nodeSet.contains(num - 1)) {
graph.get(num).add(num - 1);
graph.get(num - 1).add(num);
}
if (nodeSet.contains(num + 1)) {
graph.get(num).add(num + 1);
graph.get(num + 1).add(num);
}
}
// BFS遍历所有连通分量
Set<Integer> visited = new HashSet<>();
int maxLen = 0;
for (int num : nodeSet) {
if (visited.contains(num)) continue;
// BFS搜索当前连通分量
Queue<Integer> queue = new LinkedList<>();
queue.offer(num);
visited.add(num);
int len = 0;
while (!queue.isEmpty()) {
int current = queue.poll();
len++;
for (int neighbor : graph.get(current)) {
if (!visited.contains(neighbor)) {
queue.offer(neighbor);
visited.add(neighbor);
}
}
}
maxLen = Math.max(maxLen, len);
}
return maxLen;
}
}
4.性能分析对比
4.1 复杂度对比
| 方法 | 时间复杂度 | 空间复杂度 | 实现难度 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 哈希集合法 | O(n) | O(n) | ★★☆☆☆ | 思路清晰,通用最优解 | 需要遍历集合多次 |
| 哈希映射法 | O(n) | O(n) | ★★★☆☆ | 只需一次遍历 | 边界更新逻辑复杂 |
| 排序法 | O(n log n) | O(log n)或O(n) | ★☆☆☆☆ | 实现简单直观 | 不满足O(n)要求 |
| 并查集法 | O(nα(n)) | O(n) | ★★★★☆ | 支持动态扩展 | 实现复杂,常数大 |
| 区间DP法 | O(n) | O(n) | ★★★☆☆ | DP思想,易于理解 | 边界更新易出错 |
| 图+BFS法 | O(n) | O(n) | ★★★☆☆ | 图论视角,通用性强 | 需要额外存储图结构 |
4.2 性能分析
- 哈希集合法:实际运行时间接近2n,每个元素最多被访问两次
- 哈希映射法:每个元素只被处理一次,但更新边界操作增加常数时间
- 排序法:对于大规模数据,排序开销显著
- 并查集法:虽然理论复杂度低,但常数因子较大
- 区间DP法:与哈希映射法类似,但更直观体现了区间合并思想
- 图+BFS法:需要构建图结构,内存开销略大
4.3 选择建议
- 面试场景:推荐哈希集合法,容易解释和实现
- 竞赛场景:哈希集合法或哈希映射法
- 学习场景 :
- 理解哈希思想:哈希集合法
- 学习动态规划:区间DP法
- 掌握图论算法:图+BFS法
- 理解并查集:并查集法
- 生产环境 :
- 数据规模大且内存充足:哈希集合法
- 需要一次遍历:哈希映射法
- 需要支持动态操作:并查集法
5.边界情况处理
5.1 常见边界情况
- 空数组:返回0
- 单个元素:返回1
- 所有元素相同:返回1(重复元素不增加序列长度)
- 包含负数:正常处理,连续序列可包含负数
- 大数值边界 :注意整数溢出,但题目范围-109到109在int范围内
- 无序但连续:算法不依赖原始顺序
- 最大值相邻:正常处理,但需注意Integer.MAX_VALUE+1会溢出
5.2 边界测试用例
java
// 测试各种边界情况
int[][] testCases = {
{}, // 空数组,期望0
{1}, // 单个元素,期望1
{1, 1, 1}, // 全部重复,期望1
{-2, -1, 0, 1, 2}, // 包含负数,期望5
{Integer.MAX_VALUE-1, Integer.MAX_VALUE}, // 大数边界,期望2
{5, 4, 3, 2, 1, 0}, // 逆序连续,期望6
{1, 3, 5, 2, 4}, // 无序连续,期望5
{1, 2, 0, 1}, // 包含重复,期望3
};
5.3 边界处理技巧
- 空数组和null检查:方法开头进行防御性检查
- 重复元素处理:使用Set自动去重,或在排序法中跳过重复元素
- 整数溢出:对于通用解法,考虑使用long类型,但本题范围在int内
- 连续判断:严格检查差值是否为1,不是小于等于1
6.扩展与变体
6.1 返回最长连续序列本身
java
public List<Integer> findLongestConsecutiveSequence(int[] nums) {
if (nums == null || nums.length == 0) {
return new ArrayList<>();
}
Set<Integer> set = new HashSet<>();
for (int num : nums) set.add(num);
List<Integer> longestSequence = new ArrayList<>();
for (int num : set) {
// 只从序列起点开始
if (!set.contains(num - 1)) {
List<Integer> currentSequence = new ArrayList<>();
int current = num;
while (set.contains(current)) {
currentSequence.add(current);
current++;
}
if (currentSequence.size() > longestSequence.size()) {
longestSequence = currentSequence;
}
}
}
return longestSequence;
}
6.2 允许间隔k的"连续"序列
java
public int longestConsecutiveWithGap(int[] nums, int k) {
if (nums == null || nums.length == 0) return 0;
// 先排序
Arrays.sort(nums);
int maxLen = 1;
int currLen = 1;
for (int i = 1; i < nums.length; i++) {
// 跳过重复元素
if (nums[i] == nums[i - 1]) continue;
// 允许最大间隔为k
if (nums[i] - nums[i - 1] <= k) {
currLen++;
} else {
maxLen = Math.max(maxLen, currLen);
currLen = 1;
}
}
return Math.max(maxLen, currLen);
}
6.3 动态添加元素的最长连续序列
java
class LongestConsecutiveDynamic {
private Set<Integer> set = new HashSet<>();
private Map<Integer, Integer> leftBound = new HashMap<>();
private Map<Integer, Integer> rightBound = new HashMap<>();
private int maxLen = 0;
// 添加元素并更新最长连续序列
public void add(int num) {
if (set.contains(num)) return;
set.add(num);
// 检查左右邻居
boolean hasLeft = set.contains(num - 1);
boolean hasRight = set.contains(num + 1);
if (hasLeft && hasRight) {
// 连接左右两个序列
int left = leftBound.get(num - 1);
int right = rightBound.get(num + 1);
int newLen = left + right + 1;
// 更新边界
leftBound.put(num - left, newLen);
rightBound.put(num + right, newLen);
maxLen = Math.max(maxLen, newLen);
} else if (hasLeft) {
// 只连接左边序列
int left = leftBound.get(num - 1);
int newLen = left + 1;
leftBound.put(num - left, newLen);
rightBound.put(num, newLen);
maxLen = Math.max(maxLen, newLen);
} else if (hasRight) {
// 只连接右边序列
int right = rightBound.get(num + 1);
int newLen = right + 1;
rightBound.put(num + right, newLen);
leftBound.put(num, newLen);
maxLen = Math.max(maxLen, newLen);
} else {
// 孤立元素
leftBound.put(num, 1);
rightBound.put(num, 1);
maxLen = Math.max(maxLen, 1);
}
}
public int getLongestConsecutive() {
return maxLen;
}
}
6.4 二维矩阵中的最长连续序列
java
public int longestConsecutiveInMatrix(int[][] matrix) {
if (matrix == null || matrix.length == 0) return 0;
int rows = matrix.length, cols = matrix[0].length;
int maxLen = 0;
// 记录每个位置是否访问过
boolean[][] visited = new boolean[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
// 从每个位置开始DFS搜索
int len = dfs(matrix, i, j, visited, matrix[i][j] - 1);
maxLen = Math.max(maxLen, len);
}
}
return maxLen;
}
private int dfs(int[][] matrix, int i, int j, boolean[][] visited, int prev) {
if (i < 0 || i >= matrix.length || j < 0 || j >= matrix[0].length
|| visited[i][j] || matrix[i][j] != prev + 1) {
return 0;
}
visited[i][j] = true;
int maxLen = 1;
// 四个方向
int[][] dirs = {{0,1}, {1,0}, {0,-1}, {-1,0}};
for (int[] dir : dirs) {
int len = 1 + dfs(matrix, i + dir[0], j + dir[1], visited, matrix[i][j]);
maxLen = Math.max(maxLen, len);
}
visited[i][j] = false;
return maxLen;
}
7.总结
7.1 核心知识点总结
- 哈希集合的应用:利用O(1)查找能力判断数字是否存在
- 序列起点识别:数字num是序列起点当且仅当num-1不存在
- 时间复杂度分析:每个数字最多被访问两次,总体O(n)
- 空间换时间:使用O(n)额外空间实现O(n)时间复杂度
- 多种解法对比:根据不同场景选择合适算法
- 算法思维拓展:从不同角度(哈希、DP、图论)理解同一问题
7.2 算法思维提升
- 问题转化能力:将"寻找最长连续序列"转化为"寻找序列起点并扩展"
- 避免重复计算:只从起点扩展,确保每个数字只被处理一次
- 边界处理思维:考虑各种极端情况,编写健壮代码
- 复杂度分析能力:准确分析时间、空间复杂度
- 多视角解题:从不同算法范式思考问题
7.3 实际应用场景
- 数据分析:识别连续事件或连续时间点
- 社交网络:寻找连续的用户ID序列或连续活动记录
- 游戏开发:连续登录奖励或连续成就系统
- 数据库优化:连续范围查询的预处理
- 系统监控:连续错误或异常检测
7.4 面试建议
- 首选解法:哈希集合法,容易解释和实现
- 解题步骤 :
- 理解问题,确认要求和约束
- 分析难点,强调O(n)挑战
- 提出思路,基于序列起点观察
- 详细实现,逐步解释算法
- 复杂度分析,证明O(n)时间复杂度
- 测试验证,举例说明正确性
- 扩展讨论,提及其他解法
- 代码质量:注重可读性、健壮性和边界处理
- 沟通能力:清晰表达思路,主动解释关键点
学习建议:
- 首先掌握哈希集合法,理解核心思想
- 尝试实现哈希映射法,理解边界更新机制
- 学习区间DP法,掌握动态规划思想
- 了解图+BFS法,拓展图论思维
- 研究并查集法,理解并查集数据结构