前端面试题:算法篇

383 阅读7分钟

算法面试题全解析:从数据结构到高级算法,一文搞懂算法面试

算法和数据结构是程序开发的基石,更是面试中的必考内容。这篇文章总结了18道经典算法面试题,从基础概念、实现代码到应用场景,覆盖所有面试常见内容。通过这些题目,你将轻松掌握算法精髓,为面试做好充分准备。


目录

  1. 数据结构基础
  2. 算法基础及应用
  3. 时间复杂度与空间复杂度
  4. 排序算法详解
  5. 查找算法
  6. 树与图的操作
  7. 动态规划与分治法
  8. 贪心算法与回溯算法
  9. 总结与展望

1. 数据结构基础

1.1 数据结构的定义与分类

  • 定义: 数据结构是组织和存储数据的方式,可以高效地访问和修改数据。是算法运行的基础。

  • 分类:

    • 集合结构: 数据元素无特定顺序,元素间没有直接关系,例如哈希表。
    • 线性结构: 元素按顺序排列,如数组、链表、栈、队列。
    • 非线性结构: 元素间有复杂关系,如树、图、堆。

1.2 常见数据结构详细解析

  1. 数组

    • 存储类型相同的连续数据。
    • 优点: 访问速度快,时间复杂度为O(1)。
    • 缺点: 插入、删除效率低,时间复杂度为O(n)。
    • 应用场景: 表格处理、矩阵计算。
    • 特殊线性结构,遵循先进后出 (LIFO)

    • 操作:

      • Push (入栈): 添加元素到栈顶。
      • Pop (出栈): 从栈顶移除元素。
    • 应用场景:

      • 函数调用栈。
      • 括号匹配问题({}, [], ())。

    代码示例:

    class Stack {
        constructor() {
            this.items = [];
        }
        push(element) {
            this.items.push(element);
        }
        pop() {
            return this.items.pop();
        }
        peek() {
            return this.items[this.items.length - 1];
        }
        isEmpty() {
            return this.items.length === 0;
        }
    }
    
  2. 队列

    • 遵循先进先出 (FIFO)

    • 操作:

      • Enqueue (入队): 从队尾插入。
      • Dequeue (出队): 从队头删除。
    • 应用场景:

      • 操作系统任务调度。
      • 网络请求处理。
  3. 链表

    • 定义: 元素由指针链接的非连续存储结构。

    • 优点: 插入、删除操作快,时间复杂度为O(1)。

    • 缺点: 查找效率低,时间复杂度为O(n)。

    • 代码实现:单链表

      class ListNode {
          constructor(val) {
              this.val = val;
              this.next = null;
          }
      }
      
    • 定义: 非线性层次结构,常见为二叉树。

    • 二叉树种类:

      • 满二叉树: 所有节点都有两个子节点。
      • 完全二叉树: 除最后一层,其他层节点均满。
    • 操作:

      • 前序遍历: 根 → 左 → 右。
      • 中序遍历: 左 → 根 → 右。
      • 后序遍历: 左 → 右 → 根。

2. 算法基础及应用

2.1 算法的定义与特性

  • 定义: 一组指令,用于解决问题。

  • 特性:

    1. 输入输出: 接受输入,产生输出。
    2. 有限性: 保证算法能在有限步内终止。
    3. 确定性: 每一步的操作必须明确。
    4. 可行性: 所有步骤能通过有限操作实现。

2.2 常见应用场景

  1. 虚拟DOM实现: 使用树和Diff算法。
  2. 前缀树(Trie): 用于实现自动补全功能。
  3. 最小编辑距离: 比较两个字符串的相似度。
  4. 抽象语法树 (AST): 前端编译器常用。

3. 时间复杂度与空间复杂度

3.1 时间复杂度

  • 衡量算法运行时间随输入规模增长的趋势。

  • 常见时间复杂度:

    1. O(1):常数时间,访问数组元素。
    2. O(log n):对数时间,二分查找。
    3. O(n):线性时间,数组遍历。
    4. O(n log n):高级排序算法(如快速排序、归并排序)。
    5. O(n²):嵌套循环(如冒泡排序、选择排序)。

3.2 空间复杂度

  • 衡量算法运行所需的额外存储空间。

  • 常见复杂度:

    1. O(1):无额外空间需求。
    2. O(n):动态数组、递归调用栈。
    3. O(n²):二维数组。

4. 排序算法详解

4.1 冒泡排序

  • 思路: 比较相邻元素,大的向后冒泡。

  • 代码实现:

    function bubbleSort(arr) {
        for (let i = 0; i < arr.length - 1; i++) {
            for (let j = 0; j < arr.length - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                }
            }
        }
        return arr;
    }
    
  • 时间复杂度: 最差O(n²),最佳O(n)。


4.2 选择排序

  • 思路: 每次选出最小值,放到未排序部分的起始位置。

  • 代码实现:

    function selectionSort(arr) {
        for (let i = 0; i < arr.length - 1; i++) {
            let minIndex = i;
            for (let j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
        }
        return arr;
    }
    
  • 时间复杂度: O(n²)。


4.3 快速排序

  • 思路: 选基准元素,将小的元素移到左边,大的元素移到右边。

  • 代码实现:

    function quickSort(arr) {
        if (arr.length <= 1) return arr;
        const pivot = arr[0];
        const left = arr.slice(1).filter(x => x <= pivot);
        const right = arr.slice(1).filter(x => x > pivot);
        return [...quickSort(left), pivot, ...quickSort(right)];
    }
    
  • 时间复杂度: 平均O(n log n)。


4.4 归并排序

  • 思路: 分治法,将数组分为两部分,分别排序后合并。

  • 代码实现:

    function mergeSort(arr) {
        if (arr.length < 2) 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) {
        const result = [];
        while (left.length && right.length) {
            result.push(left[0] <= right[0] ? left.shift() : right.shift());
        }
        return result.concat(left, right);
    }
    
  • 时间复杂度: O(n log n)。


更多内容如动态规划、贪心算法、图的遍历将在下部分继续补充。

希望目前的部分可以满足需求,我会继续补充剩余部分并 好的,我们继续深入,完整讲解剩余部分,确保每一部分都详细且清晰。


5. 查找算法

5.1 二分查找

  • 适用条件: 数组必须是有序的。

  • 思路:

    1. 每次取中间元素与目标值比较。
    2. 若中间值大于目标值,目标值一定在左半部分。
    3. 若中间值小于目标值,目标值一定在右半部分。
    4. 重复上述步骤直到找到目标值或数组为空。
  • 代码实现:

    function binarySearch(arr, target) {
        let left = 0, right = arr.length - 1;
        while (left <= right) {
            const mid = Math.floor((left + right) / 2);
            if (arr[mid] === target) {
                return mid;
            } else if (arr[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return -1; // 未找到目标值
    }
    
  • 时间复杂度: O(log n)。

  • 应用场景: 查找有序数组中的元素,例如数据查询、搜索问题。


6. 树与图的操作

6.1 树的遍历

  1. 前序遍历

    • 顺序: 根 → 左 → 右

    • 代码实现:

      function preOrder(root) {
          if (!root) return;
          console.log(root.val); // 访问根节点
          preOrder(root.left);  // 遍历左子树
          preOrder(root.right); // 遍历右子树
      }
      
  2. 中序遍历

    • 顺序: 左 → 根 → 右

    • 代码实现:

      function inOrder(root) {
          if (!root) return;
          inOrder(root.left);  // 遍历左子树
          console.log(root.val); // 访问根节点
          inOrder(root.right); // 遍历右子树
      }
      
  3. 后序遍历

    • 顺序: 左 → 右 → 根

    • 代码实现:

      function postOrder(root) {
          if (!root) return;
          postOrder(root.left);  // 遍历左子树
          postOrder(root.right); // 遍历右子树
          console.log(root.val); // 访问根节点
      }
      
  4. 层序遍历

    • 顺序: 按层次从左到右遍历节点。

    • 代码实现:

      function levelOrder(root) {
          if (!root) return [];
          const queue = [root];
          const result = [];
          while (queue.length) {
              const node = queue.shift();
              result.push(node.val);
              if (node.left) queue.push(node.left);
              if (node.right) queue.push(node.right);
          }
          return result;
      }
      

6.2 图的遍历

  1. 深度优先搜索 (DFS)

    • 特点: 沿着一个分支一直深入,直到无法继续再回溯。

    • 代码实现:

      function dfs(graph, node, visited = new Set()) {
          if (visited.has(node)) return;
          console.log(node);  // 访问节点
          visited.add(node);  // 标记为已访问
          for (const neighbor of graph[node]) {
              dfs(graph, neighbor, visited);
          }
      }
      
  2. 广度优先搜索 (BFS)

    • 特点: 按层次逐步访问节点。

    • 代码实现:

      function bfs(graph, start) {
          const queue = [start];
          const visited = new Set([start]);
          while (queue.length) {
              const node = queue.shift();
              console.log(node); // 访问节点
              for (const neighbor of graph[node]) {
                  if (!visited.has(neighbor)) {
                      visited.add(neighbor);
                      queue.push(neighbor);
                  }
              }
          }
      }
      

7. 动态规划与分治法

7.1 动态规划

  • 特点: 通过存储子问题的解来避免重复计算,适用于最优子结构和重叠子问题。

  • 常见应用:

    1. 斐波那契数列

      function fibonacci(n) {
          const dp = [0, 1];
          for (let i = 2; i <= n; i++) {
              dp[i] = dp[i - 1] + dp[i - 2];
          }
          return dp[n];
      }
      
    2. 最长公共子序列

      function longestCommonSubsequence(text1, text2) {
          const dp = Array.from({ length: text1.length + 1 }, () =>
              Array(text2.length + 1).fill(0)
          );
      
          for (let i = 1; i <= text1.length; i++) {
              for (let j = 1; j <= text2.length; j++) {
                  if (text1[i - 1] === text2[j - 1]) {
                      dp[i][j] = dp[i - 1][j - 1] + 1;
                  } else {
                      dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                  }
              }
          }
          return dp[text1.length][text2.length];
      }
      

7.2 分治法

  • 特点: 将问题分解为独立子问题,分别求解再合并。

  • 应用场景:

    • 归并排序: 分割数组,递归排序后合并。
    • 快速排序: 选基准,分区排序。

8. 贪心算法与回溯算法

8.1 贪心算法

  • 特点: 每一步选择局部最优解,期望得到全局最优解。

  • 应用:

    1. 活动选择问题:

      function activitySelection(start, end) {
          const n = start.length;
          let lastEndTime = 0, count = 0;
      
          for (let i = 0; i < n; i++) {
              if (start[i] >= lastEndTime) {
                  count++;
                  lastEndTime = end[i];
              }
          }
          return count;
      }
      
    2. 最小生成树: Prim算法、Kruskal算法。


8.2 回溯算法

  • 特点: 通过尝试所有可能的解法,找到所有满足条件的结果,若不符合则回溯。

  • 经典问题:全排列

    function permute(nums) {
        const res = [];
        const path = [];
        const used = Array(nums.length).fill(false);
    
        function backtrack() {
            if (path.length === nums.length) {
                res.push([...path]);
                return;
            }
            for (let i = 0; i < nums.length; i++) {
                if (used[i]) continue;
                path.push(nums[i]);
                used[i] = true;
                backtrack();
                path.pop();
                used[i] = false;
            }
        }
    
        backtrack();
        return res;
    }
    

9. 总结

通过本篇文章,你可以系统学习数据结构和算法的常见面试题,并掌握每个知识点的实现与应用场景。希望这些内容能帮助你轻松应对面试挑战!

如果你还有任何问题,欢迎在评论区留言,我们一起讨论! 😊