树这个结构我们总在各种场合看见他,无论是文件夹系统还是mysql种索引的结构。本篇文章以二分搜索树为例,详细介绍其概念,树的操作,应用场景。
二分搜索树是什么
- 树结构本身是一种天然的组织结构
- 二分搜索树也叫做二叉查找树、有序二叉树等,是指一棵空树或者具有下列性质的二叉树:
- 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值
- 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值
- 任意节点的左、右子树也分别为二叉查找树
- 相比于其他数据结构的优势在于查找、插入的时间复杂度较低。为
- 是基础性数据结构,用于构建更为抽象的数据结构,如集合、多重集、关联数组等
- 如图所示,是一个满的二分搜索树,因为每个节点都有两个子节点,如果图中最左侧的30节点没有,那么就不是满的二分搜索树
- 存储的元素必须有可比较性,因为存储元素的时候,需要依次比对大小,来找到对应的存储位置
- 其中每一个节点都可以是一个二分搜索树:
- 以46为
根节点组成的树是一个二分搜索树 - 同样,以35为根节点组成的树也是一个二分搜索树,只不过他又被叫做以46为根节点组成的树的
左子树 - 同理,52叫做以46为根节点组成的树的
右子树 - 以30为根节点组成的树也是一个二分搜索树,只不过只有根节点,没有子节点,但是同样他被叫做以35为根节点组成的树的
左子树 - 以59为根节点组成的树也是一个二分搜索树,和30节点一样只有根节点,没有子节点,但是同样他被叫做以52为根节点组成的树的
右子树 - 小伙伴们体会一下其中涉及的几个术语,后面我们会用到
- 以46为
- 像30、48 这样的最下面的节点 叫做
叶子节点
实操
添加新元素
向开始图中的二分搜索树中添加28元素如何做呢?
根据前面的结构特性介绍我们知道左子树的值要小于根节点的值,右子树的值要大于根节点的值,所以如果新加入一个元素需要从根节点依次比较,找到合适的位置
- 28与46比较发现28小于46,所以他会出现在46的左子树一侧
- 28与35比较,发现28小于35,同理他会出现在35的左子树一侧
- 28与30比较,发现28小于30,同理他会出现在30的左子树一侧
- 这个时候,30的左子树是空,所以28作为30的左子树,到此添加元素结束
- 变成如图所示的结构
- 这里注意,这里的二分搜索树不包含相同的元素,如果加入相同的元素,例如加入52,那么依次比对后,到达52的位置将会什么都不做
查找元素
- 有了上面添加元素的经历,想必查找元素信手拈来了吧
- 只需要依次比对即可,例如查找39元素
- 39与46比较,小于,则在其左子树中查找。与35比较,大于,则在其右子树中查找,与39比较,发现相等,查找结束
遍历元素
- 在线性结构中遍历是个容易的操作,只需要从头到尾的访问一遍即可,
- 但是在树这种结构中要如何操作呢?其实也没有那么难,
- 前人给我们指出了三种遍历的方式,小伙伴们可以仔细的体会
前序遍历
- 说人话就是
根节点-左子树-右子树(根-左-右)这样的遍历方式,请看详细解析
- 这里有递归的思想,小伙伴们可以借助递归思想来帮助理解
- 先找到整棵树的根,然后找到其左子树,前面我们说到左子树也是一个二分搜索树,所以其左子树的遍历同样是需要找到根,然后找到其左子树,看到这里,小伙伴们想必体会到了递归的感觉,找到这个左子树还是一样的道理,遍历其左子树中的根,然后在是左子树,循环往复,直到其左子树没有值。
- 遍历其右子树,与遍历左子树一样的思路,其右子树中也是遍历其根节点,左子树... 循环往复。
- 思路就说到这里,下面以开始的二分搜索树为例子遍历。小伙伴们看完实际例子,想必再看这个思路会相辅相成,彻底理解
- 整棵树的根即为46 ,
遍历输出46 - 找到其左子树,(也就是(35,30,39) 组成的树)。根节点是35,
遍历输出35 - 接着找以35为根的左子树((30,28)这个树)。根节点是30,
遍历输出30 - 接着找以30为根的左子树,即28,
遍历输出28 - 接着找以28为根的左子树,为空。这个时候以28为根的左子树就遍历完了。
- 接着找以28为根的右子树,为空。这个时候以28为根的右子树就遍历完了。
- 接着找以30为根的右子树,为空。这个时候以30为根的右子树就遍历完了。
- 接着找以35为根的右子树(39这个树),根节点是39
遍历输出39 - 接着找以39为根的左子树,为空。这个时候以39为根的左子树就遍历完了。
- 接着找以39为根的右子树,为空。这个时候以39为根的右子树就遍历完了。
- 到此,以46为根的左子树就遍历完了。
- 小伙伴们可以自己体会一下以46为根的右子树的遍历
- 遍历结果是
46 35 30 28 39 52 48 59
中序遍历
- 是
左子树-根节点-右子树(左-根-右)这样的遍历方式 - 有了上面的前序遍历方式,这个中序遍历想必就容易了吧
- 即先遍历其左子树,在其左子树中还是遍历其左子树...循环往复
- 遍历结果是:28 30 35 39 46 48 52 59
- 是从小到大的排序哦,不知道小伙伴们发现没
后序遍历
- 是
左子树-右子树-根节点(左-右-根)这样的遍历方式 - 这个不用多说了吧,遍历结果是:28 30 39 35 48 59 52 46
删除元素
- 删除节点,小伙伴们可以先体会一下要如何删除
- 先从简单的 删除最大,最小的元素开始
删除最小的元素
- 最小的元素就是整个树最左边的节点
- 如果该节点没有子节点,直接删除即可
- 如果该节点有子节点,那么肯定是右子树,将右子树连接到最小节点的根节点即可。
- 如下图,删除30节点,将32节点补上去
删除最大的元素
- 最大的元素就是整个树最右边的节点
- 与删除最小的节点同理,如果最大的节点有左子树那么将其连接到最大节点的根节点即可
删除任意位置的元素
- 有了上面两个删除动作,删除任意位置的元素变得稍微简单
- 如果带删除得节点没有右子树,那么和删除最小元素一样
- 如果带删除得节点没有左子树,那么和删除最大元素一样
- 比较困难的是待删除的元素有左右子树例如删除52节点,这种情况其子节点要如何处理?这里举出一个思路
- 找到以52节点为根节点的右子树中最小的节点,即57
- 将57移除,把57添加到原来的52的位置
- 如下图所示
代码实操
//添加的元素必须要支持可比较性
public class BinTree<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 BinTree(){
root = null;
size = 0;
}
public int size(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
// 向二分搜索树中添加新的元素e
public void add(E e){
root = add(root, e);
}
// 向以node为根的二分搜索树中插入元素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){
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);
}
}
// 二分搜索树的前序遍历
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 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 inOrder(){
inOrder(root);
}
// 中序遍历以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为根的二分搜索树, 递归算法
private void postOrder(Node node){
if(node == null){
return;
}
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
// 寻找二分搜索树的最小元素
public E minimum(){
if(size == 0){
throw new IllegalArgumentException("树是空");
}
return minimum(root).e;
}
// 返回以node为根的二分搜索树的最小值所在的节点
private Node minimum(Node node){
if(node.left == null){
return node;
}
return minimum(node.left);
}
// 寻找二分搜索树的最大元素
public E maximum(){
if(size == 0){
throw new IllegalArgumentException("树是空");
}
return maximum(root).e;
}
// 返回以node为根的二分搜索树的最大值所在的节点
private Node maximum(Node node){
if(node.right == null){
return node;
}
return maximum(node.right);
}
// 从二分搜索树中删除最小值所在节点, 返回最小值
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 = maximum();
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;
}
// 从二分搜索树中删除元素为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.compareTo(node.e) == 0
// 待删除节点左子树为空的情况
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;
}
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
generateBSTString(root, 0, res);
return res.toString();
}
// 生成以node为根节点,深度为depth的描述二叉树的字符串
private void generateBSTString(Node node, int depth, StringBuilder res){
if(node == null){
res.append(generateDepthString(depth) + "null\n");
return;
}
res.append(generateDepthString(depth) + node.e +"\n");
generateBSTString(node.left, depth + 1, res);
generateBSTString(node.right, depth + 1, res);
}
private String generateDepthString(int depth){
StringBuilder res = new StringBuilder();
for(int i = 0 ; i < depth ; i ++){
res.append("--");
}
return res.toString();
}
}
到此 二分搜索树就介绍完毕了,小伙伴们可以结合理论和代码来体会树的添加元素和删除元素,递归的思想也会有更深的体会