本文使用JAVA语言进行描述。其实本质上什么语言描述数据结构都是可以的。
二叉树基础
二叉树的根节点
二叉树递归结构:
上面是一个满二叉树。但是实际中也有二叉树不是满的。
二分搜索树
二分搜索树也不一定是满的。
所以使用二分搜索树需要具备的条件:
存储的元素必须有可比较性。
树结构的定义代码实现:
/**
* Created by hongyonghan on 2020/4/19.
*/
//为了让这个二分搜索树具有可比较性。那么继承Comparable的接口。
public class BST<E extends Comparable<E>> {
private class Node{
private E e;
private Node left,right;
public Node(E e) {
this.e = e;
left=null;
right=null;
}
}
private Node root;
private int size;
public BST() {
root=null;
}
public int size()
{
return size;
}
public boolean isEmpty()
{
return size == 0;
}
}
添加新元素
如果树之前是空的话,那么添加一个新元素之后,这个元素就是树的根。比如这里添加元素41.如果新加一个元素22,那么会根据二分搜索树的性质,将22作为二分搜索树的左子树的根节点。
那么该将60添加到哪个位置呢?逻辑如下:
60大于41,在右子树,60大于58,在右子树。所以添加如下:
这里的二分搜索树不包含重复元素。
如果想包含重复元素的话,只需要改变定义:左子树小于等于节点,或者右子树大于等于节点。
增加代码和查询代码:
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;
}
//是否包含元素e
public boolean contains(E 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)
{
//如果不在node上面,那就去node的左子树去找,否则去node的右子树去找
return contains(node.left,e);
}
else
{
return contains(node.right,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);
}
对每一个节点使用递归遍历的方式,会有三次遍历的机会。分别是:在遍历左子树之前会访问这个根节点。在遍历左子树完成之后会回到根节点,会访问这个根节点,在遍历右子树之后会回到这个根节点,会再遍历一次。
那么对一棵树来说,前序遍历的过程:
这是前序遍历一棵树的部分过程,从28->16->13,下面的点表示的是这个访问了几次。
访问左子树完成之后:
前序遍历的完整过程:
这里面的蓝色的点就是第一次访问这个节点时候。然后对这个节点进行输出操作。
中序遍历:
中序遍历是当第二次访问到这个节点时候进行操作,第一次访问到该节点不进行操作。
例如这里的16,第一次访问16时,不输出16,只是第二次访问16时才输出16.
下面是左子树完成遍历的过程。
图中蓝色的点是对节点进行操作的时候:
后序遍历:
只有第三次访问节点的时候,才对节点进行操作。
部分树的操作如下:
前序遍历的非递归的写法
前序遍历的递归写法:
这里借助栈的特性记录遍历的位置:
首先,访问28节点,将节点压入栈,也就是接下来要访问28这个根节点了。
然后访问28这个节点,节点访问完成之后,28这个节点出栈
然后准备访问两个子节点,压栈30,16两个元素。因为先访问16,所以要先压栈30.
然后16出栈,压栈22和13.
然后出栈13,压入13的子节点,但是子节点为空,什么都不用压入了。然后出栈22.访问22节点。
然后出栈30,访问节点30;
然后将42和29入栈,然后访1问29节点。
然后取栈顶元素42.
遍历完成!
//前序遍历的非递归的遍历。
public void preOrderNR()
{
Stack<Node> stack=new Stack<>();
//先入栈根节点
stack.push(root);
while (!stack.isEmpty())
{
//cur节点就是当前要访问的节点
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);
}
}
广度优先遍历
按照层数来进行遍历。
这个是使用非递归的方式进行遍历的。使用队列的结构。
先将根节点入队,然后再将左右孩子入队。
具体展示如图:
初始化的时候将队首入队:
然后遍历的时候先看队首是谁,访问队首之后,将队首出队。这里是指28出队。
然后将第二层16和30入队。因为队列里面是先进先出,所以将树的左子树先入队,然后右子树再入队,这里是16先入队,然后30再入队。
然后取出队首元素16,对16进行访问。然后将16的孩子13和22入队。
然后30出队,对30进行访问。并对30的两个孩子29和42进行入队。
然后将13进行出队,并进行访问。13s是叶子节点,没有元素入队。
然后直到队首为空:遍历结束:
编程实现:
//层序遍历(广度优先遍历)
public void levelOrder()
{
Queue<Node> q=new LinkedList<>();
//将根节点入队
q.add(root);
while (!q.isEmpty())
{
Node cur=q.remove();
System.out.println(cur.e);
if (cur.left != null)
q.add(cur.left);
if(cur.right != null)
q.add(cur.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);
}
广度优先遍历可以更快的找到你想要查找的元素。主要用于搜索上面。
删除节点
找到树中的最小值和最大值
不同树的结构是不同的,所以最小值可能不都在最左边的叶子节点,这里16就是最小值。
所以最小值是向左走再也走不动的位置。最大值是向右走再也走不动了。这里最大值是30。
代码实现:
//找到树中元素的最小值
public E minimum()
{
if (size==0)
throw new IllegalArgumentException("BST is empty");
return minimum(root).e;
}
private Node minimum(Node node)
{
if (node.left == null)
return node;
return minimum(node.left);
}
//找到树中元素的最大值
public E maximum()
{
if (size==0)
throw new IllegalArgumentException("BST is empty");
return maximum(root).e;
}
private Node maximum(Node node)
{
if (node.right == null)
return node;
return maximum(node.right);
}
删除二分搜索树的最小值
如果最小值是这种情况:
这个是个叶子节点,直接将这个节点删除就行了。
但是如果最小值不是叶子节点呢?
那么就可以将22删除后,然后将后面的右子树做成41的左子树。
删除二分搜索树的最大值
如果是叶子节点,直接将叶子节点删除就好。
删除后:
如果不是叶子节点,例如这里的58这个节点,将这个节点删除之后,直接将这个节点的左子树变成41的右子树就可以了。
//从二分搜索树中删除最小值所在节点,返回最小值
public E removeMin()
{
E ret=minimum();
root=removeMin(root);
return ret;
}
//删除掉以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 ret=removeMax();
root=removeMax(root);
return ret;
}
//删除掉以node为根的二分搜索树中的最大节点
//返回删除节点后新的二分搜索树的根
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;
}
删除任意元素
1.删除的节点只有左孩子(或者右孩子)的话。例如下面删除的节点是58.
就是将这个节点删除后,然后将这个50当作根节点的右孩子。
2.删除左右都有孩子的节点d
删除58这个节点之后,要找一个新的节点来替代他,那么这里找的是比58这个节点还大的那个节点。也就是找到58的右子树中最小的那个节点。也就是59那个节点。
删除之后:
然后删除d,s成为新的子树的根。
代码如下:
//从二分搜索树中删除元素e的节点
public void remove(E e)
{
root=remove(root,e);
}
//删除掉以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
{
//e==node.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 = minimum(node.right);
successor.right=removeMin(node.right);
successor.left=node.left;
node.left=node.right=null;
return successor;
}
}
二分搜索树的顺序性
1.使用中序遍历的方法得到的就是有序的。
2.使用删除最大值和最小值的方法得到的也是有序的。