本文主要包括以下内容:
- 二分搜索树的基本概念
- 二分搜索树的基本操作
- 插入
- 删除
- 查询
- 实现二分搜索树
- 二分搜索树的不足
二分搜索树的基本概念
二分搜索树(Binary Search Tree)满足一下几个条件:
- 若它的左子树不为空,左子树上所有节点的值都小于它的根节点
- 若它的右子树不为空,右子树上所有的节点的值都大于它的根节点
- 它的左、右子树也都是二分搜索树
如果插入的元素在二分搜索中已经存在根据具体的情况而定,如果不允许重复, 直接抛弃即可;如果允许重复可以使用计数的方式,即每个节点保存自己的个数。
二分搜索树有着高效的插入、删除、查询操作。平均时间的时间复杂度为O(LogN),最差为O(N)
如下面一个二分搜索树:
二分搜索树的基本操作
插入操作
根据二分搜索树的定义,二分搜索树的插入操作就比较简单了
如果二分搜索树为空,那么新插入的节点就作为根节点
如果二分搜索树不为空,新节点和根节点作比较,如果比根节点小,则和根节点的左子树比较;如果比根节点大则和右子树作比较,直到没有左子树或者右子树了,然后把新节点当作左子树或者右子树节点。
以上图的二分搜索树为例,往二分搜索树添加节点10
首先和根节点11作比较,10比11小,和11的左子树7对比,比7大,然后和7的右子树8对比,比8大,然后和8右子树对比,发现8的右子树为空,则新节点10作为8的右子树:
删除操作
删除操作就比插入操作要稍微复杂一点,二分搜索树的删除操作一般分为以下几种情况:
如果要删除的节点只有左子树
如果要删除的节点只有左子树,那么直接把它的左子树代替要删除的节点即可。
比如要删除节点3,它只有左子树:
删除后
如果要删除的节点只有右子树
如果要删除的节点只有右子树,直接让其右子树代替要删除的节点即可。
比如要删除节点20,它只有右子树:
删除后
如果要删除的节点同时拥有左子树和右子树
如果要删除的节点同时又左子树和右子树,首先找到要删除节点的后继(该节点右子树中的最小值)
然后把后继删除并且替换要删除的节点
比如要删除下面的二分搜索树的节点20
然后把要删除的节点20的后继22删除并代替要删除的节点
也可以用待删除节点的前驱(节点的左子树中的最大值)
删除后
查询操作
二分搜索树的查询操作和插入操作类似,也是不断的比较,前提是如果相等就不用比较了,如果不等就按照插入的方式进行比较。
在比较的过程就把很多无用的元素过滤掉了,所以二分搜索树的插入、删除、查询是很高效的。
实现二分搜索树
public class BST<T extends Comparable<T>> {
private static class Node<T> {
private T value;
private Node<T> left, right;
public Node(T value) {
this.value = value;
}
}
private Node<T> root;
private int size;
public boolean isEmpty() {
return size == 0;
}
public int size() {
return size;
}
public void add(T e) {
root = add(root, e);
}
private Node<T> add(Node<T> node, T e) {
if (node == null) {
size++;
return new Node<>(e);
}
if (e.compareTo(node.value) < 0)
node.left = _add(node.left, e);
else if (e.compareTo(node.value) > 0)
node.right = _add(node.right, e);
return node;
}
public boolean contains(T e) {
return contains(root, e);
}
private boolean contains(Node<T> node, T e) {
if (node == null) {
return false;
}
if (e.compareTo(node.value) < 0) {
return contains(node.left, e);
} else if (e.compareTo(node.value) > 0) {
return contains(node.right, e);
}
return true;
}
/**
* 前序遍历
*/
public void preorder() {
preorder(root);
}
private void preorder(Node<T> node) {
if (node == null) {
return;
}
System.out.println(node.value);
preorder(node.left);
preorder(node.right);
}
/**
* 前序遍历的非递归实现
*/
public void preorderNoRecurse() {
if (root == null)
return;
Stack<Node<T>> stack = new Stack<>();
stack.add(root);
while (!stack.isEmpty()) {
Node<T> n = stack.pop();
System.out.println(n.value);
if (n.right != null)
stack.push(n.right);
if (n.left != null)
stack.push(n.left);
}
}
//中序遍历
public void inorder() {
inorder(root);
}
private void inorder(Node<T> node) {
if (node == null) {
return;
}
inorder(node.left);
System.out.println(node.value);
inorder(node.right);
}
//后序遍历
public void postorder() {
postorder(root);
}
private void postorder(Node<T> node) {
if (node == null) {
return;
}
postorder(node.left);
postorder(node.right);
System.out.println(node.value);
}
//广度优先遍历
public void levelorder() {
if (root == null)
return;
Deque<Node<T>> queue = new ArrayDeque<>();
queue.addLast(root);
while (!queue.isEmpty()) {
Node<T> node = queue.removeFirst();
System.out.println(node.value);
if (node.left != null) {
queue.addLast(node.left);
}
if (node.right != null) {
queue.addLast(node.right);
}
}
}
public T getMax() {
if (root == null) {
throw new NoSuchElementException();
}
return getMax(root).value;
}
private Node<T> getMax(Node<T> node) {
if (node.right == null) {
return node;
}
return getMax(node.right);
}
public T getMin() {
if (root == null) {
throw new NoSuchElementException();
}
return getMin(root).value;
}
private Node<T> getMin(Node<T> node) {
if (node.left == null) {
return node;
}
return getMin(node.left);
}
public T removeMin() {
T delete = getMin();
//因为可能只有一个节点,所以需要root接收removeMin的返回值null
root = removeMin(root);
return delete;
}
private Node<T> removeMin(Node<T> node) {
if (node.left == null) {
Node<T> rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
//把要删除节点的右节点赋值给 父节点的左节点
node.left = removeMin(node.left);
return node;
}
public T removeMax() {
T delete = getMax();
//因为可能只有一个节点,所以需要root接收removeMin的返回值null
root = removeMax(root);
return delete;
}
public Node<T> removeMax(Node<T> node) {
if (node.right == null) {
Node<T> leftNode = node.left;
size--;
node.left = null;
return leftNode;
}
node.right = removeMax(node.right);
return node;
}
/**
* 删除任意节点
*
* @param e
*/
public void remove(T e) {
root = remove(root, e);
}
private Node<T> remove(Node<T> node, T element) {
if (node == null) {
return null;
}
//如果要删除的节点小于当前节点,继续查询其左子树
if (element.compareTo(node.value) < 0) {
node.left = remove(node.left, element);
return node;
}
//如果要删除的节点大于当前节点,继续查询其右子树
if (element.compareTo(node.value) > 0) {
node.right = remove(node.right, element);
return node;
}
//=======要删除的节点就是当前的节点
//如果要删除节点的左子树为空
if (node.left == null) {
Node<T> rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
//如果要删除节点的右子树为空
if (node.right == null) {
Node<T> leftNode = node.left;
node.left = null;
size--;
return leftNode;
}
//=======如果要删除的节点左右子树都不为空
//找到要删除节点的后继,也就是右子树的最小值
Node<T> successor = getMin(node.right);
successor.right = removeMin(node.right);
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}
由于二分搜索树的特性,二分搜索树的中序遍历就是二分搜索树的从小到大的顺序。
上面的二分搜索树主要是通过递归实现的,下面仅分析下add插入方法的递归:
假设要往下面一个二分搜索树插入元素 7 :
插入操作的调用栈如下所示:
关于递归可以的话题可以看下我之前的文章
数据结构与算法(五)深入理解递归
二分搜索树的不足
在一般情况下,二分搜索树有着高效的插入、删除、查询等操作,时间复杂度为O(LogN)
但是如果带插入的结果集刚好是有序的,那么这个二分搜索树就退化成链表了,也就是为什么二分搜索树的最差时间复杂度为O(N)
例如下面两个退化成链表的二分搜索树:
解决这个问题,这就是后面要介绍的平衡二叉树。