LeetCode热题100算法题分类解析与JavaScript实现

96 阅读28分钟

LeetCode热题100算法题分类解析与JavaScript实现

LeetCode热题100是一组被广泛认为是面试中最常考的经典算法题目集合,涵盖数据结构和算法的核心知识点。本文将系统性地解析这些题目,并提供JavaScript实现,帮助读者更好地掌握算法思想。

一、数组类算法题

数组是编程中最基础的数据结构,热题100中有许多与数组相关的经典问题。以下是几道典型数组题的解析:

1. 移动零 (283. Move Zeroes)

题目描述:给定一个数组,将所有0移动到数组末尾,同时保持非零元素的相对顺序,要求原地操作。

解法思路:使用双指针法,一个指针pre追踪非零元素的位置,另一个指针cur遍历数组。当cur遇到非零元素时,与pre位置的元素交换,然后pre和cur都后移;若遇到0,仅cur后移。

JavaScript代码实现

var moveZeroes = function(nums) {
    let pre = -1; // 记录非零元素的下一个位置
    for (let cur = 0; cur < nums.length; cur++) {
        if (nums[cur] !== 0) {
            // 遇到非零元素,交换到pre位置
            [nums[pre + 1], nums[cur]] = [nums[cur], nums[pre + 1]];
            pre++; // 更新非零元素的下一个位置
        }
    }
};

复杂度分析

  • 时间复杂度:O(n),每个元素最多被访问两次
  • 空间复杂度:O(1),原地操作,无需额外空间

适用场景:数据整理、过滤和排序,例如在前端开发中过滤并整理用户输入的数据。

2. 盛最多水的容器 (11. Container With Most Water)

题目描述:给定n条垂线,找到两条线,使得它们与x轴构成的容器能容纳最多的水。

解法思路:双指针从两端向中间移动。容器的容量由较短的垂线高度和两线间距决定。每次移动较短垂线的指针,因为移动较长垂线的指针不会得到更大的容量。

JavaScript代码实现

var maxArea = function(height) {
    let left = 0, right = height.length - 1;
    let max_area = 0;
    while (left < right) {
        // 计算当前容器的容量
        max_area = Math.max(max_area, (right - left) * Math.min(height[left], height[right]));
        // 移动较短的垂线指针
        if (height[left] < height[right]) {
            left++;
        } else {
            right--;
        }
    }
    return max_area;
};

复杂度分析

  • 时间复杂度:O(n),双指针线性遍历数组
  • 空间复杂度:O(1),仅使用常数空间

适用场景:资源分配优化问题,如最大化利用有限空间存储物品。

3. 三数之和 (15. 3Sum)

题目描述:给定一个数组,找出所有和为0的三元组,且结果不能包含重复的三元组。

解法思路:先对数组进行排序,然后使用三指针法。固定一个指针i,另外两个指针j和k分别从i+1和数组末尾向中间移动,寻找满足条件的三元组。同时需要处理重复元素的情况。

JavaScript代码实现

var threeSum = function(nums) {
    nums.sort((a, b) => a - b);
    const res = [];
    for (let i = 0; i < nums.length - 2; i++) {
        // 跳过重复的i
        if (i > 0 && nums[i] === nums[i - 1]) continue;
        let left = i + 1, right = nums.length - 1;
        while (left < right) {
            const sum = nums[i] + nums[left] + nums[right];
            if (sum < 0) {
                left++;
            } else if (sum > 0) {
                right--;
            } else {
                // 找到符合条件的三元组
                res.push([nums[i], nums[left], nums[right]]);
                // 跳过重复的left和right
                while (left < right && nums[left] === nums[left + 1]) left++;
                while (left < right && nums[right] === nums[right - 1]) right--;
                left++;
                right--;
            }
        }
    }
    return res;
};

复杂度分析

  • 时间复杂度:O(n²),排序O(n log n) + 双指针遍历O(n²)
  • 空间复杂度:O(1),原地排序(假设使用原地排序算法)

适用场景:多条件筛选和组合优化,如在电商系统中筛选符合特定条件的组合商品。

二、字符串类算法题

字符串操作在算法题中占重要地位,热题100中有多个经典字符串问题。

1. 无重复字符的最长子串 (3. Longest Substring Without Repeating Characters)

题目描述:找出一个字符串中最长的无重复字符的子串的长度。

解法思路:使用滑动窗口算法。维护两个指针left和right表示当前窗口的左右边界。使用哈希表记录每个字符最后一次出现的位置。当遇到重复字符时,将左边界移动到重复字符上次出现位置的下一个位置。每次右边界移动时,计算当前窗口长度并更新最大值。

JavaScript代码实现

function lengthOfLongestSubstring(s) {
    const charMap = new Map(); // 存储字符及其最后出现的位置
    let left = 0, maxLen = 0;

    for (let right = 0; right < s.length; right++) {
        const c = s[right];
        // 如果字符已存在且在当前窗口内,则移动左指针
        if (charMap.has(c) && charMap.get(c) >= left) {
            left = charMap.get(c) + 1;
        }

        // 更新字符的最新位置
        charMap.set(c, right);
        // 计算当前窗口的长度并更新最大值
        maxLen = Math.max(maxLen, right - left + 1);
    }

    return maxLen;
}

复杂度分析

  • 时间复杂度:O(n),每个字符最多被访问两次
  • 空间复杂度:O(min(m, n)),其中m是字符集的大小,n是字符串的长度

适用场景:文本处理、数据验证等,如在前端开发中验证用户输入的唯一性。

2. 字母异位词分组 (49. Group Anagrams)

题目描述:将字符串数组中的字母异位词分组。字母异位词是指由相同字母重新排列而成的字符串。

解法思路:同一组的字母异位词排序后的字符串相同。使用哈希表,将排序后的字符串作为键,原字符串作为值存储。遍历每个字符串,排序后作为键,将原字符串添加到对应的哈希表中。

JavaScript代码实现

var groupAnagrams = function(strs) {
    const map = new Map(); // 键是排序后的字符串,值是原字符串数组
    for (const str of strs) {
        const sortedStr = str.split('').sort().join('');
        if (!map.has(sortedStr)) {
            map.set(sortedStr, []);
        }
        map.get(sortedStr).push(str);
    }
    return Array.from(map.values());
};

复杂度分析

  • 时间复杂度:O(nk log k),其中n是字符串的数量,k是字符串的平均长度
  • 空间复杂度:O(nk),存储所有字符串的排序版本

适用场景:文本分析、搜索优化等,如在搜索引擎中对同义词进行分组。

三、链表类算法题

链表是面试中常见的数据结构,热题100中有几道经典链表问题。

1. 反转链表 (206. Reverse Linked List)

题目描述:反转一个单链表。

解法思路:使用迭代法,维护三个指针:pre(前驱节点)、curr(当前节点)、next(后继节点)。遍历链表时,依次将每个节点的next指针指向pre节点,然后移动pre和curr指针。

JavaScript代码实现

function reverseList(head) {
    let pre = null, curr = head;
    while (curr) {
        const next = curr.next; // 保存下一个节点
        curr.next = pre; // 反转指针
        pre = curr; // 移动pre指针
        curr = next; // 移动curr指针
    }
    return pre; // 最后pre指向新链表的头节点
}

// 链表节点定义
function ListNode(val, next) {
    this.val = (val===undefined ? 0 : val);
    this.next = (next===undefined ? null : next);
}

复杂度分析

  • 时间复杂度:O(n),遍历整个链表
  • 空间复杂度:O(1),仅使用常数空间

适用场景:数据存储和检索,如在浏览器历史记录中实现后退功能。

2. 回文链表 (234. Palindrome Linked List)

题目描述:判断一个单链表是否是回文链表。

解法思路:使用快慢指针找到链表中点。然后反转后半部分链表。最后比较前半部分和反转后的后半部分是否相同。

JavaScript代码实现

var isPalindrome = function(head) {
    // 快慢指针找到中点
    let slow = head, fast = head;
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
    }

    // 反转后半部分链表
    let prev = null, curr = slow;
    while (curr) {
        const next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }

    // 比较前半部分和反转后的后半部分
    let left = head, right = prev;
    while (right) {
        if (left.val !== right.val) {
            return false;
        }
        left = left.next;
        right = right.next;
    }
    return true;
};

复杂度分析

  • 时间复杂度:O(n),遍历链表两次
  • 空间复杂度:O(1),仅使用常数空间

适用场景:数据校验和模式识别,如在密码系统中验证回文结构。

四、树类算法题

树结构在算法题中占据重要位置,热题100中有多个经典树问题。

1. 二叉树的最大深度 (104. Maximum Depth of Binary Tree)

题目描述:计算二叉树的最大深度。

解法思路:使用递归法。树的最大深度等于左右子树深度的最大值加1。递归终止条件是当节点为空时,返回0。

JavaScript代码实现

function maxDepth(root) {
    if (!root) return 0;
    // 递归计算左右子树的深度
    const leftDepth = maxDepth(root.left);
    const rightDepth = maxDepth(root.right);
    // 返回较大值加1
    return 1 + Math.max(leftDepth, rightDepth);
}

// 二叉树节点定义
function TreeNode(val, left, right) {
    this.val = (val===undefined ? 0 : val);
    this.left = (left===undefined ? null : left);
    this.right = (right===undefined ? null : right);
}

复杂度分析

  • 时间复杂度:O(n),每个节点访问一次
  • 空间复杂度:O(H),H是树的高度,最坏情况下O(n)

适用场景:数据结构分析,如在前端开发中分析组件树的深度。

2. 对称二叉树 (101. Symmetric Tree)

题目描述:判断一个二叉树是否是对称的。

解法思路:使用递归法。比较左子树和右子树是否对称。递归终止条件是两个节点都为空则对称;一个为空另一个不为空则不对称;节点值不相等则不对称。否则递归比较左子树的左孩子与右子树的右孩子,以及左子树的右孩子与右子树的左孩子。

JavaScript代码实现

function isSymmetric(root) {
    if (!root) return true;
    return isMirror(root.left, root.right);
}

function isMirror(left, right) {
    // 两个节点都为空,对称
    if (!left && !right) return true;
    // 一个为空另一个不为空,不对称
    if (!left || !right) return false;
    // 节点值不相等,不对称
    if (left.val !== right.val) return false;
    // 递归比较左子树的左孩子与右子树的右孩子,以及左子树的右孩子与右子树的左孩子
    return isMirror(left.left, right.right) && isMirror(left.right, right.left);
}

// 二叉树节点定义
function TreeNode(val, left, right) {
    this.val = (val===undefined ? 0 : val);
    this.left = (left===undefined ? null : left);
    this.right = (right===undefined ? null : right);
}

复杂度分析

  • 时间复杂度:O(n),每个节点访问一次
  • 空间复杂度:O(H),H是树的高度,最坏情况下O(n)

适用场景:数据结构验证,如在前端开发中验证组件树的对称结构。

五、哈希表类算法题

哈希表是一种高效的数据结构,热题100中有多个经典哈希表问题。

1. 两数之和 (1. Two Sum)

题目描述:给定一个整数数组和一个目标值,找出数组中和为目标值的两个数的索引。

解法思路:使用哈希表存储每个数的索引。遍历数组时,检查哈希表中是否存在目标值减去当前数的结果。如果存在,返回当前索引和哈希表中存储的索引;否则,将当前数的索引存入哈希表。

JavaScript代码实现

var twoSum = function(nums, target) {
    const numMap = new Map(); // 存储数值和对应的索引
    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];
        // 如果哈希表中存在补数,返回结果
        if (numMap.has(complement)) {
            return [numMap.get(complement), i];
        }
        // 存储当前数值和索引
        numMap.set(nums[i], i);
    }
    return [];
};

复杂度分析

  • 时间复杂度:O(n),遍历数组一次
  • 空间复杂度:O(n),哈希表存储所有元素

适用场景:数据匹配和查询优化,如在推荐系统中快速匹配用户偏好。

2. 只出现一次的数字 (136. Single Number)

题目描述:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

解法思路:使用位运算中的异或操作。异或运算的三个性质:任何数和0异或结果不变;任何数和自身异或结果为0;异或满足交换律和结合律。因此,将所有元素依次异或,重复元素会相互抵消为0,最终结果就是只出现一次的元素。

JavaScript代码实现

var singleNumber = function(nums) {
    let result = 0;
    for (const num of nums) {
        result ^= num; // 异或操作
    }
    return result;
};

复杂度分析

  • 时间复杂度:O(n),遍历数组一次
  • 空间复杂度:O(1),仅使用一个变量存储中间结果

适用场景:数据校验和错误检测,如在网络通信中检测数据包完整性。

六、动态规划类算法题

动态规划是解决最优化问题的有效方法,热题100中有多个经典动态规划问题。

1. 爬楼梯 (70. Climbing Stairs)

题目描述:你正在爬楼梯,每一步可以爬1阶或2阶。n阶楼梯有多少种不同的爬法?

解法思路:使用动态规划。定义dp[i]为爬到第i阶的步数。状态转移方程为dp[i] = dp[i-1] + dp[i-2]。可以优化为只使用两个变量存储前两个状态的值。

JavaScript代码实现

var climbStairs = function(n) {
    if (n <= 2) return n;
    let prev = 1, curr = 2;
    for (let i = 3; i <= n; i++) {
        const temp = curr;
        curr += prev;
        prev = temp;
    }
    return curr;
};

复杂度分析

  • 时间复杂度:O(n),遍历n次
  • 空间复杂度:O(1),仅使用两个变量

适用场景:路径规划和资源分配,如在物流系统中计算最优路径。

2. 不同路径 (62. Unique Paths)

题目描述:一个机器人位于一个m x n网格的左上角。机器人每次只能向下或向右移动一步。有多少种不同的路径可以让机器人到达右下角?

解法思路:使用动态规划。定义dp[i][j]为到达(i,j)位置的路径数。状态转移方程为dp[i][j] = dp[i-1][j] + dp[i][j-1]。初始化第一行和第一列为1,因为只有一种方式到达这些位置。

JavaScript代码实现

var uniquePaths = function(m, n) {
    const dp = new Array(m).fill(0).map(() => new Array(n).fill(0));
    // 初始化第一行和第一列
    for (let i = 0; i < m; i++) dp[i][0] = 1;
    for (let j = 0; j < n; j++) dp[0][j] = 1;

    for (let i = 1; i < m; i++) {
        for (let j = 1; j < n; j++) {
            // 状态转移方程
            dp[i][j] = dp[i-1][j] + dp[i][j-1];
        }
    }
    return dp[m-1][n-1];
};

复杂度分析

  • 时间复杂度:O(mn),遍历整个网格
  • 空间复杂度:O(mn),存储二维DP数组

适用场景:路径规划和组合优化,如在游戏开发中计算角色移动路径。

七、图论类算法题

图论问题在算法题中较为复杂,热题100中有几道经典图论问题。

1. 岛屿数量 (200. Number of Islands)

题目描述:给定一个由'1'(陆地)和'0'(水)组成的二维网格,计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平或竖直方向上相邻的陆地连接形成。

解法思路:使用深度优先搜索(DFS)或广度优先搜索(BFS)。遍历网格,当遇到'1'时,岛屿数量加1,并用DFS/BFS将整个岛屿标记为已访问(例如将'1'改为'0'),防止重复计数。

JavaScript代码实现(DFS)

var numIslands = function(grid) {
    if (!grid || grid.length === 0 || grid[0].length === 0) return 0;

    const m = grid.length, n = grid[0].length;
    let count = 0;

    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (grid[i][j] === '1') {
                count++;
                dfs(grid, i, j, m, n);
            }
        }
    }
    return count;
};

function dfs(grid, i, j, m, n) {
    // 边界检查
    if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] === '0') return;

    grid[i][j] = '0'; // 标记为已访问
    // 递归四个方向
    dfs(grid, i + 1, j, m, n);
    dfs(grid, i - 1, j, m, n);
    dfs(grid, i, j + 1, m, n);
    dfs(grid, i, j - 1, m, n);
}

JavaScript代码实现(BFS)

var numIslands = function(grid) {
    if (!grid || grid.length === 0 || grid[0].length === 0) return 0;

    const m = grid.length, n = grid[0].length;
    let count = 0;

    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (grid[i][j] === '1') {
                count++;
                bfs(grid, i, j, m, n);
            }
        }
    }
    return count;
};

function bfs(grid, i, j, m, n) {
    const queue = [[i, j]];
    grid[i][j] = '0'; // 标记为已访问

    const directions = [[1, 0], [-1, 0], [0, 1], [0, -1]]; // 四个方向

    while (queue.length > 0) {
        const [x, y] = queue.shift();
        // 遍历四个方向
        for (const [dx, dy] of directions) {
            const nx = x + dx, ny = y + dy;
            // 检查边界和是否为陆地
            if (nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] === '1') {
                grid[nx][ny] = '0'; // 标记为已访问
                queue.push([nx, ny]);
            }
        }
    }
};

复杂度分析

  • 时间复杂度:O(mn),每个节点最多访问一次
  • 空间复杂度:O(mn),最坏情况下递归栈或队列需要存储所有节点

适用场景:地图分析和区域划分,如在游戏开发中分析地图区域。

2. 课程表 (207. Course Schedule)

题目描述:判断是否可能完成所有课程的学习。课程之间存在先修关系,表示为二维数组prerequisites。

解法思路:使用拓扑排序算法。构建邻接表表示课程依赖关系,并使用入度数组记录每个课程的前置课程数量。将所有入度为0的课程加入队列,然后不断从队列中取出课程,减少其后续课程的入度。如果最终所有课程都被处理,则可以完成;否则存在环,无法完成。

JavaScript代码实现

var canFinish = function(numCourses, prerequisites) {
    const graph = new Array(numCourses).fill(0).map(() => []); // 邻接表
    const inDegree = new Array(numCourses).fill(0); // 入度数组

    // 构建邻接表和入度数组
    for (const [course, prereq] of prerequisites) {
        graph[prereq].push(course); // 添加后续课程
        inDegree[course]++; // 增加入度
    }

    const queue = [];
    // 将入度为0的课程加入队列
    for (let i = 0; i < numCourses; i++) {
        if (inDegree[i] === 0) {
            queue.push(i);
        }
    }

    let count = 0; // 已处理的课程数
    while (queue.length > 0) {
        const course = queue.shift();
        count++;
        // 遍历该课程的后续课程
        for (const neighbor of graph[course]) {
            inDegree[neighbor]}; // 减少入度
            if (inDegree[neighbor] === 0) {
                queue.push(neighbor);
            }
        }
    }

    return count === numCourses; // 是否所有课程都处理完毕
};

复杂度分析

  • 时间复杂度:O(N + M),N是课程数,M是先修关系数
  • 空间复杂度:O(N + M),邻接表和入度数组的存储空间

适用场景:任务调度和依赖管理,如在构建系统中处理代码依赖。

八、堆与优先队列类算法题

堆结构在解决极值问题时非常高效,热题100中有几道经典堆问题。

1. 数组中的第K个最大元素 (215. Kth Largest Element in an Array)

题目描述:在给定的整数数组中,找出第K个最大的元素。

解法思路:使用最小堆。遍历数组,将元素依次插入堆中。当堆的大小超过K时,删除堆顶元素(最小值)。最终堆顶元素就是第K个最大的元素。

JavaScript代码实现

class MinHeap {
    constructor() {
        this.heap = [];
    }

    // 交换元素
    swap(i1, i2) {
        [this.heap[i1], this.heap[i2]] = [this.heap[i2], this.heap[i1]];
    }

    // 获取父节点下标
    parenting(i) {
        return (i - 1) >> 1;
    }

    // 获取左子节点下标
    leftChild(i) {
        return i * 2 + 1;
    }

    // 获取右子节点下标
    rightChild(i) {
        return i * 2 + 2;
    }

    // 上移操作
    shiftUp(index) {
        if (index === 0) return;
        const parentIndex = this parent(i);
        if (this.heap[parentIndex] > this.heap[index]) {
            this.swap(parentIndex, index);
            this shiftUp(parentIndex);
        }
    }

    // 下移操作
    shiftDown(index) {
        const leftIndex = this.leftChild(index);
        const rightIndex = this.rightChild(index);

        let smallest = index;
        // 比较左子节点
        if (leftIndex < this.heap.length && this.heap[leftIndex] < this.heap[smallest]) {
            smallest = leftIndex;
        }
        // 比较右子节点
        if (rightIndex < this.heap.length && this.heap[rightIndex] < this.heap[smallest]) {
            smallest = rightIndex;
        }

        if (smallest !== index) {
            this.swap(index, smallest);
            this shiftDown(smallest);
        }
    }

    // 插入元素
    insert(value) {
        this.heap.push(value);
        this shiftUp(this.heap.length - 1);
    }

    // 删除堆顶元素
    pop() {
        if (this.heap.length === 1) return this.heap.pop();
        const top = this.heap[0];
        this.heap[0] = this.heap.pop();
        this shiftDown(0);
        return top;
    }

    // 获取堆顶元素
    peek() {
        return this.heap[0];
    }

    // 获取堆的大小
    size() {
        return this.heap.length;
    }
}

var findKthLargest = function(nums, k) {
    const heap = new MinHeap();
    for (const num of nums) {
        heap.insert(num);
        if (heap.size() > k) {
            heap.pop(); // 保持堆大小为k
        }
    }
    return heap peek();
};

复杂度分析

  • 时间复杂度:O(n log k),插入和删除操作的时间复杂度
  • 空间复杂度:O(k),堆的存储空间

适用场景:实时数据处理和排序优化,如在推荐系统中快速获取热门商品。

九、双指针类算法题

双指针法是一种高效的算法技巧,热题100中有几道经典双指针问题。

1. 移动零 (283. Move Zeroes)

题目描述:给定一个数组,将所有0移动到数组末尾,同时保持非零元素的相对顺序,要求原地操作。

解法思路:使用双指针法,一个指针pre追踪非零元素的位置,另一个指针cur遍历数组。当cur遇到非零元素时,与pre位置的元素交换,然后pre和cur都后移;若遇到0,仅cur后移。

JavaScript代码实现

var moveZeroes = function(nums) {
    let pre = -1; // 记录非零元素的下一个位置
    for (let cur = 0; cur < nums.length; cur++) {
        if (nums[cur] !== 0) {
            // 遇到非零元素,交换到pre位置
            [nums[pre + 1], nums[cur]] = [nums[cur], nums[pre + 1]];
            pre++; // 更新非零元素的下一个位置
        }
    }
};

复杂度分析

  • 时间复杂度:O(n),每个元素最多被访问两次
  • 空间复杂度:O(1),原地操作,无需额外空间

适用场景:数据整理和过滤,如在前端开发中过滤并整理用户输入的数据。

2. 盛最多水的容器 (11. Container With Most Water)

题目描述:给定n条垂线,找到两条线,使得它们与x轴构成的容器能容纳最多的水。

解法思路:双指针从两端向中间移动。容器的容量由较短的垂线高度和两线间距决定。每次移动较短垂线的指针,因为移动较长垂线的指针不会得到更大的容量。

JavaScript代码实现

var maxArea = function(height) {
    let left = 0, right = height.length - 1;
    let max_area = 0;
    while (left < right) {
        // 计算当前容器的容量
        max_area = Math.max(max_area, (right - left) * Math.min(height[left], height[right]));
        // 移动较短的垂线指针
        if (height[left] < height[right]) {
            left++;
        } else {
            right--;
        }
    }
    return max_area;
};

复杂度分析

  • 时间复杂度:O(n),双指针线性遍历数组
  • 空间复杂度:O(1),仅使用常数空间

适用场景:资源分配优化问题,如最大化利用有限空间存储物品。

十、滑动窗口类算法题

滑动窗口算法是一种高效的双指针技术,用于处理连续子数组或子串问题。

1. 滑动窗口最大值 (239. Sliding Window Maximum)

题目描述:给定一个整数数组nums,有一个大小为k的滑动窗口从数组的最左侧移动到数组的最右侧。每次只向右移动一位,返回滑动窗口中的最大值。

解法思路:使用单调队列(双端队列)来维护窗口中的元素,使得队列中的元素保持单调递减的顺序。队列中存储的是元素的索引,而非元素本身。这样可以方便地判断元素是否已经滑出窗口。

JavaScript代码实现

function maxSlidingWindow(nums, k) {
    if (nums.length === 0 || k === 0) return [];
    if (k === 1) return nums;

    const deque = [];
    const result = [];

    for (let i = 0; i < nums.length; i++) {
        // 移除队首超出窗口左边界的元素
        while (deque.length > 0 && deque[0] <= i - k) {
            deque shift();
        }

        // 维护单调递减队列
        while (deque.length > 0 && nums[deque[deque.length - 1]] <= nums[i]) {
            deque pop();
        }

        deque push(i);

        // 当窗口形成时,记录最大值
        if (i >= k - 1) {
            result push(nums[deque[0]]);
        }
    }

    return result;
}

复杂度分析

  • 时间复杂度:O(n),每个元素最多被加入队列一次,最多被移出队列一次
  • 空间复杂度:O(k),队列中最多容纳k个元素

适用场景:实时数据流分析,如在金融系统中监测股票价格波动。

十一、位运算类算法题

位运算是一种高效的算法技巧,热题100中有几道经典位运算问题。

1. 只出现一次的数字 (136. Single Number)

题目描述:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

解法思路:使用位运算中的异或操作。异或运算的三个性质:任何数和0异或结果不变;任何数和自身异或结果为0;异或满足交换律和结合律。因此,将所有元素依次异或,重复元素会相互抵消为0,最终结果就是只出现一次的元素。

JavaScript代码实现

var singleNumber = function(nums) {
    let result = 0;
    for (const num of nums) {
        result ^= num; // 异或操作
    }
    return result;
};

复杂度分析

  • 时间复杂度:O(n),遍历数组一次
  • 空间复杂度:O(1),仅使用一个变量存储中间结果

适用场景:数据校验和错误检测,如在网络通信中检测数据包完整性。

十二、并查集类算法题

并查集是一种高效的数据结构,用于处理集合的合并和查询问题。

1. 冗余连接 (684. Redundant Connection)

题目描述:给定一个有向图,判断哪一条边可以删去,使得结果图是一个树。如果有多个答案,则返回二维数组中最后出现的边。

解法思路:使用并查集。遍历所有边,检查边的两个节点是否已经在同一集合中。如果是,则这条边是冗余边。否则,将这两个节点合并到同一集合中。

JavaScript代码实现

class UnionFind {
    constructor() {
        this.parent = new Map();
    }

    // 查找元素所在集合
    find(x) {
        if (!this.parent.has(x)) {
            this.parent.set(x, x); // 初始化父节点为自己
        }

        // 路径压缩
        if (this.parent.get(x) !== x) {
            this.parent.set(x, this.find(this.parent.get(x)));
        }
        return this.parent.get(x);
    }

    // 合并两个集合
    union(p, q) {
        const rootP = this.find(p);
        const rootQ = this.find(q);
        if (rootP === rootQ) return false; // 已在同一集合
        // 合并两个集合
        this.parent.set(rootP, rootQ);
        return true;
    }
}

var findRedundantConnection = function(edges) {
    const uf = new UnionFind();
    for (const edge of edges) {
        const p = edge[0];
        const q = edge[1];
        if (uf.find(p) === uf.find(q)) {
            return edge; // 找到冗余边
        }
        uf.union(p, q);
    }
    return [-1, -1]; // 没有冗余边
};

复杂度分析

  • 时间复杂度:O(nα(n)),α是 Ackerman函数的反函数,近似于常数
  • 空间复杂度:O(n),存储并查集的父节点信息

适用场景:网络拓扑分析和连通性检测,如在社交网络中检测用户分组。

十三、贪心算法类算法题

贪心算法是一种通过局部最优解来达到全局最优解的方法。

1. 跳跃游戏 (55. Jump Game)

题目描述:给定一个非负整数数组,你最初位于数组的第一个位置。每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。

解法思路:使用贪心算法。维护一个最大可达位置maxReach。遍历数组时,更新maxReach为当前maxReach和i+nums[i]中的较大值。如果当前i超过maxReach,则无法到达,返回false。否则继续遍历,直到到达末尾。

JavaScript代码实现

var canJump = function(nums) {
    let maxReach = 0;
    const n = nums.length;
    for (let i = 0; i < n; i++) {
        if (i > maxReach) return false; // 无法到达当前节点
        maxReach = Math.max(maxReach, i + nums[i]); // 更新最大可达位置
        if (maxReach >= n - 1) return true; // 已到达末尾
    }
    return true;
};

复杂度分析

  • 时间复杂度:O(n),遍历数组一次
  • 空间复杂度:O(1),仅使用常数空间

适用场景:路径规划和资源分配,如在物流系统中规划最优路径。

2. 买卖股票的最佳时机II (122. Best Time to Buy and Sell Stock II)

题目描述:给定一个数组,其中第i个元素表示第i天的股票价格。你可以完成多次交易(多次买卖)。计算最大利润。

解法思路:使用贪心算法。只要第二天的价格高于前一天,就进行交易。利润是所有正差价的和。

JavaScript代码实现

var maxProfit = function(prices) {
    let profit = 0;
    for (let i = 1; i < prices.length; i++) {
        if (prices[i] > prices[i - 1]) {
            profit += prices[i] - prices[i - 1];
        }
    }
    return profit;
};

复杂度分析

  • 时间复杂度:O(n),遍历数组一次
  • 空间复杂度:O(1),仅使用常数空间

适用场景:金融交易和投资策略,如在股票交易系统中计算最大利润。

十四、回溯算法类算法题

回溯算法是一种通过尝试所有可能的解来寻找最优解的方法。

1. 组合总和 (39. Combination Sum)

题目描述:给定一个无重复元素的整数数组candidates和一个目标整数target,找出candidates中可以使数字和为目标数target的所有不同组合。每个数可以被重复使用。

解法思路:使用回溯算法。遍历数组,对于每个元素,可以选择使用它或者不使用它。当sum等于target时,记录组合。当sum超过target时,回溯。为了避免重复组合,每次选择从当前元素开始。

JavaScript代码实现

var combinationSum = function(candidates, target) {
    candidates.sort((a, b) => a - b); // 先排序,方便剪枝
    const result = [];
    const backtrack = (path, start, sum) => {
        if (sum === target) {
            result.push([...path]); // 找到目标和,记录组合
            return;
        }
        if (sum > target) return; // 超过目标,剪枝

        // 遍历从start开始的元素
        for (let i = start; i < candidates.length; i++) {
            // 剪枝:如果当前元素加上sum已经大于target,后面的元素更大,直接跳过
            if (sum + candidates[i] > target) continue;
            path.push(candidates[i]); // 加入当前元素
            backtrack(path, i, sum + candidates[i]); // 递归,允许重复使用
            path.pop(); // 回溯,移除当前元素
        }
    };

    backtrack([], 0, 0);
    return result;
};

复杂度分析

  • 时间复杂度:O(2^n),最坏情况下所有元素都可能被选中
  • 空间复杂度:O(n),递归栈和临时路径的存储空间

适用场景:组合优化和资源分配,如在推荐系统中生成不同的商品组合。

十五、学习路径建议

1. 分阶段学习计划

第一阶段(基础算法)

  • 双指针:移动零、盛最多水的容器
  • 哈希表:两数之和、只出现一次的数字
  • 链表:反转链表、环形链表
  • 树:二叉树的最大深度、对称二叉树
  • 位运算:只出现一次的数字

第二阶段(进阶算法)

  • 滑动窗口:无重复字符的最长子串、滑动窗口最大值
  • 动态规划:爬楼梯、不同路径、最长递增子序列
  • 贪心算法:跳跃游戏、买卖股票的最佳时机
  • 图论:岛屿数量、课程表

第三阶段(高级算法)

  • 回溯法:组合总和、N皇后
  • 堆:数组中的第K个最大元素、合并K个有序链表
  • 并查集:冗余连接

2. 五遍原则

  1. 第一遍:限时30分钟,尝试暴力解法
  2. 第二遍:对比最优解,理解优化思路
  3. 第三遍:独立实现最优解,调试通过
  4. 第四遍:总结通用模板(如动态规划的"状态+转移")
  5. 第五遍:定期复习,避免遗忘

十六、算法应用场景总结

算法类型实际应用场景典型题目
双指针数据整理和过滤移动零、盛最多水的容器
滑动窗口实时数据流分析无重复字符的最长子串、滑动窗口最大值
动态规划资源分配和路径规划爬楼梯、不同路径、最长递增子序列
贪心算法金融交易和投资策略跳跃游戏、买卖股票的最佳时机
哈希表数据匹配和查询优化两数之和、字母异位词分组
并查集网络拓扑分析和连通性检测冗余连接
回溯法组合优化和资源分配组合总和、N皇后
图论社交网络分析和路径规划岛屿数量、课程表

十七、总结与思考

LeetCode热题100涵盖了算法的核心知识点,通过系统性地学习这些题目,可以掌握多种算法思想和数据结构。滑动窗口算法的核心思想是通过维护一个动态的窗口区间,来避免重复计算,从而将时间复杂度从O(n²)优化到O(n) 。动态规划算法的核心是利用最优子结构和记忆化,自底向上或自顶向下求解 。并查集算法的核心是路径压缩和按秩合并优化,高效处理连通性问题 。

在实际应用中,算法的选择取决于问题的特性和需求。例如,对于连续子数组问题,滑动窗口算法通常比暴力解法更高效;对于最优化问题,动态规划算法通常能找到最优解;对于连通性问题,并查集算法通常能提供高效的解决方案。

通过本篇文章的讲解和代码示例,希望读者能够深入理解各类算法的原理和实现,并能够灵活应用这些算法解决实际问题。掌握这些算法不仅能够帮助通过技术面试,还能为实际工程中的高效数据处理提供有力工具

在实际应用中,算法可以与其他数据结构结合使用,进一步扩展其解决能力。例如,当需要处理更复杂的窗口极值问题时,可以结合单调队列;当需要处理多维度的窗口状态时,可以结合哈希表或计数器。

通过系统学习和实战提升,逐步掌握算法精髓,开启算法高手之路!