以下是针对 **数据结构与算法 20 道高频面试题** 的标准面试答案详解,符合中高级 Java 开发工程师在真实技术面试中的表达规范:逻辑清晰、原理深入、代码准确、语言专业。每道题均包含 **核心概念 + 实现机制 + 时间/空间复杂度 + 应用场景 + 注意事项**,便于精准作答。
---
### 1. 数组与链表的区别?
| 特性 | **数组(Array)** | **链表(Linked List)** |
|------|------------------|------------------------|
| 存储方式 | 连续内存空间 | 节点通过指针链接,非连续 |
| 访问性能 | 支持随机访问(O(1)) | 只能顺序访问(O(n)) |
| 插入删除 | 中间操作需移动元素(O(n)) | 修改指针即可(O(1),前提是定位到节点) |
| 扩容机制 | 固定大小或动态扩容(如 ArrayList 扩容为 1.5 倍) | 动态分配节点,无需预分配 |
| 内存开销 | 少(仅存储数据) | 多(每个节点额外存储指针) |
> ✅ 选择建议:
- 查找多 → 用数组
- 插入删除频繁 → 用链表
---
### 2. 栈和队列的实现?
#### 栈(Stack)——后进先出(LIFO)
```java
class MyStack<T> {
private List<T> data = new ArrayList<>();
public void push(T item) {
data.add(item);
}
public T pop() {
if (isEmpty()) throw new RuntimeException("Stack is empty");
return data.remove(data.size() - 1);
}
public T peek() {
if (isEmpty()) throw new RuntimeException("Stack is empty");
return data.get(data.size() - 1);
}
public boolean isEmpty() {
return data.isEmpty();
}
}
```
#### 队列(Queue)——先进先出(FIFO)
```java
class MyQueue<T> {
private LinkedList<T> data = new LinkedList<>();
public void offer(T item) {
data.addLast(item);
}
public T poll() {
if (isEmpty()) throw new RuntimeException("Queue is empty");
return data.removeFirst();
}
public T peek() {
if (isEmpty()) throw new RuntimeException("Queue is empty");
return data.getFirst();
}
public boolean isEmpty() {
return data.isEmpty();
}
}
```
> ✅ 实际开发推荐使用 `Deque` 实现双端队列功能
---
### 3. 二叉树的遍历方式?
二叉树有四种主要遍历方式:
| 遍历类型 | 顺序 | 说明 |
|---------|------|------|
| **前序(Pre-order)** | 根 → 左 → 右 | 用于复制树、序列化 |
| **中序(In-order)** | 左 → 根 → 右 | BST 中序输出有序序列 |
| **后序(Post-order)** | 左 → 右 → 根 | 适用于释放内存、求表达式值 |
| **层序(Level-order)** | 按层从左到右 | 使用队列实现广度优先搜索 |
```java
// 递归前序遍历
public void preorder(TreeNode root, List<Integer> res) {
if (root == null) return;
res.add(root.val);
preorder(root.left, res);
preorder(root.right, res);
}
// 层序遍历(BFS)
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> level = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
result.add(level);
}
return result;
}
```
> ✅ 所有递归遍历均可改为栈模拟的迭代写法
---
### 4. 图的遍历算法?
图的两种基本遍历算法:
#### 深度优先搜索(DFS)
```java
public void dfs(int v, boolean[] visited, List<List<Integer>> adj) {
visited[v] = true;
System.out.print(v + " ");
for (int neighbor : adj.get(v)) {
if (!visited[neighbor]) {
dfs(neighbor, visited, adj);
}
}
}
```
#### 广度优先搜索(BFS)
```java
public void bfs(int start, List<List<Integer>> adj, int n) {
boolean[] visited = new boolean[n];
Queue<Integer> queue = new LinkedList<>();
visited[start] = true;
queue.offer(start);
while (!queue.isEmpty()) {
int v = queue.poll();
System.out.print(v + " ");
for (int neighbor : adj.get(v)) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.offer(neighbor);
}
}
}
}
```
> ✅ DFS 适合路径探索,BFS 适合最短路径(无权图)
---
### 5. 排序算法的实现与复杂度?
| 算法 | 最好 | 平均 | 最坏 | 空间 | 是否稳定 | 说明 |
|------|------|------|------|------|----------|------|
| **冒泡排序** | O(n) | O(n²) | O(n²) | O(1) | 是 | 相邻比较,可优化提前退出 |
| **选择排序** | O(n²) | O(n²) | O(n²) | O(1) | 否 | 每次选最小放前面 |
| **插入排序** | O(n) | O(n²) | O(n²) | O(1) | 是 | 适合小规模或近有序数据 |
| **快速排序** | O(n log n) | O(n log n) | O(n²) | O(log n) | 否 | 分治+分区,平均最快 |
| **归并排序** | O(n log n) | O(n log n) | O(n log n) | O(n) | 是 | 稳定,适合链表 |
| **堆排序** | O(n log n) | O(n log n) | O(n log n) | O(1) | 否 | 利用最大堆性质 |
| **计数排序** | O(n+k) | O(n+k) | O(n+k) | O(k) | 是 | 适用于整数且范围小 |
| **桶排序** | O(n) | O(n) | O(n²) | O(n) | 是 | 数据均匀分布时高效 |
| **基数排序** | O(d*(n+k)) | 同上 | 同上 | O(k) | 是 | 按位排序,d 为位数 |
> ✅ Java 中 `Arrays.sort()` 对基本类型用双轴快排,对象用 TimSort(归并优化)
---
### 6. 查找算法的实现?
#### 顺序查找(线性查找)
```java
public int linearSearch(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) return i;
}
return -1;
}
```
> O(n),无序数组可用
#### 二分查找(Binary Search)
```java
public int binarySearch(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
```
> O(log n),要求有序数组
---
### 7. 哈希表的实现?
哈希表基于“键→索引”映射实现 O(1) 查找。
#### 核心组件:
- **哈希函数**:将 key 映射为数组下标(如 `hashCode() % tableSize`)
- **冲突解决**:
- **链地址法**(Java HashMap 使用):桶内用链表或红黑树存储多个 entry
- **开放寻址法**:线性探测、二次探测等
```java
class SimpleHashMap<K, V> {
private static class Entry<K, V> {
K key;
V value;
Entry<K, V> next;
Entry(K key, V value) {
this.key = key;
this.value = value;
}
}
private List<Entry<K, V>>[] table;
private int size = 16;
@SuppressWarnings("unchecked")
public SimpleHashMap() {
table = new LinkedList[size];
for (int i = 0; i < size; i++) {
table[i] = new LinkedList<>();
}
}
private int hash(K key) {
return Math.abs(key.hashCode()) % size;
}
public void put(K key, V value) {
int index = hash(key);
for (Entry<K, V> entry : table[index]) {
if (entry.key.equals(key)) {
entry.value = value;
return;
}
}
table[index].add(new Entry<>(key, value));
}
public V get(K key) {
int index = hash(key);
for (Entry<K, V> entry : table[index]) {
if (entry.key.equals(key)) {
return entry.value;
}
}
return null;
}
}
```
> ✅ Java 8 中 HashMap 当链表长度 > 8 且总容量 ≥ 64 时转为红黑树
---
### 8. 动态规划的解题思路?
动态规划(DP)用于解决具有重叠子问题和最优子结构的问题。
#### 解题四步法:
1. **定义状态**:明确 `dp[i]` 或 `dp[i][j]` 表示什么
2. **状态转移方程**:找出当前状态如何由之前状态推导
3. **初始化边界条件**
4. **确定遍历顺序**
#### 示例:斐波那契数列
```java
public int fib(int n) {
if (n <= 1) return n;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
```
> 可优化为滚动变量 → 空间 O(1)
---
### 9. 贪心算法的应用?
贪心算法在每一步选择当前最优解,期望最终全局最优。
#### 经典应用:
- **活动选择问题**:按结束时间排序,选不冲突的最大集合
- **霍夫曼编码**:构建最优前缀码
- **最小生成树(Prim/Kruskal)**:局部最小边构造 MST
- **找零钱问题**(特定币种下成立)
```java
// 活动选择(贪心策略:早结束优先)
public int maxActivities(int[] start, int[] end) {
int n = start.length;
List<Activity> activities = new ArrayList<>();
for (int i = 0; i < n; i++) {
activities.add(new Activity(start[i], end[i]));
}
activities.sort((a, b) -> a.end - b.end);
int count = 1, lastEnd = activities.get(0).end;
for (int i = 1; i < n; i++) {
if (activities.get(i).start >= lastEnd) {
count++;
lastEnd = activities.get(i).end;
}
}
return count;
}
```
> ⚠️ 贪心不一定能得到最优解,需数学证明
---
### 10. 分治算法的典型应用?
分治法将问题分解为独立子问题,分别求解后再合并。
#### 典型应用:
- **归并排序**:拆分 → 排序 → 合并
- **快速排序**:选 pivot → 分区 → 递归
- **大整数乘法(Karatsuba)**
- **最近点对问题**
```java
// 归并排序核心 merge
private void merge(int[] arr, int l, int m, int r) {
int[] temp = new int[r - l + 1];
int i = l, j = m + 1, k = 0;
while (i <= m && j <= r) {
temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
while (i <= m) temp[k++] = arr[i++];
while (j <= r) temp[k++] = arr[j++];
System.arraycopy(temp, 0, arr, l, temp.length);
}
```
> ✅ 分治三步:分解 → 解决 → 合并
---
### 11. 回溯算法的实现?
回溯是暴力搜索的一种,通过试错寻找所有可行解。
#### 典型问题:
- N 皇后
- 八数码
- 组合、排列、子集生成
```java
// 子集生成(回溯模板)
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(result, new ArrayList<>(), nums, 0);
return result;
}
private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, int start) {
result.add(new ArrayList<>(path)); // 每个节点都是解
for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
backtrack(result, path, nums, i + 1);
path.remove(path.size() - 1); // 撤销选择
}
}
```
> ✅ 回溯 = DFS + 状态重置
---
### 12. 字符串匹配算法?
#### 暴力匹配(Brute Force)
```java
public int strStr(String text, String pattern) {
int n = text.length(), m = pattern.length();
for (int i = 0; i <= n - m; i++) {
int j;
for (j = 0; j < m; j++) {
if (text.charAt(i + j) != pattern.charAt(j)) break;
}
if (j == m) return i;
}
return -1;
}
```
> O(nm)
#### KMP 算法(Knuth-Morris-Pratt)
利用部分匹配表(next 数组)避免重复比较,O(n + m)
```java
public int kmpSearch(String text, String pattern) {
if (pattern.isEmpty()) return 0;
int[] next = buildNext(pattern);
int j = 0;
for (int i = 0; i < text.length(); i++) {
while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
if (text.charAt(i) == pattern.charAt(j)) {
j++;
}
if (j == pattern.length()) {
return i - j + 1;
}
}
return -1;
}
private int[] buildNext(String pattern) {
int[] next = new int[pattern.length()];
int j = 0;
for (int i = 1; i < pattern.length(); i++) {
while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
j = next[j - 1];
}
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
next[i] = j;
}
return next;
}
```
> ✅ KMP 核心思想:利用已匹配部分的最长前后缀进行跳转
---
### 13. 最短路径算法?
#### Dijkstra(单源最短路径,非负权)
```java
public int[] dijkstra(int[][] graph, int start) {
int n = graph.length;
int[] dist = new int[n];
boolean[] visited = new boolean[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;
for (int i = 0; i < n; i++) {
int u = -1;
for (int j = 0; j < n; j++) {
if (!visited[j] && (u == -1 || dist[j] < dist[u])) {
u = j;
}
}
visited[u] = true;
for (int v = 0; v < n; v++) {
if (graph[u][v] != 0 && !visited[v]) {
int alt = dist[u] + graph[u][v];
if (alt < dist[v]) {
dist[v] = alt;
}
}
}
}
return dist;
}
```
> O(V²),可用优先队列优化至 O((V+E)logV)
#### Floyd-Warshall(多源最短路径)
```java
public int[][] floydWarshall(int[][] graph) {
int n = graph.length;
int[][] dist = new int[n][n];
for (int i = 0; i < n; i++)
System.arraycopy(graph[i], 0, dist[i], 0, n);
for (int k = 0; k < n; k++)
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (dist[i][k] != Integer.MAX_VALUE && dist[k][j] != Integer.MAX_VALUE)
dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);
return dist;
}
```
> O(V³)
---
### 14. 最小生成树算法?
#### Kruskal 算法(边贪心 + 并查集)
```java
class Edge implements Comparable<Edge> {
int src, dest, weight;
public int compareTo(Edge other) { return this.weight - other.weight; }
}
public List<Edge> kruskalMST(List<Edge> edges, int V) {
Collections.sort(edges);
UnionFind uf = new UnionFind(V);
List<Edge> result = new ArrayList<>();
for (Edge e : edges) {
if (uf.find(e.src) != uf.find(e.dest)) {
result.add(e);
uf.union(e.src, e.dest);
}
}
return result;
}
```
#### Prim 算法(点贪心 + 优先队列)
```java
public int primMST(int[][] graph, int start) {
int n = graph.length;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]); // [vertex, weight]
boolean[] inMST = new boolean[n];
pq.offer(new int[]{start, 0});
int totalWeight = 0;
while (!pq.isEmpty()) {
int[] curr = pq.poll();
int u = curr[0], w = curr[1];
if (inMST[u]) continue;
inMST[u] = true;
totalWeight += w;
for (int v = 0; v < n; v++) {
if (graph[u][v] > 0 && !inMST[v]) {
pq.offer(new int[]{v, graph[u][v]});
}
}
}
return totalWeight;
}
```
> ✅ Kruskal 适合稀疏图,Prim 适合稠密图
---
### 15. 红黑树的性质?
红黑树是一种自平衡二叉查找树,保证最坏情况 O(log n) 操作。
#### 五大性质:
1. 每个节点是红色或黑色
2. 根节点是黑色
3. 所有叶子(NIL)是黑色
4. 红色节点的子节点必须是黑色(不能有两个连续红节点)
5. 从任一节点到其每个叶子的所有路径包含相同数目的黑节点(黑高度一致)
> ✅ Java 中 `TreeMap` 和 `TreeSet` 基于红黑树实现
---
### 16. B树和B+树的区别?
| 特性 | **B树** | **B+树** |
|------|--------|---------|
| 数据存储 | 所有节点都存数据 | 只有叶子节点存数据,内部节点只存索引 |
| 叶子节点 | 不相连 | 通过链表相连,支持范围查询 |
| 查询效率 | 定位快,但范围查询慢 | 范围查询高效 |
| 高度 | 相对较高 | 更矮更宽,I/O 更少 |
| 应用场景 | 文件系统 | 数据库索引(MySQL InnoDB) |
> ✅ B+树更适合磁盘存储,减少 I/O 次数
---
### 17. 堆排序的实现?
堆排序基于最大堆(或最小堆)实现原地排序。
```java
public void heapSort(int[] arr) {
int n = arr.length;
// 构建最大堆(从最后一个非叶子节点开始)
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 逐个提取最大元素放到末尾
for (int i = n - 1; i > 0; i--) {
swap(arr, 0, i);
heapify(arr, i, 0);
}
}
private void heapify(int[] arr, int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) largest = left;
if (right < n && arr[right] > arr[largest]) largest = right;
if (largest != i) {
swap(arr, i, largest);
heapify(arr, n, largest);
}
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
```
> 时间 O(n log n),空间 O(1),不稳定
---
### 18. 快速排序的优化?
#### 原始快排:
```java
public void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
```
#### 优化策略:
1. **三数取中法选 pivot**:避免极端情况
2. **小数组改用插入排序**
3. **双轴快排(Dual-Pivot)**:JDK7 Arrays.sort() 使用
4. **尾递归优化**:减少栈深度
```java
private int partition(int[] arr, int low, int high) {
int mid = (low + high) / 2;
if (arr[mid] < arr[low]) swap(arr, low, mid);
if (arr[high] < arr[low]) swap(arr, low, high);
if (arr[high] < arr[mid]) swap(arr, mid, high);
swap(arr, mid, high); // 将中位数移到末尾作为 pivot
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high);
return i + 1;
}
```
> ✅ 快排平均最快,但最坏 O(n²)
---
### 19. 二分查找的变种?
#### 查找第一个等于 target 的位置
```java
public int lowerBound(int[] arr, int target) {
int left = 0, right = arr.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
```
#### 查找最后一个等于 target 的位置
```java
public int upperBound(int[] arr, int target) {
int left = 0, right = arr.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
return left - 1;
}
```
> ✅ 可用于统计某个值的出现次数:`upper - lower + 1`
---
### 20. 大数相加的实现?
模拟竖式加法,处理进位。
```java
public String addStrings(String num1, String num2) {
StringBuilder res = new StringBuilder();
int i = num1.length() - 1, j = num2.length() - 1;
int carry = 0;
while (i >= 0 || j >= 0 || carry > 0) {
int x = i >= 0 ? num1.charAt(i--) - '0' : 0;
int y = j >= 0 ? num2.charAt(j--) - '0' : 0;
int sum = x + y + carry;
res.append(sum % 10);
carry = sum / 10;
}
return res.reverse().toString();
}
```
> ✅ 可扩展至大数乘法、减法、除法
---