(06).前端知识库之算法与数据结构篇

61 阅读6分钟

一、基础排序算法

  1. 冒泡排序
  • 重复遍历数组,每次比较相邻元素并交换,将最大/最小值逐步"冒泡"到两端。
  function bubbleSort(arr) {
    const n = arr.length;
    // 外层循环控制遍历次数,最后一次无需比较
    for (let i = 0; i < n - 1; i++) {
      // 内层循环进行相邻元素的比较和交换
      for (let j = 0; j < n - 1 - i; j++) {
        if (arr[j] > arr[j + 1]) {
          // 交换相邻元素
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        }
      }
    }
    return arr;
  }
  1. 快速排序
  • 通过"分治"策略,选择一个基准值(pivot)将数组分为左右两部分,递归排序。
  • 关键点:分区(Hoare/Pivot-Last)和递归终止条件。
  function quickSort(arr, left = 0, right = arr.length - 1) {
    if (left >= right) return arr;

    // 选择最右侧元素作为pivot
    const pivot = arr[right];
    let i = left - 1; // 较小元素的索引

    for (let j = left; j < right; j++) {
      if (arr[j] < pivot) {
        i++;
        [arr[i], arr[j]] = [arr[j], arr[i]]; // 将较小元素移到左侧
      }
    }
    // 将pivot放到正确的位置
    [arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];

    // 递归排序左右子数组
    return quickSort(arr, left, i).concat(
      quickSort(arr, i + 2, right)
    );
  }
  1. 归并排序
  • 将数组拆分为最小单元后合并,合并时保持有序。
  function mergeSort(arr) {
    if (arr.length <= 1) return arr;

    // 分割数组为左右两部分
    const mid = Math.floor(arr.length / 2);
    const left = mergeSort(arr.slice(0, mid));
    const right = mergeSort(arr.slice(mid));

    // 合并两个有序数组
    return merge(left, right);
  }

  function merge(left, right) {
    let result = [];
    let i = 0; // 左数组指针
    let j = 0; // 右数组指针

    while (i < left.length && j < right.length) {
      if (left[i] < right[j]) {
        result.push(left[i]);
        i++;
      } else {
        result.push(right[j]);
        j++;
      }
    }
    // 添加剩余元素
    return result.concat(left.slice(i)).concat(right.slice(j));
  }

二、基础数据结构特性与操作

数据结构核心特点典型操作适用场景
数组固定大小,连续内存,随机访问快插入/删除 O(n),索引访问 O(1)需要快速随机访问的场景
链表动态分配,非连续内存,插入/删除 O(1)访问 O(n),头尾指针操作频繁插入删除的场景
后进先出(LIFO)Push/Pop函数调用栈、撤销操作
队列先进先出(FIFO)Enqueue/DequeueBFS、任务调度

三、数据结构实战应用

  1. 搜索框历史记录存储
  • 使用 双向链表 + 哈希表 实现高效增删改查:
    • 哈希表保存键值对(索引→节点),支持 O(1) 快速定位。
    • 双向链表维护插入顺序,支持 O(1) 头尾插入和中间删除。
  • 示例:浏览器历史记录的前进/后退功能。

四、数组去重方法

  1. Set 数据结构法
    const unique = [...new Set(arr)];
    
  • 时间复杂度:O(n),空间复杂度:O(n)。
  • 特点:简洁高效,保留原顺序(ES6 Set 遍历顺序不确定)。
  1. Filter + indexOf 法
    const unique = arr.filter((item, index) => index === arr.indexOf(item));
    
  • 时间复杂度:O(n²),空间复杂度:O(n)。
  • 特点:兼容旧环境,不改变原数组。
  1. 对象键值法
    const seen = {};
    return arr.filter(item => {
      return seen.hasOwnProperty(item) ? false : (seen[item] = true);
    });
    

五、深度优先搜索(DFS) vs 广度优先搜索(BFS)

对比项DFSBFS
实现方式递归/栈队列
探索顺序节点深入到底层再回溯层级逐层扩展
空间复杂度O(n)(最坏情况堆栈)O(n)(队列存储)
典型场景文件系统遍历、路径查找最短路径问题、层级分析

六、深拷贝实现

  1. JSON 方法
    const deepCopy = JSON.parse(JSON.stringify(obj));
    
  • 局限:无法拷贝函数、Symbol、循环引用对象。
  1. 递归实现
    function deepCopy(obj) {
      if (obj === null || typeof obj !== 'object') return obj;
      const copy = Array.isArray(obj) ? [] : {};
      for (let key in obj) {
        copy[key] = deepCopy(obj[key]);
      }
      return copy;
    }
    
  • 关键点:处理循环引用(需记录已访问对象)。

七、动态规划解法

1. 最长递增子序列(LIS)

问题描述:在一个无序数组中找出最长递增子序列的长度。 动态规划解法

function lengthOfLIS(nums) {
  const dp = new Array(nums.length).fill(1); // dp[i] 表示以 nums[i] 结尾的最长递增子序列长度
  for (let i = 0; i < nums.length; i++) {
    for (let j = 0; j < i; j++) {
      if (nums[j] < nums[i]) {
        dp[i] = Math.max(dp[i], dp[j] + 1);
      }
    }
  }
  return Math.max(...dp); // 或者直接返回 dp.reduce((a,b)=>max(a,b))
}
// 示例:nums = [10,9,2,5,3,7,101,18] → 输出 4 (对应 [2,5,7,101])
  • 时间复杂度:O(n²)
  • 优化版本:使用二分查找将时间复杂度降至 O(n log n)

2. 背包问题(Knapsack)

问题描述:给定物品重量和价值,在背包容量限制下最大化总价值。 动态规划解法(0-1背包)

function knapsack(weights, values, capacity) {
  const dp = new Array(capacity + 1).fill(0); // dp[j] 表示容量为 j 时的最大价值

  for (let i = 0; i < weights.length; i++) {
    for (let j = capacity; j >= weights[i]; j--) {
      dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
    }
  }
  return dp[capacity];
}
// 示例:weights=[2,3,4], values=[3,4,5], capacity=5 → 输出 7 (选2+3)
  • 前端应用场景:资源加载优化(如根据网络状况选择加载图片尺寸)

3. 编辑距离(Levenshtein Distance)

问题描述:计算两个字符串通过插入、删除、替换操作的最小步数。 动态规划解法

function minEditDistance(str1, str2) {
  const m = str1.length, n = str2.length;
  const dp = [[0]*(n+1)].concat([...Array(m)].map(() => [0]*(n+1)));

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (str1[i-1] === str2[j-1]) {
        dp[i][j] = dp[i-1][j-1];
      } else {
        dp[i][j] = Math.min(
          dp[i-1][j] + 1, // 删除
          dp[i][j-1] + 1, // 插入
          dp[i-1][j-1] + 1 // 替换
        );
      }
    }
  }
  return dp[m][n];
}
// 示例:str1="kitten", str2="sitting" → 输出 3
  • 前端应用场景:文本编辑器中的拼写检查、输入法候选词匹配

4. 矩阵链乘法

问题描述:通过加括号优化多个矩阵相乘的计算顺序。 动态规划解法

function matrixChainOrder(p) {
  const n = p.length - 1;
  const dp = [[0]*n for _ in range(n)];

  for (let chainLength = 2; chainLength <= n; chainLength++) { // 包含的矩阵个数
    for (let i = 0; i < n - chainLength + 1; i++) {
      const j = i + chainLength - 1;
      dp[i][j] = Infinity;
      for (let k = i; k < j; k++) {
        dp[i][j] = Math.min(
          dp[i][k] + dp[k+1][j] + p[i]*p[k+1]*p[j+1],
          dp[i][j]
        );
      }
    }
  }
  return dp[0][n-1];
}
// 示例:p = [10,20,30,40] → 输出 30000

七、防抖(Debounce)和节流(Throttle)

1. 防抖(Debounce)

核心思想:事件触发后,等待一段时间内不再触发事件,才执行函数。如果在这段时间内再次触发,则重新计时。

适用场景:搜索框输入联想、窗口大小调整(resize)、文本编辑器保存。

参数说明

  • func:需要防抖的函数。
  • wait:等待时间(毫秒)。
  • immediate:是否立即执行(true 表示首次触发立即执行,后续等待结束后再执行)。

实现代码

// 防抖函数实现
function debounce(func, delay) {
  let timer = null;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

2. 节流(Throttle)

核心思想:在一段时间内,无论事件触发多少次,函数最多执行一次。

适用场景:滚动事件(scroll)、鼠标移动(mousemove)、抢购按钮点击。

参数说明

  • func:需要节流的函数。
  • delay:执行间隔时间(毫秒)。

实现代码

// 节流函数实现
function throttle(func, delay) {
  let timer = null;
  return function() {
    const context = this;
    const args = arguments;
    if (!timer) {
      func.apply(context, args);
      timer = setTimeout(() => {
        timer = null;
      }, delay);
    }
  };
}

两者的核心区别

防抖(Debounce)节流(Throttle)
执行时机事件停止后执行固定时间间隔执行
类比电梯等人(最后一个人进后关门)地铁发车(每隔一段时间发一班)

使用示例

// 防抖:输入停止 500ms 后执行搜索
const searchInput = document.getElementById("search");
searchInput.addEventListener("input", debounce(searchHandler, 500));

// 节流:每隔 200ms 处理一次滚动事件
window.addEventListener("scroll", throttle(scrollHandler, 200));