数据结构二分搜索树
Posted zhangbiao97
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构二分搜索树相关的知识,希望对你有一定的参考价值。
什么是树?
在现实生活中有很多能体现出树的逻辑的例子。
例如:企业里的职位关系,也是一颗树。
再例如:操作系统的文件夹目录,也是一颗树。
那么以上的这些例子有什么共同点呢?为什么称它们为"树"呢?
因为它们都像自然界中的树一样,从同一个"根"衍生出许多的"枝干",再从每一个"枝干"衍生出许多更小的"枝干",最后衍生更多的"叶子"。
在数据结构中,树的定义如下:
树是n个节点的有限集。当n=0时,称为空树,在任意一个非空树中,有如下特点。
- 有且仅有一个特定的称为根的节点。
- 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一棵树,并称为根的子树。
标准树存储结构如图:
在上图中,节点1是根节点,节点4、5、6是树的末端,并且没有“孩子”,称为叶子节点。图中虚线的部分是一颗子树。
同时,树结构从根节点到叶子节点,分为不同的层级。从一个节点的角度来看,它的上下级和同级节点关系如下:
在上图中,节点4的上一级节点,是节点4的父节点,从节点2衍生出来的节点,是节点2的孩子节点。和节点2同级,由同一个父节点衍生出来的节点是节点2的兄弟节点。
树的最大层数,被称为树的高度或深度。显然,上图中的这个数的高度为3。
什么是二叉树?
二叉树是树的一种特殊形式。二叉,顾名思义,这种树的每个节点最多有两个孩子节点。注意,这里是最多有两个,也有可能有一个,或没有孩子节点。
二叉树的结构如图:
二叉树节点的两个孩子节点,一个被称为左孩子,一个被称为右孩子。这两个孩子节点的顺序是固定的,就像人的左手与右手一样。
此外,二叉树还有两种特殊的形式。如下:
-
满二叉树
那么什么是满二叉树呢?
一个二叉树的所有非叶子节点都存在左孩子与右孩子,并且所有的叶子节点都在同一层级上,那么这个树就是满二叉树。
如图:
简单点说,满二叉树的每个节点的左右孩子都不为空。
-
完全二叉树
那么什么是完全二叉树呢?
对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个数的所有节点和同样深度的满二叉树的编号从1到n的节点位置相同,则这个二叉树为完全二叉树。
如图:
在上图中,二叉树编号从1到6的6个节点,和前面的满二叉树编号从1到6的节点位置完全对应。因此这个树就是完全二叉树。
完全二叉树的条件没有满二叉树那么苛刻,满二叉树要求所有的节点必须有左右孩子,而完全二叉树只需要保证最后一个节点之前的节点都全即可。
注意:
二叉树不一定是满的。如图:
空也是一颗树。如图:
二叉树可以用哪些物理存储结构来进行存储呢?
-
链式存储结构
首先来看一看链式存储结构。如图:
链式存储是二叉树最直观的存储方式。
在链表中,链表是一对一的存储方式,每个链表拥有data变量和指向下一个节点的next指针。而二叉树稍微复杂一些,除了拥有data变量,还有指向左右孩子节点的变量。
-
数组
首先来看一看数组存储结构。如图:
使用数组存储时,会按照层级顺序把二叉树的节点放到数组中对应的位置上。如果某一个节点的左孩子或右孩子为空,则数组的相应位置也为空。
为什么这样设计呢?因为这样可以更方便地在数组中定位二叉树的孩子节点和父节点。
假设一个父节点的索引是parent,那么它的左孩子节点索引就是2 * parent+1,右孩子节点索引就是2 * parent+2。
反过来,假设一个左孩子节点的索引为leftChild,那么它的父节点索引就是(leftChild - 1)/ 2。
什么是二分搜索树?
二分搜索树是一颗二叉树,但是在二叉树的基础上加了一些条件。二分搜索树的每个节点的值大于其左子树的所有节点的值或小于其右子树的所有节点的值,并且每一刻子树也是二分搜索树,所以二分搜索树具有天然的递归结构。
如图:
注意:
-
二分搜索树存储的数据必须具有比较性!
-
二分搜索树在极端的情况下会蜕化成链表。
为什么会在极端的情况下蜕化成链表呢?假设,我们顺序的添加一组数据。例如1、2、3、4、5
二分搜索树如图:
由于二分搜索树的特点,当我们顺序插入一组数据的话,会蜕化成一个链表。
解决方式:平衡二叉树(AVL、红黑树)。
二分搜索树的实现
1、查找操作
通过上图,例如我们查找值为22的节点,步骤如下:
- 访问根节点28,进行比较,发现22<28,由于二分搜索树的特点,小于节点的值应访问它的左子树。
- 访问节点16,进行比较,发现22>16,由于二分搜索树的特点,大于节点的值应访问它的右子树。
- 访问节点22,进行比较,发现22=22,那么这个就是要查找的节点。
2、遍历操作
-
深度优先遍历
1、前序遍历
二分搜索树的前序遍历,输出的顺序是当前节点、左孩子、右孩子。
2、中序遍历
二分搜索树的中序遍历,输出的顺序是左孩子、当前节点、右孩子。
3、后序遍历
二分搜索树的后序遍历,输出的顺序是左孩子、右孩子、当前节点。
-
广度优先遍历
1、层序遍历
按层从左到右进行遍历。
其余操作大体都和查询操作的逻辑差不多,就不再一一讲解了。整体代码如下:
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
/**
* 描述:二分搜索树。
* <p>
* Create By ZhangBiao
* 2020/5/13
*/
public class BinarySearch<E extends Comparable<E>> {
/**
* 根节点
*/
private Node root;
/**
* 元素个数
*/
private int size;
public BinarySearch() {
this.root = null;
this.size = 0;
}
/**
* 获取元素个数
*
* @return
*/
public int getSize() {
return size;
}
public boolean isEmpty() {
return this.size == 0;
}
/**
* 添加元素
*
* @param e
*/
public void add(E e) {
root = add(root, e);
}
/**
* 向以node为根节点的二分搜索树中插入元素(递归算法)
*
* @param node
* @param e
* @return
*/
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;
}
/**
* 查看二分搜索树是否包含元素e
*
* @param e
* @return
*/
public boolean contains(E e) {
return contains(root, e);
}
/**
* 查看以node为跟节点的二分搜索树是否包含元素e(递归算法)
*
* @param node
* @param e
* @return
*/
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);
}
}
/**
* 二分搜索树前序遍历(深度优先遍历)
*/
public void preOrder() {
preOrder(root);
}
/**
* 前序遍历以node为根节点的二分搜索树(递归算法)
*
* @param node
*/
private void preOrder(Node node) {
if (node == null) {
return;
}
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
/**
* 二分搜索树非递归前序遍历(深度优先遍历)
*/
public void preOrderNR() {
Stack<Node> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
Node cur = stack.pop();
System.out.println(cur.e);
if (cur.right != null) {
stack.push(cur.right);
}
if (cur.left != null) {
stack.push(cur.left);
}
}
}
/**
* 二分搜索树层序遍历(广度优先遍历)
*/
public void levelOrder() {
Queue<Node> queue = new LinkedList<>();
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);
}
}
}
/**
* 二分搜索树中序遍历(深度优先遍历)
*/
public void inOrder() {
inOrder(root);
}
/**
* 中序遍历以node为根节点的二分搜索树(递归算法)
*
* @param 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);
}
/**
* 后序遍历以node为根节点的二分搜索树(递归算法)
*
* @param node
*/
private void postOrder(Node node) {
if (node == null) {
return;
}
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
/**
* 寻找二分搜索树的最小元素
*
* @return
*/
public E minimum() {
if (size == 0) {
throw new IllegalArgumentException("Binary Search Tree is empty!");
}
Node minNode = minimun(root);
return minNode.e;
}
/**
* 返回以node为跟节点的二分搜索树的最小值所在的节点
*
* @param node
* @return
*/
private Node minimun(Node node) {
if (node.left == null) {
return node;
}
return minimun(node.left);
}
/**
* 寻找二分搜索树的最大元素
*
* @return
*/
public E maximum() {
if (size == 0) {
throw new IllegalArgumentException("Binary Search Tree is empty!");
}
Node maxNode = maximum(root);
return maxNode.e;
}
/**
* 返回以node为跟节点的二分搜索树的最大值所在的节点
*
* @param node
* @return
*/
private Node maximum(Node node) {
if (node.right == null) {
return node;
}
return maximum(node.right);
}
/**
* 从二分搜索树中删除最小元素的节点并返回最小值
*
* @return
*/
public E removeMin() {
E e = minimum();
root = removeMin(root);
return e;
}
/**
* 删除掉以node为跟节点的二分搜索树中的最小元素节点并返回删除节点后的新的二分搜索树的根节点
*
* @param node
* @return
*/
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;
}
/**
* 从二分搜索树中删除最大元素的节点并返回最大值
*
* @return
*/
public E removeMax() {
E e = maximum();
root = removeMax(root);
return e;
}
/**
* 删除掉以node为跟节点的二分搜索树中的最大元素节点并返回删除节点后的新的二分搜索树的根节点
*
* @param node
* @return
*/
private Node removeMax(Node node) {
if (node.right == null) {
Node leftNode = node.left;
node.left = null;
size--;
return leftNode;
}
node.right = removeMax(node.right);
return node;
}
/**
* 从二分搜索树中删除元素为e的节点
*
* @param e
*/
public void remove(E e) {
root = remove(root, e);
}
/**
* 删除掉以node为根节点的二分搜索树中值为e的节点(递归算法)并返回删除节点后新的二分搜索树的根节点
*
* @param node
* @param e
* @return
*/
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 {
//待删除节点左子树为空的情况
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 = minimun(node.right);
successor.right = removeMin(node.right);
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
generateBSTString(root, 0, result);
return result.toString();
}
/**
* 生成以node为根节点,深度为depth的描述二叉树的字符串
*
* @param node
* @param depth
* @param result
*/
private void generateBSTString(Node node, int depth, StringBuilder result) {
if (node == null) {
result.append(generateDepthString(depth) + "null
");
return;
}
result.append(generateDepthString(depth) + node.e + "
");
generateBSTString(node.left, depth + 1, result);
generateBSTString(node.right, depth + 1, result);
}
private String generateDepthString(int depth) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < depth; i++) {
result.append("--");
}
return result.toString();
}
private class Node {
/**
* 数据
*/
public E e;
/**
* 左孩子
*/
public Node left;
/**
* 右孩子
*/
public Node right;
public Node(E e) {
this.e = e;
this.left = null;
this.right = null;
}
}
}
以上是关于数据结构二分搜索树的主要内容,如果未能解决你的问题,请参考以下文章