引言
二叉树的一个重要应用是它们在查找中的使用。
二叉查找树(二叉排序树\二叉搜索树)定义:二叉查找树或者是一棵空树,或者是具有下列性质的二叉树:
- 若左子树不空,则左子树上所有结点的值均小于它的根结点的值
- 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值
- 左、右子树也分别为二叉查找树
4. 没有键值相等的节点
上图中只有左边的是二叉查找树,右边的因为节点7属于根节点6的左子树,但是大于根。
二叉查找树的性质:对二叉查找树进行中序遍历,即可得到有序的数列。
二叉查找树的时间复杂度:它和二分查找一样,插入和查找的时间复杂度均为
O
(
l
o
g
N
)
O(logN)
O(logN),但是在最坏的情况下仍然会有
O
(
N
O(N
O(N)的时间复杂度。
二叉查找树的平均深度为
O
(
l
o
g
N
)
O(logN)
O(logN)
二叉查找树的高度决定了二叉查找树的查找效率。
实现
树节点结构
定义二叉查找树中节点结构如下:
private static class Node<E> {
E data;
Node<E> left;
Node<E> right;
Node(E data) {
this(data,null,null);
}
Node(E data,Node<E> left,Node<E> right) {
this.data = data;
this.left = left;
this.right = right;
}
}
主要含有三个域:数据域,指向左孩子和右孩子的引用。这些引用可以为空。
树的结构只是维护了根节点:
public class BinarySearchTree extends Comparable<? super E> implements BinaryTree<E> {
/**
* 根节点
*/
private Node<E> root;
...
}
contains方法
用于判断在树T中是否含有给定值X的节点。
通过上面树的结构使得这种操作很简单。
如果树为空,则直接返回false
;
否则,如果存储在T处的项是X,返回true
。否则,我们对树T的左子树或右子树进行一次递归调用。
private boolean contains(E x, Node<E> node) {
if (node == null) {
//递归跳出条件
return false;
}
//将当前节点的值与x进行比较
int cmp = compare(x, node.data);
if (cmp == 0) {
return true;
} else if (cmp < 0) {
//x < current node data
//若x小于当前节点,则比较左子树
return contains(x, node.left);
} else {
return contains(x, node.right);
}
}
要注意的是,这里的两个递归调用事实上都是尾递归并且可以用一个while
循环很容易地代替。这里所使用的栈空间的量只不过是
O
(
l
o
g
N
)
O(logN)
O(logN)而已。
尾递归就是操作的最后一步是调用自身的递归。
f(x) { ...; return f(x - 1);}
这是尾递归,而f(x) { ...; return 1 + f(x - 1);}
不是尾递归
findMin和findMax方法
分别为返回树中最小元素和最大元素的节点。利用二叉树的性质,实现起来很简单。
findMin()
: 从根节点开始遍历,只要有左孩子就向左进行,终点节点就是最小元素节点。
findMax()
: 从根节点开始遍历,只要有右孩子就向右进行,终点节点就是最大元素节点。
private Node<E> findMin(Node<E> node) {
while (node.left != null) {
node = node.left;
}
return node;
}
private Node<E> findMax(Node<E> node) {
while (node.right != null) {
node = node.right;
}
return node;
}
像这些函数一样,二叉搜索树还有其他函数也可以通过递归很容易的实现。
insert方法
为了将X插入到树T中,你可以通过类似contains()
方法一样的方式沿着树查找。如果找到X,则什么也不用做。否则,将X插入到遍历路径上的最后一个节点上。
为了插入5,我们遍历该树,在4节点处,我们要向右进行,但是没有右子树,因此5不在该树上,那么,该位置就是要插入的位置。
@Override
public void insert(E x) {
root = insert(x, root);
}
/**
* 类似contains方法实现的插入,递归地插入x到合适的子树中。
*
* @param x
* @param node
* @return
*/
private Node<E> insert(E x, Node<E> node) {
if (node == null) {
//若待查找子树为空,则返回以值x构造的新节点
return new Node<>(x);
}
//将当前节点的值与x进行比较
int cmp = compare(x, node.data);
if (cmp < 0) {
//x < current node data
node.left = insert(x, node.left);
} else if (cmp > 0) {
node.right = insert(x, node.right);
}
// cmp == 0 直接返回
return node;
}
由于node
引用该树的根,而根又在第一次插入时变化,因此insert
被写成一个返回对新树根的引用的方法。
remove方法
正如许多数据结构一样,最困难的操作是remove。一旦我们找到待删除的节点,还要考虑几种情况。
- 如果待删除节点是叶子节点,则可以直接被删除
- 如果节点只有一个孩子,让孩子节点顶替它的位置即可(让待删除节点的父节点指向其子节点)
- 如果有两个孩子,一般的删除策略是用其右子树的最小节点的数据代替该节点的数据并递归地删除该最小节点。因为右子树最小的节点不可能有左孩子,所以第二次删除很容易。
上图待删除的节点是4,它有一个孩子节点,删除后的结果图右边的树所示。
删除具有两个孩子节点2的过程,首先将该节点的值换成3,然后再删除右子树最小节点即可。因为这个最小节点没有左孩子,因此即变成删除只有一个(或0个,也可能也没有右孩子)孩子节点的情况。
@Override
public void remove(E x) {
checkEmpty();
root = remove(x, root);
}
private Node<E> remove(E x, Node<E> node) {
/**
*
* 如果待删除节点是叶子节点,则可以直接被删除
* 如果节点只有一个孩子,让孩子节点顶替它的位置即可(让待删除节点的父节点指向其子节点)
* 如果有两个孩子,让其右子树的最小节点的数据代替该节点的数据并递归地删除该最小节点。
*/
if (node == null) {
return null;
}
//将当前节点的值与x进行比较
int cmp = compare(x, node.data);
if (cmp < 0) {
//x < current node data
node.left = remove(x, node.left);
} else if (cmp > 0) {
node.right = remove(x, node.right);
} else {
//找到了待删除节点
if (node.left != null && node.right != null) {
//如果有两个孩子
//其右子树的最小节点的数据代替该节点的数据
node.data = findMin(node.right).data;
//递归地删除右子树的最小节点,同时node.right指向更新后的节点
node.right = remove(node.data,node.right);
} else {
node = node.left != null ? node.left : node.right;
}
}
return node;
}
完整版
完整版实现如下:
package com.algorithms.tree;
import com.algorithms.queue.Queue;
import com.algorithms.stack.Stack;
import java.util.Comparator;
import java.util.NoSuchElementException;
/**
* 二叉查找树
*
* @author yjw
* @date 2019/6/11/011
*/
public class BinarySearchTree<E extends Comparable<? super E>> implements BinaryTree<E> {
/**
* 根节点
*/
private Node<E> root;
/**
* 比较器,可以通过构造函数注入自定义比较器
*/
private Comparator<E> comparator;
/**
* 若comparator不为空,则通过它比较;否则通过left.compareTo(right)比较
*
* @param left
* @param right
* @return
*/
private int compare(E left, E right) {
if (comparator != null) {
return comparator.compare(left, right);
}
return left.compareTo(right);
}
private void checkEmpty() {
if (isEmpty()) {
throw new NoSuchElementException();
}
}
public E findMin() {
checkEmpty();
return findMin(root).data;
}
/**
* 一直向左走就能找到最小值
*
* @param node
* @return
*/
private Node<E> findMin(Node<E> node) {
while (node.left != null) {
node = node.left;
}
return node;
}
public E findMax() {
checkEmpty();
return findMax(root).data;
}
private Node<E> findMax(Node<E> node) {
while (node.right != null) {
node = node.right;
}
return node;
}
@Override
public void insert(E x) {
root = insert(x, root);
}
/**
* 类似contains方法实现的插入
*
* @param x
* @param node
* @return
*/
private Node<E> insert(E x, Node<E> node) {
if (node == null) {
//若待查找节点为空,则返回以值为x构造的新节点
return new Node<>(x);
}
//将当前节点的值与x进行比较
int cmp = compare(x, node.data);
if (cmp < 0) {
//x < current node data
node.left = insert(x, node.left);
} else if (cmp > 0) {
node.right = insert(x, node.right);
}
// cmp == 0 直接返回
return node;
}
@Override
public void remove(E x) {
checkEmpty();
root = remove(x, root);
}
private Node<E> remove(E x, Node<E> node) {
/**
*
* 如果待删除节点是叶子节点,则可以直接被删除
* 如果节点只有一个孩子,让孩子节点顶替它的位置即可(让待删除节点的父节点指向其子节点)
* 如果有两个孩子,让其右子树的最小节点的数据代替该节点的数据并递归地删除该最小节点。
*/
if (node == null) {
return null;
}
//将当前节点的值与x进行比较
int cmp = compare(x, node.data);
if (cmp < 0) {
//x < current node data
node.left = remove(x, node.left);
} else if (cmp > 0) {
node.right = remove(x, node.right);
} else {
//找到了待删除节点
if (node.left != null && node.right != null) {
//如果有两个孩子
//其右子树的最小节点的数据代替该节点的数据
node.data = findMin(node.right).data;
//递归地删除右子树的最小节点,同时node.right指向更新后的节点
node.right = remove(node.data,node.right);
} else {
node = node.left != null ? node.left : node.right;
}
}
return node;
}
/**
* 删除二叉树中最小节点
*/
public void removeMin() {
root = removeMin(root);
}
/**
* 删除node节点最小子树节点
*
* @param node
* @return 新的子树根节点,而不是返回删除的节点
*/
private Node<E> removeMin(Node<E> node) {
//找到了最小左子树节点,返回其右子树
//目的是让其父节点的left指向其右子树
if (node.left == null) {
return node.right;
}
//更新引用
node.left = removeMin(node.left);
return node;
}
/**
* 判断树中是否含有值为x的节点
*
* @param x
* @return
*/
@Override
public boolean contains(E x) {
return contains(x, root);
}
private boolean contains(E x, Node<E> node) {
if (node == null) {
//递归跳出条件
return false;
}
//将当前节点的值与x进行比较
int cmp = compare(x, node.data);
if (cmp == 0) {
return true;
} else if (cmp < 0) {
//x < current node data
//若x小于当前节点,则比较左子树
return contains(x, node.left);
} else {
return contains(x, node.right);
}
}
/**
* 返回树中的节点数
*
* @return
*/
public int size() {
return size(root);
}
private int size(Node<E> node) {
if (node == null) {
return 0;
}
//当前节点(1) + 左子树节点数 + 右子树节点数
return 1 + size(node.left) + size(node.right);
}
@Override
public boolean isEmpty() {
return root == null;
}
@Override
public void makeEmpty() {
root = null;
}
/**
* 先序遍历(先根遍历):先访问根节点,再访问左右子节点
*/
public void preOrder() {
preOrder(root);
System.out.println();
}
private void preOrder(Node<E> node) {
if (node != null) {
System.out.print(node.data + " ");
preOrder(node.left);
preOrder(node.right);
}
}
/**
* 中序遍历=:先访问左子节点,再访问根节点,最后访问右子节点
*/
public void inOrder() {
inOrder(root);
System.out.println();
}
private void inOrder(Node<E> node) {
if (node != null) {
inOrder(node.left);
System.out.print(node.data + " ");
inOrder(node.right);
}
}
/**
* 后序遍历:先访问左子节点,再访问右子节点,最后访问根节点
*/
public void postOrder() {
postOrder(root);
System.out.println();
}
private void postOrder(Node<E> node) {
if (node != null) {
postOrder(node.left);
postOrder(node.right);
System.out.print(node.data + " ");
}
}
/**
* 层次遍历
*/
public void levelOrder() {
levelOrder(root);
System.out.println();
}
/**
* 和图的广度优先遍历类似,也需要用到队列
* 一层一层的遍历
*
* @param node
*/
private void levelOrder(Node<E> node) {
if (node != null) {
Queue<Node<E>> queue = new Queue<>();
queue.enqueue(node);
while (!queue.isEmpty()) {
Node<E> p = queue.dequeue();
System.out.print(p.data + " ");
if (p.left != null) {
queue.enqueue(p.left);
}
if (p.right != null) {
queue.enqueue(p.right);
}
}
}
}
/**
* 深度优先遍历,相当于先序遍历
*/
public void depthOrder() {
depthOrder(root);
System.out.println();
}
/**
* 和图的深度优先搜索类似
*
* @param node
*/
private void depthOrder(Node<E> node) {
if (node != null) {
Stack<Node<E>> stack = new Stack<>();
stack.push(node);
while (!stack.isEmpty()) {
Node<E> p = stack.pop();
System.out.print(p.data + " ");
//先push right,再push left 达到pop时先访问left的效果
if (p.right != null) {
stack.push(p.right);
}
if (p.left != null) {
stack.push(p.left);
}
}
}
}
@Override
public void printTree() {
if (isEmpty()) {
System.out.println("Empty tree");
} else {
print(root);
}
}
private void print(Node<E> node) {
System.out.println(node);
}
private static class Node<E> {
E data;
Node<E> left;
Node<E> right;
Node(E data) {
this(data, null, null);
}
Node(E data, Node<E> left, Node<E> right) {
this.data = data;
this.left = left;
this.right = right;
}
private void fillString(String prefix, boolean isTail, StringBuilder sb) {
if (right != null) {
right.fillString(prefix + (isTail ? "│ " : " "), false, sb);
}
sb.append(prefix).append(isTail ? "└── " : "┌── ").append(data).append("\n");
if (left != null) {
left.fillString(prefix + (isTail ? " " : "│ "), true, sb);
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
this.fillString("", true, sb);
return sb.toString();
}
}
}
本文相关代码实现涉及到栈和队列的请访问:栈和队列的实现