今天的目标是学习二分搜索树。那么首先我们来了解下什么是树。
树是一种抽象数据类型(ADT)的数据结构,用来模拟具有树状结构性质的数据集合,是一个种天然的组织结构,和链表一样是一个动态数据结构。是由n(n>=1)个有限节点组成一个具有层次关系的集合。根朝上,叶朝下。
树的特点是:
1.每个节点有零个或多个子节点;
2.没有父节点的节点成为根节点;
3.每一个非根节点有且只有一个父节点;
4.除了根节点外,每个子节点可以分为多个不相交的子树;
树的常见应用有:xml、html文档,MySQL数据库索引、文件系统的目录结构等。
树的优点:高效。
很多高效的运算结果,它的背后其实是因为有树这样的数据结构作为支撑,其实这也是我们要学习数据结构,包括数据结构在计算机科学领域非常重要的一个原因。数据结构虽然解决的是数据存储的问题,但是在使用的层面上,不仅仅是因为要存储数据,更重要的是当我们使用某种特殊的数据结构存储数据时,将帮助我们更加高效的解决某些算法问题。
那么接下来我们来看看什么是二叉树。在树中,二叉树是最常用的一种数据结构。具有唯一的根节点,每个节点最多有两个子树,通常子树被称作“左子树”和“右子树”,每个节点最多有一个父节点。
二叉树具有天然的递归性。每个节点的左子树也是二叉树,每个节点的右子树也是二叉树。但是二叉树不一定是满二叉树。
说完二叉树,我们来看看今天的重点:二分搜索树。
对于二分搜索树来说,首先是一个二叉树,其次存储的元素必须有可比较性,对于二分搜索树来说,其值:
- 大于其左子树的所有节点的值,小于其右子树的所有节点的值。
2. 每一颗子树也是二分搜搜树。
接下来我们一起操作下增删查改。
那么对于添加元素来说,我们基于递归思想来实现,并且本次实现不包含重复元素。直接上代码:
// 向二分搜索树种添加新的元素e
public void add(E e) {
if (root == null) {
root = new Node(e);
} else {
add(root, e);
}
}
// 返回插入新节点后二分搜索树的根,不需要对左子树或右子树是否为空进行判断
private Node add(Node node, E e) {
if (node == null) {
size++;
return new Node(e); // 节点和二叉树联系起来
}
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;
}
对于查询操作,我们用contains,看是否包含元素e。
// 查看二分搜索树中是否包含元素e
public boolean contains(E e) {
return contains(root, e);
}
// 查看以node为根的二分搜索树中是否包含元素e,递归算法
private boolean contains(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 contains(node.left, e);
} else {
return contains(node.right, e);
}
}
对于删除元素来说,有三种情况:
1.删除最大元素
2.删除最小元素
3.删除任意位置元素
在删除任意位置元素时,需要再分情况判断,删除的只有左节点的,删除只有右节点的,以及如果左右节点都有的话,思路就是要找到比待删除节点大的最小节点,即待删除节点右子树的最小节点,然后用这个节点顶替待删除节点的位置。
那么首先对于删除最大元素来说,需要查找到最大元素的节点位置。
// 寻找二分搜索树的最大元素
public E maxinum() {
if (size == 0) {
throw new IllegalArgumentException("BST is empty.");
}
return maxinum(root).e;
}
// 返回以node为根的二分搜索树的最小值所在的节点
private Node maxinum(Node node) {
if (node.right == null) {
return node;
}
return maxinum(node.right);
}
当确定最大元素位置后,即可以删除最大元素。
// 删除最大值所在节点,返回最大值
public E removeMax() {
E ret = maxinum();
root = removeMax(root);
return ret;
}
// 删除以node节点为根的最大节点
// 返回删除节点后新的二分搜索树的根
private Node removeMax(Node node) {
if (node.right == null) {
Node leftNode = node.left;
node.left = null; // 资源释放,删除该node节点
size--;
return leftNode;
}
node.right = removeMax(node.right);
return node;
}
对于删除最小元素的操作,我们就不过多阐述了,跟删除最大元素基本一样。
那么对于删除任意位置的元素,我们来看代码:
// 删除以node为根的二分搜索树中值为e的节点,递归算法
// 返回删除节点后新的二分搜索树的根
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 { // node.e==e
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;
}
// 待删除节点左右子树均不为空的情况
// 找得到比待删除节点大的最小节点,即待删除节点右子树的最小节点
// 用这个节点顶替待删除节点的位置
Node successor = mininum(node.right);
successor.right=removeMin(node.right);
successor.left = node.left;
node.left = node.left = null;
return successor;
}
}
那么对于删除任意位置的元素e时,考虑的情况就会很多。具体的逻辑参考上述代码,不过多阐述了。
操作完增删查改后,我们一起来看看遍历。说起遍历,就是把所有节点都访问一遍。对于遍历,我们采用深度优先,也就是递归思想来实现。
前序遍历,也是我们常说的最自然最常用的遍历方式。采用根 左 右的方法,进行遍历。具体实现如下:
// 二分搜索树的前序遍历
public void preOrder() {
preOrder(root);
}
// 前序遍历以node为根的二分搜索树,递归算法
private void preOrder(Node node) {
if (node == null) {
return;
}
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
中序遍历,采用左 根 右的方式来实现。
// 中序遍历
public void inOrder() {
inOrder(root);
}
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);
}
好了,今天关于二分搜索树的介绍就到这里,重点就是二分搜索树的特点,以及增删查改和遍历的方式。其中删除任意位置的元素e,考虑的情况会有多种。
整体来说,二分搜索树的操作很有意思,在应用方面有很多场景,长路漫漫,继续努力!
OK,今天就到这里,拜了个拜~