数据结构 六

数据结构 六

承接单向链表基础增删改查,本文继续深入讲解链表高频面试算法,系统梳理栈与队列的底层实现与经典应用,并完整拆解二叉搜索树(BST)的插入、遍历、查找、删除全流程,所有代码均对齐你提供的源文件,补充完整注释与原理说明。


一、链表进阶高频算法题

链表算法的核心技巧是双指针思想,通过设置不同移动速度、不同起点的指针,在仅一次遍历的前提下完成复杂操作,是力扣热门考点。

1. 环形链表 II:寻找环的入口节点

题目描述

给定一个链表,判断是否存在环;如果有环,返回环的起始节点;无环则返回 null

解题思路:快慢指针 + 数学推导

第一步:判断是否有环

定义快慢指针 fastslow,同时从头节点出发:

  • slow 每次走 1 步
  • fast 每次走 2 步

fast 最终走到链表末尾(指向 null),说明无环;若 fastslow 相遇,则证明链表存在环。

第二步:定位环的入口

两指针相遇后,将其中一个指针重置回头节点,两个指针以**相同速度(每次1步)**同时移动,再次相遇的节点就是环的入口节点。

数学推导证明

设三个变量:

  • 头节点到环入口的距离为 x
  • 环入口到两指针相遇点的距离为 y
  • 相遇点沿环回到入口的距离为 z
  • 环的周长 = y + z

相遇时路程关系:

  • slow 总路程:x + y
  • fast 总路程:x + n*(y+z) + y(n 为 fast 在环内绕的圈数,n ≥ 1)

因为 fast 速度是 slow 的 2 倍,路程也为 2 倍:

2(x+y)=x+n(y+z)+y 2(x + y) = x + n(y+z) + y 2(x+y)=x+n(y+z)+y

化简得:

x=(n−1)(y+z)+z x = (n-1)(y+z) + z x=(n−1)(y+z)+z

该式含义:头节点到入口的距离 x,等于从相遇点出发,绕 n-1 圈后再走 z 的距离。因此两个指针同速移动,一定会在环入口处精准相遇。

代码实现
java 复制代码
/**
 * 寻找环形链表的入口节点
 * @return 环入口节点,无环返回null
 */
public Node detectCycle() {
    if (root == null || root.next == null) {
        return null;
    }

    Node fast = root;
    Node slow = root;

    // 第一步:快慢指针找相遇点
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;

        // 相遇,确认有环,开始定位入口
        if (slow == fast) {
            // 慢指针重置回头节点
            slow = root;
            // 同速移动,再次相遇即为入口
            while (slow != fast) {
                slow = slow.next;
                fast = fast.next;
            }
            return slow;
        }
    }
    // fast走到终点,无环
    return null;
}

2. 反转链表

题目描述

原地反转一个单链表,返回反转后的新头节点,要求不额外创建新链表。

解题思路:双指针迭代法

核心是逐个修改节点的 next 指针,让每个节点的 next 指向它的前驱节点:

  1. 定义两个指针:pre 指向前驱节点(初始为 null),cur 指向当前节点(初始为头节点)
  2. 遍历链表,每次先临时保存当前节点的后继节点(防止断链丢失)
  3. 将当前节点的 next 指向 pre,完成该节点的反转
  4. pre 和 cur 同时后移,直到 cur 遍历完整个链表
  5. 遍历结束后,pre 就是反转后的新头节点
代码实现
java 复制代码
/**
 * 原地反转链表
 */
public void reverseList() {
    Node pre = null;    // 前驱指针,初始为空
    Node cur = root;    // 当前指针,从头节点开始
    Node nextTemp;      // 临时保存后继节点,防止断链

    while (cur != null) {
        nextTemp = cur.next;  // 先保存下一个节点
        cur.next = pre;       // 当前节点指向前驱,完成反转
        pre = cur;            // 前驱指针后移
        cur = nextTemp;       // 当前指针后移
    }
    // 遍历结束,pre成为新的头节点
    root = pre;
}

核心注意点:必须先保存后继节点再修改 next 指针,否则一旦修改指向,后续节点会彻底丢失引用,导致链表断裂。

3. 完整链表工具类(整合所有功能)

对应你提供的 MyLinkedList.java,整合基础增删改查 + 进阶算法的完整版本:

java 复制代码
public class MyLinkedList {
    public Node root;

    // 头插法
    public void HeadInsert(int value){
        Node newNode = new Node(value);
        if(root == null){
            root = newNode;
            return;
        }
        newNode.next = root;
        root = newNode;
    }

    // 尾插法
    public void EndInsert(int value){
        Node newNode = new Node(value);
        if(root == null){
            root = newNode;
            return;
        }
        Node current = root;
        while(current.next != null){
            current = current.next;
        }
        current.next = newNode;
    }

    // 打印链表
    public void Print(){
        Node cur = root;
        while(cur != null){
            System.out.print(cur.data + "->");
            cur = cur.next;
        }
        System.out.println("null");
    }

    // 获取链表长度
    public int size(){
        int count = 0;
        Node cur = root;
        while(cur != null){
            count++;
            cur = cur.next;
        }
        return count;
    }

    // 删除指定下标节点(虚拟头节点法,统一边界逻辑)
    public void delete(int index){
        if(index < 0 || index >= size()){
            return;
        }
        Node dummy = new Node(0);
        dummy.next = root;
        Node cur = dummy;
        // 定位到待删节点的前驱
        for(int i = 0; i < index; i++){
            cur = cur.next;
        }
        cur.next = cur.next.next;
        root = dummy.next;
    }

    // 按值查找是否存在
    public boolean find(int value){
        Node cur = root;
        while(cur != null){
            if(cur.data == value){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

    // 查找链表中间节点(快慢指针)
    public Node findMiddle() {
        Node fast = root;
        Node slow = root;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    // 查找倒数第k个节点
    public Node findLastK(int k) {
        if (k <= 0 || k > size()) return null;
        Node fast = root;
        Node slow = root;
        // 快指针先走k步
        for (int i = 0; i < k; i++) {
            fast = fast.next;
        }
        // 同步移动
        while (fast != null) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }

    // 判断链表是否有环
    public boolean hasCycle() {
        Node fast = root;
        Node slow = root;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) return true;
        }
        return false;
    }

    // 寻找环的入口节点
    public Node detectCycle() {
        if (root == null || root.next == null) return null;
        Node fast = root;
        Node slow = root;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            // 相遇,开始找入口
            if (slow == fast) {
                slow = root;
                while (slow != fast) {
                    slow = slow.next;
                    fast = fast.next;
                }
                return slow;
            }
        }
        return null;
    }

    // 原地反转链表
    public void reverseList() {
        Node pre = null;
        Node cur = root;
        Node nextTemp;
        while (cur != null) {
            nextTemp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = nextTemp;
        }
        root = pre;
    }
}

二、栈与队列

栈和队列是两种操作受限的线性表:栈是「后进先出(LIFO)」,只能在栈顶操作;队列是「先进先出(FIFO)」,队尾入队、队头出队。二者底层均可通过数组或链表实现。

1. 栈的数组实现

数组实现栈非常简单,用一个 top 变量记录栈顶下标,初始为 -1 表示空栈,所有操作都在栈顶完成。

对应你提供的 StringStackDemo.java,泛型版本实现:

java 复制代码
public class StringStackDemo<T> {
    // 底层数组,模拟栈空间
    T[] arr;
    // 栈顶指针,初始为-1表示空栈
    int top = -1;

    /**
     * 构造方法:初始化指定容量的栈
     * @param size 栈的最大容量
     */
    public StringStackDemo(int size) {
        // 泛型数组的标准创建方式
        arr = (T[]) new Object[size];
    }

    /**
     * 入栈:向栈顶添加元素
     * @param value 要入栈的元素
     */
    public void push(T value){
        // 栈满判断
        if(arr.length - 1 == top){
            System.out.println("栈已满,无法入栈");
            return;
        }
        top++;
        arr[top] = value;
    }

    /**
     * 出栈:弹出栈顶元素并打印
     */
    public void pop(){
        // 栈空判断
        if(top == -1){
            System.out.println("栈为空,无法出栈");
            return;
        }
        System.out.println(arr[top]);
        top--;
    }

    /**
     * 查看栈顶元素(不弹出)
     * @return 栈顶元素
     */
    public T peek() {
        if(top == -1) return null;
        return arr[top];
    }

    /**
     * 判断栈是否为空
     */
    public boolean isEmpty() {
        return top == -1;
    }
}

2. 队列的链表实现

队列需要同时操作队头和队尾,用链表实现时会同时维护 head 头指针和 tail 尾指针,让入队和出队都达到 O(1) 时间复杂度。

对应你提供的 LinkQueue.java,泛型版本链式队列:

java 复制代码
public class LinkQueue<W> {
    // 内部节点类
    public static class Node<W>{
        W data;
        Node<W> next;
        Node(W data){
            this.data = data;
            this.next = null;
        }
    }

    Node<W> head;   // 队头指针
    Node<W> tail;   // 队尾指针
    int size = 0;   // 当前元素个数
    int maxSize;    // 队列最大容量

    /**
     * 构造方法:创建指定最大容量的链式队列
     * @param maxSize 最大容量
     */
    public LinkQueue(int maxSize){
        this.maxSize = maxSize;
        head = null;
        tail = null;
    }

    /**
     * 入队:向队尾添加元素
     * @param value 要添加的元素
     */
    public void push(W value){
        // 队列已满
        if(size == maxSize){
            System.out.println("队列满");
            return;
        }
        Node<W> newNode = new Node<>(value);
        // 空队列:头尾指针都指向新节点
        if(head == null){
            head = newNode;
            tail = newNode;
        }else{
            // 非空:尾节点next指向新节点,尾指针后移
            tail.next = newNode;
            tail = newNode;
        }
        size++;
    }

    /**
     * 出队:弹出队头元素并打印
     */
    public void pop(){
        if(size == 0){
            System.out.println("队列空");
            return;
        }
        System.out.println(head.data);
        // 头指针后移
        head = head.next;
        // 出队后队列为空,尾指针也要置空,避免野指针
        if (head == null) {
            tail = null;
        }
        size--;
    }

    /**
     * 查看队头元素
     */
    public W peek() {
        if (size == 0) return null;
        return head.data;
    }

    /**
     * 判断队列是否为空
     */
    public boolean isEmpty() {
        return size == 0;
    }
}

3. 栈经典面试题:有效的括号

题目描述

给定一个只包含 '('')''['']''{''}' 的字符串,判断括号是否有效。要求左括号必须用相同类型的右括号闭合,且必须以正确的顺序闭合。

解题思路

利用栈「后进先出」的匹配特性:

  1. 遍历字符串,遇到左括号时,将对应的右括号压入栈
  2. 遇到右括号时:
    • 栈为空或栈顶元素不匹配 → 直接返回 false
    • 匹配成功 → 弹出栈顶
  3. 遍历结束后,栈为空说明所有括号都正确闭合

对应你提供的 StackTest1.java

java 复制代码
import java.util.Stack;

public class StackTest1 {
    /**
     * 判断括号字符串是否有效
     * @param s 括号字符串,例如 "({})"
     * @return 是否有效
     */
    public static boolean isValid(String s){
        Stack<Character> stack = new Stack<>();
        for(int i = 0; i < s.length(); i++){
            char c = s.charAt(i);
            // 左括号:压入对应的右括号
            if(c == '('){
                stack.push(')');
            }else if(c == '['){
                stack.push(']');
            }else if(c == '{'){
                stack.push('}');
            }else if(stack.isEmpty() || stack.peek() != c){
                // 右括号不匹配或栈已提前为空
                return false;
            }else{
                // 匹配成功,弹出栈顶
                stack.pop();
            }
        }
        // 所有左括号都匹配完成,栈为空
        return stack.isEmpty();
    }
}

三、二叉搜索树(BST)

二叉搜索树(Binary Search Tree,也叫有序二叉树)是一种特殊二叉树,核心规则:任意节点的左子树所有节点值都小于该节点,右子树所有节点值都大于该节点。它的中序遍历结果是天然的升序序列,这是 BST 最核心的特性。

1. 树节点结构

二叉树每个节点包含三部分:数据域、左孩子指针、右孩子指针。

对应你提供的 TreeNode.java

java 复制代码
package tree;

public class TreeNode {
    public Integer data;          // 节点存储的数据
    public TreeNode leftNode;     // 左孩子指针
    public TreeNode rightNode;    // 右孩子指针

    /**
     * 带参构造方法:创建节点时直接赋值
     * @param x 节点值
     */
    public TreeNode(Integer x) {
        this.data = x;
        // 左右孩子默认初始化为null
    }
}

2. 二叉搜索树的插入

核心思路
  1. 创建新节点
  2. 如果树为空,新节点直接作为根节点
  3. 树非空时,从根节点开始循环比较:
    • 新值 < 当前节点值 → 往左子树走,左孩子为空则插入
    • 新值 ≥ 当前节点值 → 往右子树走,右孩子为空则插入
  4. 找到空位置完成插入,结束

注意:你提供的 BinaryTree0.java 存在经典 bug:cur 移动后直接 return,没有继续循环比较,导致只能插入两层节点。下面给出的是 BinaryTree.java 中的正确循环实现。

正确插入代码
java 复制代码
/**
 * 向二叉搜索树中插入节点
 * @param x 要插入的值
 */
public void createBinaryTree(Integer x){
    // 1. 创建新节点
    TreeNode newNode = new TreeNode(x);

    // 2. 树为空,新节点作为根
    if (root == null){
        root = newNode;
        return;
    }

    // 3. 从根节点遍历,寻找插入位置
    TreeNode cur = root;
    while (cur != null){
        if (x < cur.data){
            // 往左子树查找
            if(cur.leftNode != null){
                cur = cur.leftNode;
            }else {
                // 左孩子为空,在此插入
                cur.leftNode = newNode;
                return;
            }
        }else{
            // 往右子树查找
            if (cur.rightNode != null){
                cur = cur.rightNode;
            }else {
                // 右孩子为空,在此插入
                cur.rightNode = newNode;
                return;
            }
        }
    }
}

3. 二叉树的四种遍历方式

二叉树遍历分为深度优先遍历(DFS)广度优先遍历(BFS)。深度优先又分为先序、中序、后序三种,区别在于根节点的访问时机。

(1)先序遍历(根 → 左 → 右)

访问顺序:先打印当前节点,再递归遍历左子树,最后递归遍历右子树。

java 复制代码
/**
 * 先序遍历
 * @param treeNode 当前遍历节点
 */
public void beforeOrder(TreeNode treeNode){
    // 递归出口:节点为空则返回
    if(treeNode == null){
        return;
    }
    // 1. 访问根节点
    System.out.println(treeNode.data);
    // 2. 递归遍历左子树
    beforeOrder(treeNode.leftNode);
    // 3. 递归遍历右子树
    beforeOrder(treeNode.rightNode);
}
(2)中序遍历(左 → 根 → 右)

访问顺序:先递归遍历左子树,再打印当前节点,最后递归遍历右子树。

核心特性:二叉搜索树的中序遍历结果是严格升序的序列。

java 复制代码
/**
 * 中序遍历
 * @param treeNode 当前遍历节点
 */
public void middleOrder(TreeNode treeNode){
    if(treeNode == null){
        return;
    }
    // 1. 递归遍历左子树
    middleOrder(treeNode.leftNode);
    // 2. 访问根节点
    System.out.println(treeNode.data);
    // 3. 递归遍历右子树
    middleOrder(treeNode.rightNode);
}
(3)后序遍历(左 → 右 → 根)

访问顺序:先递归遍历左子树,再递归遍历右子树,最后打印当前节点。

java 复制代码
/**
 * 后序遍历
 * @param treeNode 当前遍历节点
 */
public void afterOrder(TreeNode treeNode){
    if(treeNode == null){
        return;
    }
    // 1. 递归遍历左子树
    afterOrder(treeNode.leftNode);
    // 2. 递归遍历右子树
    afterOrder(treeNode.rightNode);
    // 3. 访问根节点
    System.out.println(treeNode.data);
}
(4)层序遍历(广度优先)

按层级从上到下、每层从左到右依次访问节点,需要借助队列实现:

  1. 根节点先入队
  2. 队列不为空则循环:出队一个节点并打印
  3. 将当前节点的左、右孩子依次入队(非空才入队)
  4. 队列为空时遍历结束
java 复制代码
import java.util.LinkedList;
import java.util.Queue;

/**
 * 层序遍历(广度优先遍历)
 */
public void levelOrder(){
    if (root == null) return;

    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);

    while(!queue.isEmpty()){
        TreeNode cur = queue.poll();
        // 访问当前节点
        System.out.println(cur.data);
        // 左孩子入队
        if(cur.leftNode != null){
            queue.offer(cur.leftNode);
        }
        // 右孩子入队
        if(cur.rightNode != null){
            queue.offer(cur.rightNode);
        }
    }
}

4. 查找操作

(1)查找目标节点

利用 BST 有序特性,从根节点开始比较大小,左小右大,直到找到目标或遍历到空。

java 复制代码
/**
 * 查找值为target的节点
 * @param node 起始节点
 * @param target 目标值
 * @return 找到的节点,找不到返回null
 */
public TreeNode find(TreeNode node, int target){
    if(node == null){
        return null;
    }
    if(node.data == target){
        // 找到目标节点
        return node;
    }else if(node.data > target){
        // 目标值更小,去左子树找
        return find(node.leftNode, target);
    }else{
        // 目标值更大,去右子树找
        return find(node.rightNode, target);
    }
}
(2)查找目标节点的父节点

删除节点时需要修改父节点的指针,因此需要单独实现查找父节点的方法。

java 复制代码
/**
 * 查找目标节点的父节点
 * @param node 起始节点
 * @param target 目标值
 * @return 父节点,找不到返回null
 */
public TreeNode findParent(TreeNode node, int target){
    if(node == null){
        return null;
    }
    // 判断当前节点的左/右孩子是不是目标节点
    if( (node.leftNode != null && node.leftNode.data == target)
            || (node.rightNode != null && node.rightNode.data == target) ){
        return node;
    }else {
        // 目标值小,去左子树继续找
        if(node.leftNode != null && node.data > target){
            return findParent(node.leftNode, target);
        }
        // 目标值大,去右子树继续找
        else if(node.rightNode != null && node.data < target){
            return findParent(node.rightNode, target);
        }else {
            return null;
        }
    }
}

5. 二叉搜索树的删除

BST 删除是最复杂的操作,需要分三种场景处理:

  1. 叶子节点:直接删除,将父节点对应的指针置空
  2. 只有一棵子树:让父节点直接指向该节点的子树,跳过被删节点
  3. 有两棵子树:不能直接删除,用「右子树最小值」或「左子树最大值」替换当前节点的值,再删除那个最小/最大节点(该节点一定是叶子或单子树,不会递归触发双孩子场景)
辅助方法:查找右子树的最小值

右子树的最小值就是右子树最左侧的节点,用来替换被删节点的值,保证有序性。

java 复制代码
/**
 * 查找以node为根的子树的最小值,并删除该最小节点
 * @param node 子树根节点
 * @return 最小值
 */
public int findRightTreeMin(TreeNode node){
    TreeNode cur = node;
    // 一直往左走,最左节点就是最小值
    while (cur.leftNode != null){
        cur = cur.leftNode;
    }
    // 删除这个最小节点
    delete(node, cur.data);
    return cur.data;
}
完整删除方法
java 复制代码
/**
 * 删除值为target的节点
 * @param node 起始节点
 * @param target 要删除的值
 */
public void delete(TreeNode node, int target){
    // 树为空
    if(node == null){
        System.out.println("树为null");
        return;
    }

    // 1. 定位目标节点
    TreeNode targetNode = find(node, target);
    if(targetNode == null){
        System.out.println("没有找到该节点");
        return;
    }

    // 2. 定位目标节点的父节点
    TreeNode parentNode = findParent(node, target);

    // 情况一:目标节点是叶子节点(左右都为空)
    if(targetNode.leftNode == null && targetNode.rightNode == null){
        // 特殊情况:删除的是根节点,且整棵树只有一个节点
        if(parentNode == null){
            root = null;
            return;
        }
        // 目标是父节点的左孩子
        if(parentNode.leftNode != null && parentNode.leftNode.data == target){
            parentNode.leftNode = null;
        }
        // 目标是父节点的右孩子
        else if(parentNode.rightNode != null && parentNode.rightNode.data == target){
            parentNode.rightNode = null;
        }
    }
    // 情况二:目标节点有左右两棵子树
    else if(targetNode.leftNode != null && targetNode.rightNode != null){
        // 用右子树的最小值替换当前节点的值
        targetNode.data = findRightTreeMin(targetNode.rightNode);
    }
    // 情况三:目标节点只有一棵子树(左或右)
    else {
        // 目标节点有左子树
        if(targetNode.leftNode != null){
            // 删除的是根节点
            if(parentNode == null){
                root = targetNode.leftNode;
                return;
            }
            // 目标是父节点的左孩子
            if(parentNode.leftNode.data == target){
                parentNode.leftNode = targetNode.leftNode;
            }
            // 目标是父节点的右孩子
            else {
                parentNode.rightNode = targetNode.leftNode;
            }
        }
        // 目标节点有右子树
        else{
            // 删除的是根节点
            if(parentNode == null){
                root = targetNode.rightNode;
                return;
            }
            // 目标是父节点的左孩子
            if(parentNode.leftNode.data == target){
                parentNode.leftNode = targetNode.rightNode;
            }
            // 目标是父节点的右孩子
            else {
                parentNode.rightNode = targetNode.rightNode;
            }
        }
    }
}

6. 完整 BST 类 + 测试代码

BinaryTree 完整类
java 复制代码
package tree;

import java.util.LinkedList;
import java.util.Queue;

public class BinaryTree {
    // 根节点指针
    public TreeNode root;

    /**
     * 插入节点,构建二叉搜索树
     */
    public void createBinaryTree(Integer x){
        TreeNode newNode = new TreeNode(x);
        if (root == null){
            root = newNode;
            return;
        }
        TreeNode cur = root;
        while (cur != null){
            if (x < cur.data){
                if(cur.leftNode != null){
                    cur = cur.leftNode;
                }else {
                    cur.leftNode = newNode;
                    return;
                }
            }else{
                if (cur.rightNode != null){
                    cur = cur.rightNode;
                }else {
                    cur.rightNode = newNode;
                    return;
                }
            }
        }
    }

    /**
     * 先序遍历
     */
    public void beforeOrder(TreeNode treeNode){
        if(treeNode == null) return;
        System.out.println(treeNode.data);
        beforeOrder(treeNode.leftNode);
        beforeOrder(treeNode.rightNode);
    }

    /**
     * 中序遍历
     */
    public void middleOrder(TreeNode treeNode){
        if(treeNode == null) return;
        middleOrder(treeNode.leftNode);
        System.out.println(treeNode.data);
        middleOrder(treeNode.rightNode);
    }

    /**
     * 后序遍历
     */
    public void afterOrder(TreeNode treeNode){
        if(treeNode == null) return;
        afterOrder(treeNode.leftNode);
        afterOrder(treeNode.rightNode);
        System.out.println(treeNode.data);
    }

    /**
     * 层序遍历
     */
    public void levelOrder(){
        if (root == null) return;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while(!queue.isEmpty()){
            TreeNode cur = queue.poll();
            System.out.println(cur.data);
            if(cur.leftNode != null) queue.offer(cur.leftNode);
            if(cur.rightNode != null) queue.offer(cur.rightNode);
        }
    }

    /**
     * 查找目标节点
     */
    public TreeNode find(TreeNode node, int target){
        if(root == null){
            System.out.println("树为空");
            return null;
        }
        if(node.data == target){
            return node;
        }else if(node.data > target){
            if(node.leftNode == null) return null;
            return find(node.leftNode, target);
        }else{
            if(node.rightNode == null) return null;
            return find(node.rightNode, target);
        }
    }

    /**
     * 查找目标节点的父节点
     */
    public TreeNode findParent(TreeNode node, int target){
        if(root == null){
            System.out.println("树为空");
            return null;
        }
        if( (node.leftNode!=null && node.leftNode.data == target)
                || (node.rightNode!=null && node.rightNode.data == target) ){
            return node;
        }else {
            if(node.leftNode!=null && node.data > target){
                return findParent(node.leftNode, target);
            }else if(node.rightNode!=null && node.data < target){
                return findParent(node.rightNode, target);
            }else {
                return null;
            }
        }
    }

    /**
     * 查找右子树最小值并删除该节点
     */
    public int findRightTreeMin(TreeNode node){
        TreeNode cur = node;
        while (cur.leftNode != null){
            cur = cur.leftNode;
        }
        delete(node, cur.data);
        return cur.data;
    }

    /**
     * 删除节点
     */
    public void delete(TreeNode node, int target){
        if(node == null){
            System.out.println("树为null");
            return;
        }
        // 整棵树只有一个节点
        if(node.leftNode == null && node.rightNode == null){
            node = null;
            return;
        }

        TreeNode targetNode = find(node, target);
        if(targetNode == null){
            System.out.println("没有找到该节点");
            return;
        }

        TreeNode parentNode = findParent(node, target);

        // 情况1:叶子节点
        if(targetNode.leftNode == null && targetNode.rightNode == null){
            if(parentNode == null){
                root = null;
                return;
            }
            if(parentNode.leftNode != null && parentNode.leftNode.data == target){
                parentNode.leftNode = null;
            }else if(parentNode.rightNode!=null && parentNode.rightNode.data == target){
                parentNode.rightNode = null;
            }
        }
        // 情况2:有左右两棵子树
        else if(targetNode.leftNode != null && targetNode.rightNode != null){
            targetNode.data = findRightTreeMin(targetNode.rightNode);
        }
        // 情况3:只有一棵子树
        else {
            if(targetNode.leftNode !=null){
                if(parentNode == null){
                    root = targetNode.leftNode;
                    return;
                }
                if(parentNode.leftNode.data == target){
                    parentNode.leftNode = targetNode.leftNode;
                }else {
                    parentNode.rightNode = targetNode.leftNode;
                }
            }else{
                if(parentNode == null){
                    root = targetNode.rightNode;
                    return;
                }
                if(parentNode.leftNode.data == target){
                    parentNode.leftNode = targetNode.rightNode;
                }else {
                    parentNode.rightNode = targetNode.rightNode;
                }
            }
        }
    }
}
测试类 Test.java
java 复制代码
package tree;

public class Test {
    public static void main(String[] args) {
        BinaryTree binaryTree = new BinaryTree();
        // 构建二叉搜索树
        binaryTree.createBinaryTree(5);
        binaryTree.createBinaryTree(3);
        binaryTree.createBinaryTree(7);
        binaryTree.createBinaryTree(1);
        binaryTree.createBinaryTree(4);
        binaryTree.createBinaryTree(6);
        binaryTree.createBinaryTree(9);

        System.out.println("===== 先序遍历 =====");
        binaryTree.beforeOrder(binaryTree.root);

        System.out.println("===== 中序遍历 =====");
        binaryTree.middleOrder(binaryTree.root);

        System.out.println("===== 后序遍历 =====");
        binaryTree.afterOrder(binaryTree.root);

        System.out.println("===== 层序遍历 =====");
        binaryTree.levelOrder();

        // 测试删除
        System.out.println("===== 删除节点7,中序遍历 =====");
        binaryTree.delete(binaryTree.root, 7);
        binaryTree.middleOrder(binaryTree.root);
    }
}

本篇总结

  1. 链表进阶:环形链表入口、反转链表都是双指针思想的经典应用,通过调整指针的速度与起点,在一次遍历内完成多轮遍历才能实现的功能,是面试必背题型。
  2. 栈与队列:栈后进先出,适合括号匹配、表达式求值、回溯类场景;队列先进先出,适合层序遍历、任务排队场景。数组实现栈简单高效,链表实现队列可避免扩容开销。
  3. 二叉搜索树:核心特性是左小右大,中序遍历天然有序;插入、查找可通过循环或递归实现,删除需分叶子、单子树、双子树三种情况处理,双子树用右子树最小值替换是标准解法。