第6章. 二叉搜索树(BST)
二叉搜索树(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)
步骤:
- 找到父节点parent
- 创建新节点node
- 将新节点"挂"上。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种:
- 前序遍历(PreOrder Traversal)
- 中序遍历(InOrder Traversal)
- 后序遍历(PostOrder Traversal)
- 层序遍历(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)——非递归
根节点先入
栈
循环执行以下操作,直到栈为空,循环结束:
- 弹出栈顶元素,打印
- 如果有右孩子,则压入栈中
- 如果有左孩子,则压入栈中
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)——非递归
使用
两个栈
,一个栈用于输出最后结果现将根节点入栈
循环执行以下操作,直到栈为空:
- 弹出栈顶元素,将它放入到输出栈中
- 如果有左孩子,将左孩子入栈
- 如果有右孩子,将右孩子入栈
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)(重点)
节点访问顺序:
从上到下,从左到右
依次访问每一个节点
实现思路:
使用队列
- 根节点入队
- 循环执行以下操作,直到队列为空
访问队头
节点- 将队头节点的
左子节点入队
- 将队头节点的
右子节点入队
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)
前驱节点是
中序遍历
时的前一个节点后继节点是
中序遍历
时的后一个节点
4.6 删除二叉搜索树中的节点
删除节点——叶子节点
先找到叶子节点,直接删除
删除节点——度为1的节点
删除节点——度为2的节点
本文作者:Ac_c0mpany丶
本文链接:https://www.cnblogs.com/keyongkang/p/17880477.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步