偶遇天才算法题 | 拼劲全力,无法战胜 😓

一、题目描述------缺失的第一个正数

给你一个未排序的整数数组 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 = 数组长度)。

  • 如果数组包含 1n,那缺失的是 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 的话,岂不代表我又可以少循环一次了吗
步骤

  1. 若发现存在元素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,则结果一定是 1,直接返回。
  2. 数据清洗:

    • 将所有小于等于 0 和大于 n(数组长度)的数替换为 1。这样保证数组中的值都在 1n 的范围内,并且不影响后续hash标记步骤 。
  3. 原地Hash标记:

    • 遍历数组 nums

      • 对于每个元素 nums[i],计算其绝对值 a = Math.abs(nums[i])
      • 如果 1 <= a <= n,则将 nums[a - 1] 变为其绝对值的负数。 这样做相当于标记了数字a在数组中出现过。注意,需要先取绝对值,因为可能之前已经被标记为负数了。
  4. 寻找结果:

    • 再次遍历数组 nums

      • 找到第一个正数 nums[i]。 此时, i + 1 就是缺失的最小正整数。 直接返回 i + 1
  5. 特殊情况:

    • 如果遍历到最后全部是小于等于0 的数,代表缺失的是 n + 1,直接返回。 说明数组包含了从 1n 的所有正整数。

动态题解示意图(运行该代码即可获得动态题解)

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>

四、结语

再见!

相关推荐
&白帝&5 分钟前
vue右键显示菜单
前端·javascript·vue.js
Wannaer6 分钟前
从 Vue3 回望 Vue2:事件总线的前世今生
前端·javascript·vue.js
yychen_java20 分钟前
R-tree详解
java·算法·r-tree
羽球知道26 分钟前
在Spark搭建YARN
前端·javascript·ajax
MarkHard1231 小时前
Leetcode (力扣)做题记录 hot100(62,64,287,108)
算法·leetcode·职场和发展
光影少年1 小时前
vue中,created和mounted两个钩子之间调用时差值受什么影响
前端·javascript·vue.js
青苔猿猿1 小时前
node版本.node版本、npm版本和pnpm版本对应
前端·npm·node.js·pnpm
一只鱼^_1 小时前
牛客练习赛138(首篇万字题解???)
数据结构·c++·算法·贪心算法·动态规划·广度优先·图搜索算法
一只码代码的章鱼2 小时前
Spring的 @Validate注解详细分析
前端·spring boot·算法
邹诗钰-电子信息工程2 小时前
嵌入式自学第二十一天(5.14)
java·开发语言·算法