主题
思路
想象有一间教室,座位从左到右编号为
有
学生们交换座位后,从左往右看,第一个学号与座位编号不匹配的学生,其座位编号就是答案。
特别地,如果所有学生都坐在正确的座位上,那么答案是
第一个例子
为方便描述思路,假设数组的下标是从
假设
- 从
开始。这个座位上的学生,学号是 ,他应当坐在 上,所以他和 交换。交换后 。 - 仍然看
,这个座位上的学生,学号是 ,他应当坐在 上,所以他和 交换。交换后 。 - 仍然看
,这个座位上的学生,学号是 ,他坐在正确的座位上。 - 向后遍历,
,他坐在正确的座位上。 - 向后遍历,
,他坐在正确的座位上。 - 换座位过程结束。
- 再次遍历
,发现 都满足,说明数组中 都有,所以缺失的第一个正数是 。
第二个例子
假设
- 从
开始。这个座位上的学生,学号是 ,他应当坐在 上,所以他和 交换。交换后 。 - 仍然看
,这个座位上的学生,学号是 ,忽略。 - 向后遍历,
,他应当坐在 上,所以他和 交换。交换后 。 - 仍然看
,他应当坐在 上,所以他和 交换。交换后 。 - 仍然看
,这个座位上的学生,学号是 ,忽略。 - 向后遍历,
,他坐在正确的座位上。 - 向后遍历,
,他坐在正确的座位上。 - 换座位过程结束。
- 再次遍历
,发现 ,说明教室中没有学号为 的学生(否则他会坐在 上),所以答案是 。
第三个例子
注意
假设
- 从
开始。这个座位上的学生坐在正确的座位上。 - 继续遍历,
,这是 号学生的影分身。由于 号学生的真身已经坐在正确的座位上,我们可以在第二次遍历中知道「数组中有 」这个信息,所以可以忽略 ,向后遍历。 ,他应当坐在 上,所以他和 交换。交换后 。 - 仍然看
,同样地,由于 号学生已经坐在正确的座位上,所以可以忽略 。 - 换座位过程结束。
- 再次遍历
,发现 ,说明教室中没有学号为 的学生,所以答案是 。
细节
判断「学生是否坐在正确的座位上」,能用
在第三个例子中,虽然
为避免死循环,可以改成判断
一般地,为了兼容「当前学生是真身,坐在正确的座位上」和「当前学生是影分身,且其真身坐在正确的座位上」两种情况,我们可以把
- 无论「当前学生是真身,坐在正确的座位上」还是「当前学生是影分身,且其真身坐在正确的座位上」,上式都是成立的。
- 如果「当前学生是真身,不坐在正确的座位上」,那么上式左边是当前学生的学号,右边是要交换的学生的学号。
- 如果「当前学生是影分身,且其真身不坐在正确的座位上」,那么上式左边是当前学生的学号,右边是要交换的学生的学号。虽然是用影分身交换的,但交换后,可以认为真身已经坐在了正确的座位上。
代码实现时,由于
注:用 Python 的同学请注意,下面代码中的
nums[i], nums[j] = nums[j], nums[i]不能写成nums[i], nums[nums[i] - 1] = nums[nums[i] - 1], nums[i]。这会先更新nums[i]为nums[nums[i] - 1],然后再更新nums[nums[i] - 1],但此时nums[i] - 1已经不是原来的值了。
python
class Solution:
def firstMissingPositive(self, nums: list[int]) -> int:
n = len(nums)
for i in range(n):
# 如果当前学生的学号在 [1,n] 中,但(真身)没有坐在正确的座位上
while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
# 那么就交换 nums[i] 和 nums[j],其中 j 是 i 的学号
j = nums[i] - 1 # 减一是因为数组下标从 0 开始
nums[i], nums[j] = nums[j], nums[i]
# 找第一个学号与座位编号不匹配的学生
for i in range(n):
if nums[i] != i + 1:
return i + 1
# 所有学生都坐在正确的座位上
return n + 1cpp
// C++ 版待补充cpp
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n; i++) {
// 如果当前学生的学号在 [1,n] 中,但(真身)没有坐在正确的座位上
while (1 <= nums[i] && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
// 那么就交换 nums[i] 和 nums[j],其中 j 是 i 的学号
int j = nums[i] - 1; // 减一是因为数组下标从 0 开始
swap(nums[i], nums[j]);
}
}
// 找第一个学号与座位编号不匹配的学生
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
// 所有学生都坐在正确的座位上
return n + 1;
}
};复杂度分析
- 时间复杂度:
,其中 是 的长度。虽然我们写了个二重循环,但每次交换都会把一个学生换到正确的座位上,所以总交换次数至多为 ,所以内层循环的总循环次数是 的,所以时间复杂度是 。 - 空间复杂度:
。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府