前言 :链表和二叉搜索树是后端、Java开发面试必考基础数据结构,本文整理可直接运行的手写源码,包含单向链表反转、判环、找环入口,二叉搜索树增删查、遍历全套功能,同时拆解底层数学原理、代码踩坑点和高频面试题,零基础也能看懂吃透!
一、单向链表核心操作实战
链表是离散存储的线性结构,无连续内存空间,每个节点包含数值域和指针域。相较于数组,链表增删元素无需移动大量数据,效率更高,唯一缺点是无法随机访问。
1.1 链表节点基础定义
自定义链表节点类,包含存储值value和指向下一节点的next指针,构造方法完成节点初始化。
public class Node {
// 节点存储数值
int value;
// 指向下一个节点的指针
Node next;
// 构造方法:初始化节点值,默认后继节点为空
public Node(int value) {
this.value = value;
this.next = null;
}
}
1.2 链表工具类完整代码
封装链表核心高频操作:迭代法反转链表、判断链表是否有环、查找环形链表入口节点,全部为最优时间/空间复杂度解法。
public class LinkList {
// 链表头节点
public Node head = null;
/**
* 链表反转(迭代法)
* 空间复杂度O(1),时间复杂度O(n),最优解法
*/
public void reverse() {
Node pre = null; // 记录当前节点的前一个节点
Node index = head; // 遍历指针,从头部开始
while (index != null) {
// 缓存下一个节点,防止断链(核心步骤)
Node temp = index.next;
// 当前节点反向指向前置节点
index.next = pre;
// 前置节点后移
pre = index;
// 遍历指针后移
index = temp;
}
// 循环结束,pre指向最后一个节点,即新的头节点
head = pre;
}
/**
* 快慢指针法:判断链表是否存在环
* @return true-有环,false-无环
*/
public boolean hasCycle() {
// 快慢指针均从头部开始
Node fast = head;
Node slow = head;
// 快指针一次走两步,需判断自身和下一节点非空,防止空指针异常
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
// 快慢指针相遇,证明存在环
if (fast == slow) {
return true;
}
}
// 遍历到末尾节点,无环
return false;
}
/**
* 查找环形链表的环入口节点
* @return 环入口节点,无环返回null
*/
public Node hashCycleNode() {
Node fast = head;
Node slow = head;
// 第一步:快慢指针相遇,确认有环
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
// 第二步:慢指针重置到头部,快慢指针同速前进
slow = head;
while (slow != fast) {
fast = fast.next;
slow = slow.next;
}
// 再次相遇点即为环入口
return slow;
}
}
// 无环返回空
return null;
}
}
1.3 快慢指针数学原理(面试核心)
很多开发者只会写代码不懂原理,面试极易被问倒,以下是环入口查找的纯数学推导:
-
设定变量:头节点到环入口距离为 x ,环入口到快慢指针相遇点距离为 y ,相遇点回到环入口的距离为 z,环总长度为 y+z;
-
核心逻辑:快指针速度是慢指针2倍,相同时间内,快指针路程=2*慢指针路程;
-
公式推导:2(x+y) = x + n(y+z) + y(n为快指针绕环的圈数);
-
公式化简:x = (n-1)(y+z) + z;
-
结论:链表头到环入口的距离 = 相遇点绕环n-1圈后到环入口的距离。因此相遇后,慢指针重置头部、双指针同速前进,相遇点即为环入口。
1.4 链表反转逐行踩坑详解
链表反转最核心的问题是断链,新手90%的报错都源于此,核心避坑点:
-
必须先缓存后继节点:修改当前节点next指针前,先用temp保存index.next,否则修改指针后会丢失后续所有节点,造成链表断裂;
-
pre指针的作用:全程保存已经完成反转的前置节点,是反向链表的核心依托;
-
循环收尾赋值:循环结束后index指向null,pre是反转后的新头节点,必须手动赋值 head = pre,否则头节点不变,反转失效。
二、有序二叉搜索树(BST)完整实战
二叉搜索树(BST)是面试高频树结构,核心规则:左子树所有节点值 < 根节点值 < 右子树所有节点值。BST中序遍历结果为严格升序数组,查询、插入、删除效率极高。
2.1 二叉树节点定义
相较于链表节点,二叉树节点包含左、右孩子双指针。
public class Node {
// 节点数值
int value;
// 左孩子节点
Node left;
// 右孩子节点
Node right;
// 构造方法初始化节点
public Node(int value) {
this.value = value;
this.left = null;
this.right = null;
}
}
2.2 BST完整工具类
封装BST全套核心功能:插入、查询、查询父节点、获取最小值、删除节点、层序遍历、中序遍历,覆盖笔试面试所有考点。
import java.util.LinkedList;
import java.util.Queue;
public class BinaryTree {
// 二叉树根节点
Node root = null;
/**
* 二叉搜索树插入节点
*/
public void insert(int value) {
Node node = new Node(value);
// 空树:直接赋值根节点
if (root == null) {
root = node;
return;
}
Node index = root;
while (true) {
// 数值小于当前节点,遍历左子树
if (node.value < index.value) {
// 左子树为空,直接挂载
if (index.left == null) {
index.left = node;
return;
}
index = index.left;
} else {
// 数值大于等于当前节点,遍历右子树
if (index.right == null) {
index.right = node;
return;
}
index = index.right;
}
}
}
/**
* 根据数值查找节点
*/
public Node find(int value) {
Node index = root;
while (index != null) {
if (index.value == value) {
return index; // 匹配成功,返回节点
} else if (index.value > value) {
index = index.left; // 目标值更小,查左子树
} else {
index = index.right; // 目标值更大,查右子树
}
}
return null; // 无匹配节点
}
/**
* 查找目标节点的父节点
*/
public Node findParentNode(int value) {
Node index = root;
while (index != null) {
// 判断当前节点的左右孩子是否为目标节点
if ((index.left != null && index.left.value == value)
|| (index.right != null && index.right.value == value)) {
return index;
}
// 向下遍历查找
if (index.value > value) {
index = index.left;
} else {
index = index.right;
}
}
return null; // 目标为根节点/节点不存在,无父节点
}
/**
* 获取指定节点右子树的最小值节点(BST删除专用)
*/
public int rightMin(Node node) {
Node index = node;
// BST最左侧节点即为最小值
while (index.left != null) {
index = index.left;
}
return index.value;
}
/**
* 二叉搜索树删除节点(三大场景)
*/
public void delete(int value) {
Node target = find(value);
// 节点不存在,直接返回
if (target == null) {
System.out.println("不存在该节点,删除失败");
return;
}
Node parent = findParentNode(value);
// 场景1:删除叶子节点(无左右子节点)
if (target.left == null && target.right == null) {
// 删除根节点
if (parent == null) {
root = null;
return;
}
// 非根节点,断开父节点指向
if (parent.left == target) {
parent.left = null;
} else {
parent.right = null;
}
return;
}
// 场景2:删除仅有单侧子树的节点(只有左/右孩子)
if (target.left == null || target.right == null) {
// 删除根节点
if (parent == null) {
root = (target.left != null) ? root.left : root.right;
return;
}
// 非根节点,父节点直接指向唯一子节点
if (parent.left == target) {
parent.left = (target.left != null) ? target.left : target.right;
} else {
parent.right = (target.left != null) ? target.left : target.right;
}
return;
}
// 场景3:删除左右子树都存在的节点(最复杂)
// 1. 取右子树最小值覆盖当前节点值
// 2. 递归删除右子树最小值节点
int minVal = rightMin(target.right);
delete(minVal);
target.value = minVal;
}
/**
* 层序遍历(BFS广度优先,队列实现)
* 从上到下、从左到右遍历
*/
public void levelOrder() {
Queue<Node> queue = new LinkedList<>();
if (root != null) {
queue.add(root);
}
while (!queue.isEmpty()) {
Node temp = queue.poll();
System.out.print(temp.value + " ");
// 左右孩子依次入队
if (temp.left != null) {
queue.add(temp.left);
}
if (temp.right != null) {
queue.add(temp.right);
}
}
}
/**
* 中序遍历(DFS深度优先)
* 遍历顺序:左子树 - 根节点 - 右子树(BST中序遍历为升序)
*/
public void inOrder(Node node) {
if (node == null) {
return;
}
inOrder(node.left);
System.out.print(node.value + " ");
inOrder(node.right);
}
// 对外暴露的中序遍历入口
public void printInOrder() {
inOrder(root);
}
}
2.3 BST节点删除三大核心场景(面试重难点)
删除是二叉搜索树最难的操作,所有场景均严格遵循BST有序规则:
场景1:删除叶子节点(无左右子节点)
根叶子节点:直接置空 root = null; 非根叶子节点:判断目标是父节点的左/右孩子,将对应指针置空即可。
场景2:删除单侧子树节点(仅有左/右孩子)
根节点:直接将根节点替换为唯一的左/右子节点; 非根节点:父节点的指针直接指向目标节点的唯一子节点,跳过待删除节点。
场景3:删除双子树节点(左右孩子均存在)
核心规则 :用目标节点右子树的最小值节点(后继节点) 覆盖当前节点值,再递归删除该最小值节点。 原理:右子树最小值是比目标值大的最小数值,替换后完全满足「左小右大」的BST规则,不会破坏树结构。
2.4 二叉树遍历方式对比
1. 深度优先DFS(递归实现)
- 中序遍历:左→根→右,BST专属升序遍历,面试最常用; - 前序遍历:根→左→右,适合获取树结构; - 后序遍历:左→右→根,适合节点销毁、统计树深度。
2. 广度优先BFS(队列实现/层序遍历)
从上到下、同层从左到右遍历,适合求树的高度、按层打印节点、判断完全二叉树等场景。
2.5 完整测试Demo
可直接运行,验证插入、遍历、删除功能有效性。
public class Test {
public static void main(String[] args) {
BinaryTree tree = new BinaryTree();
// 插入节点构建二叉树
tree.insert(5);
tree.insert(4);
tree.insert(2);
tree.insert(7);
// 中序遍历(默认升序)
System.out.println("中序遍历(升序):");
tree.printInOrder();
// 层序遍历
System.out.println("\n\n层序遍历:");
tree.levelOrder();
// 测试删除节点
tree.delete(4);
System.out.println("\n\n删除节点4后,中序遍历:");
tree.printInOrder();
}
}
三、面试高频踩坑点+真题汇总
3.1 单向链表常见坑
- 链表反转未缓存next节点,直接修改指针导致链表断裂; 2. 快慢指针判环,循环条件漏写 fast.next != null,触发空指针异常; 3. 查找环入口,相遇后未重置慢指针到头部,返回错误节点。
3.2 二叉搜索树常见坑
- 节点插入循环未写return,造成死循环; 2. 双子树节点删除时直接替换节点,而非替换数值,指针逻辑混乱出错; 3. 查找父节点未判断根节点,根节点无父节点,未处理会返回异常; 4. 层序遍历未判空根节点,空树时入队空节点触发异常。
3.3 高频面试真题
- 链表反转迭代法和递归法的空间、时间复杂度对比? 2. 环形链表快慢指针的底层数学原理是什么? 3. BST删除三种场景分别如何处理?为什么用右子树最小值替换? 4. 深度优先和广度优先遍历的适用场景? 5. 手写二叉树前、中、后序递归遍历代码。
四、总结
本文覆盖了Java后端面试最核心的两种基础数据结构:单向链表聚焦指针操作、快慢指针算法;二叉搜索树聚焦增删查核心逻辑,尤其是删除节点的三种场景,是笔试面试的必考重难点。
所有代码均为手写可直接运行版本,无冗余、无bug。数据结构学习核心在于动手实操,建议大家本地运行代码、修改测试数据,吃透底层逻辑,告别死记硬背!后续可拓展学习平衡二叉树、哈希表、红黑树等进阶结构。