树是一种十分重要的数据结构,树的设计来源生活,生活中就处处可见(如下图),由于树的天然组织结构,在处理数据上可以达到高效,所以我们有必要学习一下树,本文主要介绍二分搜索树(binary search tree)。
要点
树科普
如上图:这是我们见得典型的树结构 --------二叉树。
二叉树的性质:
1 根节点 唯一
2 每个节点最多有两个孩子
3 每个节点最多只有一个父亲节点
叶子节点: 一个孩子也没有 (节点的左右孩子都为空)
二叉树也就有天然的递归结构:
每个节点的左孩子可以看成一个二叉树的根节点 成为左子树
每个节点的右孩子可以看成一个二叉树的根节点 成为右子树
满二叉树:对于每个节点来说,除了叶子节点外,都有两个孩子,所以二叉树不一定是满的。
ps:一个节点也可以看做二叉树 左右节点都为空,满足节点的定义就行。
二分搜索树
不同叫法:二叉排序树 二叉查找树,二叉搜索树
特点: 二分搜索树是一个二叉树,具备二叉树的所有性质。
二分搜索树的独特性质:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值。
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
二分搜索树优点:
大大加快了查询速度(给了数值就知道沿着那个方向查)
参考生活实例,图书馆寻找特定书。去那个方向类的阅览室找xxx就行了
二分搜索树缺点:
存储的元素要有可比较性,可以看作二分搜索树的局限性。
类的设计
1 、首先设计一个我们自己的二分搜索树类----BST,这个类中先进行节点以及重要成员、方法的设计。
public class BST<E extends Comparable<E>> {
/**
* 节点设计
*/
private class Node {
public E e;
public Node left;
public Node right;
public Node(E e) {
this.e = e;
left = null;
right = null;
}
}
// 根节点的设计 用于标记根节点
private Node root;// 根节点
private int size;// 元素数目
public BST() {
root = null;
size = 0;
}
public int getSize() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
}
设计缘由:
1 可以存储任意类型设计为 泛型
2 存储的对象要可比较性 故对泛型约束满足Comparable接口(参见二分搜索树的缺点)
使用参考:
1 Comparable 的使用
可以参考 Integer 类的compareTo比较源码
可以参考:文章1
2 super extends的使用区别
可以参考:文章2
2 插入(递归实现)
插入思路图
如上图我们插入元素时,从根节点开始逐个比较,比根节点小则向左子树比较,比根节点大则向右子树比较,一直往下比较直到目标节点的下一节点为空,便插入空的位置。
具体实现:
此处我们先搞个测试(后面上优化后的代码)
/**
* 向以node为根的二分搜索树中添加元素 e
*
* @param e 要插入的元素
* @param node 根节点
* 此方法我们自己使用 ,本方法使用递归实现。
* 思路:采用递归思想,则树的根是不断变化的。
* <p>
* addTest 为测试方法 add 为addTest的优化版
*/
private void addTest(Node node, E e) {
//元素重复 直接返回
if (e.equals(node.e)) {
return;
} else if (e.compareTo(node.e) < 0 && node.left == null) {
node.left = new Node(e);
size++;
return;
} else if (e.compareTo(node.e) > 0 && node.right == null) {
node.right = new Node(e);
size++;
return;
}
// 开始递归调用 往下面遍历
if (e.compareTo(node.e) < 0) {
addTest(node.left, e);
} else if (e.compareTo(node.e) > 0) {
addTest(node.right, e);
}
}
我们设想,反正插入时,都是插入空的位置(目标节点的左孩子,或者右孩子为空)
所以:
1 树为空时 创建节点,吧此节点当做根节点
2 树不为空时 插入目标元素
3 递归实现
// 1 树为空时 创建节点,吧此节点当做根节点
/* if (root == null) {
root = new Node(e);//创建根节点,把元素插入到根节点
size++;
} else {
//2 树不为空时 插入目标元素
add(root, e);
}*/
// 优化
root = add(root, e);
/**
* 插入的递归优化 (参考addTest )
*/
private Node add(Node node, E e) {
// 为空时创建元素 这个空的就是要插入的位置
if (node == null) {
size++;
return new Node(e);
}
// 3 递归调用 返回插入位置节点
if (e.compareTo(node.e) < 0) {
node.left = add(node.left, e);
} else if (e.compareTo(node.e) > 0) {
node.right = add(node.right, e);
}
return node;
}
3 查找(递归实现)
查找树种是否包含此元素
思路:
1 树是空的不包含
2 从根节点开始元素比较,相等时就是此节点。
3 从根节点开始元素比较,小于时递归左找。
4 从根节点开始元素比较,大于时递归有找。
具体操作如下:
/**
* 是否包含元素
*
* @param e 目标元素
*/
public boolean contain(E e) {
return contain(root, e);
}
/**
* @param node 以node为根节点的节点
* @param e 目标元素
*/
private boolean contain(Node node, E e) {
// 树空
if (node == null) {
return false;
}
//此节点含有元素时
if (e.compareTo(node.e) == 0) {
return true;
} else if (e.compareTo(node.e) < 0) {
// 比此元素小 递归左面找
return contain(node.left, e);
} else {
//(e.compareTo(node.e) > 0)
// 比此元素大 递归右面找
return contain(node.right, e);
}
4 遍历
深度优先遍历的基本思想:
对每一个可能的分支路径深入到不能再深入为止,而且每个结点只能访问一次。深度优先遍历的非递归的通用做法是采用栈。要特别注意的是,二分搜索树的深度优先遍历比较特殊,可以细分为前序遍历、中序遍历、后序遍历。
前序遍历:先访问当前节点,再依次递归访问左右子树 ,访问到前面节点才继续
中序遍历:先递归访问左子树,再访问自身,再递归访问右子树,访问到中间节点才继续
后序遍历:先递归访问左右子树,再访问自身节点,访问到后面节点才继续
广度优先遍历:
深度优先遍历的基本思想:从上往下对每一层依次访问,在每一层中,从左往右(也可以从右往左)访问结点,访问完一层就进入下一层,直到没有结点可以访问为止。广度优先遍历的非递归的通用做法是采用队列。
前序遍历(递归实现)
/**
* 二分搜索树 --前序遍历
*/
public void preOrder() {
System.out.println("树中的元素:");
preOrder(root);
}
/**
* 前序遍历以node为根的二分搜索树
**/
private void preOrder(Node node) {
/* if (node == null){
return;
}*/
// 熟练递归后写法 不拘谨与定义
if (node != null) {
System.out.print(node.e + " ");
// 递归遍历
preOrder(node.left);
preOrder(node.right);
}
/*递归总结(通常):
* 先写递归终止条件,在写递归组成逻辑。
* */
}
前序遍历(非递归实现)
思路图:
具体实现:
/**
* 二分搜索树非递归前序遍历
* <p>
* 前序: 根节点->左子树-->右子树
*/
public void preOrderNR() {
Stack<Node> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
Node currentElement = stack.pop();
System.out.println(currentElement.e);
if (currentElement.right != null) {
stack.push(currentElement.right);
}
if (currentElement.left != null) {
stack.push(currentElement.left);
}
}
}
中序遍历
知道了前中后序遍历的区别就好写了,只是访问顺序不同
/**
* 中序遍历 递归实现
*/
public void inOrder() {
inOrder(root);
}
/**
* 中序遍历
*
* @param node 以node为根节点的节点
*/
private void inOrder(Node node) {
if (node == null) {
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
后序遍历
知道了前中后序遍历的区别就好写了,只是访问顺序不同
public void postOrder() {
postOrder(root);
}
private void postOrder(Node node) {
if (node == null) {
return;
}
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
广度优先遍历:
思路图:
具体实现:
/**
* 二分搜索树的广度优先遍历(使用队列实现)
*/
public void levelOrder() {
Queue<Node> queue = new LinkedList<Node>();
queue.add(root);
while (!queue.isEmpty()) {
Node cur = queue.remove();
System.out.println(cur.e);
if (cur.left != null) {
queue.add(cur.left);
}
if (cur.right != null) {
queue.add(cur.right);
}
}
}
5 元素删除
最值的查找删除:
/**
* 二分搜索树的最小元素 递归实现
*/
public E minimum() {
if (size == 0) {
throw new IllegalArgumentException("binary search tree is empty");
}
return minimum(root).e;
}
private Node minimum(Node node) {
if (node.left == null) {
return node;
}
return minimum(node.left);
}
/**
* 二分搜索树的最大元素
*/
public E maxmum() {
if (size == 0) {
throw new IllegalArgumentException("binary search tree is empty");
}
return maxmum(root).e;
}
private Node maxmum(Node node) {
if (node.right == null) {
return node;
}
return maxmum(node.right);
}
/**
* 删除二分搜索树的最小值
*/
public E removeMin() {
E rec = minimum();
// 删除操作
root = removeMin(root);
return rec;
}
/**
* 删除以node为根节点的二分搜索树的最小节点
* 返回删除节点后新的二分搜索树的节点
*/
private Node removeMin(Node node) {
if (node.left == null) {
Node rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}
/**
* 删除二分搜索树的最大值
*/
public E removeMax() {
E rec = minimum();
// 删除操作
root = removeMax(root);
return rec;
}
/**
* 删除以node为根节点的二分搜索树的最大节点
* 返回删除节点后新的二分搜索树的节点
*/
private Node removeMax(Node node) {
if (node.right == null) {
Node leftNode = node.left;
node.left = null;
size--;
return leftNode;
}
node.right = removeMin(node.right);
return node;
}
删除任意元素
思路
找到了删除节点后:
1 待删除的节点左子树为空
2 待删除节点的右子树为空
3 待删除节点左右子树都不为空
情况1,2时:为最值的删除,使用最值处理即可(参考最值的删除)
情况3时:找到以待删除节点为根节点的最小节点, 用这个节点顶替待删除节点。
具体实现:
/**
* 删除以e 为节点的元素
*/
public void remove(E e) {
root = remove(root, e);
}
/**
* 删除以node为根的二分搜索树中 值为e的节点 递归算法
* <p>
* 返回删除节点后,新的二分搜索树的根
*/
private Node remove(Node node, E e) {
if (node == null) {
return null;
}
// 递归寻找
if (e.compareTo(node.e) < 0) {
node.left = remove(node.left, e);
return node;
} else if (e.compareTo(node.e) > 0) {
node.right = remove(node.right, e);
return node;
} else {
/*e.compareTo(node.e)==0
找到了删除节点
三种情况:
1 待删除的节点左子树为空
2 待删除节点的右子树为空
3 待删除节点左右子树都不为空
*/
if (node.left==null){
Node rightNode= node.right;
node.right = null;
size--;
return rightNode;
}
if (node.right==null){
Node leftNode= node.left;
node.left = null;
size--;
return leftNode;
}
/*
* 待删除节点的左右孩子都不为空时思路:
* 1 找到以待删除节点为根节点的最小节点
* 2 用这个节点顶替待删除节点
* */
Node successor = minimum(node.right);
successor.right = removeMin(node.right);
successor.left = node.left;
node.left=node.right=null;//call gc
return successor;
/*
本栗子:找的后继:以删除节点右子树为根节点找最小值
前驱:以删除节点左子树为根节点找最大值
*
* */
}
}
小结
至此有关二分搜索树的相关操作就简单的搞了一遍,简单的看了看,自己写了400多行的BST代码,蛮有成就感哈。本文的图片、截图有的来源于自己所画,有的来源于慕课网的学习视屏(我直接省事截的图哈哈)
源码下载