树与二叉树

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。

树具有与大自然中的树相似的结构,自然界的树都有一条主干,而主干会分出若干条支干,一般来说,这些支干互不相交,其中每条支干又可以分出若干条支干。数据结构中的树是类似的,每棵树都会有一个根结点,通过根节点可以引出若干个集合,而这些集合又可以看作是一颗子树,每颗子树的根节点都有一个前驱,若干个后继。

树的定义由递归实现。

树的概念

结点的度:一个结点含有子树的个数称为该结点的度;例如下图1结点的度为3

树的度:一棵树中,所有结点度的最大值称为树的度;例如下图树的结点为3

叶子结点或终端结点:度为0的结点称为叶结点;例如下图的8、10、11、15、99、5、6均为叶子节点

双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点;例如下图9是99的父结点

孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点;例如下图99是9的子结点

根结点:一棵树中,没有双亲结点的结点;例如下图的1

结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;例如下图99结点所在层次为3,8结点所在层次为4

树的高度或深度:树中结点的最大层次;例如下图树的高度为4

一颗普通的树

树的表示方法有多种,例如双亲表示法、孩子表示法、孩子双亲表示法、孩子兄弟表示法等

最常用的为孩子兄弟表示法

class TreeNode {
    public int val;
    public TreeNode left;//左孩子的引用
    public TreeNode right;//右孩子的引用
}

二叉树

二叉树是一种特殊的树形结构,它表现为每个结点最多只能有两颗子树,两棵子树有左右之分。两棵子树如果颠倒顺序,则成为另一颗二叉树。

特殊的二叉树

二叉树中也有一些特殊的二叉树,例如:左斜树和右斜树,满二叉树和完全二叉树。

左斜树和右斜树:所有结点都只有左子树的二叉树称为左斜树,同样的,所有结点都只有右子树的二叉树称为右斜树。

满二叉树:每层的结点数都达到最大值的二叉树就是满二叉树。如果一棵树的高度为h,它的结点总数为2^h - 1,那么就称它为满二叉树。

完全二叉树:对于高度为h、有n个结点的二叉树,其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。

如上图所示:左图为满二叉树,右图为完全二叉树

实际上满二叉树也属于完全二叉树

二叉树的性质

  1. 对于任何一棵树,若结点个数为n,则总边数为n-1

  1. 一棵非空二叉树的第i层上最多有2^(i - 1)(i>0)个结点

  1. 深度为K的二叉树最多有2^k - 1(k>=0)个结点

  1. 对任何非空二叉树,其叶子结点个数比度为2的结点个数多1,即n0=n2+1

  1. 具有n个结点的完全二叉树的深度为log2(n+1)向上取整

  1. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:

  • 若i>0,双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点

  • 若2i+1<n,左孩子序号:2i+1,否则无左孩子

  • 若2i+2<n,右孩子序号:2i+2,否则无右孩子

二叉树的存储

二叉树的存储可以是顺序存储也可以是链式存储。

顺序存储由于是连续的,用来存放满二叉树或者完全二叉树非常方便,可以快速访问的同时,大大减少了内存浪费。但对于一般的二叉树,需要添加一些空结点来表示某些不存在的结点,可能造成一定的空间浪费。

链式存储的适用性更强,它通过一个个结点将二叉树串联起来。

二叉树的遍历

所谓遍历,就是指沿着某条路线,依次对二叉树中每个结点进行访问。由于访问路线的不同,遍历顺序也不同,因此二叉树有多种遍历方式。

前中后序遍历

前序遍历:访问根节点->访问根的左子树->访问根的右子树

中序遍历:访问根的左子树->访问根节点->访问根的右子树

后序遍历:访问根的左子树->访问根的右子树->访问根节点

前序遍历(也称先序遍历)的先访问根节点,接着访问左子树,左子树同样属于二叉树,也要先访问根节点,在访问左子树,当左子树中所有结点都遍历完之后,才能返回遍历右子树。右子树同样是先访问根节点->访问左子树->访问右子树。例如:

对于中序遍历和后序遍历,仅仅是访问根节点的时机不同罢了。

前序遍历可以通过递归实现:

public void preOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    System.out.print(root.val + " ");
    preOrder(root.left);
    preOrder(root.right);
}

前序遍历也可以通过非递归实现,它需要借助栈完成,当结点不为空时,沿着左子树依次入栈,当结点为空时,说明左子树走完,可以走右子树,将入栈的结点依次出栈可以找到其对应的右子树:

public List<Character> preOrderTraversal2(TreeNode root) {
    TreeNode cur = root;
    Deque<TreeNode> stack = new ArrayDeque<>();
    List<Character> res = new LinkedList<>();//用来记录遍历结果
    while (cur != null || !stack.isEmpty()) {
        if (cur != null) {
            stack.push(cur);
            res.add(cur.val);
            cur = cur.left;
        }else {
            cur = stack.pop();//pop出的元素刚好是仅仅遍历完的左子树的结点
            cur = cur.right;
        }
    }
    return res;
}

中序遍历以及后序遍历的递归实现类似,只需改变打印的位置即可。

非递归实现中序遍历与前序遍历类似,后序遍历略有不同,后序遍历要求左右子树都遍历完才能遍历根节点,在遍历完左子树时,第一次返回根结点,不能遍历根结点,需要先遍历完右树,只有第二次返回根结点时才能遍历根结点。因此,考虑每次在根结点处加一个判断,先判断右子树遍历是否完成,可以在已经访问过的结点增加一个标记,并以此判断。算法如下:

public void postOrderNor(TreeNode root) {
    if (root == null) {
        return;
    }
    Deque<TreeNode> stack = new ArrayDeque<>();
    TreeNode cur = root;
    TreeNode prev = null;
    while (cur != null || !stack.isEmpty()) {
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        TreeNode top = stack.peek();
        //top == prev说明右子树已遍历完
        if (top.right == null || top == prev) {
            prev = stack.pop();
            System.out.println(top.val + " ");
        }else {
            cur = top.right;
        }
    }

}

后序遍历的另一种递归实现:由于后序遍历与先序遍历完全相反,可以考虑把先序遍历的实现反过来进行,结果上也是可行的:

//前序遍历反过来搞 ->先左再右变为先右再左    尾插变为头插
public List<Character> postOrderTraversal2(TreeNode root) {
    TreeNode cur = root;
    Deque<TreeNode> stack = new ArrayDeque<>();
    LinkedList<Character> res = new LinkedList<>();
    while(cur!=null || !stack.isEmpty()){
        if(cur!=null){
            stack.push(cur);
            res.addFirst(cur.val);
            cur = cur.right;
        } else {
            cur = stack.pop();
            cur = cur.left;
        }
    }
    return res;
}

层序遍历

层序遍历是按照从上到下,从左到右依次遍历的顺序,由于它在没有遍历完一整棵子树又去遍历另一棵子树,因此不能靠递归实现。它又是从前到后遍历,考虑使用队列实现,依次把结点入队列,这样即使后续找不到该结点,遍历的顺序也不会改变。具体做法是先入根结点,每次出队列时,把出队列的结点的左右子树结点入队列:

public void levelOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    //存放要出的结点
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        TreeNode tmp = queue.poll();
        System.out.print(tmp.val);
        if (tmp.left != null) {
            queue.offer(tmp.left);
        }
        if (tmp.right != null) {
            queue.offer(tmp.right);
        }
    }
    System.out.println();
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值