Skip to content

数组与链表核心技巧 🚀

一、双指针技巧精讲 👆

1. 什么是双指针? 🤔

想象你在读一本书:

  • 一个手指指着当前页的第一行(左指针)
  • 另一个手指指着最后一行(右指针)
  • 通过移动这两个手指来定位要找的内容

这就是双指针技巧的核心思想 - 通过两个指针的配合来解决问题。

💡 重要提示:双指针用在数组题目时,数组必须是有序的。这就像在字典中查找单词,只有按字母排序才能通过移动手指快速定位。

2. 链表中的双指针应用

2.1 虚拟头节点技巧

虚拟头节点就像是链表的"引导员",它站在链表的最前面:

  • 统一了所有节点的操作逻辑
  • 不用再特殊处理第一个节点
  • 让代码更简洁优雅
js
/**
 * LeetCode 86: 分隔链表
 * @param {ListNode} head - 链表头节点
 * @param {number} x - 分隔的基准值
 * @return {ListNode} - 重组后的链表
 */
function partition(head, x) {
    // 创建两个虚拟头节点
    const dummy1 = new ListNode(-1);  // 存放小于x的节点
    const dummy2 = new ListNode(-1);  // 存放大于等于x的节点
    
    // 两个工作指针,就像两个分拣员
    let p1 = dummy1;  // 负责小于x的链表
    let p2 = dummy2;  // 负责大于等于x的链表
    
    // 遍历原链表,像流水线一样处理每个节点
    let p = head;
    while (p !== null) {
        // 根据节点值进行分类
        if (p.val < x) {
            p1.next = p;
            p1 = p1.next;
        } else {
            p2.next = p;
            p2 = p2.next;
        }
        // 断开原链接,准备处理下一个
        const temp = p.next;
        p.next = null;
        p = temp;
    }
    
    // 连接两个链表,就像拼接两条绳子
    p1.next = dummy2.next;
    return dummy1.next;
}

2.2 快慢指针技巧

想象两个人在跑步:

  • 小明速度快,一次跑两步
  • 小红速度慢,一次跑一步
  • 他们之间的关系可以帮我们解决很多问题
a) 寻找链表中点
js
/**
 * 查找链表的中点
 * @param {ListNode} head - 链表头节点
 * @return {ListNode} - 中点节点
 */
function findMiddle(head) {
    // 快慢指针初始化
    let slow = head;  // 慢指针小红
    let fast = head;  // 快指针小明
    
    // 当小明能跑时,就继续跑
    while (fast !== null && fast.next !== null) {
        slow = slow.next;       // 小红跑1步
        fast = fast.next.next;  // 小明跑2步
    }
    
    // 当小明跑到终点时,小红正好在中点
    return slow;
}
b) 判断链表是否有环
js
/**
 * 检测链表是否有环
 * @param {ListNode} head - 链表头节点
 * @return {boolean} - 是否存在环
 */
function hasCycle(head) {
    // 快慢指针初始化
    let slow = head;
    let fast = head;
    
    // 想象在跑圈
    while (fast !== null && fast.next !== null) {
        slow = slow.next;       // 慢跑者
        fast = fast.next.next;  // 快跑者
        
        // 如果是在跑圈,快跑者总会追上慢跑者
        if (slow === fast) {
            return true;
        }
    }
    
    // 如果快跑者到终点了,说明不是环形跑道
    return false;
}

3. 实战应用场景 💡

在前端开发中,双指针技巧的应用场景:

  1. 虚拟列表优化
js
// 使用双指针维护可视区域
let start = 0;  // 可视区域起点
let end = visibleCount;  // 可视区域终点

function updateVisibleItems() {
    // 根据滚动位置更新双指针
    start = Math.floor(scrollTop / itemHeight);
    end = start + visibleCount;
    
    // 渲染可视区域的内容
    renderItems(start, end);
}
  1. 轮播图实现
js
class Carousel {
    constructor() {
        this.current = 0;  // 当前显示的图片
        this.prev = -1;    // 上一张图片
    }
    
    slide() {
        // 使用双指针记录图片切换状态
        this.prev = this.current;
        this.current = (this.current + 1) % total;
        
        // 执行动画
        animate(this.prev, this.current);
    }
}

4. 性能优化建议 ⚡

场景优化建议
数组操作1. 优先考虑是否可以排序后使用双指针
2. 注意边界条件的处理
链表操作1. 使用虚拟头节点简化代码
2. 先画图理清指针关系
代码实现1. 指针命名要有意义
2. 关键步骤加注释

5. 常见误区提醒 ⚠️

  1. 忘记判断指针是否为null
  2. 链表操作时忘记断开原有连接
  3. 快慢指针初始位置设置错误
  4. 边界条件考虑不周全

二、滑动窗口技巧精讲 🪟

1. 什么是滑动窗口? 🤔

想象你在看风景:

  • 你的视野就是一个"窗口"
  • 窗口可以左右滑动来观察不同的景色
  • 通过控制窗口的大小和移动来找到最佳风景

这就是滑动窗口的核心思想 - 通过维护一个可变窗口来解决子串/子数组的问题。

💡 关键点:滑动窗口通常用于处理连续的序列问题,比如:

  • 寻找最长的不重复子串
  • 寻找包含特定字符的最小子串
  • 寻找定长子数组的最大和

2. 滑动窗口框架 📝

js
/**
 * 滑动窗口算法框架
 * @param {string} s - 待处理的字符串
 * @return {void}
 */
function slidingWindow(s) {
    // 1. 初始化窗口计数器
    const window = new Map();
    
    // 2. 定义双指针
    let left = 0;      // 窗口左边界
    let right = 0;     // 窗口右边界
    
    // 3. 定义其他需要的变量
    let valid = 0;     // 窗口中的有效数据量
    let start = 0;     // 结果子串的起始位置
    let len = Infinity; // 结果子串的长度
    
    // 4. 移动右边界,扩大窗口
    while (right < s.length) {
        // 即将进入窗口的字符
        const c = s[right];
        right++;  // 扩大窗口
        
        // 5. 进行窗口内数据的更新
        updateWindow(c);
        
        // 6. 判断左侧窗口是否要收缩
        while (windowNeedsShrink()) {
            // 即将移出窗口的字符
            const d = s[left];
            left++;  // 缩小窗口
            
            // 7. 进行窗口内数据的更新
            updateWindow(d);
        }
    }
}

3. 实战案例:最小覆盖子串 💡

js
/**
 * LeetCode 76: 最小覆盖子串
 * @param {string} s - 源字符串
 * @param {string} t - 目标字符串
 * @return {string} - 最小覆盖子串
 */
function minWindow(s, t) {
    // 1. 统计目标字符串中字符出现次数
    const needs = new Map();
    for (let c of t) {
        needs.set(c, (needs.get(c) || 0) + 1);
    }
    
    // 2. 初始化窗口计数器
    const window = new Map();
    
    // 3. 定义变量
    let left = 0, right = 0;
    let valid = 0;  // 已经匹配的字符数
    let start = 0, len = Infinity;
    
    // 4. 移动右边界
    while (right < s.length) {
        // 进入窗口的字符
        const c = s[right];
        right++;
        
        // 5. 更新窗口数据
        if (needs.has(c)) {
            window.set(c, (window.get(c) || 0) + 1);
            if (window.get(c) === needs.get(c)) {
                valid++;  // 字符 c 的出现次数符合要求
            }
        }
        
        // 6. 判断是否需要收缩窗口
        while (valid === needs.size) {
            // 更新最小覆盖子串
            if (right - left < len) {
                start = left;
                len = right - left;
            }
            
            // 移出窗口的字符
            const d = s[left];
            left++;
            
            // 7. 更新窗口数据
            if (needs.has(d)) {
                if (window.get(d) === needs.get(d)) {
                    valid--;  // 字符 d 的出现次数不再符合要求
                }
                window.set(d, window.get(d) - 1);
            }
        }
    }
    
    return len === Infinity ? "" : s.substr(start, len);
}

4. 前端实战应用 💻

  1. 图片懒加载
js
class LazyLoader {
    constructor(images) {
        this.images = images;
        this.windowStart = 0;  // 可视区域起始位置
        this.windowEnd = window.innerHeight;  // 可视区域结束位置
    }
    
    checkVisible() {
        this.images.forEach(img => {
            const rect = img.getBoundingClientRect();
            // 使用滑动窗口思想判断图片是否在可视区域
            if (rect.top >= this.windowStart && rect.bottom <= this.windowEnd) {
                this.loadImage(img);
            }
        });
    }
}
  1. 无限滚动列表
js
class InfiniteScroll {
    constructor() {
        this.data = [];
        this.visibleWindow = {
            start: 0,
            size: 20  // 窗口大小
        };
    }
    
    updateVisibleItems() {
        // 根据滚动位置更新窗口
        const scrollTop = document.scrollingElement.scrollTop;
        this.visibleWindow.start = Math.floor(scrollTop / itemHeight);
        
        // 只渲染窗口内的数据
        this.renderItems(
            this.data.slice(
                this.visibleWindow.start,
                this.visibleWindow.start + this.visibleWindow.size
            )
        );
    }
}

5. 性能优化建议 ⚡

优化点具体建议
空间复杂度1. 使用Map/Set代替数组存储窗口数据
2. 及时清理不需要的窗口数据
时间复杂度1. 避免不必要的窗口扩张和收缩
2. 合理设置窗口的初始大小
代码优化1. 抽象公共的窗口操作函数
2. 使用常量存储固定的窗口大小

6. 常见误区 ⚠️

  1. 窗口更新时机选择错误
  2. 忘记维护窗口内的计数器
  3. 窗口收缩条件设置不当
  4. 边界条件处理不当

三、单调栈技巧精讲 📚

1. 什么是单调栈?🤔

想象你在看一排人站队:

  • 如果按照身高从矮到高排,这就是一个单调递增的队伍
  • 如果按照身高从高到矮排,这就是一个单调递减的队伍
  • 单调栈就是维护这样一个单调序列的数据结构

💡 核心特点:栈内元素始终保持单调递增或单调递减。当新元素破坏这个单调性时,就需要弹出栈顶元素,直到恢复单调性。

2. 单调栈的应用场景

主要用于解决以下类型的问题:

  • 寻找下一个更大元素
  • 寻找下一个更小元素
  • 寻找前一个更大元素
  • 寻找前一个更小元素

3. 经典问题:每日温度 🌡️

js
/**
 * LeetCode 739: 每日温度
 * @param {number[]} temperatures - 温度数组
 * @return {number[]} - 等待天数数组
 */
function dailyTemperatures(temperatures) {
    const n = temperatures.length;
    // 存放结果:对于每个温度,需要等几天才能遇到更高的温度
    const res = new Array(n).fill(0);
    // 单调栈:存放温度的索引,栈内温度单调递减
    const stack = [];
    
    // 从左到右遍历温度数组
    for (let i = 0; i < n; i++) {
        // 当前温度
        const currTemp = temperatures[i];
        
        // 如果栈不为空且当前温度大于栈顶温度
        // 说明找到了一个"更暖和的日子"
        while (
            stack.length > 0 && 
            currTemp > temperatures[stack[stack.length - 1]]
        ) {
            // 获取栈顶温度的索引
            const prevIndex = stack.pop();
            // 计算等待天数
            res[prevIndex] = i - prevIndex;
        }
        
        // 将当前温度的索引入栈
        stack.push(i);
    }
    
    return res;
}

// 使用示例
const temperatures = [73, 74, 75, 71, 69, 72, 76, 73];
console.log(dailyTemperatures(temperatures));
// 输出: [1, 1, 4, 2, 1, 1, 0, 0]

4. 接雨水问题 🌧️

js
/**
 * LeetCode 42: 接雨水
 * @param {number[]} height - 柱子高度数组
 * @return {number} - 可以接的雨水总量
 */
function trap(height) {
    let stack = [];  // 单调递减栈,存放柱子的索引
    let total = 0;   // 总雨水量
    
    // 遍历每个柱子
    for (let i = 0; i < height.length; i++) {
        // 当前柱子可以接住之前的水
        while (
            stack.length > 0 && 
            height[i] > height[stack[stack.length - 1]]
        ) {
            // 获取凹槽底部的索引
            const bottom = stack.pop();
            
            // 如果栈空了,说明没有左边界,无法接水
            if (stack.length === 0) break;
            
            // 计算凹槽的左右边界
            const left = stack[stack.length - 1];
            const right = i;
            
            // 计算水的高度和宽度
            const h = Math.min(height[left], height[right]) - height[bottom];
            const w = right - left - 1;
            
            // 累加这个凹槽能接的水
            total += h * w;
        }
        
        stack.push(i);
    }
    
    return total;
}

5. 前端实战应用 💻

  1. 股票价格监控
js
class StockMonitor {
    constructor() {
        this.priceStack = [];  // 价格单调栈
        this.timeStack = [];   // 对应的时间戳
    }
    
    // 监控价格变化
    updatePrice(price, timestamp) {
        // 找到第一个比当前价格高的历史价格
        while (
            this.priceStack.length && 
            price > this.priceStack[this.priceStack.length - 1]
        ) {
            this.priceStack.pop();
            const prevTime = this.timeStack.pop();
            this.notifyPriceIncrease(prevTime, timestamp);
        }
        
        this.priceStack.push(price);
        this.timeStack.push(timestamp);
    }
}
  1. 任务优先级队列
js
class TaskQueue {
    constructor() {
        this.priorityStack = [];  // 优先级单调递减栈
        this.tasks = [];          // 对应的任务
    }
    
    addTask(task, priority) {
        // 高优先级任务会打断之前的低优先级任务
        while (
            this.priorityStack.length && 
            priority > this.priorityStack[this.priorityStack.length - 1]
        ) {
            this.priorityStack.pop();
            const prevTask = this.tasks.pop();
            this.pauseTask(prevTask);
        }
        
        this.priorityStack.push(priority);
        this.tasks.push(task);
        this.executeTask(task);
    }
}

6. 性能优化建议 ⚡

场景优化建议
空间使用1. 只存储必要的信息(如索引而非完整元素)
2. 及时清理不再需要的栈元素
时间效率1. 选择合适的遍历方向(正向/反向)
2. 避免不必要的栈操作
代码实现1. 使用数组模拟栈操作
2. 预先分配合适的数组大小

7. 常见误区 ⚠️

  1. 栈的单调性选择错误(递增/递减)
  2. 忘记处理栈为空的情况
  3. 入栈/出栈时机判断失误
  4. 结果计算逻辑错误

8. 练习题推荐 📝

  1. LeetCode 496: 下一个更大元素 I
  2. LeetCode 503: 下一个更大元素 II
  3. LeetCode 84: 柱状图中最大的矩形
  4. LeetCode 85: 最大矩形

四、前缀和技巧精讲 📊

1. 什么是前缀和?🤔

想象你是一个会计:

  • 需要经常计算从1月到某个月的总收入
  • 与其每次都重新计算,不如提前记录每个月的累计收入
  • 这就是前缀和的思想:提前计算累加值,以便快速查询区间和

💡 核心思想:prefix[i] 表示数组 nums[0..i-1] 的累加和。通过 prefix[j] - prefix[i] 可以快速得到 nums[i..j-1] 的区间和。

2. 一维前缀和实现

js
/**
 * 前缀和数组构建与查询
 */
class PrefixSum {
    /**
     * 构建前缀和数组
     * @param {number[]} nums - 原始数组
     */
    constructor(nums) {
        const n = nums.length;
        // 前缀和数组比原数组多一位,便于计算
        this.prefix = new Array(n + 1).fill(0);
        
        // 构建前缀和数组
        for (let i = 1; i <= n; i++) {
            this.prefix[i] = this.prefix[i-1] + nums[i-1];
        }
    }
    
    /**
     * 查询闭区间 [i,j] 的区间和
     * @param {number} i - 起始索引
     * @param {number} j - 结束索引
     * @return {number} - 区间和
     */
    query(i, j) {
        return this.prefix[j + 1] - this.prefix[i];
    }
}

// 使用示例
const nums = [3, 5, 2, -2, 4, 1];
const prefixSum = new PrefixSum(nums);
console.log(prefixSum.query(0, 2));  // 输出: 10 (3+5+2)
console.log(prefixSum.query(2, 5));  // 输出: 5 (2-2+4+1)

3. 二维前缀和实现

js
/**
 * 二维前缀和数组构建与查询
 */
class PrefixSum2D {
    /**
     * 构建二维前缀和数组
     * @param {number[][]} matrix - 原始矩阵
     */
    constructor(matrix) {
        if (matrix.length === 0 || matrix[0].length === 0) return;
        
        const m = matrix.length;
        const n = matrix[0].length;
        // 构建(m+1)*(n+1)的前缀和数组,便于计算
        this.prefix = Array.from(
            {length: m + 1}, 
            () => new Array(n + 1).fill(0)
        );
        
        // 计算二维前缀和
        for (let i = 1; i <= m; i++) {
            for (let j = 1; j <= n; j++) {
                this.prefix[i][j] = (
                    this.prefix[i-1][j] + 
                    this.prefix[i][j-1] - 
                    this.prefix[i-1][j-1] + 
                    matrix[i-1][j-1]
                );
            }
        }
    }
    
    /**
     * 查询子矩阵的和 [(r1,c1), (r2,c2)]
     * @param {number} r1 - 左上角行索引
     * @param {number} c1 - 左上角列索引
     * @param {number} r2 - 右下角行索引
     * @param {number} c2 - 右下角列索引
     * @return {number} - 子矩阵的和
     */
    sumRegion(r1, c1, r2, c2) {
        return (
            this.prefix[r2 + 1][c2 + 1] - 
            this.prefix[r2 + 1][c1] - 
            this.prefix[r1][c2 + 1] + 
            this.prefix[r1][c1]
        );
    }
}

4. 前端实战应用 💻

  1. 性能监控数据统计
js
class PerformanceMonitor {
    constructor() {
        this.timeData = [];
        this.prefixSum = null;
    }
    
    // 记录性能数据
    record(timeSpent) {
        this.timeData.push(timeSpent);
        // 重新构建前缀和
        this.prefixSum = new PrefixSum(this.timeData);
    }
    
    // 查询某段时间的性能统计
    getTimeRange(start, end) {
        return {
            totalTime: this.prefixSum.query(start, end),
            avgTime: this.prefixSum.query(start, end) / (end - start + 1)
        };
    }
}
  1. 图片像素数据处理
js
class ImageProcessor {
    constructor(pixelData) {
        // 构建像素数据的二维前缀和
        this.prefixSum2D = new PrefixSum2D(pixelData);
    }
    
    // 计算区域平均亮度
    getRegionBrightness(x1, y1, x2, y2) {
        const sum = this.prefixSum2D.sumRegion(x1, y1, x2, y2);
        const area = (x2 - x1 + 1) * (y2 - y1 + 1);
        return sum / area;
    }
}

5. 性能优化建议 ⚡

场景优化建议
空间优化1. 对于静态数据,一次性构建前缀和
2. 对于动态数据,权衡更新成本
时间优化1. 避免频繁重建前缀和数组
2. 合理设计查询接口
内存使用1. 及时释放不需要的前缀和数组
2. 考虑使用类型数组优化

6. 常见误区 ⚠️

  1. 忽略数组越界问题
  2. 前缀和数组大小设置错误
  3. 区间计算时索引错误
  4. 更新操作处理不当

7. 练习题推荐 📝

  1. LeetCode 303: 区域和检索 - 数组不可变
  2. LeetCode 304: 二维区域和检索 - 矩阵不可变
  3. LeetCode 560: 和为K的子数组
  4. LeetCode 1074: 元素和为目标值的子矩阵数量

五、差分数组技巧精讲 📊

1. 什么是差分数组?🤔

想象你是一个人事经理:

  • 每个月都有员工入职和离职
  • 你需要知道每个月的员工总数变化
  • 与其每次都重新统计,不如记录每月的人数变化(增减)

这就是差分数组的思想:记录相邻元素的差值,用于高效处理区间增减操作。

💡 核心思想:diff[i] 表示 nums[i]nums[i-1] 的差值。当我们要对区间 [i,j] 进行增减操作时,只需要修改 diff[i]diff[j+1] 两个位置。

2. 差分数组实现

js
/**
 * 差分数组的实现
 */
class Difference {
    /**
     * 构造差分数组
     * @param {number[]} nums - 原始数组
     */
    constructor(nums) {
        // 差分数组
        this.diff = new Array(nums.length);
        
        // 构建差分数组
        this.diff[0] = nums[0];
        for (let i = 1; i < nums.length; i++) {
            this.diff[i] = nums[i] - nums[i - 1];
        }
    }
    
    /**
     * 对区间[i,j]增加val
     * @param {number} i - 起始索引
     * @param {number} j - 结束索引
     * @param {number} val - 增加的值
     */
    increment(i, j, val) {
        this.diff[i] += val;  // 起始位置增加val
        if (j + 1 < this.diff.length) {
            this.diff[j + 1] -= val;  // 结束位置后减少val
        }
    }
    
    /**
     * 返回结果数组
     * @return {number[]} - 结果数组
     */
    result() {
        const res = new Array(this.diff.length);
        res[0] = this.diff[0];
        // 根据差分数组还原结果
        for (let i = 1; i < this.diff.length; i++) {
            res[i] = res[i - 1] + this.diff[i];
        }
        return res;
    }
}

// 使用示例
const nums = [8, 2, 6, 3, 1];
const diff = new Difference(nums);
diff.increment(1, 3, 2);  // 对区间[1,3]增加2
console.log(diff.result());  // [8, 4, 8, 5, 1]

3. 前端实战应用 💻

  1. 航班预订统计
js
class FlightBooking {
    constructor(n) {
        this.diff = new Difference(new Array(n).fill(0));
    }
    
    // 预订航班
    book(start, end, seats) {
        // 对区间[start, end]增加seats个座位
        this.diff.increment(start, end, seats);
    }
    
    // 获取当前航班预订情况
    getBookings() {
        return this.diff.result();
    }
}
  1. 动画帧变化计算
js
class AnimationController {
    constructor(duration) {
        this.diff = new Difference(new Array(duration).fill(0));
    }
    
    // 添加动画效果
    addEffect(startFrame, endFrame, value) {
        this.diff.increment(startFrame, endFrame, value);
    }
    
    // 获取每一帧的状态
    getFrameStates() {
        return this.diff.result();
    }
}

4. 性能优化建议 ⚡

场景优化建议
空间优化1. 原地修改差分数组
2. 避免不必要的数组拷贝
时间优化1. 批量处理区间操作
2. 延迟计算结果数组
实现优化1. 使用类型数组提升性能
2. 预分配合适的数组大小

5. 常见误区 ⚠️

  1. 差分数组大小设置错误
  2. 区间边界处理不当
  3. 忘记处理负数情况
  4. 结果还原时计算错误

6. 实战技巧 💡

  1. 处理区间重叠
js
// 合并重叠区间的差分处理
function mergeIntervals(intervals, values) {
    const diff = new Difference(new Array(maxEnd + 1).fill(0));
    intervals.forEach((interval, i) => {
        diff.increment(interval[0], interval[1], values[i]);
    });
    return diff.result();
}
  1. 处理循环数组
js
// 处理循环数组的差分
function handleCircularArray(nums, operations) {
    // 复制一份数组处理循环情况
    const extendedDiff = new Difference([...nums, ...nums]);
    operations.forEach(op => {
        const [start, end, val] = op;
        if (start <= end) {
            extendedDiff.increment(start, end, val);
        } else {
            // 处理跨越数组末尾的情况
            extendedDiff.increment(start, nums.length - 1, val);
            extendedDiff.increment(0, end, val);
        }
    });
    return extendedDiff.result().slice(0, nums.length);
}

7. 练习题推荐 📝

  1. LeetCode 1109: 航班预订统计
  2. LeetCode 1094: 拼车
  3. LeetCode 1589: 所有排列中的最大和
  4. LeetCode 370: 区间加法

六、二维数组遍历技巧精讲 🎯

1. 创建二维数组 🏗️

💡 在JavaScript中创建二维数组需要特别注意引用类型的陷阱。

1.1 常见创建方法对比

js
/**
 * 方法1: 使用 fill + for循环(推荐)
 * 优点:避免引用类型陷阱
 * 缺点:代码较长
 */
function createArray1(rows, cols) {
    let arr = new Array(rows).fill(0);
    for (let i = 0; i < rows; i++) {
        arr[i] = new Array(cols).fill(1);
    }
    return arr;
}

/**
 * 方法2: 两层for循环(推荐)
 * 优点:最直观,最安全
 * 缺点:代码较长
 */
function createArray2(rows, cols) {
    let arr = new Array();
    for (let i = 0; i < rows; i++) {
        arr[i] = new Array();
        for (let j = 0; j < cols; j++) {
            arr[i][j] = 1;
        }
    }
    return arr;
}

/**
 * 方法3: Array.from + fill(推荐)
 * 优点:代码简洁
 * 缺点:需要理解Array.from的用法
 */
function createArray3(rows, cols) {
    return Array.from(new Array(rows), () => new Array(cols).fill(1));
}

/**
 * 方法4: fill + map(不推荐)
 * 警告:可能导致引用类型问题
 */
function createArray4(rows, cols) {
    return new Array(rows).fill(new Array(cols).fill(0));
}

1.2 引用类型陷阱示例

js
// ❌ 错误示例
const matrix = new Array(3).fill(new Array(3).fill(0));
matrix[0][0] = 1;
console.log(matrix);
// 输出: [[1,0,0], [1,0,0], [1,0,0]]
// 所有行都被修改,因为它们引用同一个数组!

// ✅ 正确示例
const matrix = Array.from(new Array(3), () => new Array(3).fill(0));
matrix[0][0] = 1;
console.log(matrix);
// 输出: [[1,0,0], [0,0,0], [0,0,0]]

2. 遍历技巧 🔄

2.1 常见遍历模式

js
/**
 * 1. 按行遍历(最常用)
 * @param {number[][]} matrix
 */
function traverseByRow(matrix) {
    const rows = matrix.length;
    const cols = matrix[0].length;
    
    for (let i = 0; i < rows; i++) {
        for (let j = 0; j < cols; j++) {
            console.log(matrix[i][j]);
        }
    }
}

/**
 * 2. 按列遍历
 * @param {number[][]} matrix
 */
function traverseByCol(matrix) {
    const rows = matrix.length;
    const cols = matrix[0].length;
    
    for (let j = 0; j < cols; j++) {
        for (let i = 0; i < rows; i++) {
            console.log(matrix[i][j]);
        }
    }
}

/**
 * 3. 对角线遍历
 * @param {number[][]} matrix
 */
function traverseDiagonal(matrix) {
    const rows = matrix.length;
    const cols = matrix[0].length;
    
    // 遍历主对角线及上半部分
    for (let i = 0; i < rows; i++) {
        let row = i, col = 0;
        while (row >= 0 && col < cols) {
            console.log(matrix[row][col]);
            row--;
            col++;
        }
    }
}

2.2 矩阵旋转

js
/**
 * 矩阵顺时针旋转90度
 * @param {number[][]} matrix
 */
function rotate(matrix) {
    const n = matrix.length;
    
    // 1. 先沿对角线翻转
    for (let i = 0; i < n; i++) {
        for (let j = i; j < n; j++) {
            [matrix[i][j], matrix[j][i]] = 
            [matrix[j][i], matrix[i][j]];
        }
    }
    
    // 2. 再左右翻转
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < n/2; j++) {
            [matrix[i][j], matrix[i][n-1-j]] = 
            [matrix[i][n-1-j], matrix[i][j]];
        }
    }
}

2.3 螺旋遍历

js
/**
 * 矩阵螺旋遍历
 * @param {number[][]} matrix
 * @return {number[]}
 */
function spiralOrder(matrix) {
    if (!matrix.length) return [];
    
    const result = [];
    let top = 0,
        bottom = matrix.length - 1,
        left = 0,
        right = matrix[0].length - 1;
    
    while (result.length < matrix.length * matrix[0].length) {
        // 向右遍历
        if (top <= bottom) {
            for (let i = left; i <= right; i++) {
                result.push(matrix[top][i]);
            }
            top++;
        }
        
        // 向下遍历
        if (left <= right) {
            for (let i = top; i <= bottom; i++) {
                result.push(matrix[i][right]);
            }
            right--;
        }
        
        // 向左遍历
        if (top <= bottom) {
            for (let i = right; i >= left; i--) {
                result.push(matrix[bottom][i]);
            }
            bottom--;
        }
        
        // 向上遍历
        if (left <= right) {
            for (let i = bottom; i >= top; i--) {
                result.push(matrix[i][left]);
            }
            left++;
        }
    }
    
    return result;
}

3. 前端实战应用 💻

  1. 图片处理
js
class ImageMatrix {
    constructor(imageData) {
        this.matrix = this.createImageMatrix(imageData);
    }
    
    // 图片旋转
    rotate90Degrees() {
        rotate(this.matrix);
        return this;
    }
    
    // 图片区域裁剪
    crop(startRow, startCol, endRow, endCol) {
        const result = [];
        for (let i = startRow; i <= endRow; i++) {
            result.push(
                this.matrix[i].slice(startCol, endCol + 1)
            );
        }
        return result;
    }
}
  1. 游戏地图
js
class GameMap {
    constructor(rows, cols) {
        this.map = Array.from(
            new Array(rows), 
            () => new Array(cols).fill(0)
        );
    }
    
    // 获取某点周围的元素
    getSurrounding(row, col) {
        const directions = [
            [-1,-1], [-1,0], [-1,1],
            [0,-1],          [0,1],
            [1,-1],  [1,0],  [1,1]
        ];
        
        return directions
            .map(([dx, dy]) => {
                const newRow = row + dx;
                const newCol = col + dy;
                if (this.isValid(newRow, newCol)) {
                    return this.map[newRow][newCol];
                }
                return null;
            })
            .filter(val => val !== null);
    }
    
    isValid(row, col) {
        return row >= 0 && 
               row < this.map.length && 
               col >= 0 && 
               col < this.map[0].length;
    }
}

4. 性能优化建议 ⚡

场景优化建议
内存优化1. 使用类型数组(TypedArray)
2. 及时释放不用的数组空间
遍历优化1. 选择合适的遍历顺序
2. 避免重复遍历
边界处理1. 统一的边界检查函数
2. 预处理边界条件

5. 常见误区 ⚠️

  1. 创建二维数组时的引用类型问题
  2. 边界条件处理不当
  3. 行列索引混淆
  4. 遍历顺序选择不当

6. 练习题推荐 📝

  1. LeetCode 48: 旋转图像
  2. LeetCode 54: 螺旋矩阵
  3. LeetCode 73: 矩阵置零
  4. LeetCode 240: 搜索二维矩阵 II

七、堆(Heap)数据结构精讲 🌳

1. 什么是堆?🤔

想象一个公司的组织架构:

  • CEO在最顶层
  • 经理在中层
  • 普通员工在底层 这就像一个最大堆,每个上级都比下级"大"。

💡 堆是一个完全二叉树,分为最大堆和最小堆:

  • 最大堆:父节点总是大于等于子节点
  • 最小堆:父节点总是小于等于子节点

2. 堆的实现

2.1 最大堆实现

js
/**
 * 最大堆的实现
 */
class MaxHeap {
    constructor() {
        this.heap = [];
    }
    
    /**
     * 获取父节点索引
     * @param {number} i - 当前节点索引
     * @return {number} - 父节点索引
     */
    getParentIndex(i) {
        return Math.floor((i - 1) / 2);
    }
    
    /**
     * 获取左子节点索引
     * @param {number} i - 当前节点索引
     * @return {number} - 左子节点索引
     */
    getLeftIndex(i) {
        return 2 * i + 1;
    }
    
    /**
     * 获取右子节点索引
     * @param {number} i - 当前节点索引
     * @return {number} - 右子节点索引
     */
    getRightIndex(i) {
        return 2 * i + 2;
    }
    
    /**
     * 交换两个节点
     * @param {number} i1 - 第一个节点索引
     * @param {number} i2 - 第二个节点索引
     */
    swap(i1, i2) {
        [this.heap[i1], this.heap[i2]] = 
        [this.heap[i2], this.heap[i1]];
    }
    
    /**
     * 向上调整
     * @param {number} index - 需要调整的节点索引
     */
    shiftUp(index) {
        while (index > 0) {
            const parentIndex = this.getParentIndex(index);
            if (this.heap[parentIndex] >= this.heap[index]) {
                break;
            }
            this.swap(index, parentIndex);
            index = parentIndex;
        }
    }
    
    /**
     * 向下调整
     * @param {number} index - 需要调整的节点索引
     */
    shiftDown(index) {
        const leftIndex = this.getLeftIndex(index);
        const rightIndex = this.getRightIndex(index);
        let largest = index;
        
        if (leftIndex < this.heap.length && 
            this.heap[leftIndex] > this.heap[largest]) {
            largest = leftIndex;
        }
        
        if (rightIndex < this.heap.length && 
            this.heap[rightIndex] > this.heap[largest]) {
            largest = rightIndex;
        }
        
        if (largest !== index) {
            this.swap(index, largest);
            this.shiftDown(largest);
        }
    }
    
    /**
     * 插入新值
     * @param {number} value - 要插入的值
     */
    insert(value) {
        this.heap.push(value);
        this.shiftUp(this.heap.length - 1);
    }
    
    /**
     * 删除并返回最大值
     * @return {number} - 堆顶的最大值
     */
    extractMax() {
        if (this.heap.length === 0) return null;
        if (this.heap.length === 1) return this.heap.pop();
        
        const max = this.heap[0];
        this.heap[0] = this.heap.pop();
        this.shiftDown(0);
        
        return max;
    }
}

3. 前端实战应用 💻

  1. 任务优先级队列
js
class PriorityTaskQueue {
    constructor() {
        this.maxHeap = new MaxHeap();
    }
    
    /**
     * 添加任务
     * @param {Object} task - 任务对象
     * @param {number} priority - 优先级
     */
    addTask(task, priority) {
        this.maxHeap.insert({
            task,
            priority
        });
    }
    
    /**
     * 执行最高优先级任务
     */
    executeHighestPriorityTask() {
        const taskObj = this.maxHeap.extractMax();
        if (taskObj) {
            console.log(`执行任务,优先级:${taskObj.priority}`);
            taskObj.task();
        }
    }
}
  1. 前K个最热门商品
js
class TopKProducts {
    constructor(k) {
        this.k = k;
        this.minHeap = new MinHeap();
    }
    
    /**
     * 添加商品
     * @param {Object} product - 商品对象
     * @param {number} heat - 热度值
     */
    addProduct(product, heat) {
        if (this.minHeap.size() < this.k) {
            this.minHeap.insert({ product, heat });
        } else if (heat > this.minHeap.peek().heat) {
            this.minHeap.extractMin();
            this.minHeap.insert({ product, heat });
        }
    }
    
    /**
     * 获取最热门的K个商品
     */
    getTopK() {
        return this.minHeap.heap
            .map(item => item.product)
            .sort((a, b) => b.heat - a.heat);
    }
}

4. 性能优化建议 ⚡

场景优化建议
空间优化1. 使用数组实现而非指针
2. 预分配合适的数组大小
时间优化1. 批量建堆而非逐个插入
2. 避免频繁的堆调整
实现优化1. 使用迭代代替递归
2. 优化比较函数

5. 常见误区 ⚠️

  1. 混淆最大堆和最小堆的性质
  2. 忘记维护堆的完全二叉树特性
  3. 堆化过程中的索引计算错误
  4. 删除操作后忘记调整堆

6. 练习题推荐 📝

  1. LeetCode 215: 数组中的第K个最大元素
  2. LeetCode 347: 前K个高频元素
  3. LeetCode 23: 合并K个排序链表
  4. LeetCode 295: 数据流的中位数

八、算法技巧精讲 🎯

1. 预处理数组技巧 📊

💡 核心思想:空间换时间,通过提前计算和存储信息来加速查询。

js
/**
 * 示例:前缀和预处理
 * 场景:频繁查询数组区间和
 */
class RangeSum {
    constructor(nums) {
        this.preSum = new Array(nums.length + 1).fill(0);
        // 预处理前缀和
        for (let i = 0; i < nums.length; i++) {
            this.preSum[i + 1] = this.preSum[i] + nums[i];
        }
    }
    
    /**
     * 查询区间和
     * @param {number} left - 左边界
     * @param {number} right - 右边界
     * @return {number} - 区间和
     */
    queryRange(left, right) {
        return this.preSum[right + 1] - this.preSum[left];
    }
}

// 使用示例
const rangeSum = new RangeSum([1, 2, 3, 4, 5]);
console.log(rangeSum.queryRange(1, 3)); // 输出: 9 (2+3+4)

2. 等概率生成器 🎲

js
/**
 * 将非等概率转换为等概率
 * 核心思想:通过组合两次调用消除概率倾斜
 */
class ProbabilityGenerator {
    /**
     * 原始函数:以p概率返回1,1-p概率返回0
     */
    random() {
        return Math.random() < 0.7 ? 1 : 0; // 假设p=0.7
    }
    
    /**
     * 转换为等概率0/1生成器
     */
    generateEqual() {
        let result;
        do {
            const a = this.random();
            const b = this.random();
            // 01组合返回0,10组合返回1
            if (a === 0 && b === 1) {
                result = 0;
                break;
            }
            if (a === 1 && b === 0) {
                result = 1;
                break;
            }
            // 00和11的情况继续循环
        } while (true);
        
        return result;
    }
}

3. 打表法技巧 📝

js
/**
 * 打表法示例:计算斐波那契数列
 * 适用场景:输入范围小,计算规律明显
 */
class FibonacciTable {
    constructor(maxN) {
        this.table = new Map();
        // 预计算所有可能的结果
        this.generateTable(maxN);
    }
    
    generateTable(n) {
        let a = 0, b = 1;
        this.table.set(0, a);
        this.table.set(1, b);
        
        for (let i = 2; i <= n; i++) {
            const c = (a + b) % 1000000007;
            this.table.set(i, c);
            a = b;
            b = c;
        }
    }
    
    /**
     * O(1)时间复杂度获取结果
     */
    getFib(n) {
        return this.table.get(n);
    }
}

4. 子串子数组技巧 📊

js
/**
 * 示例:最长递增子数组
 * 核心思想:以每个位置结尾的最优解
 */
function longestIncreasingSubarray(nums) {
    if (!nums.length) return 0;
    
    // dp[i]表示以nums[i]结尾的最长递增子数组长度
    const dp = new Array(nums.length).fill(1);
    let maxLen = 1;
    
    for (let i = 1; i < nums.length; i++) {
        // 如果当前数字大于前一个数字,可以接在前面的子数组后面
        if (nums[i] > nums[i - 1]) {
            dp[i] = dp[i - 1] + 1;
            maxLen = Math.max(maxLen, dp[i]);
        }
    }
    
    return maxLen;
}

5. Map的巧妙应用 🗺️

js
/**
 * 示例:两数之和
 * 核心思想:将查找问题转化为求差问题
 */
function twoSum(nums, target) {
    const map = new Map();
    
    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];
        
        if (map.has(complement)) {
            return [map.get(complement), i];
        }
        
        map.set(nums[i], i);
    }
    
    return [];
}

/**
 * 示例:连续子数组和
 * 核心思想:前缀和 + Map
 */
function checkSubarraySum(nums, k) {
    const map = new Map();
    map.set(0, -1);
    let sum = 0;
    
    for (let i = 0; i < nums.length; i++) {
        sum += nums[i];
        // 当k不为0时,我们关心的是余数
        const remainder = k !== 0 ? sum % k : sum;
        
        if (map.has(remainder)) {
            if (i - map.get(remainder) >= 2) {
                return true;
            }
        } else {
            map.set(remainder, i);
        }
    }
    
    return false;
}

6. 性能优化建议 ⚡

技巧类型优化建议
预处理数组1. 合理选择预处理的数据范围
2. 权衡空间成本和查询效率
概率生成器1. 尽量减少循环次数
2. 考虑边界情况的处理
打表法1. 仅在输入范围有限时使用
2. 注意表的大小和生成成本
子数组处理1. 善用动态规划思想
2. 考虑滑动窗口优化

7. 常见误区 ⚠️

  1. 过度预处理导致空间浪费
  2. 忽略边界条件和特殊情况
  3. 打表范围选择不当
  4. Map使用时的键值选择错误

8. 练习题推荐 📝

  1. LeetCode 560: 和为K的子数组
  2. LeetCode 525: 连续数组
  3. LeetCode 974: 和可被K整除的子数组
  4. LeetCode 523: 连续的子数组和

九、大数处理与模运算精讲 🔢

1. 为什么需要模运算?🤔

想象你在计算一个巨大的阶乘数:

  • 20! 就已经是 2432902008176640000
  • 100! 更是一个有158位的巨大数字
  • 这些数字可能超出JavaScript的Number类型范围(2^53 - 1)

💡 使用模运算可以:

  1. 防止数字溢出
  2. 保持计算结果在可控范围内
  3. 满足特定题目的要求

2. 模运算的性质 📐

js
/**
 * 模运算的基本性质
 * 1. (a + b) % p = (a % p + b % p) % p
 * 2. (a - b) % p = (a % p - b % p + p) % p
 * 3. (a * b) % p = ((a % p) * (b % p)) % p
 */
class ModOperations {
    constructor(mod = 1000000007) {
        this.MOD = mod;
    }
    
    /**
     * 加法运算
     */
    add(a, b) {
        return ((a % this.MOD) + (b % this.MOD)) % this.MOD;
    }
    
    /**
     * 减法运算
     */
    subtract(a, b) {
        return ((a % this.MOD) - (b % this.MOD) + this.MOD) % this.MOD;
    }
    
    /**
     * 乘法运算
     */
    multiply(a, b) {
        return ((a % this.MOD) * (b % this.MOD)) % this.MOD;
    }
}

3. 为什么选择1000000007?🎯

js
/**
 * 1000000007的特点:
 * 1. 是一个质数
 * 2. 足够大,可以避免大多数整数溢出
 * 3. 在int32范围内
 * 4. 平方后在int64范围内,便于中间计算
 */
class ModCalculator {
    static MOD = 1000000007;
    
    /**
     * 斐波那契数列示例
     */
    static fibonacci(n) {
        if (n <= 1) return n;
        
        let prev = 0, curr = 1;
        for (let i = 2; i <= n; i++) {
            const next = (prev + curr) % this.MOD;
            prev = curr;
            curr = next;
        }
        
        return curr;
    }
}

4. 大数处理技巧 💡

4.1 字符串加法

js
/**
 * 大数加法
 * @param {string} num1 
 * @param {string} num2
 * @return {string}
 */
function addStrings(num1, num2) {
    let i = num1.length - 1;
    let j = num2.length - 1;
    let carry = 0;
    let result = '';
    
    while (i >= 0 || j >= 0 || carry > 0) {
        const digit1 = i >= 0 ? parseInt(num1[i]) : 0;
        const digit2 = j >= 0 ? parseInt(num2[j]) : 0;
        const sum = digit1 + digit2 + carry;
        
        result = (sum % 10) + result;
        carry = Math.floor(sum / 10);
        
        i--;
        j--;
    }
    
    return result;
}

4.2 字符串乘法

js
/**
 * 大数乘法
 * @param {string} num1
 * @param {string} num2
 * @return {string}
 */
function multiply(num1, num2) {
    if (num1 === '0' || num2 === '0') return '0';
    
    const m = num1.length;
    const n = num2.length;
    const result = new Array(m + n).fill(0);
    
    // 逐位相乘
    for (let i = m - 1; i >= 0; i--) {
        for (let j = n - 1; j >= 0; j--) {
            const product = parseInt(num1[i]) * parseInt(num2[j]);
            const p1 = i + j;
            const p2 = i + j + 1;
            
            const sum = product + result[p2];
            result[p2] = sum % 10;
            result[p1] += Math.floor(sum / 10);
        }
    }
    
    // 去除前导零
    while (result[0] === 0) {
        result.shift();
    }
    
    return result.length ? result.join('') : '0';
}

5. 前端实战应用 💻

  1. 精确金额计算
js
class PreciseCalculator {
    /**
     * 处理JavaScript浮点数精度问题
     */
    static add(num1, num2) {
        const scale = Math.max(
            (num1.toString().split('.')[1] || '').length,
            (num2.toString().split('.')[1] || '').length
        );
        const factor = Math.pow(10, scale);
        
        return (Math.round(num1 * factor) + 
                Math.round(num2 * factor)) / factor;
    }
}
  1. 大数据统计
js
class DataStatistics {
    constructor() {
        this.count = '0';
        this.MOD = 1000000007;
    }
    
    /**
     * 增加统计数量
     */
    increment(value) {
        this.count = addStrings(this.count, value.toString());
        // 需要时进行取模
        if (this.count.length > 15) {
            this.count = (BigInt(this.count) % BigInt(this.MOD)).toString();
        }
    }
}

6. 性能优化建议 ⚡

场景优化建议
模运算1. 及时取模避免溢出
2. 利用模运算性质简化计算
大数处理1. 使用字符串存储大数
2. 合理使用BigInt
精度处理1. 统一小数位数
2. 使用整数计算代替浮点数

7. 常见误区 ⚠️

  1. 忘记处理负数情况
  2. 模运算顺序错误
  3. 溢出检测不完整
  4. 精度丢失问题

8. 练习题推荐 📝

  1. LeetCode 415: 字符串相加
  2. LeetCode 43: 字符串相乘
  3. LeetCode 306: 累加数
  4. LeetCode 166: 分数到小数