7-1 树


目录

7.1.1 树结构

7.1.1.1 树的概念

前面讲的都是 线性存储结构,而树是一种典型的非线性存储结构,一个元素可以有多个直接后继元素

树的一些术语如下所示:
|称呼|含义|
|—-|—-|
|叶子|没有后继结点的 结点称为叶子结点|
|子结点|某一个结点的直接后继结点|
|父结点|某个子结点的直接前驱结点|
|兄弟|具有同一父结点的 一群结点|
|祖先|从根结点开始走到该结点的所有上级结点,都是该结点的祖先|
|结点的度|直接后继结点的数目(子结点数目)|
|结点的层次|根结点为1,其它结点的层次等于它的父结点层次+1|
|树的深度|结点的最大层次值|
|森林|不同的树的集合;如果一棵树删除了根结点,那么剩下的子树就组成一片森林|

7.1.1.2 争议问题

关于树的根结点的数目的问题
这个似乎一直以来都有点矛盾,不同的书上可能说的不一样。
有的书说,树有且只有一个根结点,即根结点数目为1;有的书说,树的根结点可以为0,这时候称为空树

而且连网上的各种资料也都存在这两种说法
维基百科截图如下

还是维基百科

按照这里的说法: 二叉树结点可以为0,普通树不行;
那么,二叉树是不是树???二叉树当然是树。所以要说【树的结点】最小可以是1还是0呢?这里感觉说法确实很难自洽。
那就看具体情况来回答这个问题吧,也不用过多纠结。

7.1.1.3 树的种类

树依据不同的划分基准可以分为很多种类,这里大致罗列出常见的一些,并不完整,只是作为基本了解:

7.1.2 二叉树

本文简要介绍几种常见的二叉树。简单地理解,满足以下两个条件的树就是二叉树:

①本身是有序树(区分左右)!!!
②树中包含的各个结点的度不能超过 2,即只能是 0、1 或者 2;

7.1.2.1 满二叉树

定义 如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。
性质 ①满二叉树中第 k 层的结点数为 2^ (k-1) 个。(假设根结点层数是1)
②深度为 k 的满二叉树必有 2^k - 1 个结点(等比数列求和公式) ,叶子数为 2^ (k-1)
③满二叉树中不存在度为 1 的结点,每一个分支点都有两棵深度相同的子树,且叶子结点都在最底层。
④具有 n 个结点的满二叉树的深度为 log2(n+1)

7.1.2.2 完全二叉树

定义 二叉树中 除去最后一层结点 为满二叉树,且最后一层的结点依次从左到右有序分布;
或者这么理解:一个具有n个结点的二叉树,如果其结点编号 和 一颗满二叉树的1---n个结点的编号完全一致,那这棵树就是完全二叉树。
性质 ①N 个结点的完全二叉树的深度为 ⌊log2 N⌋+1。(向下取整再加1)
②如果将含有的结点按照层次从左到右依次标号,对于任意一个结点 i 完全二叉树,有以下几个结论成立:
(首先假设i=0 时,表示的是根结点,无父亲结点;在python中,往往是索引从0开始计数)
a. i>0 时,父亲结点为结点 [(i-1)/2] (向下取整)。
(在实际python的code中,用int取整即可,如果使用//向下取整,会在-0.5时取到-1,当i=0时使用//无法正确取数,所以用int向靠近0取整,能将i=0也直接统一进来)
b. i 结点的左孩子( 如果有的话 )是结点 2*i +1 。
c. i 结点的右孩子( 如果有的话 )是结点 2*i +2。。

在讲排序部分和优先队列的时候,遇到的大根堆/小根堆,就是属于完全二叉树;
大根堆:根结点是整棵树的最大值;并且对于每一棵子树而言,其最大值也都在子树根结点
小根堆:根结点是整棵树的最小值;并且对于每一棵子树而言,其最小值也都在子树根结点

7.1.2.3 排序二叉树(二叉查找树、二叉搜索树)

又称二叉查找树(Binary Search Tree - BST),亦称二叉搜索树。
定义为:一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树。

二叉搜索树有着高效的插入、删除、查询操作。
平均时间的时间复杂度为 O(log n),最差情况为 O(n)。
二叉搜索树与堆不同,不一定是完全二叉树,底层不容易直接用数组表示,一般是用链表来进行构造的;
其实二叉搜索树查找元素的过程,就是在实现“二分查找”。

7.1.2.4 平衡二叉搜索树

平衡二叉搜索树又被称为 AVL树,是 Adelson-Velsky and Landis Tree的缩写,分别是两个提出者的名字。
其实是对普通二叉搜索树的改进。考虑以下情况:

右侧的情况很显然其查找时间是O(N),但它确实是一个二叉搜索树,只是形状太极端了。
可以将其进行改进为AVL树,定义为一棵空树,或者是具有下列性质的二叉树::
(1)本身是一棵二叉搜索树;
(2)每个结点的左右两子树高度差都不超过一;(这就是平衡的含义)
(3)左、右子树也分别为平衡二叉搜索树。
称为平衡二叉搜索树。

平衡二叉搜索树就不会出现上面图(b)的这种极端情况了。
注意:由于维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树.(下面会提及)
如果应用场景中对插入删除不频繁,只是对查找要求较高,还是可以用AVL树,其查找效率是O(logN)。

7.1.2.4 红黑树

红黑树(Red Black Tree)算是对AVL树进行的一种改进,但是它的左右子树高差有可能大于 1,所以红黑树不是严格意义上的平衡二叉树(AVL),但 对之进行平衡的代价较低, 其平均统计性能要强于 AVL 。
红黑树的得名原因是,其结点有颜色之分,分为红色结点和黑色结点,所以每个结点要增加一个存储位表示结点的颜色,其性质如下:

  • 每个结点非红即黑.
  • 根结点是黑的。
  • 每个叶结点都是黑的.(叶子结点是NULL)
  • 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
  • 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。

红黑树的一些具体操作不在这里细讲,只需要知道: 它是查找,插入和删除效率都是(logN)。

7.1.3 二叉树的存储结构

7.1.3.1 顺序存储结构

使用顺序表(数组)存储二叉树。需要注意的是,顺序存储只适用于完全二叉树。
完全二叉树的顺序存储,仅需从根结点开始,按照层次依次将树中结点存储到数组即可。
如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。方法很简单,人为添加一些并不存在的空结点(其元素值为“空”),使之成为一颗完全二叉树的形式。但是这种方式明显会浪费大量内存,这时就应考虑链式存储方式。

从顺序表中还原完全二叉树也很简单。我们知道,完全二叉树具有这样的性质,将树中结点按照层次并从左到右依次标号(0,1,2,3,…),若结点 i 有左右孩子,则其左孩子结点为 2 i + 1,右孩子结点为 2 i+ 2。此性质可用于还原数组中存储的完全二叉树。

7.1.3.2 链式存储结构

只需从树的根结点开始,将各个结点及其左右孩子使用链表存储即可。不必非得是完全二叉树。下面是二叉树的二叉链表示意图:

二叉树的结点:
其结点结构由 3 部分构成:
指向左孩子结点的指针(Lchild);结点存储的数据(data);指向右孩子结点的指针(Rchild)
这样的链表结构,通常称为二叉链表。

class BiTNode(object):
def __init__(self, val = None):
self.data = val; //数据域
self.lchild = None
self.rchild = None

7.1.4 二叉树的遍历方式

7.1.4.1 深度优先遍历

深度优先搜索(Depth First Search - DFS)是沿着树的深度遍历树的结点,尽可能深的搜索树的分支。
那么深度遍历有重要的三种方法。这三种方式常被用于访问树的结点,它们之间的不同在于访问每个结点的次序不同。这三种遍历分别叫做先序遍历(preorder),中序遍历(inorder)和后序遍历(postorder)

7.1.4.1.1 先序遍历

在先序遍历中,我们对每一棵子树,都是先访问其根结点,然后访问其左孩子结点,最后访问其又孩子结点。

路径为:根结点->左子树->右子树

'''递归法'''
# 递归法就是调用系统栈
def preorder(root):
if not root:
return
# 对每棵子树的结构,都先访问根结点的值
res.append(root.val)
# 然后递归地分别处理左右子结点
preorder(root.left)
preorder(root.right)

res =[]
preorder(root)
return res

7.1.4.1.2 中序遍历

在先序遍历中,我们对每一棵子树,都是先访问其左子结点,然后访问其根结点,最后访问其右孩子结点。

路径为:左子树->根结点->右子树

'''递归法'''
# 递归法就是调用系统栈
def inorder(node):
if not node:
return
inorder(node.left)
res.append(node.val)
inorder(node.right)

res =[]
preorder(root)
return res

7.1.4.1.3 后序遍历

在先序遍历中,我们对每一棵子树,都是先访问其左子结点,然后访问其右孩子结点,最后访问其根结点。

路径为:左子树->右子树->根结点

'''递归法'''
# 递归法就是调用系统栈
def postorder(node):
if not node:
return
postorder(node.left)
postorder(node.right)
res.append(node.val)

res = []
postorder(root)
return res

7.1.4.2 广度优先遍历(层次遍历)

广度优先遍历(Breath First Search - BFS)是横向遍历,即从上到下,一层一层的遍历结点。

'''迭代法'''
# 宽度优先遍历,是利用的队列的思想,先进先出
if root is None:
return []

q, res = [root], []
while q:
layer =[]
# 这个 n 其实就是在求要遍历的那一层的宽度
n = len(q)
for i in range(n):
node = q.pop(0)
layer.append(node.val)
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
# 根据自己想要的输出格式,选用 append 或者 extend
res.append(layer)
return res