数据结构之树

简介: 数据结构之树

一、树的定义及常用术语



1.树的定义


树是由n(n>=0)个结点所构成的有限集合,当n=0时,称为空树。当n>0时,n个结点满足以下条件:


1)有且仅有一个称为根的节点。

2)其余结点可分为m(m>=0)个互不相交的有限集合,且每个集合又构成一颗树,这棵树称为根结点的子树。



20210517163014353.png


如上几种都可以称为树结构,a图是只有根节点的树,b图是只拥有一个子树的树结构,c图是一个三个子树的树结构。


2.树的常用术语


这里介绍的都是关于树的基础概念,用以参考,防止发生概念混淆。
1)树的结点
树的结点是由一个数据元素及关联其子树的边所组成,上图中的a图是只有一个结点的树,b图是拥有三个结点的树。
2)结点的路径
结点的路径是指从根节点到该节点所经历的结点和分支的顺序排列。比如上图中c图中H结点的路径是:A-->C-->H。
3)路径的长度
路径的长度是指结点路径中所包含的分支数。比如c图中H结点的路径长度是2。因为A是根节点不计数,C、H两个含有分支的结点计数。
4)结点的度
结点的度是指该结点所拥有子树的数目。例如c图中A的度为3,B的度为2,C的度为3。
5)树的度
树的度指的是树中所有结点的度的最大值。注意这个值并不是指根的度,而是指所有结点度的最大值。比如上图c中树的度是3。
6)叶结点(终端结点)
叶结点是指树中度为0的结点,叶结点也叫终端结点。比如c图中的E、F、G、H、I都是叶结点。
7)分支结点(非终端结点)
分支结点是指树中度不为0的结点,分支结点也称为非终端结点。树中除了叶结点之外都是分支结点。
8)子结点(孩子结点)
一个结点的子结点是指这个结点的子树的根节点。上图c中,A的子结点是B、C、D。
9)父结点(双亲结点)
一个结点的父结点是指:若树中某个就结点有子结点,则这个结点就称为其子结点的父结点。比如c图中A是B、C、D的父结点。
10)子孙结点
一个结点的子孙结点是指这个结点的所有子树中的任意结点。如c图中A的子孙结点是B、C、D、E、F、G、H、I。
11)祖先结点
一个结点的祖先结点是指该结点的路径中除此节点以外的所有结点。比如c图中G的祖先结点是A、C。
12)兄弟结点
兄弟结点是指具有同一双亲的结点。比如c图中B、C、D都是A的子结点,所以B、C、D拥有共同的双亲,所以B、C、D是兄弟结点。
13)结点的层次
规定树中根结点的层次为0,则其他结点的层次是其父结点的层次加1。比如c图中G的层次是2。
14)树的深度
树的深度是指树中所有结点的层次树的最大值加1。这个值和树的度有些类似,5)中描述了树的度。他们是两个完全不同的概念。c图中国树的深度是3。
15)有序树
有序数是指树中各结点的所有子树之间从左到右有严格的次序关系,不能互换。也就是说,如果子树的次序不同则对应着不同的有序树。常说的二叉树就是一种有序树。
16)无序树
与有序树相反,无序树是指树中各结点的所有子树之间没有严格的次序关系。按前面树的定义可知,树是无序树,即树中的所有结点从左到右没有次序之分,其次序可以任意颠倒。
17)森林
森林是指由m(m>=0)棵互不相交的树所构成的集合。如上图中a、b、c三棵树可以构成一个森林。


二、二叉树、满二叉树和完全二叉树



二叉树是一种特殊的树,它的每个结点最多只有两棵子树,并且这两棵子树也是二叉树。由于二叉树的两棵子树有左右之分,所以二叉树是有序树。


1.二叉树的定义


二叉树是由n(n>=0)个结点所构成的有限集合。当n=0时,这个集合为空,此时的二叉树为空树;当n>0时,这个集合是由一个根节点和两个互不相交的分别称为左子树和右子树的二叉树构成。


2.满二叉树的定义


满二叉树是二叉树的一种特殊状态,如果在一棵二叉树中,它的所有结点或者是叶结点,或者是左右子树都非空,并且所有叶结点都在同一层上,则称这棵二叉树为满二叉树。如下图所示便是满二叉树:




3.完全二叉树


完全二叉树也是二叉树的一种特殊形态。如果在一棵具有n(n>=0)个结点的二叉树中,它的逻辑结构与满二叉树的前n个结点的逻辑结构相同,则称这样的二叉树为完全二叉树。如下图所示便是完全二叉树:

202105171830139.png


4.单分支树


若二叉树的所有结点都没有右孩子或没有左孩子,则称此二叉树为单分支树。单分支树又可分为左支树和右支树,其中所有结点都没有右孩子的二叉树称为左支树,所有结点都没有左孩子的二叉树称为右支树。不难看出左支树或者右支树的深度就是树中结点的个数。


20210517184216999.png


5.二叉树的几个性质


1)性质1:**二叉树中**第i(i>=0)层上的结点数最多为 2^i^ 个。
2)性质2:深度为h的**二叉树**最多有 2^h^-1 个结点(因为根节点只有一个,所以是 2^h^-1)。
3)性质3:对于任何一棵**二叉树**,若其叶子节点个数为n,度为2的结点个数为h。则有h+1 = n。(观察满二叉树的结构很容易看出来)
4)性质4:具有n个结点的**完全二叉树**,结点数为奇数时其深度为log~2~n ,结点数为偶数时为 log~2~(n+1) 。(根据性质1和性质2不难推导出)
5)性质5:具有n个结点的**完全二叉树**,若从根节点开始从上到下按照层次关系从左到右从0开始对所有结点进行编号,则对于任意一个编号为 i(0<= i < n) 的结点有:
  ①若i=0,则编号为i的结点是二叉树的根节点,它没有双亲;若i>1,则编号为i的结点其双亲的编号为(i-1)/2。(观察满二叉树易于理解)
  ②若2i+1>=n,则编号为i的结点无左孩子,否则编号为2i+1的结点就是其左孩子。(结合性质1和性质2,观察完全二叉树易于理解)
  ③若2i+2>=n,则编号为i的结点无右孩子,否则编号为2i+2的结点就是其右孩子。(②中的公式:2i+1>=n加1自然就是右结点了的描述了)


6.二叉树的存储结构


1)二叉树的顺序存储结构
二叉树的存储实现方法有很多种,但归纳起来主要分为顺序存储和链式存储两大类。
二叉树的顺序存储是指按照某种顺序依次将二叉树中的各个结点值存放在一组地址连续的存储单元中。由于二叉树是非线性结构的,所以需要将二叉树的各个结点按照一定的次序排列成线性序列,再通过这些结点在线性序列中的相对位置,确定二叉树中的各个结点之间的逻辑关系。由二叉树的性质5可知,对于一棵**完全二叉树**,可以从根节点开始自上而下并按照层次从左到右对结点进行依次编号,然后根据编号顺序将其存放在一维数组中,其结点之间的逻辑关系可由性质5中父结点与子结点编号之间的关系式计算得到。
上面所说是对于完全二叉树,那么**非完全二叉树**该怎么使用线性结构来表示呢?对于非完全二叉树通常的做法都是在树中增加一些虚结点,使其成为完全二叉树,剩下的处理就与完全二叉树相同了,只不过在数据存储在数组是上时,虚节点对应的位置不存放任何值。
**对于满二叉树和完全二叉树,顺序存储是一种最简单、最节省空间的存储方式,而且能使操作简单,所以顺序存储方式非常适合用于满二叉树和完全二叉树**。对于非完全二叉树来说由于有虚结点的存在会造成空间浪费,对于有n个结点的右支树来说,他的深度是n,就会有2^n^-1个结点被分配,但只存储了n个结点,空间上是一种极大的浪费。
2)二叉树的链式存储结构
二叉树的链式存储结构是指将二叉树的结点随机存放在位置任意的内存空间中,各个结点之间的逻辑关系通过指针来反应。因为任意二叉  树的结点最多会有一个父结点,两个子结点。所以二叉树的链式存储结构又划分为了**二叉链表存储结构**和**三叉链表存储结构**。
  ①二叉链表存储结构
  在二叉链表存储结构中,二叉树的每个结点设置有三个域:数据域、左孩子域、右孩子域,其中数据域用来存储该结点的数据,左、右孩子用来存放指向左、右孩子结点的指针。
  ②三叉链表存储结构
  三叉链表存储结构是指在二叉链表的结点结构中增加一个父结点域,用以存放指向父结点的指针。
3)总结
顺序存储结构适合满二叉树和完全二叉树只在非完全二叉树时比较浪费空间,三叉链表存储结构在访问父结点和子结点时特别方便但是空间开销较大,在实际中二叉树的实现最多的还是二叉链表存储结构,可以认为这是一种折中的方案,空间上节省介于顺序存储结构与三叉链表存储结构之间。下面展示下二叉链表存储结构的结点类的实现:


/**
 * 二叉树的节点类
 * 包含三个属性,一个数据域,一个指向左孩子结点的指针域,一个指向右孩子结点的指针域
 * @author pcc
 * @version 1.0.0
 * @className BiTreeNode
 * @date 2021-05-18 14:26
 */
public class BiTreeNode {
    Object data;//数据域
    BiTreeNode leftChild,rightChild;//指向孩子结点的指针域
//    提供构造方法
    public BiTreeNode(){
    }
    public BiTreeNode(Object object){
        data = object;
    }
    public BiTreeNode(Object object,BiTreeNode leftChild,BiTreeNode rightChild){
        data = object;
        this.leftChild = leftChild;
        this.rightChild = rightChild;
    }
}


7.二叉树的遍历


所谓遍历就是沿着某种路径,将集合中的每一个元素都访问一遍,且每个元素仅是访问一遍。有如下所示的一个二叉树:


20210518160530717.png


7.1.二叉树的遍历方法


根据二叉树的特点可以将二叉树划分为三个部分,根节点(D)、左子树(L)、右子树(R),因此就可以得到7种不同的遍历二叉树的方法:层次遍历、LDR、LRD、DLR、DRL、RDL、RLD,因为先左后右与先右后左没有本质的区别,算法实现类似,这里只介绍其中四种,一种层次遍历,一种先根遍历、一种中根遍历、一种后根遍历。

1)层次遍历操作

若二叉树为空,则为空操作,否则从上到下从左到右,根据二叉树的层次依次访问其所有结点。

这种遍历对应上图中的二叉树结点访问顺序应该是:ABCDEFGH,从上而下从左到右就是这个输出应该很好理解。

2)先根遍历操作(DLR)

先根遍历又叫先序遍历。若二叉树为空,则为空操作;否则

①访问根节点。

②先根遍历左子树

③先根遍历右子树


这种遍历对应上图中的二叉树结点访问顺序应该是:ABDEGCFH,先根遍历、中根遍历、后根遍历都是一种递归的思想,理解这一点就会明白遍历的顺序了,如下图列出了根节点A的左子树的遍历过程,根结点A的右子树的遍历过程与其一模一样,就不在重复画图了。所有的结点的访问顺序就是ABDEGCFH了。


20210518164538969.png


3)中根遍历操作(LDR)

中根遍历又叫中序遍历。若二叉树为空,则为空操作;否则

①中根遍历左子树

②访问根节点

③中根遍历右子树

这种遍历对应上图中的二叉树结点访问顺序应该是:DBGEAFHC,遍历的细想都是递归细想,如下图列出了左子树的遍历过程。


20210518170420284.png


4)后根遍历操作(LRD)

后根遍历又叫后序遍历。若二叉树为空,则为空操作;否则

①后根遍历左子树

②后根遍历右子树

③访问根节点

这种遍历对应上图中的二叉树结点访问顺序应该是:DGEBHFCA,遍历的思想都是递归,如下图是后根遍历A的左子树的输出图:


20210518172915949.png


7.2 递归实现二叉树的遍历操作


递归操作不是一种很优秀的遍历方法,还是很消耗栈的内存的,但是递归的优势就是代码简洁,通俗易懂,下面假设二叉树的遍历就是输出结点的数据,结点类使用上方声明的结点类:BiTreeNode ,下面给出先根、中根、后根三种遍历的递归写法:

//递归遍历二叉树:先根遍历
    public void preRootTraverse(BiTreeNode node){
        if(node != null){
            System.out.println(node.data);
            preRootTraverse(node.leftChild);
            preRootTraverse(node.rightChild);
        }
    }
    //递归遍历二叉树:中根遍历
    public void inRootTraverse(BiTreeNode node){
        if(node != null){
            preRootTraverse(node.leftChild);
            System.out.println(node.data);
            preRootTraverse(node.rightChild);
        }
    }
    //递归遍历二叉树:后根遍历
    public void afterRootTraverse(BiTreeNode node){
        if(node != null){
            preRootTraverse(node.leftChild);
            preRootTraverse(node.rightChild);
            System.out.println(node.data);
        }
    }


可以看出递归算法的写法十分的简洁,易于理解,唯一的缺陷就是性能不好。


7.2 非递归实现二叉树的遍历操作


前面已经给出了二叉树遍历的递归操作的实现,但是递归并不是一种优秀的遍历操作,它无论在空间还是时间上都不够优秀,显然我们在实际使用中不可能直接使用递归来进行遍历二叉树,因此我们还需要研究其他的遍历操作。

1.非递归遍历二叉树:先根遍历

//非递归便利二叉树:先根遍历
    //根据书中提供的两次循环改进而来,这里只需要一次循环就可以实现二叉树的遍历,
    // 其实本质上差别不大,时间复杂度其实是一样的
    /**
     * 思路:
     * 第一步:将根节点放入到栈中。
     * 第二步:然后不断遍历左节点并输出,当前结点有右结点就放入栈,有左节点就继续遍历
     * 第四步:全部遍历完成后,退出循环
     *
     * @param node
     * @throws Exception
     */
    public static void inRootTraverseMy(BiTreeNode node) throws Exception{
        if(node != null){
            IStack stack = new LinkedStack();
            stack.push(node);
            if(!stack.isEmpty()){
                BiTreeNode popNode = (BiTreeNode)stack.pop();
                while(popNode.data!=null){
                    System.out.println(popNode.data);
                    if(popNode.rightChild!=null){
                        stack.push(popNode.rightChild);
                    }
                    if(popNode.leftChild!=null){
                        popNode = popNode.leftChild;
                        continue;
                    }
                    popNode = stack.isEmpty()?new BiTreeNode():(BiTreeNode)stack.pop();
                }
            }
        }
    }


2.非递归遍历二叉树:中根遍历


/**
     * 思路:
     * 第一步:先将根节点放入栈中,然后依次往下遍历左节点,将其全部放入栈中
     * 第二步:第一步完成后,取出栈顶结点,此时栈顶结点应该是最左下次的结点。
     * 第三步:循环输出结点,第二步取出的结点不为空则进入循环,因为是最左节点,不为空可直接输出,
     *         然后判断是否拥有右节点,有的话,从新从第一步走。没有右节点的话则可以直接向上遍历结点。
     *
     * @param node
     * @throws Exception
     */
    public static void inRootTraverseNew(BiTreeNode node) throws Exception{
        if(node != null){
            IStack stack = new LinkedStack();
            stack.push(node);
            while(!stack.isEmpty()){
                while(((BiTreeNode)stack.peek()).leftChild!=null){
                    stack.push(((BiTreeNode)stack.peek()).leftChild);
                }
                while(!stack.isEmpty()){
                BiTreeNode nodePop = (BiTreeNode)stack.pop();
                    if(nodePop.data!=null){
                        System.out.println(nodePop.data);
                    }
                    if(nodePop.rightChild!=null){
                        stack.push(nodePop.rightChild);
                        break;
                    }
                }
            }
        }
    }


未完待续。。。。


相关文章
|
3月前
|
存储 算法 C语言
"揭秘C语言中的王者之树——红黑树:一场数据结构与算法的华丽舞蹈,让你的程序效率飙升,直击性能巅峰!"
【8月更文挑战第20天】红黑树是自平衡二叉查找树,通过旋转和重着色保持平衡,确保高效执行插入、删除和查找操作,时间复杂度为O(log n)。本文介绍红黑树的基本属性、存储结构及其C语言实现。红黑树遵循五项基本规则以保持平衡状态。在C语言中,节点包含数据、颜色、父节点和子节点指针。文章提供了一个示例代码框架,用于创建节点、插入节点并执行必要的修复操作以维护红黑树的特性。
102 1
|
1月前
|
存储 算法 搜索推荐
探索常见数据结构:数组、链表、栈、队列、树和图
探索常见数据结构:数组、链表、栈、队列、树和图
99 64
|
16天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
59 16
|
1月前
|
存储 算法 关系型数据库
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
这篇文章主要介绍了多路查找树的基本概念,包括二叉树的局限性、多叉树的优化、B树及其变体(如2-3树、B+树、B*树)的特点和应用,旨在帮助读者理解这些数据结构在文件系统和数据库系统中的重要性和效率。
20 0
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
|
1月前
|
存储 编译器 C++
【初阶数据结构】掌握二叉树遍历技巧与信息求解:深入解析四种遍历方法及树的结构与统计分析
【初阶数据结构】掌握二叉树遍历技巧与信息求解:深入解析四种遍历方法及树的结构与统计分析
|
1月前
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解(三)
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解
|
1月前
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解(二)
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解
|
1月前
|
存储
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解(一)
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解
|
1月前
|
Java C++
【数据结构】探索红黑树的奥秘:自平衡原理图解及与二叉查找树的比较
本文深入解析红黑树的自平衡原理,介绍其五大原则,并通过图解和代码示例展示其内部机制。同时,对比红黑树与二叉查找树的性能差异,帮助读者更好地理解这两种数据结构的特点和应用场景。
28 0
|
2月前
|
JSON 前端开发 JavaScript
一文了解树在前端中的应用,掌握数据结构中树的生命线
该文章详细介绍了树这一数据结构在前端开发中的应用,包括树的基本概念、遍历方法(如深度优先遍历、广度优先遍历)以及二叉树的先序、中序、后序遍历,并通过实例代码展示了如何在JavaScript中实现这些遍历算法。此外,文章还探讨了树结构在处理JSON数据时的应用场景。
一文了解树在前端中的应用,掌握数据结构中树的生命线