第6章. 二叉搜索树(BST)

发布时间 2023-12-06 20:38:04作者: Ac_c0mpany丶

二叉搜索树(Binary Search Tree)


使用二叉搜索树,可以使添加、删除、搜索的最坏时间复杂度优化至O(logn)

一、BST的相关概念

  • 二叉搜索树是二叉树的一种,又被称为二叉查找树二叉排序树,是应用非常广泛的一种二叉树,简称BST。
  • 任意一个节点的值都大于子树所有节点的值
  • 任意一个节点的值都小于子树所有节点的值
  • 它的左右子树也是一棵二叉搜索树
  • 二叉搜索树可以大大提高搜索数据的效率
  • (重要)二叉搜索树存储的元素必须具备可比较性
    • 比如int、double等
    • 如果是自定义类型,需要指定比较方式
    • (重要)节点不允许为null

二、BST的接口设计

public class BinarySearchTree<E> {
    private int size;
    private Node<E> root;  // 根节点
    private Comparator<E> comparator; // 外部比较器

    public BinarySearchTree() {
        this(null);
    }

    public BinarySearchTree(Comparator<E> comparator) {
        this.comparator = comparator;
    }
    
    // 元素的数量
    public int size() {}
    
    // 是否为空
    public boolean isEmpty() {}
    
    // 清空所有元素
    public void clear() {}
    
    // 添加元素
    public void add(E element) {}
    
    // 删除元素
    public E remove(E element) {}
    
    // 是否包含某元素
    public boolean contains(E element) {}
}

注意:二叉树的元素是没有索引的概念。

三、BST的设计

public class BinarySearchTree<E> {
    private int size;
    private Node<E> root;  // 根节点
    private Comparator<E> comparator; // 外部比较器

    public BinarySearchTree() {
        this(null);
    }

    public BinarySearchTree(Comparator<E> comparator) {
        this.comparator = comparator;
    }

    // 内部维护一个节点类
    private class Node<E> {
        E element;
        Node<E> left; // 左子节点
        Node<E> right; // 右子节点
        Node<E> parent; // 父节点

        public Node(E element, Node<E> parent) {// 创建一个节点,必然有父节点,不一定有左右子节点
            this.element = element;
            this.parent = parent;
        }
    }

四、BST的相关操作

4.1 添加操作add(E element)

步骤:

  1. 找到父节点parent
  2. 创建新节点node
  3. 将新节点"挂"上。parent.left = node 或者 parent.right = node

如果有值相等的元素就覆盖旧的值。

// 添加元素
public void add(E element) {
    elementNotNullCheck(element);

    if (root == null) {
        root = new Node(element, null);
        size ++;
        return;
    }

    Node<E> cur = root;
    Node<E> parent = root;
    int cmp = 0;
    while (cur != null) {
        parent = cur;  // 在cur向左向右之间,保存父节点
        cmp = compare(element, cur.element);
        if (cmp > 0) {
            cur = cur.right;
        } else if (cmp < 0) {
            cur = cur.left;
        } else {
            cur.element = element;  // 树中存在该节点,执行覆盖操作
            return;
        }
    }
    
    // 此时cur保存的就是父节点
    Node newNode = new Node(element, parent);

    // 将节点挂在父节点的左/右
    if (cmp > 0) {
        parent.right = newNode;
    } else if (cmp < 0) {
        parent.left = newNode;
    }
    size ++;
}
4.2 元素的比较方案设计
public class BinarySearchTree<E> implements BinaryTreeInfo {
    private int size;
    private Node<E> root;
    private Comparator<E> comparator; // 外部比较器

    public BinarySearchTree() {
        this(null);
    }

    public BinarySearchTree(Comparator<E> comparator) {
        this.comparator = comparator;
    }
    
    private int compare(E e1, E e2) {
        // 传外部比较器,可以定制比较规则
        if (comparator != null) {
            return comparator.compare(e1, e2); // 不要忘记return
        }
        // 没有传外部比较器,就调用类的compareTo方法
        return ((Comparable<E>)e1).compareTo(e2);
    }
}

元素的比较方案设计:

  • 允许在外界传入一个Comparator自定义比较方案,重写其Compare方法
  • 如果没有传入Comparator,就必须让这个类实现Comparable接口,重写CompareTo方法

Tips:P97~P100讲解了Comparable和Comparator的完美结合,需要理解。

补充:Java自定义类型的比较(Comparable和Comparator)
在实际开发中,我们不仅需要对基本数据类型进行比较,有时还需要比较自定类型,那么我们怎么样实现自定义类型之间的比较呢?
在Java开发中,自定义类型之间不可以通过>、<、==进行比较。一般情况下,我们会通过实现①Comparable接口;②Comparator接口
class Student implements Comparable<Student>{
    public String name;
    public int age;
 
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
 
    @Override
    public int compareTo(Student o) {
        if(this.age > o.age) {
            return 1;
        }
        if(this.age == o.age) {
            return 0;
        }
        return -1;
    }
}
public class TestDemo {
 
    public static void main(String[] args) {
        Student student1 = new Student("bit",12);
        Student student2 = new Student("gaobo",13);
 
        if(student1.compareTo(student2) > 0 ) {
            System.out.println("student1的年龄 > student2的年龄");
        }else if(student1.compareTo(student2) == 0){
            System.out.println("student1的年龄 == student2的年龄");
        }else {
            System.out.println("student1的年龄 < student2的年龄");
        }
    }
}

运行结果:

student1的年龄 < student2的年龄

我们可以看到,上面的代码是实现了对年龄的比较,但是如果我们想对姓名进行比较呢?这也就显示了这个接口的弊端了,如果按照姓名比较,我们需要重新实现compareTo方法。

可以看出弊端:这种方式的比较方法会在类中写死,如果想更改比较方法,就需要重新修改这个compareTo的实现方式。

方法二:Comparator:此接口也叫做比较器。

class Person {
    public String name;
    public int age;
    public int score;
    public Person(String name, int age, int score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}
 
class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person o1, Person o2) {
        //如果从小到大排序 o1.age  < o2.age;  交换
        return o1.age-o2.age;
    }
}
 
class ScoreComparator implements Comparator<Person>{
    @Override
    public int compare(Person o1, Person o2) {
        return o1.score-o2.score;
    }
}
public class TestComparator {
  public static void main2(String[] args) {
        Person person1 = new Person("gao",14,89);
        Person person2 = new Person("bit",54,19);
        Person person3 = new Person("hello",24,79);
        //根据分数进行比较
        ScoreComparator scoreComparator = new ScoreComparator();
        System.out.println(scoreComparator.compare(person1,person2));
        System.out.println(scoreComparator.compare(person2,person3));
        System.out.println(scoreComparator.compare(person1,person3));
     }
 
    public static void main1(String[] args) {
        Person person1 = new Person("gao",14,89);
        Person person2 = new Person("bit",54,19);
        Person person3 = new Person("hello",24,79);
        //根据年龄进行比较
        AgeComparator ageComparator = new AgeComparator();
        System.out.println(ageComparator.compare(person1,person2));
        System.out.println(ageComparator.compare(person2,person3));
        System.out.println(ageComparator.compare(person1,person3));
    }
}

上面的方法比较方便,想拿什么比较,自己写个比较器就好了,非常方便。

4.3 二叉树的遍历(适用于所有的二叉树,只要是二叉树就行)

根据节点访问顺序的不同,二叉树的常见遍历方式有4种:

  1. 前序遍历(PreOrder Traversal)
  2. 中序遍历(InOrder Traversal)
  3. 后序遍历(PostOrder Traversal)
  4. 层序遍历(Level Order Traveral)

不管是什么二叉树,这四种遍历的代码都是统一的。

4.3.1 前序遍历(PreOrder Traversal)——递归

节点访问顺序:节点、前序遍历子树、前序遍历子树

// 前序遍历
public void preOrederTraversal(Node<E> root) {
    if (root == null) {
        return;
    }
    System.out.print(root.element+ " ");
    preOrederTraversal(root.left);
    preOrederTraversal(root.right);
}
4.3.2 前序遍历(PreOrder Traversal)——非递归

根节点先入

循环执行以下操作,直到栈为空,循环结束:

  1. 弹出栈顶元素,打印
  2. 如果有右孩子,则压入栈中
  3. 如果有左孩子,则压入栈中
public void preOrderTraversalOfNoneRecursion(Node<E> root) {
    if (root == null) return;
    Stack<Node> stack = new Stack<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        Node head = stack.pop();
        System.out.print(head.element + " ");
        if (head.right != null) {
            stack.push(head.right);
        }
        if (head.left != null) {
            stack.push(head.left);
        }
    }
}
4.3.3 中序遍历(InOrder Traversal)——递归

节点访问顺序:中序遍历子树、节点、中序遍历子树

// 中序遍历
public void inOrderTraversal(Node<E> root) {
    if (root == null) {
        return;
    }
    inOrderTraversal(root.left);
    System.out.print(root.element + " ");
    inOrderTraversal(root.right);
}
4.3.4 中序遍历(InOrder Traversal)——非递归

4.3.5 后序遍历(PostOrder Traversal)——递归

节点访问顺序:后序遍历子树、后序遍历子树、节点

// 后序遍历
public void postOrderTraversal(Node<E> root) {
    if (root == null) {
        return;
    }
    postOrderTraversal(root.left);
    postOrderTraversal(root.right);
    System.out.print(root.element + " ");
}
4.3.6 后序遍历(PostOrder Traversal)——非递归

使用两个栈,一个栈用于输出最后结果

现将根节点入栈

循环执行以下操作,直到栈为空:

  1. 弹出栈顶元素,将它放入到输出栈中
  2. 如果有左孩子,将左孩子入栈
  3. 如果有右孩子,将右孩子入栈
public void postOrderTraversalOfNoneRecursion(Node<E> root) {
    if (root == null) return;
    Stack<Node> stack = new Stack<>();
    Stack<Node> outputStack = new Stack<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        Node head = stack.pop();
        outputStack.push(head);
        if (head.left != null) {
            stack.push(head.left);
        }
        if (head.right != null) {
            stack.push(head.right);
        }
    }
    while (!outputStack.isEmpty()) {
        Node head = outputStack.pop();
        System.out.print(head.element + " ");
    }
}
4.3.7 层序遍历(Level Order Traversal)(重点)

节点访问顺序:从上到下,从左到右依次访问每一个节点

实现思路:使用队列

  1. 根节点入队
  2. 循环执行以下操作,直到队列为空
  • 访问队头节点
  • 将队头节点的左子节点入队
  • 将队头节点的右子节点入队
public void levelOrderTraversal(Node<E> root) {
    if (root == null) return;
    Queue<Node<E>> queue = new LinkedList<>();

    // 根节点入队;
    queue.offer(root);

    while (!queue.isEmpty()) {
        // 取队头元素
        Node<E> head = queue.poll(); // 访问队头元素
        System.out.print(head.element + " ");
        if (head.left != null) { // 左子节点入队
            queue.offer(head.left);
        }
        if (head.right != null) {// 右子节点入队
            queue.offer(head.right);
        }
    }
}
4.4 根据遍历结果重构二叉树(适用于所有二叉树)
  • 以下结果可以保证重构出唯一的一棵二叉树
  • 前序遍历 + 中序遍历
  • 后序遍历 + 中序遍历
  • 层序遍历 + 中序遍历
  • 前序遍历 + 后序遍历
  • 如果它是一棵真二叉树(Proper Binary Tree),结果是唯一的
  • 不然结果不唯一
4.5 二叉树的前驱节点和后继节点(P120、P121)

前驱节点中序遍历时的一个节点

后继节点中序遍历时的一个节点

https://blog.csdn.net/qq_43923045/article/details/103580414

4.6 删除二叉搜索树中的节点

删除节点——叶子节点

先找到叶子节点,直接删除

删除节点——度为1的节点

删除节点——度为2的节点