一分钟解决 | 海底火旺都不会的高频面试算法题——三数之和(双指针最优解法)

一、前置知识(可跳过)

1. nums.sort()

  • sort() 是 JavaScript 数组对象的一个内置方法,用于对数组的元素进行排序。
  • 默认情况下,sort() 方法会将数组的元素转换为字符串,然后按照 Unicode 码点的顺序进行排序。 这意味着,如果数组包含数字,默认排序可能会产生意想不到的结果(例如,[1, 10, 2] 会被排序为 [1, 10, 2],因为字符串 "1" 小于字符串 "10",也小于字符串 "2")。

2. (a, b) => a - b (比较函数)

  • 为了对数字进行正确排序(升序排列),我们需要提供一个比较函数sort() 方法。 比较函数定义了两个元素 ab 应该如何比较。
  • (a, b) => a - b 是一个箭头函数,它接受两个参数 ab(代表数组中的两个元素),并返回 a - b 的结果。

3. 比较函数的返回值规则

比较函数的返回值决定了 ab 的相对顺序:

  • 如果 a - b 小于 0 (负数): 说明 a 应该排在 b 前面,也就是 ab 小。
  • 如果 a - b 等于 0: 说明 ab 相等,它们的相对顺序不变。
  • 如果 a - b 大于 0 (正数): 说明 a 应该排在 b 后面,也就是 ab 大。

4. 完整过程

将以上两部分结合起来,nums.sort((a, b) => a - b) 的作用是:

  1. nums 数组调用 sort() 方法。
  2. sort() 方法会遍历 nums 数组,每次取出两个元素 ab,并将它们传递给比较函数 (a, b) => a - b
  3. 比较函数计算 a - b 的值,并根据返回值,sort() 方法决定 ab 的相对位置,从而实现升序排序。

5. 示例

css 复制代码
  
const nums = [3, 1, 4, 1, 5, 9, 2, 6];

nums.sort((a, b) => a - b);

console.log(nums); // 输出: [1, 1, 2, 3, 4, 5, 6, 9]

在这个例子中,nums 数组经过 sort((a, b) => a - b) 排序后,会按照从小到大的顺序排列。

6. 为什么要使用 a - b

  • a - b 是一种简洁且直接的数字比较方式。
  • 如果你需要降序排列,可以使用 (a, b) => b - a
  • 对于字符串的比较,可以使用 a.localeCompare(b) 来进行基于本地语言环境的排序。

总结

nums.sort((a, b) => a - b) 这段代码实现了对 nums 数组进行数值升序排序 。 比较函数的 a - b 是实现数值排序的关键,它保证 sort() 方法能够正确地比较数字的大小并调整数组元素的顺序。

二、题目描述

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意: 答案中不可以包含重复的三元组。

示例 1:

scss 复制代码
输入: nums = [-1,0,1,2,-1,-4]
输出: [[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

ini 复制代码
输入: nums = [0,1,1]
输出: []
解释: 唯一可能的三元组和不为 0 。

示例 3:

lua 复制代码
输入: nums = [0,0,0]
输出: [[0,0,0]]
解释: 唯一可能的三元组和为 0 。

提示:

  • 3 <= nums.length <= 3000
  • -105 <= nums[i] <= 105

三、题解(双指针)

js 复制代码
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(nums) {
  nums.sort((a, b) => a - b); 

  const result = [];
  const n = nums.length;

  for (let i = 0; i < n - 2; i++) {

    if (nums[i] > 0) break;

    if (i > 0 && nums[i] === nums[i - 1]) continue;

    let left = i + 1;
    let right = n - 1;

    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right];

      if (sum === 0) {
        result.push([nums[i], nums[left], nums[right]]);


        while (left < right && nums[left] === nums[left + 1]) left++;
        while (left < right && nums[right] === nums[right - 1]) right--;

        left++;
        right--;
      } else if (sum < 0) {
        left++;
      } else {
        right--;
      }
    }
  }

  return result;
};

核心思想

核心思想:排序 + 双指针 + 细节优化

  1. 排序 (Sorting): 通过排序将问题转化为在有序数组中寻找目标值,为双指针技巧创造条件。

  2. 双指针 (Two Pointers): 利用双指针高效地在有序数组中寻找两个数,使它们与当前固定数之和等于目标值(本题中目标值为0)。

  3. 细节优化 (Fine-grained Optimizations):

    • 提前终止 (Early Termination): 如果当前固定数已经大于0,则直接结束循环,因为不可能找到和为0的三元组。
    • 跳过重复元素 (Skip Duplicates): 在固定第一个数和移动双指针时,都跳过重复的数值,避免产生重复的结果。

详细解析

  1. /** @param {number[]} nums @return {number[][]} */

    • 这是一段 JSDoc 注释,用于描述函数的输入参数类型和返回值类型。
    • @param {number[]} nums 表示函数接受一个名为 nums 的参数,它是一个数字数组。
    • @return {number[][]} 表示函数返回一个数字数组的数组(一个二维数组),其中每个内部数组都是一个三元组。
  2. var threeSum = function(nums) {

    • 定义一个名为 threeSum 的函数,它接受一个参数 nums (一个数字数组)。
  3. nums.sort((a, b) => a - b);

    • 对输入的数组 nums 进行升序排序。 sort() 方法默认按字符串顺序排序,所以我们需要传入一个比较函数 (a, b) => a - b 来实现数字的升序排序。

    • 重要性: 排序是解决这个问题的关键。

      • 它使得我们可以使用双指针技巧。
      • 它使得我们可以更容易地跳过重复的元素,避免生成重复的结果。
  4. const result = [];

    • 创建一个空数组 result,用于存储所有找到的三元组。
  5. const n = nums.length;

    • 获取数组 nums 的长度,并将其存储在变量 n 中。 这样做是为了避免在循环中重复计算数组长度,提高代码效率。
  6. for (let i = 0; i < n - 2; i++) {

    • 外层循环遍历数组 nums,循环变量 i 从 0 开始,到 n - 2 结束。
    • i < n - 2 的原因:我们需要至少三个元素才能组成一个三元组,所以留出至少两个元素给 leftright 指针。
    • nums[i] 将作为三元组的第一个元素。
  7. if (nums[i] > 0) break; (优化 1: 提前终止)

    • 优化目的: 如果当前元素 nums[i] 大于 0,由于数组已经排序,后面的所有元素也都会大于 0。 因此,不可能找到三个数之和等于 0 的三元组。
    • 作用: 提前结束循环,避免不必要的计算,提高代码效率。
  8. if (i > 0 && nums[i] === nums[i - 1]) continue; (优化 2: 跳过重复元素)

    • 优化目的: 避免生成重复的三元组。

    • 解释:

      • i > 0: 确保 i - 1 是一个有效的索引,防止数组越界。
      • nums[i] === nums[i - 1]: 检查当前元素 nums[i] 是否与前一个元素 nums[i - 1] 相同。
      • 如果两个条件都满足,则说明我们已经考虑过以 nums[i - 1] 作为第一个元素的所有可能的三元组。
      • continue: 跳过当前循环迭代,继续下一个元素。
  9. let left = i + 1;

    • left 指针初始化为 i + 1left 指针指向 nums[i] 之后的第一个元素,它将作为三元组的第二个元素。
  10. let right = n - 1;

    • right 指针初始化为 n - 1,指向数组的最后一个元素。 right 指针将作为三元组的第三个元素。
  11. while (left < right) {

    • while 循环,只要 left 指针小于 right 指针,就继续执行。 这确保我们总是考虑两个不同的元素,并且 left 指针始终在 right 指针的左侧。
  12. const sum = nums[i] + nums[left] + nums[right];

    • 计算当前三个元素的和 sumnums[i] 是固定的,nums[left]nums[right] 是在 while 循环中调整的。
  13. if (sum === 0) {

    • 如果 sum 等于 0,表示我们找到了一个满足条件的三元组。
  14. result.push([nums[i], nums[left], nums[right]]);

    • 将找到的三元组 [nums[i], nums[left], nums[right]] 添加到 result 数组中。
  15. while (left < right && nums[left] === nums[left + 1]) left++; (优化 3: 跳过重复元素 - left)

    • 优化目的: 避免生成重复的三元组。

    • 解释:

      • left < right: 确保 left 指针仍然在 right 指针的左侧,防止数组越界。
      • nums[left] === nums[left + 1]: 检查 left 指针指向的元素是否与它后面的元素相同。
      • 如果以上两个条件都满足,说明 nums[left] 是一个重复的元素,我们需要跳过它。
      • left++: 将 left 指针向右移动一位,继续检查下一个元素。
  16. while (left < right && nums[right] === nums[right - 1]) right--; (优化 3: 跳过重复元素 - right)

    • 优化目的: 避免生成重复的三元组。

    • 解释: 与跳过重复的 left 指针的逻辑类似,只是方向相反。

      • 如果以上两个条件都满足,说明 nums[right] 是一个重复的元素,我们需要跳过它。
      • right--: 将 right 指针向左移动一位,继续检查下一个元素。
  17. left++;

    • 找到一个有效的三元组后,将 left 指针向右移动一位,继续寻找新的三元组。 即使 sum 等于 0,我们也需要移动指针,因为可能会有其他的三元组以 nums[i] 开头。
  18. right--;

    • 找到一个有效的三元组后,将 right 指针向左移动一位,继续寻找新的三元组。
  19. else if (sum < 0) {

    • 如果 sum 小于 0,说明当前三个元素的和太小,需要增加和的值。
  20. left++;

    • left 指针向右移动一位。 由于数组是排序的,向右移动 left 指针会选择一个更大的元素,从而增加 sum 的值。
  21. else {

    • 如果 sum 大于 0,说明当前三个元素的和太大,需要减小和的值。
  22. right--;

    • right 指针向左移动一位。 由于数组是排序的,向左移动 right 指针会选择一个更小的元素,从而减小 sum 的值。
  23. return result;

    • for 循环结束后,返回包含所有找到的三元组的 result 数组。

实例与展示(在编译器上运行下面代码得到动态示意图)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>三数之和算法交互式图示(长示例)</title>
    <style>
        body {
            font-family: 'Arial', sans-serif;
            max-width: 1000px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
            color: #333;
        }
        h1, h2 {
            color: #2c3e50;
        }
        .array-container {
            display: flex;
            justify-content: center;
            margin: 30px 0;
            position: relative;
            height: 120px;
            flex-wrap: wrap;
        }
        .array-element {
            width: 50px;
            height: 50px;
            border: 2px solid #3498db;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            margin: 5px;
            font-weight: bold;
            background-color: #f8f9fa;
            transition: all 0.3s;
            position: relative;
            flex-shrink: 0;
        }
        .array-element.active {
            background-color: #3498db;
            color: white;
            transform: scale(1.1);
        }
        .array-element.fixed {
            background-color: #e74c3c;
            color: white;
        }
        .array-element.left-pointer {
            background-color: #2ecc71;
            color: white;
        }
        .array-element.right-pointer {
            background-color: #f39c12;
            color: white;
        }
        .pointer-label {
            position: absolute;
            bottom: -25px;
            font-size: 12px;
            color: #7f8c8d;
            white-space: nowrap;
        }
        .controls {
            display: flex;
            justify-content: center;
            margin: 20px 0;
            gap: 10px;
            flex-wrap: wrap;
        }
        button {
            padding: 8px 16px;
            background-color: #3498db;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #2980b9;
        }
        button:disabled {
            background-color: #95a5a6;
            cursor: not-allowed;
        }
        .speed-control {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-left: 20px;
        }
        .explanation {
            background-color: #f8f9fa;
            padding: 15px;
            border-radius: 8px;
            margin: 20px 0;
        }
        .sum-display {
            text-align: center;
            font-size: 18px;
            margin: 15px 0;
            font-weight: bold;
            min-height: 50px;
        }
        .result-container {
            margin-top: 30px;
        }
        .result-item {
            display: inline-block;
            margin: 5px;
            padding: 5px 10px;
            background-color: #e8f4f8;
            border-radius: 4px;
        }
        .history-steps {
            margin-top: 20px;
            max-height: 150px;
            overflow-y: auto;
            border: 1px solid #ddd;
            padding: 10px;
            background-color: #f9f9f9;
        }
        .history-step {
            margin-bottom: 5px;
            padding: 5px;
            border-bottom: 1px solid #eee;
        }
        .history-step.current {
            background-color: #e8f4f8;
            font-weight: bold;
        }
        .config-panel {
            background-color: #f0f7fb;
            padding: 15px;
            border-radius: 8px;
            margin-bottom: 20px;
        }
        .config-group {
            margin-bottom: 10px;
        }
        label {
            display: inline-block;
            width: 120px;
        }
        select, input {
            padding: 5px;
            border-radius: 4px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h1>三数之和算法交互式图示(长示例)</h1>
    
    <div class="config-panel">
        <div class="config-group">
            <label for="exampleSelect">选择示例数组:</label>
            <select id="exampleSelect">
                <option value="example1">[-4, -2, -2, -1, 0, 1, 2, 2, 3, 4, 5, 6]</option>
                <option value="example2">[-5, -3, -2, -1, 0, 0, 1, 2, 2, 3, 4, 5, 6]</option>
                <option value="example3">[-6, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7]</option>
                <option value="custom">自定义数组</option>
            </select>
        </div>
        <div class="config-group" id="customInputGroup" style="display:none;">
            <label for="customArray">自定义数组:</label>
            <input type="text" id="customArray" placeholder="例如: -2,-1,0,1,2,3">
        </div>
        <button id="applyConfig">应用配置</button>
    </div>
    
    <div class="controls">
        <button id="prevStep">上一步</button>
        <button id="nextStep">下一步</button>
        <button id="autoPlay">自动播放</button>
        <button id="reset">重置</button>
        <div class="speed-control">
            <span>播放速度:</span>
            <select id="speedSelect">
                <option value="500">慢速</option>
                <option value="300" selected>中速</option>
                <option value="100">快速</option>
            </select>
        </div>
    </div>
    
    <div class="sum-display" id="sumDisplay">和为: -</div>
    
    <div class="array-container" id="arrayContainer">
        <!-- 数组元素将通过JavaScript动态生成 -->
    </div>
    
    <div class="explanation" id="explanation">
        <p>点击"下一步"按钮开始算法演示。算法步骤如下:</p>
        <ol>
            <li>首先对数组进行排序</li>
            <li>固定一个数 nums[i]</li>
            <li>使用双指针(left和right)在剩余部分寻找两个数,使得三数之和为0</li>
            <li>跳过重复值以避免重复解</li>
        </ol>
    </div>
    
    <div class="result-container">
        <h2>找到的解: <span id="resultCount">0</span>个</h2>
        <div id="results"></div>
    </div>
    
    <div class="history-steps" id="historySteps">
        <h3>步骤历史</h3>
        <div id="historyContent"></div>
    </div>

    <script>
        // 示例数组集合
        const examples = {
            example1: [-4, -2, -2, -1, 0, 1, 2, 2, 3, 4, 5, 6],
            example2: [-5, -3, -2, -1, 0, 0, 1, 2, 2, 3, 4, 5, 6],
            example3: [-6, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7]
        };
        
        let sortedNums = [...examples.example1].sort((a, b) => a - b);
        
        // 算法状态
        let state = {
            i: 0,
            left: 1,
            right: sortedNums.length - 1,
            step: 0,
            results: [],
            history: [],
            autoPlayInterval: null
        };
        
        // DOM元素
        const arrayContainer = document.getElementById('arrayContainer');
        const sumDisplay = document.getElementById('sumDisplay');
        const explanation = document.getElementById('explanation');
        const resultsContainer = document.getElementById('results');
        const resultCount = document.getElementById('resultCount');
        const historyContent = document.getElementById('historyContent');
        const prevStepBtn = document.getElementById('prevStep');
        const nextStepBtn = document.getElementById('nextStep');
        const autoPlayBtn = document.getElementById('autoPlay');
        const resetBtn = document.getElementById('reset');
        const exampleSelect = document.getElementById('exampleSelect');
        const customInputGroup = document.getElementById('customInputGroup');
        const customArrayInput = document.getElementById('customArray');
        const applyConfigBtn = document.getElementById('applyConfig');
        const speedSelect = document.getElementById('speedSelect');
        
        // 初始化数组显示
        function renderArray() {
            arrayContainer.innerHTML = '';
            
            for (let j = 0; j < sortedNums.length; j++) {
                const element = document.createElement('div');
                element.className = 'array-element';
                element.textContent = sortedNums[j];
                
                // 标记当前指针位置
                if (j === state.i) {
                    element.classList.add('fixed');
                    const label = document.createElement('div');
                    label.className = 'pointer-label';
                    label.textContent = `i=${j}`;
                    element.appendChild(label);
                } else if (j === state.left) {
                    element.classList.add('left-pointer');
                    const label = document.createElement('div');
                    label.className = 'pointer-label';
                    label.textContent = `L=${j}`;
                    element.appendChild(label);
                } else if (j === state.right) {
                    element.classList.add('right-pointer');
                    const label = document.createElement('div');
                    label.className = 'pointer-label';
                    label.textContent = `R=${j}`;
                    element.appendChild(label);
                }
                
                arrayContainer.appendChild(element);
            }
            
            // 显示当前和
            if (state.step > 0 && state.left < state.right) {
                const sum = sortedNums[state.i] + sortedNums[state.left] + sortedNums[state.right];
                sumDisplay.textContent = `和为: ${sum} (${sortedNums[state.i]} + ${sortedNums[state.left]} + ${sortedNums[state.right]})`;
                
                if (sum === 0) {
                    sumDisplay.style.color = '#27ae60';
                } else if (sum < 0) {
                    sumDisplay.style.color = '#e74c3c';
                } else {
                    sumDisplay.style.color = '#f39c12';
                }
            } else {
                sumDisplay.textContent = state.i >= sortedNums.length - 2 
                    ? '算法已完成' 
                    : '和为: -';
                sumDisplay.style.color = '#333';
            }
            
            // 更新说明
            updateExplanation();
            
            // 更新历史
            updateHistory();
            
            // 更新按钮状态
            updateButtonStates();
        }
        
        // 更新说明文本
        function updateExplanation() {
            let explanationText = '';
            
            if (state.step === 0) {
                explanationText = '<p>点击"下一步"开始算法演示。首先对数组进行排序。</p>';
            } else {
                const sum = state.left < state.right 
                    ? sortedNums[state.i] + sortedNums[state.left] + sortedNums[state.right]
                    : null;
                
                explanationText = `
                    <p><strong>当前步骤 ${state.step}:</strong></p>
                    <p>固定元素 nums[${state.i}] = ${sortedNums[state.i]}</p>
                    ${state.left < state.right ? `
                        <p>左指针 L = ${state.left} (值: ${sortedNums[state.left]})</p>
                        <p>右指针 R = ${state.right} (值: ${sortedNums[state.right]})</p>
                        <p>三数之和 = ${sum}</p>
                    ` : '<p>准备移动固定指针 i</p>'}
                `;
                
                if (sum === 0) {
                    explanationText += `
                        <p style="color: #27ae60; font-weight: bold;">找到有效三元组!</p>
                        <p>将三元组添加到结果中,然后同时移动左右指针,并跳过重复值。</p>
                    `;
                } else if (sum < 0) {
                    explanationText += `
                        <p style="color: #e74c3c;">和小于0,需要更大的数,移动左指针向右</p>
                    `;
                } else if (sum > 0) {
                    explanationText += `
                        <p style="color: #f39c12;">和大于0,需要更小的数,移动右指针向左</p>
                    `;
                }
                
                if (state.i >= sortedNums.length - 2) {
                    explanationText += '<p style="font-weight: bold;">算法已完成所有可能的组合检查</p>';
                }
            }
            
            explanation.innerHTML = explanationText;
        }
        
        // 更新步骤历史
        function updateHistory() {
            historyContent.innerHTML = '';
            
            state.history.forEach((step, index) => {
                const stepElement = document.createElement('div');
                stepElement.className = `history-step ${index === state.history.length - 1 ? 'current' : ''}`;
                
                let stepText = `步骤 ${index + 1}: `;
                if (step.action === 'init') {
                    stepText += '初始化';
                } else if (step.action === 'move_i') {
                    stepText += `移动固定指针 i 从 ${step.from} 到 ${step.to}`;
                } else if (step.action === 'move_L') {
                    stepText += `移动左指针 L 从 ${step.from} 到 ${step.to}`;
                } else if (step.action === 'move_R') {
                    stepText += `移动右指针 R 从 ${step.from} 到 ${step.to}`;
                } else if (step.action === 'found') {
                    stepText += `找到解: [${step.triplet.join(', ')}]`;
                } else if (step.action === 'skip_dup') {
                    stepText += `跳过重复值: ${step.value}`;
                }
                
                stepElement.textContent = stepText;
                historyContent.appendChild(stepElement);
            });
            
            // 滚动到底部
            historyContent.scrollTop = historyContent.scrollHeight;
        }
        
        // 更新按钮状态
        function updateButtonStates() {
            prevStepBtn.disabled = state.history.length <= 1;
            nextStepBtn.disabled = state.i >= sortedNums.length - 2;
            autoPlayBtn.textContent = state.autoPlayInterval ? '停止自动播放' : '自动播放';
        }
        
        // 添加历史记录
        function addHistory(action, data = {}) {
            state.history.push({
                action,
                ...data,
                timestamp: new Date().toLocaleTimeString()
            });
        }
        
        // 下一步
        function nextStep() {
            if (state.i >= sortedNums.length - 2) return;
            
            if (state.left < state.right) {
                const sum = sortedNums[state.i] + sortedNums[state.left] + sortedNums[state.right];
                
                if (sum === 0) {
                    // 找到解
                    const triplet = [sortedNums[state.i], sortedNums[state.left], sortedNums[state.right]];
                    state.results.push(triplet);
                    
                    // 显示结果
                    const resultElement = document.createElement('div');
                    resultElement.className = 'result-item';
                    resultElement.textContent = `[${triplet.join(', ')}]`;
                    resultsContainer.appendChild(resultElement);
                    
                    resultCount.textContent = state.results.length;
                    
                    // 记录历史
                    addHistory('found', {triplet});
                    
                    // 跳过重复值
                    while (state.left < state.right && sortedNums[state.left] === sortedNums[state.left + 1]) {
                        addHistory('skip_dup', {value: sortedNums[state.left], pointer: 'L'});
                        state.left++;
                    }
                    while (state.left < state.right && sortedNums[state.right] === sortedNums[state.right - 1]) {
                        addHistory('skip_dup', {value: sortedNums[state.right], pointer: 'R'});
                        state.right--;
                    }
                    
                    // 移动指针
                    const oldLeft = state.left;
                    const oldRight = state.right;
                    state.left++;
                    state.right--;
                    addHistory('move_L', {from: oldLeft, to: state.left});
                    addHistory('move_R', {from: oldRight, to: state.right});
                } else if (sum < 0) {
                    // 和太小,移动左指针
                    const oldLeft = state.left;
                    state.left++;
                    addHistory('move_L', {from: oldLeft, to: state.left, reason: `sum=${sum}<0`});
                } else {
                    // 和太大,移动右指针
                    const oldRight = state.right;
                    state.right--;
                    addHistory('move_R', {from: oldRight, to: state.right, reason: `sum=${sum}>0`});
                }
            } else {
                // 跳过重复的nums[i]
                const oldI = state.i;
                while (state.i < sortedNums.length - 2 && sortedNums[state.i] === sortedNums[state.i + 1]) {
                    addHistory('skip_dup', {value: sortedNums[state.i], pointer: 'i'});
                    state.i++;
                }
                state.i++;
                state.left = state.i + 1;
                state.right = sortedNums.length - 1;
                addHistory('move_i', {from: oldI, to: state.i});
            }
            
            state.step++;
            renderArray();
        }
        
        // 上一步
        function prevStep() {
            if (state.history.length <= 1) return;
            
            // 移除最后一步历史
            state.history.pop();
            const lastState = state.history[state.history.length - 1];
            
            // 还原状态
            if (lastState) {
                state.i = lastState.i || 0;
                state.left = lastState.left || state.i + 1;
                state.right = lastState.right || sortedNums.length - 1;
                state.step = lastState.step || 0;
                
                // 如果上一步是找到解,需要从结果中移除
                if (lastState.action === 'found') {
                    state.results.pop();
                    resultsContainer.removeChild(resultsContainer.lastChild);
                    resultCount.textContent = state.results.length;
                }
            }
            
            renderArray();
        }
        
        // 自动播放
        function toggleAutoPlay() {
            if (state.autoPlayInterval) {
                clearInterval(state.autoPlayInterval);
                state.autoPlayInterval = null;
            } else {
                const speed = parseInt(speedSelect.value);
                state.autoPlayInterval = setInterval(() => {
                    if (state.i >= sortedNums.length - 2) {
                        clearInterval(state.autoPlayInterval);
                        state.autoPlayInterval = null;
                        updateButtonStates();
                        return;
                    }
                    nextStep();
                }, speed);
            }
            updateButtonStates();
        }
        
        // 重置
        function reset() {
            // 停止自动播放
            if (state.autoPlayInterval) {
                clearInterval(state.autoPlayInterval);
                state.autoPlayInterval = null;
            }
            
            // 重置状态
            state = {
                i: 0,
                left: 1,
                right: sortedNums.length - 1,
                step: 0,
                results: [],
                history: [{action: 'init'}],
                autoPlayInterval: null
            };
            
            // 清空结果
            resultsContainer.innerHTML = '';
            resultCount.textContent = '0';
            
            renderArray();
        }
        
        // 应用配置
        function applyConfig() {
            const exampleType = exampleSelect.value;
            
            if (exampleType === 'custom') {
                try {
                    const customArray = customArrayInput.value
                        .split(',')
                        .map(item => parseInt(item.trim()));
                    if (customArray.some(isNaN)) {
                        throw new Error('包含非数字');
                    }
                    sortedNums = customArray.sort((a, b) => a - b);
                } catch (e) {
                    alert('请输入有效的数字数组,例如: -2,-1,0,1,2,3');
                    return;
                }
            } else {
                sortedNums = [...examples[exampleType]].sort((a, b) => a - b);
            }
            
            reset();
        }
        
        // 事件监听
        nextStepBtn.addEventListener('click', nextStep);
        prevStepBtn.addEventListener('click', prevStep);
        autoPlayBtn.addEventListener('click', toggleAutoPlay);
        resetBtn.addEventListener('click', reset);
        applyConfigBtn.addEventListener('click', applyConfig);
        exampleSelect.addEventListener('change', function() {
            customInputGroup.style.display = this.value === 'custom' ? 'block' : 'none';
        });
        speedSelect.addEventListener('change', function() {
            if (state.autoPlayInterval) {
                clearInterval(state.autoPlayInterval);
                toggleAutoPlay();
            }
        });
        
        // 初始化
        reset();
    </script>
</body>
</html>

四、结语

再见!

相关推荐
每一天,每一步1 分钟前
AI语音助手 React 组件使用js-audio-recorder实现,将获取到的语音转成base64发送给后端,后端接口返回文本内容
前端·javascript·react.js
上趣工作室1 分钟前
vue3专题1------父组件中更改子组件的属性
前端·javascript·vue.js
冯诺一没有曼5 分钟前
无线网络入侵检测系统实战 | 基于React+Python的可视化安全平台开发详解
前端·安全·react.js
getapi10 分钟前
flutter app实现分辨率自适应的图片资源加载
前端·javascript·flutter
东雁西飞34 分钟前
MATLAB 控制系统设计与仿真 - 39
开发语言·算法·matlab·自动化·工业机器人
—Qeyser38 分钟前
用 Deepseek 写的html油耗计算器
前端·javascript·css·html·css3·deepseek
萌萌哒草头将军41 分钟前
VsCode Colipot 🚗 + MCP Tools ✈️ = 让你的编程体验直接起飞 🚀🚀🚀
前端·visual studio code·mcp
萌萌哒草头将军1 小时前
🚀🚀🚀MCP SDK 快速接入 DeepSeek 并添加工具!万万没想到MCP这么简单好用!
前端·javascript·mcp
SuperCandyXu1 小时前
leetcode0113. 路径总和 II - medium
数据结构·c++·算法·leetcode
硬匠的博客1 小时前
C++继承与派生
数据结构·算法