树(数据结构), since 2020.02.07

(2020.02.07, 2022.05.04补细节)
树结构在数据库设计中扮演重要作用

基本概念:

树叶和分支节点:没有子节点的节点称为树叶,其余都是分支节点。

(节点)度数degree: 一个节点的子节点个数。

层: 根的层数为0,位于k层的节点,其子节点是k+1层的元素。

高度(or深度): 树中节点的最大层数,只有根节点的树,其高度为0.
高度是从树叶往上数到目标节点。深度是从根节点往下数到目标节点。

满二叉树:二叉树中所有分支节点的度数为2。
满二叉树性质:树叶比分支节点多一个。
证明:从一个非空满二叉树,每次取走一个叶节点和它的母节点,并将剩余的节点相连形成新树,并对新树执行相似的过程。最终只剩下一个叶节点。证明树叶比分支节点多一个。

扩充二叉树:对已有二叉树T加入足够多的新叶节点,使T的原有节点都变为度数为2的节点,形成的新二叉树. 分为内部节点和外部节点。空树的扩充二叉树规定为空树。

扩充二叉树都是满二叉树(?)

完全二叉树:共h层,0- (h-1)层的节点数是.如果最下一层的节点不满,则所有节点在左边连续排列,空位都在右边。此为完全二叉树。(除最下两层外,其余都是度数为2的节点。节点数为n的完全二叉树,其高度是不大于log2(n)的最大整数) 完全二叉树可自然的存入一个连续表。

二叉树的list实现:

atree = [root, lleaf, rleaf] 其中的lleaf和rleaf都可以是一个子树

用二叉树实现表达式求值。

二叉树的简单实现

(2020.12.21 Mon)

# -*- coding: utf-8 -*-
class btree:
    def __init__(self, value):
        self.left = None
        self.right = None
        self.data = value
    def insert_left(self, value):
        self.left = btree(value) //插入的也是一个树
        return self.left
    def insert_right(self, value):
        self.right = btree(value)
        return self.right
    def show(self):
        print(self.data)
if __name__ == '__main__':
    root = btree('root')
    a = root.insert_left('a')
    c = a.insert_left('c')
    d = a.insert_right('d')
    f = d.insert_left('f')
    root.show()

遍历(traverse): 深度优先、宽度优先

深度优先(Depth First Search, DFS):根节点P左子节点L右子节点R,先根遍历顺序PLR,后根遍历顺序LRP,中序遍历LPR。

宽度优先(Breadth First Search, BFS):按路径长度由近及远的访问节点,亦即按层次访问树的各层节点。用队列(Queue)实现宽度遍历。

(2020.12.21 Mon)
树的深度遍历(DFS)方法有三种,先序遍历、中序遍历和后序遍历。

一个记忆技巧:先、中、后序遍历,是针对根节点在遍历中位置来定义的,也就如果顺序是根节点-左-右,则称为先序遍历,以此类推。

  • 先序遍历: 如果二叉树不为空,则访问根节点,然后访问左子树,最后访问右子树;否则程序退出
  • 中序遍历: 如果二叉树不为空,则访问左子树,然后访问根节点,最后访问右子树;否则程序退出
  • 后序遍历: 如果二叉树不为空,则访问左子树,然后访问右子树,最后访问根节点;否则程序退出

递归形式的遍历代码

# node = btree(5) 
def preorder(node):
    if node.data:
        node.show()
        if node.left:
            preorder(node.left)
        if node.right:
            preorder(node.right)
    
def inorder(node):
    if node.data:
        if node.left:
            inorder(node.left)
        node.show()
        if node.right:
            inorder(node.right)
    
def postorder(node):
    if node.left:
        postorder(node.left)
    if node.right:
        postorder(node.right)
    if node.data:
        node.show()
    
if __name__ == '__main__':
    root = btree('root')
    # some operations, insert trees
    ...
    inorder(root)
    preorder(root)
    postorder(root)
树的BFS遍历

(2020.12.22 Tues)
也就是按层遍历,需要用到queue作为工具。

class queue:
    default_capacity = 30
    def __init__(self):
        self.data = [None] * queue.default_capacity
        self.size = 0
        self.front = 0 # front index

    def enqueue(self, value):
        if self.size == len(self.data):
            self.resize(2*len(self.data))
        tail = (self.front + self.size)% len(self.data)
        self.data[tail] = value
        self.size += 1

    def dequeue(self):
        if self.size == 0:
            raise Empty('queue empty.')
        ans = self.data[self.front]
        self.data[self.front] = None
        self.front = (self.front+1) % len(self.data)
        self.size -= 1
        return ans

    def resize(self, newlen):
        obs = deepcopy(self.data)
        obsind = self.front
        self.data = [None] * newlen
        for k in range(self.size):
            self.data[k] = obs[obsind]
            obsind = (obsind + 1)%len(obs)
        self.front = 0
    def is_empty(self):
        return self.size == 0

#2020.12.24
if __name__ == '__main__':
    root = btree('root')
    # some insertion operation
    ...
    q = queue()
    q.enqueue(root)
    result = []
    while q.is_empty():
        tmp = q.dequeue()
        result.append(tmp.data)
        if tmp.left:
            q.enqueue(tmp.left)
        if tmp.right:
            q.enqueue(tmp.right)
    # result is what we want

堆(heap)及性质

代码参考《树-堆heap, since 2022-05-03》
堆: 用树形结构实现的优先队列,完全二叉树。

任意节点保存的数据,在优先级上优于或等于其子节点的数据。从根到任意叶的路径,优先级(非严格)递减。堆中最优先数据在根节点,检索复杂度O(1)。

插入元素:向堆(完全二叉树)的结尾加入一个元素,此时二叉树不是堆。新加入元素和父节点做对比,如果新的优先级高就与父节点交换,并重复该过程;否则结束插入元素的过程。复杂度O(log2(n)).该过程称为向上筛选

弹出元素:弹出优先级最高的元素,即堆顶的元素。弹出后堆顶空缺,从堆的尾部取出最后一个元素放在堆顶,并比较堆顶和两个子节点,将优先级最高的放在顶点,此时原来处在堆尾的点就下移,重复这个过程直到堆形成。称为向下筛选。复杂度O(log2(n)).

堆的应用:数组排序。

*非递归的遍历方法(没看)

*用生成器函数遍历(没看)

(2020.02.09)

Huffman tree

路径长途:根节点到叶节点的路径长度之和,i.e., 沿途节点个数(含根节点,不含终点节点)

用E = {[图片上传失败...(image-8474c1-1608347722558)]

}表示。

W = {[图片上传失败...(image-283282-1608347722558)]

} 是n个叶节点的值(or权重).

带权扩充外部二叉树的外部路径长度WPL = [图片上传失败...(image-730933-1608347722558)]

当扩充二叉树T以W为权,而T的WPL在所有这样的扩充二叉树中达到最小,T被称为最优二叉树/Huffman二叉树。

构造Huffman二叉树:

1 从W中找出最小的两个[图片上传失败...(image-4fb6f7-1608347722558)]

形成一棵二叉树,叶节点是两个[图片上传失败...(image-8d8d87-1608347722558)],根节点是他们的权重之和。

2 用1生成的根节点权重代替生成这个根节点的两个叶节点放回W

3 重复1/2,直到生成一棵树。

实现:优先队列。

算法分析:略。

Huffman编码:略

树(二叉树的扩展)

分为有序树和无序树。两棵树按无序树观点来看相同,但按照有序树的观点看可能不同。

树的结点度数是连接子树的个数。一棵树的度数定义为该树中度数最大的结点的度数。

树的性质:

1 度数为k的树,第i层至多有[图片上传失败...(image-35b6c3-1608347722558)]个结点

2 度数为k,高度为h的树,至多有...个结点(计算方法: 等比数列求和)

3 n个结点的树里n-1条边。(除了根节点,每个点都有一条边连接父节点,并且这些边不重合.)

4 n个结点的k度完全树,高度h是不大于[图片上传失败...(image-e14a9-1608347722558)]的最大整数。

二叉排序树

一种结点存储数据的二叉树,有如下性质:

1 根节点保存一个数据项和其关键码(value & key)

2 如果有左子树,则左子树保存的所有关键码,都小于(或不大于)根节点的关键码;

3 如果有右子树,则右子树保存的所有关键码,都大于根节点的关键码;

4 非空左子树/右子树也是二叉排序树;

5 中序排列这棵二叉树得到的关键码序列是递增序列。

检索:从上到下依次比较结点的关键码,如果比结点关键码大,则向右,小则向左。

插入:(假设与树中key重合,则新值代替旧值)从根节点开始,与结点的key比较,当应该向左走而左子树为空时,则添加成左子树;当应该向右走而右子树为空时,则添加为右子树;当与结点key重合,则更新结点的key-value。

删除:设删除的点是c,其父节点为p,设c是p的左子树(右子树情况以此类推)

1 如果c没有结点,则直接删除c,p的左子树更新为None;

2 如果c没有左子树,则删除c,c的右子树作为p的左子树;

3 如果c有左子树(有无右子树操作相同),则删除c,找出c的左子树中的最右结点r(从c开始往右下走,第一个没有右子树的结点?)。用c的左子结点代替c称为p的左子结点,c的右子树作为r的右子树.给出我的思路: c的最右结点r取代c,原r的位置空出,对r的左子树(如果有),从步骤1开始迭代执行。

复杂度分析:

2020.02.10

如果树形非畸形,即链条从上到下的没有分支,则检索的复杂度是O(n);而如果树形良好,即高度与结点数目成对数关系,则检索的复杂度是[图片上传失败...(image-9d404b-1608347722558)].

数据更新、插入和删除的操作都是在检索的基础上做常数时间操作,复杂度与检索相同。

最佳二叉排序树

最佳的标准应该基于检索效率。平均检索长度的概念。

设计最佳二叉排序树依据:检索关键码(key)的频度(次数,也称分布?).

假设二叉树排序树由n个内部节点和n+1个叶节点构成,其中不管内部节点构成的二叉树为何形状,加入外部节点扩展后,形成扩充二叉树。

key的平均检索长度[图片上传失败...(image-38fe41-1608347722558)]

E(n) = \frac{1}{w} [\sum_{0}^{n-1} p_{i}(l_{i}+1) +\sum_{0}^{n} q_{i} l_{i}^{'} ]

其中1/w = sigma p_i + sigma q_i

l_i是内部节点i的层数,l_{i}^{'}是外部节点

最优二叉排序树是使平均检索次数E(n)达到最少的二叉排序树。

最简单情况:所有key的检索频度相等。公式略。

结论是最低的树最好。

构造最简单情况的最优二叉排序树:

设a按关键码排序的字典,

1 low = 0, high = len(a) - 1

2 m = (low + high) /2

3 a[m]存入二叉树的根节点;片段a[low, m-1] 作为a[m]的左子树,a[m+1, high]作为a[m]的右子树;片段为空,则None,表示空树。循环此过程得到最优二叉排序树。由排序字典构成排序树的复杂度O(n),考虑到生成排序字典的复杂度最低是O(nlogn),则由任意一组关键码出发,构建最优二叉树的复杂度是O(nlogn).

一般情况的最优二叉树:用动态规划寻找

平衡二叉(排序)树 a.k.a. AVL树

最优二叉树的缺点:需要提前知道key的分布并且字典是静态,任何加入或删除的操作都会破坏树的结构。

平衡二叉树亦称AVL树,与其结构类似的有红黑树和B树。

定义:特殊的二叉排序树,或空树,或左右子树都是平衡二叉排序树,其左右子树的高度最大差距是1。(我的理解:平衡二叉排序树中的任何子树,其根节点的左右子树高度差都不超过1.)

BF平衡因子(balance factor): 对结点定义,该结点的左子树高度与右子树高度差,左高-右高。对平衡二叉树来说,BF=-1,0,1.即决定值不超过1. (每个结点如果能同时记录该点的最高高度和BF就非常好了.)

平衡二叉树的树高h与结点数n的关系:推导

2020.02.12

用[图片上传失败...(image-492b77-1608347722558)]代表高度为[图片上传失败...(image-c69d5f-1608347722558)]的'临界'平衡二叉树,也就是相同高度[图片上传失败...(image-7fc40d-1608347722558)]的平衡二叉树中节点最少的。容易得到[图片上传失败...(image-a2982-1608347722558)] = 1, [图片上传失败...(image-fe1076-1608347722558)]

= 2,从高度为2的临界平衡二叉树开始,都可以当做是一个根节点与高度为[图片上传失败...(image-6e32dc-1608347722558)]和[图片上传失败...(image-9c718f-1608347722558)]的两棵临界平衡二叉树的组合。有下面推导式
[图片上传失败...(image-bfbfa6-1608347722558)]
[图片上传失败...(image-7e334c-1608347722558)]

  • [图片上传失败...(image-dc0250-1608347722558)]
  • 1
    其中的1代表一个根节点。

Fibonacci数列 [图片上传失败...(image-ffedb4-1608347722558)]

由数学归纳法可证(?)

[图片上传失败...(image-1a665e-1608347722558)]

且[图片上传失败...(image-d28842-1608347722558)]

树高[图片上传失败...(image-c9bbc6-1608347722558)]

.

结论,与最优二叉树相比,最长路径的长度仅差一个常量,检索复杂度[图片上传失败...(image-6ea7a1-1608347722558)].

对平衡二叉树的设计核心,是控制不同子树的高度差,保证高度和所存项数的对数关系,保证检索效率

对平衡二叉树进行插入操作的再平衡

最小非平衡子树(minimum unbalanced subtree, MUS):包含新插入结点位置的、根结点的BF非0的最小子树。

如果在MUS中插入新结点后,MUS扔保持平衡,并且高度不变,则整棵树的高度也不变。

插入新结点后,结构调整能够在子树内的一条路径上完成,插入的复杂度O(logn).

插入新结点如果MUS失去平衡的调整和恢复的情况分以下4种:

LL: MUS的左子树较高,新结点插到左子树的左子树

LR: MUS的左子树较高,新结点插到左子树的右子树

RL: MUS的左子树较高,新结点插到右子树的左子树

RR: MUS的左子树较高,新结点插到右子树的右子树

其中LL、RR的处理方式相似,LR、RL的处理方式相似。

LL的处理方式:

图中a是MUS的根结点,b的左右子树等高。

image

在没有插入新结点之前,各结点和子树的排列顺序是AbBaC。新结点插入子树A,形成A'。此时顺时针调整MUS,b取代a成为根节点,b的右子树成为a的左子树,a成为b的右子树。调整后的子树和结点排序时A'bBaC。与调整前保持一致。(2020.02.18补充我的理解:在树的下面有一个平面,这个树在平面上的project的顺序就是树中节点的排列顺序。调整二叉树失衡之后得到的新树,只要其project顺序与原project顺序相同即可。)

a.left = b.right
b.right = a
a.bf = b.bf = 0
# RR的情况:
a.right = b.left
b.left = a
a.bf = b.bf = 0

LR的调整:

插入新结点前的排序时AbBcCaD。新结点插入到B或C的结点之下,从而导致c为根节点的子树失衡。


image

失衡之后的状态如图(2)所示。此时调整为(3)所示的关系,插入后的排序时AbB'(B)cC(C')aD,顺序没有变化,高度没有变化,新形成的子树仍然是平衡二叉树。

b = c.left
b.right = c.left 
a.left = c.right
c.right = a
c.left = b

之后调整各结点bf(略略略).

AVL平衡二叉树的插入操作复杂度O(logn).

动态多分支排序树

B/B+树: placeholder....

(2020.02.19)
红黑树
一种平衡二叉树,满足5个性质

  1. 每个节点是红色或者黑色;
  2. 根节点是黑色;
  3. 每个叶子节点(nill)是黑色(注意这里即便叶节点为空,也要标注在图中);
  4. 每个红色结点的叶子节点一定是黑色;
  5. 任意一个结点都叶子结点一定包含相同数量的黑色结点。

插入和删除结点后,如果平衡二叉树失衡,则需要通过变色和旋转调整树。

左旋转

以某点P为支点旋转,其右子节点V旋转后称为P的父节点,V的左子节点称为P的右子节点,V的右子节点不变,V的新左子节点是P,如下图[2]

image

右旋转

以某点P为支点旋转,其左子节点F旋转后称为P的父节点,F的右子节点成为P的左子节点,F的左子节点不变,F的新右子节点是P,如下图[2]

image

旋转之后根据5个性质对变化后的节点变色。

reference:

1 裘宗燕,数据结构与算法 Python语言描述.

2 https://www.jianshu.com/p/e136ec79235c

3 http://www.360doc.com/content/18/0904/19/25944647_783893127.shtml

你可能感兴趣的:(树(数据结构), since 2020.02.07)