向链表+二叉搜索树

前言 :链表和二叉搜索树是后端、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 快慢指针数学原理(面试核心)

很多开发者只会写代码不懂原理,面试极易被问倒,以下是环入口查找的纯数学推导

  1. 设定变量:头节点到环入口距离为 x ,环入口到快慢指针相遇点距离为 y ,相遇点回到环入口的距离为 z,环总长度为 y+z;

  2. 核心逻辑:快指针速度是慢指针2倍,相同时间内,快指针路程=2*慢指针路程

  3. 公式推导:2(x+y) = x + n(y+z) + y(n为快指针绕环的圈数);

  4. 公式化简:x = (n-1)(y+z) + z

  5. 结论:链表头到环入口的距离 = 相遇点绕环n-1圈后到环入口的距离。因此相遇后,慢指针重置头部、双指针同速前进,相遇点即为环入口。

1.4 链表反转逐行踩坑详解

链表反转最核心的问题是断链,新手90%的报错都源于此,核心避坑点:

  1. 必须先缓存后继节点:修改当前节点next指针前,先用temp保存index.next,否则修改指针后会丢失后续所有节点,造成链表断裂;

  2. pre指针的作用:全程保存已经完成反转的前置节点,是反向链表的核心依托;

  3. 循环收尾赋值:循环结束后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 单向链表常见坑

  1. 链表反转未缓存next节点,直接修改指针导致链表断裂; 2. 快慢指针判环,循环条件漏写 fast.next != null,触发空指针异常; 3. 查找环入口,相遇后未重置慢指针到头部,返回错误节点。

3.2 二叉搜索树常见坑

  1. 节点插入循环未写return,造成死循环; 2. 双子树节点删除时直接替换节点,而非替换数值,指针逻辑混乱出错; 3. 查找父节点未判断根节点,根节点无父节点,未处理会返回异常; 4. 层序遍历未判空根节点,空树时入队空节点触发异常。

3.3 高频面试真题

  1. 链表反转迭代法和递归法的空间、时间复杂度对比? 2. 环形链表快慢指针的底层数学原理是什么? 3. BST删除三种场景分别如何处理?为什么用右子树最小值替换? 4. 深度优先和广度优先遍历的适用场景? 5. 手写二叉树前、中、后序递归遍历代码。

四、总结

本文覆盖了Java后端面试最核心的两种基础数据结构:单向链表聚焦指针操作、快慢指针算法;二叉搜索树聚焦增删查核心逻辑,尤其是删除节点的三种场景,是笔试面试的必考重难点。

所有代码均为手写可直接运行版本,无冗余、无bug。数据结构学习核心在于动手实操,建议大家本地运行代码、修改测试数据,吃透底层逻辑,告别死记硬背!后续可拓展学习平衡二叉树、哈希表、红黑树等进阶结构。