一、题目描述------缺失的第一个正数
给你一个未排序的整数数组 nums
,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n)
并且只使用常数级别额外空间的解决方案。
示例 1:
ini
输入: nums = [1,2,0]
输出: 3
解释: 范围 [1,2] 中的数字都在数组中。
示例 2:
ini
输入: nums = [3,4,-1,1]
输出: 2
解释: 1 在数组中,但 2 没有。
示例 3:
ini
输入: nums = [7,8,9,11,12]
输出: 1
解释: 最小的正数 1 没有出现。
提示:
1 <= nums.length <= 105
-231 <= nums[i] <= 231 - 1
二、思考历程
1. 初见题目:排序法(天真版)
拿到题目,一看是无序整数数组 ,要求第一个缺失的正整数 。
第一反应 :排序!排完序后,第一个缺失的正整数不就是 nums[0] - 1
吗?
内心OS:这也太简单了吧,无趣无趣......
然而 :题目要求 O(n) 时间复杂度 ,而排序至少 O(n log n) ,直接GG。
结论:排序法不可行,得另寻他法。
2. 优化思路:哈希表(Set)
既然不让排序,那就用 Set 存储所有数字,然后从 1 开始查找第一个不在 Set 里的数。
代码
ini
var firstMissingPositive = function(nums) {
const set=new Set(nums)
for(let i=1;i<=100000000000;i++){
if(set.has(i)) continue;
return i;
}
};
优点 :时间复杂度 O(n) (Set 查找是 O(1))。
缺点 :额外 O(n) 空间,总觉得i<=100000000000怪怪的...
3. 关键突破:利用数组长度信息
突然想到:缺失的正整数一定在 [1, n+1]
范围内(n = 数组长度)。
- 如果数组包含
1
到n
,那缺失的是n+1
。 - 否则,缺失的一定在
[1, n]
之间。
优化点:
- 可以减少循环次数
- 只需检查
[1, n]
范围内的数字。
代码优化(伪代码):
ini
var firstMissingPositive = function(nums) {
let len=nums.length;
const set=new Set(nums)
for(let i=1;i<=len+1;i++){
if(set.has(i)) continue;
return i;
}
};
内心OS:这下总该最优了吧?
4. 终极优化:循环次数再次减少
虽然现在是遍历了len次,但是能不能再少一点?
思路 :如果我便利到的数组元素满足nums[i]>nums.length 的话,岂不代表我又可以少循环一次了吗
步骤:
- 若发现存在元素nums[i]>nums.length,执行len--
代码
ini
var firstMissingPositive = function(nums) {
let len=nums.length;
const set=new Set(nums)
for(let i=1;i<=len+1;i++){
if(nums[i]>nums.length) len--;
if(set.has(i)) continue;
return i;
}
};
内心OS:这下肯定最优了!
5. 残酷现实:只击败 33% 的解法
自信提交,结果:只击败 33% 的解法 😱
三、最优题解
我败了没关系,AI大人会救我
我:"给出该算法题js最优题解"
AI:"收到!....."
js
var firstMissingPositive = function(nums) {
const n = nums.length;
// 1. 检查 1 是否存在。若不存在,则答案为 1。
if (!nums.includes(1)) {
return 1;
}
// 2. 将负数,0,以及大于 n 的数替换为 1。
// 在转换之后,nums 只会包含正数。
for (let i = 0; i < n; i++) {
if (nums[i] <= 0 || nums[i] > n) {
nums[i] = 1;
}
}
// 3. 使用索引作为哈希键,数组元素作为哈希值。
// 例如,如果 nums[1] = 3,表示数字 3 出现在数组中。
// 同时,将索引 3 位置的数字变为负数,以标记数字 3 出现过。
for (let i = 0; i < n; i++) {
let a = Math.abs(nums[i]);
// 如果见到数字 a,则将索引 a 对应的元素变为负数。
// 小心重复元素,只赋值一次。
if (a <= n) {
nums[a - 1] = - Math.abs(nums[a - 1]);
}
}
// 4. 再次遍历数组。返回第一个正数元素的索引 + 1。
for (let i = 0; i < n; i++) {
if (nums[i] > 0) {
return i + 1;
}
}
// 5. 如果 nums = [1, 2, 3],则缺少 4。
return n + 1;
};
核心思想
利用数组自身的索引作为哈希键,通过将对应的元素标记为负数来表示该数已存在,从而在O(n)时间内找到缺失的最小正整数。
详细步骤
-
初步检查:
- 如果数组中没有
1
,则结果一定是1
,直接返回。
- 如果数组中没有
-
数据清洗:
- 将所有小于等于
0
和大于n
(数组长度)的数替换为1
。这样保证数组中的值都在1
到n
的范围内,并且不影响后续hash标记步骤 。
- 将所有小于等于
-
原地Hash标记:
-
遍历数组
nums
:- 对于每个元素
nums[i]
,计算其绝对值a = Math.abs(nums[i])
。 - 如果
1 <= a <= n
,则将nums[a - 1]
变为其绝对值的负数。 这样做相当于标记了数字a
在数组中出现过。注意,需要先取绝对值,因为可能之前已经被标记为负数了。
- 对于每个元素
-
-
寻找结果:
-
再次遍历数组
nums
:- 找到第一个正数
nums[i]
。 此时,i + 1
就是缺失的最小正整数。 直接返回i + 1
。
- 找到第一个正数
-
-
特殊情况:
- 如果遍历到最后全部是小于等于0 的数,代表缺失的是
n + 1
,直接返回。 说明数组包含了从1
到n
的所有正整数。
- 如果遍历到最后全部是小于等于0 的数,代表缺失的是
动态题解示意图(运行该代码即可获得动态题解)
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>第一个缺失的正整数 - 可视化解析</title>
<style>
.container {
display: flex;
flex-direction: column;
align-items: center;
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
}
.array-container {
display: flex;
margin: 20px 0;
}
.array-element {
width: 50px;
height: 50px;
border: 1px solid #333;
display: flex;
justify-content: center;
align-items: center;
margin: 0 5px;
position: relative;
background-color: #f5f5f5;
}
.index {
position: absolute;
top: -20px;
font-size: 12px;
color: #666;
}
.highlight {
background-color: #ffeb3b;
transition: background-color 0.3s;
}
.marked {
background-color: #4caf50;
color: white;
}
.negative {
background-color: #f44336;
color: white;
}
.controls {
margin: 20px 0;
}
button {
padding: 8px 16px;
margin: 0 5px;
cursor: pointer;
}
.explanation {
background-color: #e3f2fd;
padding: 15px;
border-radius: 5px;
margin: 10px 0;
width: 100%;
}
</style>
</head>
<body>
<div class="container">
<h2>第一个缺失的正整数 - 可视化解析</h2>
<div class="controls">
<button id="prev-btn">上一步</button>
<button id="next-btn">下一步</button>
<button id="reset-btn">重置</button>
</div>
<div class="explanation" id="explanation">
点击"下一步"开始算法演示...
</div>
<div class="array-container" id="array-container"></div>
<h3>算法步骤说明:</h3>
<ol>
<li>检查1是否存在,若不存在则直接返回1</li>
<li>将所有非正数和大于n的数替换为1</li>
<li>使用索引作为哈希键,将出现过的数字对应的索引位置标记为负数</li>
<li>扫描数组,第一个正数位置的索引+1就是缺失的正整数</li>
<li>如果全部为负数,则返回n+1</li>
</ol>
</div>
<script>
const nums = [3, 4, -1, 1]; // 示例输入
let step = 0;
let workingArray = [...nums];
let highlightedIndex = -1;
function renderArray() {
const container = document.getElementById('array-container');
container.innerHTML = '';
for (let i = 0; i < workingArray.length; i++) {
const element = document.createElement('div');
element.className = 'array-element';
if (i === highlightedIndex) {
element.classList.add('highlight');
}
if (workingArray[i] < 0) {
element.classList.add('negative');
}
element.innerHTML = `
<div class="index">${i}</div>
${workingArray[i]}
`;
container.appendChild(element);
}
}
function updateExplanation() {
const explanation = document.getElementById('explanation');
switch(step) {
case 0:
explanation.innerHTML = `
<strong>初始数组:</strong> [${nums.join(', ')}]<br>
准备开始处理...
`;
break;
case 1:
explanation.innerHTML = `
<strong>步骤1:</strong> 检查数组中是否存在1<br>
${nums.includes(1) ? '数组包含1,继续处理' : '数组不包含1,直接返回1'}
`;
break;
case 2:
explanation.innerHTML = `
<strong>步骤2:</strong> 将所有非正数和大于n的数替换为1<br>
处理后的数组: [${workingArray.join(', ')}]
`;
break;
case 3:
explanation.innerHTML = `
<strong>步骤3:</strong> 使用索引作为哈希键<br>
当前处理元素: nums[${highlightedIndex}] = ${workingArray[highlightedIndex]}<br>
将索引 ${Math.abs(workingArray[highlightedIndex])-1} 位置标记为负数
`;
break;
case 4:
explanation.innerHTML = `
<strong>步骤4:</strong> 扫描数组寻找第一个正数<br>
当前检查索引: ${highlightedIndex}<br>
${workingArray[highlightedIndex] > 0 ?
`找到第一个正数位置,返回 ${highlightedIndex+1}` :
'继续扫描...'}
`;
break;
case 5:
explanation.innerHTML = `
<strong>步骤5:</strong> 所有位置都为负数,返回n+1 (${workingArray.length+1})
`;
break;
case 6:
const missing = findMissing();
explanation.innerHTML = `
<strong>算法完成!</strong><br>
第一个缺失的正整数是: ${missing}
`;
break;
}
}
function findMissing() {
for (let i = 0; i < workingArray.length; i++) {
if (workingArray[i] > 0) {
return i + 1;
}
}
return workingArray.length + 1;
}
function nextStep() {
if (step === 0) {
step = 1;
highlightedIndex = -1;
} else if (step === 1) {
if (!nums.includes(1)) {
step = 6; // 直接跳到结果
} else {
step = 2;
// 执行步骤2
for (let i = 0; i < workingArray.length; i++) {
if (workingArray[i] <= 0 || workingArray[i] > workingArray.length) {
workingArray[i] = 1;
}
}
}
} else if (step === 2) {
step = 3;
highlightedIndex = 0;
} else if (step === 3) {
// 执行步骤3
const a = Math.abs(workingArray[highlightedIndex]);
if (a <= workingArray.length) {
workingArray[a - 1] = -Math.abs(workingArray[a - 1]);
}
highlightedIndex++;
if (highlightedIndex >= workingArray.length) {
step = 4;
highlightedIndex = 0;
}
} else if (step === 4) {
if (workingArray[highlightedIndex] > 0) {
step = 6;
} else {
highlightedIndex++;
if (highlightedIndex >= workingArray.length) {
step = 5;
}
}
} else if (step === 5) {
step = 6;
}
renderArray();
updateExplanation();
}
function prevStep() {
if (step > 0) {
step--;
// 重置数组到上一步状态
resetWorkingArray();
// 设置高亮索引
if (step === 3) {
highlightedIndex = Math.min(highlightedIndex, workingArray.length - 1);
} else if (step === 4) {
highlightedIndex = 0;
} else {
highlightedIndex = -1;
}
renderArray();
updateExplanation();
}
}
function resetWorkingArray() {
workingArray = [...nums];
if (step >= 2) {
// 重新执行步骤2
for (let i = 0; i < workingArray.length; i++) {
if (workingArray[i] <= 0 || workingArray[i] > workingArray.length) {
workingArray[i] = 1;
}
}
}
if (step >= 3) {
// 重新执行步骤3
for (let i = 0; i < workingArray.length && i < highlightedIndex; i++) {
const a = Math.abs(workingArray[i]);
if (a <= workingArray.length) {
workingArray[a - 1] = -Math.abs(workingArray[a - 1]);
}
}
}
}
function reset() {
step = 0;
workingArray = [...nums];
highlightedIndex = -1;
renderArray();
updateExplanation();
}
document.getElementById('next-btn').addEventListener('click', nextStep);
document.getElementById('prev-btn').addEventListener('click', prevStep);
document.getElementById('reset-btn').addEventListener('click', reset);
// 初始渲染
renderArray();
updateExplanation();
</script>
</body>
</html>
四、结语
再见!