您好!欢迎访问 www.kaoacca.cn —— 专注数据结构与算法面试辅导
数据结构常见面试题总结
系统梳理 · 真题解析 · 高频考点 · 面试通关指南
⚡

数据结构常见面试题总结:面试通关核心指南

从基础概念到高频算法,全面覆盖技术面试核心考点

📌 为什么数据结构是技术面试的必考项?

在互联网企业技术面试中,数据结构作为计算机科学的核心基础,是考察候选人逻辑思维能力、问题抽象能力与编码实践能力的关键载体。无论是大厂算法岗、后端开发岗,还是系统级编程岗位,面试官都会通过数据结构题目评估候选人的工程素养。

常见考察方式包括:

  • 口头提问:如“请简述二叉搜索树的性质与操作复杂度”
  • 白板编程:现场手写链表反转、DFS/BFS遍历等算法
  • 在线编程平台:LeetCode、牛客网等平台的中等难度题目
  • 系统设计题前置:如“如何设计一个支持O(1)插入删除的缓存?” → 涉及哈希表+双向链表

据2023年《中国程序员算法能力报告》统计,92%的头部互联网企业将数据结构列为笔试/面试第一轮必考内容;平均每人需掌握15+种核心数据结构模型。本总结基于近年BAT、TMD、华为、字节等企业真实面试题库,结合考研408大纲,系统梳理高频考点与易错点。

📖 内容结构概览

本文采用模块化设计,覆盖以下核心模块:

模块 核心考点 典型题型 面试频率
线性结构 数组/链表/哈希表 双指针、原地操作、碰撞指针 ★★★★★
栈与队列 单调栈、双端队列、括号匹配 表达式求值、滑动窗口最大值 ★★★★☆
树形结构 二叉树遍历、BST、AVL、红黑树 递归/迭代遍历、LCA、路径和 ★★★★★
图结构 DFS/BFS、最短路径、拓扑排序 并查集、最小生成树、关键路径 ★★★★☆
排序与查找 快排/归并/堆排、二分变形 旋转数组查找、第K大元素 ★★★★★

所有内容均按“概念→原理→代码→变式→易错点”五层递进结构组织,确保理解深度与实战能力同步提升。

⚙️

一、线性结构:数组与链表

高频考点:双指针技巧、原地操作、空间优化

数组 Array
链表 Linked List
哈希表 Hash
• 数组核心考点

数组是最基础的线性结构,面试中高频考察点包括:

  • 原地操作:如“删除排序数组中的重复项”(LeetCode 26)要求空间O(1)
  • 双指针技巧:快慢指针(移除元素)、对撞指针(三数之和)
  • 二分查找变形:旋转数组查找最小值、有序矩阵查找

【经典例题】旋转数组的最小数字(剑指Offer 11)

题目:把一个数组最开始的若干个元素搬到数组的末尾,称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。


function minArray(arr) {
  let left = 0, right = arr.length - 1;
  while (left < right) {
    let mid = left + Math.floor((right - left) / 2);
    if (arr[mid] < arr[right]) {
      right = mid;
    } else if (arr[mid] > arr[right]) {
      left = mid + 1;
    } else {
      right--;
    }
  }
  return arr[left];
}
// 测试
console.log(minArray([3,4,5,1,2])); // 1
console.log(minArray([2,2,2,0,1])); // 0

易错点提示:当arr[mid] == arr[right]时,不能简单丢弃右半区,但可以安全地将right--(因mid不等于right,且可能有重复元素),最坏时间复杂度退化为O(n)。

• 链表核心考点

链表在面试中常考操作包括:

  • 指针操作:反转链表(LeetCode 206)、合并两个有序链表(21)
  • 快慢指针:找中点、判断环、找环入口
  • 虚拟头节点:简化边界处理(如删除头节点)

【经典例题】链表中环的入口节点(LeetCode 142)

题目:给一个链表,返回链表开始入环的第一个节点。若无环则返回null。


function detectCycle(head) {
  if (!head || !head.next) return null;
  let slow = head, fast = head;
  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow === fast) {
      // 有环,找入口
      slow = head;
      while (slow !== fast) {
        slow = slow.next;
        fast = fast.next;
      }
      return slow;
    }
  }
  return null;
}

面试高频变式:

  • 合并K个有序链表(优先队列/分治)
  • 重排链表(L→L1→L2→…→Ln → L→Ln→L1→Ln-1→…)
  • 两数相加(链表逆序存储数字)
• 哈希表核心考点

哈希表(Hash Table)是实现O(1)时间复杂度查找的关键结构,面试重点包括:

  • 哈希冲突处理:开放地址法、链地址法
  • 哈希函数设计:字符串哈希(进制转换)、大整数取模
  • 实际应用:两数之和、最长无重复子串、LRU缓存

【经典例题】两数之和(LeetCode 1)


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 [];
}
// 示例
console.log(twoSum([2,7,11,15], 9)); // [0,1]

注意:JS中Map比Object更适合存储键值对(避免原型污染),且保持插入顺序。

⏱️ 线性结构发展关键节点(时间轴)
1950s
数组(Array)作为最基础的线性结构被引入FORTRAN语言,成为程序设计的基石。
1960s
链表(Linked List)在ALGOL 60中被形式化定义,支持动态内存分配。
1970s
哈希表理论成熟,Donald Knuth在《计算机程序设计艺术》中系统阐述哈希函数设计。
1990s
Java集合框架引入HashMap,链地址法成为主流冲突处理方式。
2010s
Redis采用跳跃表(Skip List)优化有序集合,成为高性能缓存核心结构。
〔〕

二、栈与队列:后进先出与先进先出

典型应用:括号匹配、表达式求值、滑动窗口

栈 Stack
队列 Queue
单调结构
• 栈的核心应用

栈(Stack)遵循后进先出(LIFO)原则,常见场景:

  • 括号匹配:检查表达式中()、[]、{}是否成对
  • 函数调用栈:递归转迭代、表达式求值
  • 单调栈:下一个更大元素、柱状图最大矩形

【经典例题】有效的括号(LeetCode 20)


function isValid(s) {
  const stack = [];
  const pairs = { '(': ')', '[': ']', '{': '}' };
  for (let char of s) {
    if (pairs[char]) {
      stack.push(char);
    } else {
      if (stack.length === 0 || pairs[stack.pop()] !== char) {
        return false;
      }
    }
  }
  return stack.length === 0;
}
// 测试
console.log(isValid("()[]{}")); // true
console.log(isValid("([)]"));   // false

面试高频变式:计算器实现(中缀→后缀→求值)、最小栈(O(1)获取最小值)。

• 队列的核心应用

队列(Queue)遵循先进先出(FIFO)原则,关键应用:

  • BFS算法:层序遍历、最短路径
  • 缓冲区设计:生产者-消费者模型
  • 双端队列:滑动窗口最大值、构建最大子序列

【经典例题】二叉树层序遍历(LeetCode 102)


function levelOrder(root) {
  if (!root) return [];
  const result = [];
  const queue = [root];
  while (queue.length > 0) {
    const levelSize = queue.length;
    const level = [];
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift();
      level.push(node.val);
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    result.push(level);
  }
  return result;
}

双端队列变式:用双端队列实现滑动窗口最大值(LeetCode 239),时间复杂度O(n)。

• 单调栈与单调队列

单调结构(Monotonic Structure)通过维护栈/队列的单调性,实现高效查询:

  • 单调栈:求“下一个更大元素”、“柱状图最大矩形”
  • 单调队列:滑动窗口最值(O(n)时间)

【经典例题】下一个更大元素 I(LeetCode 496)


function nextGreaterElement(nums1, nums2) {
  const map = new Map();
  const stack = [];
  // 遍历nums2,构建映射
  for (let num of nums2) {
    while (stack.length > 0 && stack[stack.length - 1] < num) {
      map.set(stack.pop(), num);
    }
    stack.push(num);
  }
  // 处理无更大元素的情况
  while (stack.length > 0) {
    map.set(stack.pop(), -1);
  }
  // 生成结果
  return nums1.map(num => map.get(num));
}
// 测试
console.log(nextGreaterElement([4,1,2], [1,3,4,2])); // [-1,3,-1]

关键点:栈中保持递减,当新元素更大时,弹出所有更小元素并记录映射。

▲

三、树形结构:二叉树与搜索树

核心考点:遍历方式、递归/迭代实现、BST性质

遍历方式
二叉搜索树
平衡树
• 二叉树遍历方式详解

四种遍历方式:

  • 前序:根→左→右(递归/迭代/Morris)
  • 中序:左→根→右(BST中序为升序)
  • 后序:左→右→根(删除树节点)
  • 层序:按层访问(BFS)

【经典例题】二叉树中序遍历(LeetCode 94)


function inorderTraversal(root) {
  const result = [];
  const stack = [];
  let current = root;
  while (current || stack.length > 0) {
    // 一直向左走到底
    while (current) {
      stack.push(current);
      current = current.left;
    }
    // 弹出栈顶并访问
    current = stack.pop();
    result.push(current.val);
    // 转向右子树
    current = current.right;
  }
  return result;
}

Morris遍历:利用树的空指针建立线索,空间O(1),但会临时修改树结构。

• 二叉搜索树(BST)

BST性质:左子树所有节点 < 根节点 < 右子树所有节点;中序遍历有序。

核心操作:

  • 查找:O(log n)(平衡时)→ O(n)(退化为链表)
  • 插入:按BST性质递归/迭代插入
  • 删除:分三种情况(无子/单子/双子)

【经典例题】二叉搜索树中第K小的元素(LeetCode 230)


function kthSmallest(root, k) {
  let count = 0;
  let result = -1;
  function inorder(node) {
    if (!node || result !== -1) return;
    inorder(node.left);
    count++;
    if (count === k) {
      result = node.val;
      return;
    }
    inorder(node.right);
  }
  inorder(root);
  return result;
}

进阶优化:在节点中存储子树大小,实现O(log n)时间查找第K小。

• 平衡二叉树与红黑树

平衡树:任意节点左右子树高度差≤1(如AVL树、红黑树)。

红黑树五大性质:

  1. 节点是红或黑
  2. 根节点是黑
  3. 叶子节点(NULL)是黑
  4. 红节点的子节点是黑
  5. 任一节点到其叶子节点的所有路径含相同黑节点数

【实际应用】:Java HashMap(1.8+)、Linux进程调度(CFS)、数据库索引。

【面试高频问题】为什么HashMap用红黑树不用AVL?

答:红黑树牺牲部分平衡性(允许高度差2),但旋转次数更少,插入/删除效率更高。HashMap中节点数≤8时用链表,>8且容量≥64时转红黑树,平衡性要求适中。

⏱️ 树结构发展关键节点
1962年
Adelson-Velsky和Landis提出AVL树,首个自平衡BST。
1972年
Bayer提出B树,用于磁盘存储系统(数据库索引)。
1978年
Rudolf Bayer发明红黑树,成为工业级平衡树首选。
1985年
Sleator和Tarjan提出伸展树(Splay Tree),自适应优化。
2006年
Bayer与McCreight提出B+树,成为现代数据库主流索引结构。
●

四、图结构:连通性与最短路径

高频考点:DFS/BFS、拓扑排序、最小生成树

遍历算法
最短路径
拓扑排序
• 图的遍历:DFS vs BFS

DFS(深度优先搜索):递归/栈实现,适合路径存在性、连通分量。

BFS(广度优先搜索):队列实现,适合最短路径(无权图)、层级遍历。

【经典例题】课程表(LeetCode 207)

题目:判断是否能完成所有课程(即图中无环)。


function canFinish(numCourses, prerequisites) {
  const graph = Array.from({ length: numCourses }, () => []);
  const indegree = new Array(numCourses).fill(0);
  // 构建邻接表与入度
  for (const [course, pre] of prerequisites) {
    graph[pre].push(course);
    indegree[course]++;
  }
  // 初始化队列
  const queue = [];
  for (let i = 0; i < numCourses; i++) {
    if (indegree[i] === 0) queue.push(i);
  }
  let visited = 0;
  while (queue.length > 0) {
    const node = queue.shift();
    visited++;
    for (const neighbor of graph[node]) {
      indegree[neighbor]--;
      if (indegree[neighbor] === 0) {
        queue.push(neighbor);
      }
    }
  }
  return visited === numCourses;
}

DFS解法:用三色标记法(0=未访问,1=访问中,2=已完成),出现回边→有环。

• 最短路径算法

Dijkstra算法:求单源最短路径(非负权图),时间O((V+E)log V)。

Floyd-Warshall:多源最短路径,O(V³),适合小规模图。

Bellman-Ford:支持负权边,检测负环。

【经典例题】网络延迟时间(LeetCode 743)


function networkDelayTime(times, n, k) {
  // 构建邻接表
  const graph = Array.from({ length: n + 1 }, () => []);
  for (const [u, v, w] of times) {
    graph[u].push([v, w]);
  }
  // 初始化距离数组
  const dist = new Array(n + 1).fill(Infinity);
  dist[k] = 0;
  // 优先队列:[距离, 节点]
  const pq = [[0, k]];
  while (pq.length > 0) {
    // 取最小距离节点
    pq.sort((a, b) => a[0] - b[0]);
    const [d, node] = pq.shift();
    // 已访问过更短路径则跳过
    if (d > dist[node]) continue;
    // 更新邻居
    for (const [neighbor, weight] of graph[node]) {
      const newDist = dist[node] + weight;
      if (newDist < dist[neighbor]) {
        dist[neighbor] = newDist;
        pq.push([newDist, neighbor]);
      }
    }
  }
  const maxDist = Math.max(...dist.slice(1));
  return maxDist === Infinity ? -1 : maxDist;
}

面试高频变式:带约束的最短路径(如最多转机K次)、最小生成树(Prim/Kruskal)。

• 拓扑排序与关键路径

拓扑排序:仅适用于有向无环图(DAG),常用于任务调度、依赖解析。

Kahn算法:基于入度的BFS;DFS法:后序遍历的逆序。

【典型应用】:课程安排、构建顺序、编译依赖。

【扩展知识】关键路径(Critical Path):

  • AOE网(Activity On Edge):边表示活动,点表示事件
  • 关键路径:从源点到汇点的最长路径
  • 关键活动:最早开始时间=最晚开始时间的活动

实际场景:项目管理(PERT)、流水线调度。

↑↓

五、排序与查找:O(n log n)与O(log n)

核心考点:稳定性、原地性、时间复杂度分析

排序算法
二分查找
第K大元素
• 经典排序算法对比
算法 平均时间 最坏时间 空间 稳定 适用场景
冒泡排序 O(n²) O(n²) O(1) ✓ 小规模数据
选择排序 O(n²) O(n²) O(1) ✗ 交换次数少
插入排序 O(n²) O(n²) O(1) ✓ 基本有序数据
归并排序 O(n log n) O(n log n) O(n) ✓ 稳定排序
快速排序 O(n log n) O(n²) O(log n) ✗ 通用首选
堆排序 O(n log n) O(n log n) O(1) ✗ Top-K问题

【经典例题】快速排序(LeetCode 912)

function quickSort(arr, left = 0, right = arr.length - 1) {
  if (left < right) {
    const pivotIndex = partition(arr, left, right);
    quickSort(arr, left, pivotIndex - 1);
    quickSort(arr, pivotIndex + 1, right);
  }
  return arr;
}
function partition(arr, left, right) {
  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]];
    }
  }
  [arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];
  return i + 1;
}
// 测试
console.log(quickSort([5,2,3,1])); // [1,2,3,5]

面试高频问题:如何优化快排最坏情况?→ 随机选择枢轴、三数取中。

• 二分查找变形

标准二分:有序数组查找目标值。

变形场景:

  • 旋转数组:[4,5,6,7,0,1,2]中找0
  • 重复元素:找第一个/最后一个位置
  • 二维矩阵:[1,3,5,7],[10,11,16,20],[23,30,34,60]中查找

【经典例题】在排序数组中查找元素的第一个和最后一个位置(LeetCode 34)

function searchRange(nums, target) {
  function findBound(isFirst) {
    let left = 0, right = nums.length - 1;
    while (left <= right) {
      const mid = left + Math.floor((right - left) / 2);
      if (nums[mid] > target || (isFirst && nums[mid] === target)) {
        right = mid - 1;
      } else {
        left = mid + 1;
      }
    }
    return left;
  }
  const start = findBound(true);
  const end = findBound(false) - 1;
  return (start <= end && nums[start] === target)
    ? [start, end]
    : [-1, -1];
}

关键点:通过调整条件控制向左/右边界收缩。

• 第K大/小元素解法

四种解法对比:

方法 时间 空间 适用场景
排序 O(n log n) O(1) 小规模数据
堆(大小顶堆) O(n log k) O(k) 流式数据、Top-K
快速选择 O(n)(平均) O(1) 静态数组、中位数
计数排序 O(n + k) O(k) 数据范围小

【经典例题】数组中的第K个最大元素(LeetCode 215)


function findKthLargest(nums, k) {
  k = nums.length - k; // 转为第k小
  function quickSelect(left, right) {
    const pivotIndex = partition(left, right);
    if (pivotIndex === k) return nums[pivotIndex];
    if (pivotIndex > k) return quickSelect(left, pivotIndex - 1);
    return quickSelect(pivotIndex + 1, right);
  }
  function partition(left, right) {
    const pivot = nums[right];
    let i = left;
    for (let j = left; j < right; j++) {
      if (nums[j] <= pivot) {
        [nums[i], nums[j]] = [nums[j], nums[i]];
        i++;
      }
    }
    [nums[i], nums[right]] = [nums[right], nums[i]];
    return i;
  }
  return quickSelect(0, nums.length - 1);
}

堆解法:维护大小为k的小顶堆,堆顶即第K大。

〓

六、高频算法题分类精讲

覆盖90%技术面试算法题型

动态规划
贪心算法
回溯算法
• 动态规划核心模板

DP解题四要素:

  • 状态定义:dp[i]表示什么?
  • 状态转移:dp[i]如何由dp[i-1]等推导?
  • 初始状态:dp[0]、dp[1]的值
  • 返回结果:dp[n]还是max(dp)?

【经典例题】爬楼梯(LeetCode 70)


function climbStairs(n) {
  if (n <= 1) return 1;
  let prev2 = 1, prev1 = 1;
  for (let i = 2; i <= n; i++) {
    const current = prev1 + prev2;
    prev2 = prev1;
    prev1 = current;
  }
  return prev1;
}

高频DP题型:

  • 0-1背包:分割等和子集(LeetCode 416)
  • 完全背包:零钱兑换(LeetCode 322)
  • 子序列:最长公共子序列(LeetCode 1143)
  • 区间DP:戳气球(LeetCode 312)
• 贪心算法适用条件

贪心选择性质:局部最优可导致全局最优。

证明方法:数学归纳法、交换论证。

【经典例题】跳跃游戏(LeetCode 55)


function canJump(nums) {
  let maxReach = 0;
  for (let i = 0; i < nums.length; i++) {
    if (i > maxReach) return false;
    maxReach = Math.max(maxReach, i + nums[i]);
    if (maxReach >= nums.length - 1) return true;
  }
  return false;
}

反例:零钱兑换(不满足贪心):[1,3,4]找6元,贪心→4+1+1(3枚),最优→3+3(2枚)。

• 回溯算法模板

回溯框架:

function backtrack(path, choices) {
  if (结束条件) {
    记录结果;
    return;
  }
  for (选择 in 可选选择) {
    做选择;
    backtrack(path, 新选择);
    撤销选择;
  }
}

【经典例题】全排列(LeetCode 46)

function permute(nums) {
  const result = [];
  function backtrack(path) {
    if (path.length === nums.length) {
      result.push([...path]);
      return;
    }
    for (let i = 0; i < nums.length; i++) {
      if (path.includes(nums[i])) continue;
      path.push(nums[i]);
      backtrack(path);
      path.pop();
    }
  }
  backtrack([]);
  return result;
}

剪枝技巧:N皇后问题中检查列、主对角线、副对角线是否冲突。

⏱️ 算法发展里程碑
1946年
John von Neumann提出归并排序,首个O(n log n)排序算法。
1955年
George Dantzig发明单纯形法,奠定线性规划基础。
1965年
Cooley与Tukey提出FFT算法,加速傅里叶变换。
1976年
Wirth提出快速排序的Hoare分区方案。
1997年
Ahuja等提出Fibonacci堆,优化Dijkstra算法。
〈〉

七、常见面试问题FAQ

高频问题精准解答

Q1:如何向面试官展示对数据结构的理解深度?

答:建议采用“原理→优缺点→适用场景→代码实现→优化思路”五层结构:

  • 原理:如“红黑树通过旋转和重新着色维持平衡”
  • 优缺点:如“AVL更平衡但旋转频繁,红黑树插入更快”
  • 适用场景:如“JDK8 HashMap用红黑树替代链表”
  • 代码实现:手写插入/删除/查找核心逻辑
  • 优化思路:如“用哨兵节点简化边界处理”
Q2:面试中遇到不会的题怎么办?

答:展示思考过程比直接给出答案更重要:

  1. 明确问题:用自己的话复述题目要求
  2. 分析约束:时间/空间复杂度要求、边界条件
  3. 分步求解:从暴力法→优化法逐步推进
  4. 坦诚沟通:若卡住可请求提示,但避免直接要答案

示例:“这题让我想到链表反转,我先写个双指针解法,再思考能否空间O(1)……”

Q3:如何准备算法面试?

答:推荐三阶段准备法:

阶段 时长 重点
基础巩固 1-2周 掌握8大基础数据结构+10种核心算法
专项突破 2-3周 按题型刷题(数组/树/图/DP)
模拟实战 1周 限时刷题+白板模拟+代码Review

推荐平台:LeetCode(中等题为主)、CodeTop(国内企业题库)、牛客网。

斌知网
蜀ICP备2026017620号-5