目录:
1.二分搜索树定义
2.添加数据(递归 / 非递归)
3.查询数据(递归)
4.前序、中序、后序遍历(递归 / 非递归)
5.层序遍历
6.查找最值(递归 / 非递归)
7.删除数据
8.寻找BTS的front和ceil
在说二分搜索树(Binary Search Tree)之前,我们来介绍一下二叉树。
所谓二叉树,就是每个节点最多含有两个子树的树。二叉树,具有唯一的根节点(root),而二叉树的左节点和右节点,可以为空,我们分别称左右节点为左孩子和右孩子。如果一个节点的左孩子和右孩子都不存在,我们就叫它为叶子节点。下图中13、22、29、42就为叶子节点。二叉树和链表一样,都是动态数据结构。
二叉树的不一定是满的,下图也是一颗二叉树。
二叉树从根开始,每一个节点又可以分出一颗小的二叉树。即使是叶子结点,它也是一颗二叉树,虽然它的左孩子和右孩子都为 NULL (空值也是二叉树)。
说完了二叉树的概念,我们就来介绍二分搜索树。
对于二分搜索树的每一个节点的值,他的左子树所有的值一定比这个节点的值小,右子树所有的值一定比这个节点的值大。二分搜索树的每一颗子树,都是一颗二分搜索树。我们可以看到第一张图,那就是一颗二分搜索树。
我们在树状数据结构中存储的元素,必须具有可比较性。
下面我们利用 Java 语言来简单实现一个树结构。
我们知道,在二叉树中,一个个数据就是一个个节点。在新建的类中,还要为一个节点创建左右两个左右孩子节点。
public class BST<E extends Comparable<E>> { //使我们新建的类支持泛型,而这个泛型变量应具有可比较性
private class Node{
public E e;
public Node left, 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 size(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
这样一个简单的二分搜索树就完成了。下面我们将探索在二分搜索树中的基本操作。
对于二叉树而言,其结构是个天然的递归结构。在二分搜索树中使用递归实现基本操作,可以大大简化代码量。在这里,我将介绍递归和非递归两种方法来实现基本操作。
首先,我们来看看如何实现添加数据操作。
在二分搜索树中,我们要判断添加的数据和当前节点的值的大小。如果比当前节点值小,则进入左孩子节点进行下一轮判断,反之则进入右孩子节点判断。在本文中,我们不考虑重复元素的情况。
说到这里,你应该想到利用递归来实现了。在这里我做一个小提醒,由于我们用的数据类型不属于 Java 的基础数据类型,所以不能用大于小于号来比较数据大小。我们之前定义的元素 E 类型是满足 Comparable 的,所以我们应该使用 compareTo() 方法进行比较。其用法如下:
compareTo() 方法用于将 Number 对象与方法的参数进行比较。可用于比较 Byte, Long, Integer等。
该方法用于两个相同数据类型的比较,两个不同类型的数据不能用此方法来比较。
如果指定的数与参数相等返回0。
如果指定的数小于参数返回 -1。
如果指定的数大于参数返回 1。
完成了知识储备,接下来我们来用代码实现。
我们的递归从根节点开始进行递归,由于采用递归方法,我们需要构造两个方法。
public void add(E e){
//从根节点开始进行递归
if(root == null){
root = new Node(e);
size ++;
}else{
add(root, e);
}
}
private void add(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)
add(node.left, e);
else //e.compareTo(node.e) > 0
add(node.right, e);
}
实际上,我们只要判断该节点的左右孩子是否为空就可以添加。
我们改写一下代码,直接返回根节点即可。
public void add(E e){
root = 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;
}
利用非递归方法也同理,检测到节点为空就可以添加。但由于我们需要连接节点,我们需要利用一个变量存储父节点,然后再将子节点用父节点的left或right节点表示出来。最后在连接起来。(prev.left=cur;)
public void add(E e) {
if (root == null){
root = new Node(e);
}else{
Node cur = root;
Node prev = root;
while (cur != null) {
if (e.compareTo(cur.e) < 0) {
prev=cur;
cur = cur.left;
if (cur == null) {
cur = new Node(e);
prev.left=cur;
break;
}
}else if (e.compareTo(cur.e) > 0) {
prev=cur;
cur = cur.right;
if (cur == null) {
cur = new Node(e);
prev.right=cur;
break;
}
}else{
return;
}
}
}
size++;
}
写完了添加语句,查询某元素是否存在应该很简单了,我们使用递归来实现。
首先从根节点开始找起,依然是构造两个函数。
public boolean contains(E e){
return contains(root, 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 // e.compareTo(node.e) > 0
return contains(node.right, e);
}
接下来,我们来谈谈二分搜索树的遍历操作。
所谓遍历,就是把各个节点的值全部遍历一遍。在线性结构下,我们遍历数据十分容易。在二分搜索树中,也十分容易。我们知道一颗二分搜索树有两个子节点和自己。我们可以先从左子树开始遍历,遍历完后再查询自己,之后再进行右子树的遍历。这就叫做二分搜索树的中序遍历。那么前序遍历和后序遍历就是左中右三颗子树的顺序不同而命名的。
与上文一样,我们使用递归来实现前序遍历。
public void preOrder(){
preOrder(root);
}
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);
}
// 中序遍历以node为根的二分搜索树, 递归算法
private void inOrder(Node node){
if(node == null)
return;
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
后续遍历(左右中)在此不再赘述。
当然,我们可以利用非递归方法实现遍历。
由于二分搜索树有两个节点,而节点又可以分出多个子节点,那我们怎么记录当前访问到拿个节点了呢?在这里,我们可以运用栈(Stack)来实现。
前序遍历比较简单,我们首先将root压入栈,也就是图中的28。
开始进入循环,我们首先将栈顶的28拿出,接下来将28的右孩子先压入栈,再将左孩子压入,因为栈是后入先出的。于是进行下一轮循环,将16拿出,然后将其右孩子先入栈,左孩子后入栈,如此往复,直到栈内无元素即完成遍历。
public void preOrderNR(){
Stack<Node> stack = new Stack<Node>();
if(root != null)
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 inOrderNR() {
Stack<Node> stack = new Stack<Node>();
Node cur = root;
while (!stack.isEmpty() || cur != null) {
while (cur != null) { //遍历到最深层的左孩子
stack.push(cur);
cur = cur.left;
}
Node prev = stack.pop();
System.out.println(prev.e);
cur = prev.right;
}
}
同样以下图数据为例。
1.找到最深层的左孩子13,此时栈内情况 Bottom [13,16,28] Top,cur=13所在节点
之后将13出栈,输出13,cur为13的右孩子,进入下一层循环,此时栈内情况 Bottom [16,28] Top;
2.由于13没有右孩子,故第二层循环不执行。此时prev等于栈顶元素16,此时栈内情况 Bottom [16,28] Top,输出16,cu等于16的右孩子22;
3.进入第二层循环,22入栈,cur等于22的左孩子,此时栈内情况 Bottom [22,28] Top。prev=22,22出栈并输出,此时cur等于22的右孩子。
4.由于22没有右孩子,故第二层循环不执行。prev=28,28出栈并输出。此时cur等于28的右孩子30.
……
最后我们来看后序遍历(我写这个代码的时候被中间的死循环卡住了)。我们在后续遍历时需找到最深层元素(即叶子节点),由于从深层向上进行遍历,我们还要多加一个变量记录上一层节点,当其所指向的节点的左子树和右子树均被访问过,则将该节点从栈中弹出。
public void postOrderNR(){
Stack<Node> stack = new Stack<Node>();
if(root != null)
stack.push(root);
Node prev = stack.peek();
while(!stack.isEmpty()){
Node cur = stack.peek();
while((cur.left != null || cur.right != null) && prev != cur.left && prev != cur.right){
if(cur.right != null)
stack.push(cur.right);
if(cur.left != null)
stack.push(cur.left);
cur=stack.peek();
}
cur=stack.pop();
System.out.println(cur.e);
prev=cur;
}
}
讲完了二分搜索树遍历,我们来学习层序遍历。
所谓层序遍历,就是按层进行遍历。如上图,先遍历第一层28,之后再是第二层16、30,最后到第三层。对于层序遍历,我们采取的是先进先出的队列(Queue)数据结构。
public void levelOrder(){
if(root == null)
return;
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);
}
}
关于BTS的最值问题,我们知道,一棵树最深层的左节点就是最小值,最大值就是最深层的右节点。
最小值递归实现:
public E minimum(){
if(size == 0)
throw new IllegalArgumentException("BST is empty");
Node minNode = minimum(root);
return minNode.e;
}
// 返回以node为根的二分搜索树的最小值所在的节点
private Node minimum(Node node){
if( node.left == null )
return node;
return minimum(node.left);
}
非递归实现:
public E minimum(){
if(size == 0)
throw new IllegalArgumentException("BST is empty");
E min=root.e;
Stack<Node> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
Node cur= stack.pop();
min=cur.e;
if (cur.left != null)
stack.push(cur.left);
}
return min;
}
接下来我们来探索如何删除BTS的最值元素。
以最小值为例,当删除叶子结点时,我们直接删除即可。
当删除非叶子节点时,将其删除,再将其子节点接上来即可。
当最左边的元素没有子节点时,我们就可以判定其为最小值。我们将最小值节点的右孩子返回给上一层元素。例如删除图2的22时,我们将22的右节点返回给上一层节点的左节点,即41的左孩子为33。
public E removeMin(){
E ret = minimum();
root = removeMin(root);
return ret;
}
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;
}
我们再来探索如何删除任意值。
要删除58这种左右都有节点的元素,我们要找到它的继承者,即他右节点中最小的元素,也是和58最相近的元素,然后将其在原位置删除,代替58成为父节点。
我们有三种情况,当前节点比待删除大、小、相等的情况。前两种情况是继续进行下一层的递归。当相等时,我们又有三种情况:待删除节点的左子树为空、待删除节点的右子树为空、待删除节点的左右子树都不为空。前两种情况比较好处理,直接把子节点接上即可。第三种情况,我们需要找到他的继承者,即利用之前写的minimum函数找到该节点右子树的最小值,进行上面所讲述的步骤进行连接即可。
public void remove(E e){
root=remove(root,e);
}
public Node remove(Node node,E e){
if (node==null)
return node;
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;
}
Node successor=minimum(node.right);
successor.right = removeMin(node.right); //size--,此时node还未被删除
successor.left = node.left;
node.left = node.right=null;//此时node被删除
return successor;
}
}
来看看如何寻找二分搜索树的front和ceil。所谓front和ceil,就是给定一个数,这个数不一定存在在BTS中,front就是比这个数小的最大的元素,ceil就是比这个大的最小的元素。在下图中,45的floor就是42,ceil就是50。
我们需要一层一层进行对比。当45输入时,我们应该判断他和根元素的大小关系。他比41大,所以我们应该进入右子树进行对比,也就是58。58又比41大。所以我们应该进入58的左子树50。50又比45大,所以进入左子树。由于42比45小,而42没有子树,所以42就是front。而50肯定是比45大的最小的元素,所以ceil就是50。
我们同样适用递归来实现。
public E floor(E e) {
Node minNode = floor(root, e);
return minNode == null ? null : minNode.e;
}
private Node floor(Node node, E e) {
if (node == null)
return null;
if (e.compareTo(node.e) == 0)
return node;
if (e.compareTo(node.e) < 0)
return floor(node.left, e);
Node rightNode = floor(node.right, e);//当前节点值小于输入值
if (rightNode != null)//查看当前节点的右节点是否为空,为空则为front,不为空查询其右子树
return rightNode;
else
return node;
}
文章写了三个晚上...就先这样吧,之后想到再补充,希望能给你带来帮助。:)