树是什么
计算机中的树是一种数据结构,它是由n个有限节点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根在上而叶子在下。
树的特点
- 每个节点有零个或多个子节点
- 没有父节点的节点称为根结点
- 每一个非根节点有且只有一个父节点
- 除了根结点外,每个子节点可以分为多个不相交的子树
树的术语
- 节点:树中的每一个元素
- 父节点:生出该节点的节点,每一个节点只能有一个父节点
- 子节点:该节点生出的节点,一个节点没有或者有多个子节点
- 根节点:没有父节点的节点
- 叶子节点:没有子节点的节点
- 兄弟节点:同一个父节点的节点
- 节点高度:节点到叶子节点的最长路径
- 节点深度:节点到根节点的路径
- 节点层:节点深度+1
- 树的高度:根节点的高度
二叉树
- 二叉树:顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。
- 满二叉树:叶子节点全部在最底层,除了叶子结点外,每个节点都有左右两个子节点。满二叉树是完全二叉树的一种特殊情况。如图中2。
- 完全二叉树:叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大。如图中3。
(图来自王争《数据结构与算法之美》专栏)
二叉树的存储
- 链式存储:每个节点有三个字段,其中一个存储数据,另外两个分别指向左右子节点。
- 顺序存储:基于数组存储。适合于完全二叉树。可以将根节点存储于i=1i=1i=1的位置,左子节点存储于i=i∗2i=i*2i=i∗2的位置,右子节点存储于i=i∗2+1i=i*2+1i=i∗2+1的位置。如图:
(图来自王争《数据结构与算法之美》专栏)
二叉树的遍历
经典的遍历二叉树的方法有三种:前序遍历、中序遍历、后序遍历。其中前、中、后序,表示的是节点与它的左右子树节点遍历打印的先后顺序。实际上,二叉树的前、中、后序遍历就是一个递归的过程。
二叉树遍历的时间复杂度为O(n)O(n)O(n)
二叉查找树
- 概念:二叉查找树是二叉树中最常用的一种类型, 也叫二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。不过,它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。
- 要求:二叉查找树要求在树中的任意一个节点,其左子树中的每个节点的值都要小于这个节点的值。而右子树中的每个节点的值都要大于这个节点的值。如下图:
(图来自王争《数据结构与算法之美》专栏)
二叉查找树的操作
- 查找:先比较根节点,若该数等于根节点,则查找完成;若该数大于根节点,则比较右子树的根节点;若该数小于根节点,则比较左子树的根节点;在不等于比较节点的情况下,递归对子树进行上述步骤,直至找到该数。如图:
(图来自王争《数据结构与算法之美》专栏) - 插入:在无重复数的情况下,新插入节点都会插入到叶子节点,所以先比较根节点,若该数大于根节点,则比较右子树的根节点;若该数小于根节点,则比较左子树的根节点;递归进行上述操作,当遇到根节点无右子树或左子树时,将该节点插入即可。如图:
(图来自王争《数据结构与算法之美》专栏) - 删除:删除节点时要分三种情况:
(1)被删除的节点无子节点。此时直接将被删除的节点的父节点指向该被删除节点的指针指向NULL。如图删除55
(2)被删除的节点有一个子节点。此时将被删除的节点的父节点指向该被删除节点的指针指向被删除节点的子节点。如图删除13
(3)被删除的节点有两个子节点。此时将被删除的节点替换为被删除的节点的右子树中的最小的节点。如图删除18
(图来自王争《数据结构与算法之美》专栏) - 中序遍历:二叉查找树在中序遍历的情况下,可以从小到大顺序输出数据。时间复杂度是O(n)。
支持重复数据的二叉查找树
前面讨论的均是默认没有重复数据的情况。当二叉查找树中有重复数据时,有两种解决方案:
- 将重复数据通过链表法放入同一个节点中存储
- 在插入重复数据时,若比较知道当前二叉树中有此数据,那么将待插入数据插入到该节点的右子树中。在查找时,查找到数据之后不停止查找,接着在该节点的右子树继续进行查找。如图:
(图来自王争《数据结构与算法之美》专栏)
二叉查找树的时间复杂度
二叉查找树的形态各异,在最糟糕的情况下可以退化为链表,在最好的情况下是一个完全二叉树。如图:
(图来自王争《数据结构与算法之美》专栏)
所以,最坏情况下的查找、插入、删除时间复杂度均为O(n)
最好的情况下的查找、插入、删除时间复杂度经计算均为O(logn)
要保持最好情况,那么就要用到二叉查找树的特殊形态:平衡二叉查找树
散列表与二叉查找树比较
- 散列表的查找、插入、删除时间复杂度均为O(1)。二叉查找树的查找、插入、删除最好时间复杂度为O(logn)。
- 散列表在进行散列函数计算及解决散列冲突时需要耗费时间。二叉查找树则无需额外的计算。所以,常量的操作时间不一定会比O(logn)的操作时间小。
- 散列表在设计时需要考虑散列函数的设计、如何解决散列冲突、扩容、缩容。二叉查找树则仅需考虑如何保持平衡,该解决方案相对更成熟稳定些。
- 在需要顺序输出数据时,因为散列表是随机存储的,而二叉查找树可以中序遍历输出,所以二叉查找树更占优势。
- 散列表在扩容时需要耗费时间,而二叉查找树无需扩容。
所以,不能单看操作时间复杂度来判断数据结构的好坏,我们在开发过程中,需要结合具体的需求来选择使用哪一个。