树-Tree

作为基本的数据结构,树在各种算法题中或者现实中有得到广泛的应用。除了基本的普通二叉树,衍生出了二叉排序树、红黑树、B+/B树等等。这里我们对树的一些基本的操作以及高级应用进行总结。

一、 基本操作

既然是对树进行总结,那么首先要总结的就是树的遍历。树的遍历分为前序、中序、后序。
一般对于树的遍历,常见的是用递归实现。递归的实现,可以使得代码更加清晰易懂。但是递归这种操作在JVM中,假如递归层数太多,会占用栈的大部分空间。因此我们不仅要掌握递归操作,也需要掌握非递归操作。非递归操作可以查看我的另一篇博客

前序

递归

public void preOrder(TreeNode root ,List<TreeNode> list) {
    if(root == null) {
        return;
    }
    list.add(root);
    preOrder(root.left , list);
    preOrder(root.right , list);
}

非递归

public List<TreeNode> preOrder(TreeNode root) {
    List<TreeNode> resultList = new ArrayList<TreeNode>();
    if(root == null){
        return resultList;
    }
    Deque<TreeNode> stack = new ArrayDeque<>();
    TreeNode p = root;
    while(!stack.isEmpty() || p!=null) {
        if(p != null){
            stack.push(p);
            resultList.add(p);
            p=p.left;
        }else {
            TreeNode temp = stack.pop();
            p=temp.right;
        }
    }
}

中序

递归

public void preOrder(TreeNode root ,List<TreeNode> list) {
    if(root == null) {
        return;
    }
    preOrder(root.left , list);
    list.add(root);
    preOrder(root.right , list);
}

非递归

public List<TreeNode> preOrder(TreeNode root) {
    List<TreeNode> resultList = new ArrayList<TreeNode>();
    if(root == null){
        return resultList;
    }
    Deque<TreeNode> stack = new ArrayDeque<>();
    TreeNode p = root;
    while(!stack.isEmpty() || p!=null) {
        if(p != null){
            stack.push(p);
            p=p.left;
        }else {
            TreeNode temp = stack.pop();
            resultList.add(p);
            p=temp.right;
        }
    }
}

后序

递归

public void preOrder(TreeNode root ,List<TreeNode> list) {
    if(root == null) {
        return;
    }
    preOrder(root.left , list);
    preOrder(root.right , list);
    list.add(root);
}

非递归

public List<TreeNode> preOrder(TreeNode root) {
    List<TreeNode> resultList = new ArrayList<TreeNode>();
    if(root == null){
        return resultList;
    }
    Deque<TreeNode> stack = new ArrayDeque<>();
    TreeNode p = root;
    while(!stack.isEmpty() || p!=null) {
        if(p != null){
            stack.push(p);
            result.addFirst(p.val); 
            p=p.right;
        }else {
            TreeNode temp = stack.pop();
            p=temp.left;
        }
    }
}

二、遍历提升

在上述的非递归方法中,我们所占用了O(n)空间复杂度,但是其实还有占用O(1)的非递归遍历方法。这种方法就是Morris遍历。使用Morris遍历可以只需要占用O(1)的空间复杂度。但是他的运行也需要占用更多的时间,有时其实就是时间和空间的守恒转换。

在上面的非递归中,我们之所以需要占用O(n)的空间,是因为我们在遍历的过程中,需要回退到父节点。在Morris中,通过给叶节点添加左右儿子,就能实现回退到父节点的效果,但是用时增加了。

Talk is cheap ,show me the code.

中序

//首先介绍中序遍历。中序遍历是Morris的基本形式,前序和后续都是基于中序修改的

public List<TreeNode> inOrder(TreeNode root) {
    List<TreeNode> result = new ArrayList<TreeNode>();
    TreeNode cur = root;
    while(cur!=null){
        if(cur.left == null){
            result.add(cur);
            cur = cur.right;
        }
        else{
            TreeNode pre = cur.left;
            while(pre.right!=null && pre.right!=cur){
                pre = pre.right;
            }
            if(pre.right == null){
                pre.right=cur;
                cur=cur.left;
            }else{
                pre.right =null;
                result.add(cur);
                cur = cur.right;
            }
        }
    }
    return result;


}

//空间O(1),时间O(n)

前序

public List<TreeNode> inOrder(TreeNode root) {
    List<TreeNode> result = new ArrayList<TreeNode>();
    TreeNode cur = root;
    while(cur!=null){
        if(cur.left == null){
            result.add(cur);
            cur = cur.right;
        }
        else{
            TreeNode pre = cur.left;
            while(pre.right!=null && pre.right!=cur){
                pre = pre.right;
            }
            if(pre.right == null){
                result.add(cur);  //唯一和中序的不同点
                pre.right=cur;
                cur=cur.left;
            }else{
                pre.right =null;
                cur = cur.right;
            }
        }
    }
    return result;
}

后序

后序和前序和中序不一样,需要辅助存储。这里就不讨论了,思想都是一样的。有兴趣的同学可以自己操作一下。

三、高级二叉树

1. 二叉查找树(BST)

二叉查找树,其特点是一个节点左边的所有节点的值都小于自己,右边的所有节点都大于自己。同一个有序的序列,都可以生成不同的BST。在具体的应用中,对于BST的实现都是用非递归形式,会比较高效。

BST的运行时间完全取决于BST树的形状。最好的情况下,含有N个阶段的BST是一个完全平衡的二叉树,那么这样的时间是O(LgN),但是极端情况下,有可能得全部遍历为N。对于很多应用来树,插入得Key都是随机的,因此我们假设极端情况出现的比较少。

根据统计,由N个随即键构造的BST,插入或者查找未命中时平均比较次数为小于2InN(约1.39lgN)。可以看到BST比二分在查找上慢了39%,但是对于插入操作,插入一个新键BST会非常有优势,也是不到2lgN,而二分查找是线性的。当N越大,这个结论约接近。

在1979年J.Robson证明了,随机键构造的二叉查找树平均高度为树中节点数的对数级别,对于足够大的N,为2.99lgN。因此可以认为当N足够大且随机,树内路径不会超过3lgN。如果键不是随机,则可以用平衡二叉查找树。

2.平衡二叉树

首先我们要知道为什么会有平衡二叉树的存在。在BST中,我们知道BST的查找用时完全取决于二叉树的形状。因此在随机建树的过程中,可能出现最高为N的访问次数,因此不可控。那么如果控制二叉树的建立,尽量让BST变的平衡,那么就可以使得随机BST的访问时间都能为O(lgN)。这也就是平衡二叉树存在的原因。因此,奔着要让执行二叉树的查找、插入、删除等等操作都是对数级别这一目的,我们来讨论一下二叉平衡树。

首先提前明确一下,红黑树,是我们这里讨论的平衡二叉树的一种具体实现。

2-3节点树

我们上面讨论了二叉查找树,但是希望二叉查找树是尽量平衡的,因此我们引入2-3节点树的概念。2-3节点树的定义是树内包含2节点和3节点的树。那什么是2节点?

  • 2-节点:普通的,包含一个键(A)和两个左右儿子的链接(X/Y)的节点就是2节点。
  • 3-节点:是有两个键(A/B)和三个链接(X/Y/Z)。左边X是小于A的所有节点字树,Y是在AB之间的节点,Z是大于B的节点子树。

2-3节点树的性质使得2-3节点树就是平衡的。具体的2-3节点树的说明,可以参考这篇博客。总之,2-3树就是一个平衡树。而作为有序的平衡树,已经能够满足我们上面提到的“希望BST是平衡二叉树“的要求了。

那对于2-3平衡树的具体实现,就是我们了解到的红黑树了。红黑树的应用非常广泛,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。红黑树和2-3节点树的关系是,在2-3节点树中,将3节点拆分为2个2节点,并用红链连接,这就是红黑树中的红点。黑链就是普通的节点,也就是黑点。在2-3节点转为红黑树的时候,需要遵循的原则是:

1.红链接均为左链接
2.没有一个节点同时和两条红链接相连
3.红黑树是完全黑树平衡的,也就是任意叶子到根的路径上黑链数量一致(将红链画成水平的)

这三条原则,也是知道红黑树在插入或者删除节点时,所需要执行的“旋转“操作的依据。

红黑树的代码这里就不贴上来了,感觉有点占篇幅。这里总结一下红黑树的性质。

1.大小为N的红黑树高度不超过2lgN。但是平均情况下,一般时1.00lgN。所以相对于上面的BST的平均1.39lgN,红黑树能够较少0.39的用时。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值