小肥柴慢慢手写数据结构(C篇)(5-1 二叉搜索树)

目录

5-1 为何需要树?

在前面的学习中,我们讨论的数据结构一般操作的时间复杂度在 O ( n ) O(n) O(n)级别;于是人们开始思考:是否能够设计一种不同于已有的数据结构(线性表)去实现更高效的操作呢? 答案自然是树了。

个人始终认为树的概念学习,新手们不必一上来就理解大量的术语,我们先回忆一下大家熟悉的二分查找:在有序数组中,二分法能够快速查找数据。

<1> 此例中经过二分法三次查找就能找到目标;
<2> 对比顺序查找,遍历到index =1时就能找到目标,好像快了许多,但这只是最好情况,若目标数据在后半部分呢?
<3> 可以明显看到查找次数的期望如果使用二分法可以有原来的 O ( n ) O(n) O(n)级别下降到 l o g 2 M log_2{M} log2M级别(M此处不做精确讨论,仅估算即可, M ⩽ N M\leqslant{N} MN)。

在这里插入图片描述

由上面的讨论可知:如果能够设计一种数据结构,天然具有左右两个部分的有序数据存储,那岂不美哉?查询时仅需要和中间的数据做比较,小了就去左边继续查,大了就去右边继续查,正好命中就返回当前位置,哈哈哈哈。

看完上面的粗糙推演之后,我相信新手们都会对为何会出现树这种数据结构有简单的回应。当然,其实生活中很多数据天然就是树状结构的,譬如(以下示例图片来自百度图库,侵删):

(1)家谱

在这里插入图片描述

(2)思维导图

在这里插入图片描述

(3)文件目录结构(参考黑皮书P67 4.1.2)

在这里插入图片描述

(4)计算表达式:叶节点存储数据,其他节点存储运算符号(参考黑皮书P67 4.1.2,注意此处可借用stack)
1 + 2 * ( 3 - 4 ) - 5 / 6

在这里插入图片描述

(5)决策树:AI和数据分析领域的常用套路

在这里插入图片描述

(7)自然语言处理(NLP)和编译器语法分析

在这里插入图片描述

在这里插入图片描述
可见,树的表达(形态)和应用不仅仅只有查询那么简单,却是实实在在能够接触到的。

5-2 预备知识(简要)

【注】树的学习阶段总结时再整理给出完整的复习内容

(1)树的定义:其实树无论什么形态,必定有一个根节点,在根节点一下会有很多分支,这些分支也都是树状结构的;个人认为理解了下面这个图就基本掌握了树的核心定义了。

在这里插入图片描述

<1> 《黑皮书》:树是一些节点的集合,这些集合可以是空集。若非空,则一颗树由称作根(root)的节点r以及0个或者多个非空的(子)树 T 1 T_1 T1 T 2 T_2 T2…, T k T_k Tk组成,这些子树中的每一根都被来自r的一条有向边(edge)所连接。
==>
1)此处的定义不正好是我们给出的抽象图吗?且需要意识到:命题描述中的“子树”可以退化为没有子树的结构,即“叶子”。
2)树的这个结构非常类似数学中讨论的“分形图”,这点暗示树这种数据结构天然适合递归。(其实这是废话,作者已经注明了这样的表达就是递归形式的表达)
3)需要注意到,这些 T 1 T_1 T1 T 2 T_2 T2…, T k T_k Tk的规模很可能是不一样的!

<2> 《计算机程序设计艺术》:树定义为一个或者多个节点的有限集合T,使得
1)有一个特别指定的节点,叫做树的根 r o o t ( T ) root(T) root(T),以及
2)剩余的节点(排除根)被划分为 m ⩾ 0 m\geqslant{0} m0个不相交的集合 T 1 T_1 T1…, T m T_m Tm,而且这些集合的每个也都是树。树 T 1 T_1 T1…, T m T_m Tm称为这个根的子树。
==>
这个定义比<1>更加规范,且指出了一个事实:各子树之间是相互独立,没有交集的!这点可以说就是树与图的区别;等大家学完图之后,再回头看树,会很容易得到两个推论:
1)树是特殊的图;
2)链表是特殊的树。

接下来的讨论以下图作为基准:
在这里插入图片描述

(2)节点(node)、 根(root)、叶节点(leaf)
<1>图中每一个带字母的圆圈就是一个节点,当然用别的图形代替也行,譬如很多场合用矩形;圆圈中装在的元素,既可以是数据,也可以是所谓的关键词。
<2>图中只有“A”一个根节点。(标记为红色)
<3>没有子节点的节点,被称为“叶子”,如图中的B、C、H、I、P、Q、K、L、M、N。(标记为浅绿色)
<4>以朝向root为正向,普通节点上一个节点被称为父节点,下一个节点为子节点;例如:A是D的父节点,H是D的子节点。

在这里插入图片描述

(3)路径(path)与 路径长(length)
<1> 路径是从某个节点到另一个节点需要经过的线路,这里要求从父节点出发到子节点。
<2> 从树的结构易知,这条路径是唯一的。
<3> 数学语言表达为:从节点 n 1 n_1 n1到节点 n k n_k nk的路径为节点 n 1 n_1 n1 n 2 n_2 n2 n 3 n_3 n3,…, n k n_k nk的一个序列 { n 1 , n 2 , . . . , n k } \{n_1,n_2,...,n_k\} {n1,n2,...,nk} ,且对 1 ⩽ i < k 1\leqslant{i}\lt{k} 1i<k有节点 n i n_i ni是节点 n i + 1 n_{i+1} ni+1的父节点。
<4> 假设每条边记为1(即权重为1),那么从节点 n 1 n_1 n1到节点 n k n_k nk的路径长就为序列 { n 1 , n 2 , . . . , n k } \{n_1,n_2,...,n_k\} {n1,n2,...,nk}中边的数量,此处为 k − 1 k-1 k1。例如图中 E → Q E\rightarrow{Q} EQ的路径已经被标红(此处我故意选择不从root开始,就是为了帮助大家清楚理解这个概念),路径序列为 { E , J , Q } \{E,J,Q\} {E,J,Q},路径长度为 3 − 1 = 2 3-1=2 31=2;后续非必要场合,我们不需要使用那么抽象的表达,直接给出路径长度即可。
【注】在某些树形数据结构中,边的权重是有差异的,也不都是1。

在这里插入图片描述

(4)深度(depth)与 高(height)
<1>深度是从root开始到目标节点的唯一路径的长度。例如图中标记为绿色的路径 A → J A\rightarrow{J} AJ。对节点J来讲,其深度为2;而根节点的深度为0。
<2>高指的是一棵树的从根节点到最深叶子的路径长度。例如:图中以A为根的树其高为3,以F为根的子树其高为1,而叶子节点高为0。
<3>由此可知:深度和高不是一回事。例如:图中E的深度为1而高位2,F的深度为1高也是1。
<4>一棵树的的深度等于它最深的树叶深度,且这个深度等于这棵树的高。
<5>一棵树的高度等于它最高子树的高度+1。

在这里插入图片描述

(5)度(degree )
<1>某个节点拥有子节点的个数,例如:F节点的度为3,J节点的度为2,叶子节点H的度为0。
<2>这个概念其实在图中更多提及和使用。

在这里插入图片描述

(6)祖先(ancestor) 与 后裔(descendant)
如果存在一条从上层节点 n 1 n_1 n1到下层节点 n 2 n_2 n2的一条路径,那么 n 1 n_1 n1 n 2 n_2 n2的祖先, n 2 n_2 n2 n 1 n_1 n1的后裔。

当然,还有类似兄弟节点、堂兄弟节点、左孩子/右孩子…等等命名方式,看一眼就会,没有必要强制统一,只要满足:(1)代码简洁好用,效率高;(2)理论推导正确;(3)能灵活应用,就算是真的掌握了知识/技能了,不必纠结。

5-3 二叉搜索树

【注】本章节仅讨论树的递归实现,个人认为树的非递归实现应该放在“堆”部分讨论更加合适。

首先定义ADT(BSTree.h):树的节点结构简单明了,保留左右两支的地址(指针)+存储定制结构的数据即可(例如黑皮书中称节点存储的元素为关键字:key)。

截然是二叉搜索树,那么必然有以下功能:
<1> 寻找目标数据(核心技能)
<2> 寻找最大元素/最小元素
<3> 插入(包含空树插入/生成)元素 和 删除元素
<4> 其他根据具体需求拓展的功能

#ifndef _Tree_H
#define _Tree_H

typedef int ElementType;

struct Node {
	ElementType Element;
    struct Node *Left;
    struct Node *Right;
};

typedef struct Node *TreeNode;

TreeNode MakeEmpty(TreeNode T);
TreeNode Find(ElementType X, TreeNode T);
TreeNode FindMin(TreeNode T);
TreeNode FindMax(TreeNode T);
TreeNode Insert(ElementType X, TreeNode T);
TreeNode Delete(ElementType X, TreeNode T);
ElementType Retrieve(TreeNode P);

#endif  /* _Tree_H */

具体实现代码如下:

(1)重置树(清空),释放顺序:先左右,在中间

TreeNode MakeEmpty(TreeNode T){
	if(T != NULL){
		MakeEmpty(T->Left);
		MakeEmpty(T->Right);
		free(T);
	}
	return NULL;
}

(2)寻找目标数据,根据之前约定的BSTree的性质,左枝>右枝,递归查询

TreeNode Find(ElementType X, TreeNode T){
	if(T == NULL)
		return NULL;
		
	if(X < T->Element)
		return Find(X, T->Left);
	else if(X > T->Element)
		return Find(X, T->Right);
	else
		return T;
}

(3)寻找树中的最小元素节点,递归目标是左枝,但:
<1> 若当前已经是空树,直接返回空即可;
<2> 若当前是没有左枝仅有右枝的最左侧节点,或就是叶子节点,那么当前节点即为所求。

TreeNode FindMin(TreeNode T){
	if(T == NULL)
		return NULL;
	
	if(T->Left == NULL)
		return T;
	else
		return FindMin(T->Left);
}

(4)寻找树中的最大元素节点:为了丰富表现手法,此处用非递归方式,直接遍历所有右枝,直到叶子节点,或者仅有左枝的最右侧节点

TreeNode FindMax(TreeNode T){
	if(T != NULL){
		while(T->Right != NULL)
			T = T->Right;
	}
	return T;
}

(5)插入元素,注意处理空树的情形

TreeNode Insert(ElementType X, TreeNode T){
	if(T == NULL){
		T = malloc(sizeof(struct Node));
		if(T == NULL){
			printf("Create Tree ERROR\n");
			exit(0);
		}
		
		T->Element = X;
		T->Left = NULL;
		T->Right = NULL;
	} else {
		if(X < T->Element)
			T->Left = Insert(X, T->Left);
		else if(X > T->Element)
			T->Right = Insert(X, T->Right);
	}
	return T;
}

(6)删除元素,注意细节:
<1> 若目标元素比当前节点元素小,必然要去左枝尝试删除目标节点,返回的结果就是新的左枝;
<2> 同理,若目标元素比当前节点元素大,必然要去右枝尝试删除目标节点,返回的结果就是新的右枝;
以上都是很容易理解的正常递归情况,关键是递归出口。
<3> 若目标元素就是当前节点(找到啦!),需要分两种情况处理
i. 当前节点左右两支均存在,则需要从现有树的所有元素中挑选一个合适的元素的当前根节点!这个根节点仅需要找到合适的元素,赋值给当前节点,再删除这个合适元素原来所在节点即可(此处还要考虑套娃——递归的情况!),不必真的删除当前节点。那么如果如何挑选才是最合适的呢?
==> 自然是要比当前所有左枝元素大(必然它在右枝),且比所有右枝元素小(FindMin(右枝))的节点咯!
ii. 反之,如果当前节点是不健全的,那么直接删除当前节点即可(还记得 curr = curr->next吗?)。

TreeNode Delete(ElementType X, TreeNode T){
	if(T == NULL){
		printf("Tree is null, delete fail\n");
		return NULL;
	}
	
	if(X < T->Element)
		T->Left = Delete(X, T->Left);
	else if(X > T->Element)
		T->Right = Delete(X, T->Right);
	else{
		TreeNode TmpCell;
		if(T->Left && T->Right){
			TmpCell = FindMin(T->Right);
			T->Element = TmpCell->Element;
			T->Right = Delete(T->Element, T->Right);
		} else {
			TmpCell = T;
			if(T->Left == NULL)
				T = T->Right;
			else if(T->Right == NULL)
				T = T->Left;
			
			free(TmpCell);
		}
	}
	return T;
}

(7)返回当前节点元素

ElementType Retrieve(TreeNode T){
	return T->Element;
}

(8)测试代码和结果

#include <stdio.h>
#include <stdlib.h>
#include "BSTree.h"
int main(int argc, char *argv[]) {
	TreeNode T, P;
    int i, j = 0;

    T = MakeEmpty( NULL );
    for(i = 0; i < 50; i++, j = ( j + 7 ) % 50)
        T = Insert( j, T );
    for(i = 0; i < 50; i++ )
        if((P = Find(i, T)) == NULL || Retrieve(P) != i )
            printf("Error at %d\n", i);

    for(i = 0; i < 50; i += 2)
        T = Delete(i, T);

    for(i = 1; i < 50; i += 2)
        if((P = Find(i, T)) == NULL || Retrieve(P) != i)
            printf("Error at %d\n", i);
    for(i = 0; i < 50; i += 2)
        if((P = Find(i, T)) != NULL)
            printf( "Error at %d\n", i );

    printf("Min is %d, Max is %d\n", Retrieve(FindMin(T)),Retrieve(FindMax(T)));

    return 0;
}

在这里插入图片描述
【注】有关树的遍历,是一个非常值得结合实际应用深刻来探讨的话题,我将在后续帖子中列出专题讨论;且使用link的树和使用array的堆的遍历讨论也应该分开;等后期某些应用场景下使用时,才可以不再做那么细致的区分,都叫树也没错。

5-4 二叉搜索树的相关数学理论推导(不断完善中,看不懂先跳过,慢慢学)

【写在前面的话(tu cao)】关于树的专业术语和数学知识有很多,尤其是相关性质的数学讨论不少需要很长的前置准备;有时间的话可以自己顺着提供的参考资料慢慢走一遍,如果比较着急,那就可以放弃相关复杂理论的学习,简单记忆结论即可。 ==> 目前网上很多博客都以黑皮书作为主线开讲数据结构,但对其中一些理论性的描述和推导(特别是复杂度相关的推导)都不太到位,作为授课内容话,我认为至少应把文献出处整理给读者。

命题1(《黑皮书》P84 4.3.6):对随机创建的二叉查找树,若所有子树出现机会相等,则该树的平均深度为 O ( l o g N ) O(logN) O(logN)

【说明】
(1)此处的描述其实和《算法导论》第12章 二叉搜索树 习题1问题几乎一样,除了符号名称有少许差异;我怀疑是翻译的问题或者作者默认大家会去查阅参考资料列表,所以没有表述的那么细致。
(2)这个命题其实包含几个部分,首先我们会明确给出内部路径长度和平均路径长度的定义和推导过程,然后会提前介绍《黑皮书》7.7.5 快速排序的分析的推导方法,用于得到最终结论。

【命题1证明】第1部分:内部路径长(internal path lenght)和平均路径长度
<1> 对二叉搜索树 T T T,某个节点 x x x的深度记为: d ( x , T ) d(x,T) d(x,T)
<2> 设T有N个节点,且内部所有节点的深度之和定义为内部路径长度 D ( N ) D(N) D(N),即: D ( N ) = ∑ x d ( x , T ) D(N)=\sum_{x}{d(x,T)} D(N)=xd(x,T),且 D ( 1 ) = 0 D(1)=0 D(1)=0
<3> 要求解的“平均深度问题”可表述为: d A v g = 1 N ∑ x d ( x , T ) = 1 N D ( N ) d_{Avg}= \frac{1}{N}{\sum_x{d(x,T)}}=\frac{1}{N}{D(N)} dAvg=N1xd(x,T)=N1D(N),即问题转化为尝试求出 D ( N ) D(N) D(N)的表达式。
<4> 令 T T T的左右子树分别为 T L T_L TL(有 i i i个节点)和 T R T_R TR(有 N − i + 1 N-i+1 Ni+1个节点),对 T L T_L TL中任意节点x,有 d ( x , T ) = d ( x , T L ) + 1 d(x,T)=d(x,T_L)+1 d(x,T)=d(x,TL)+1,于是
D ( N ) = ∑ x ∈ T d ( x , T ) = ∑ x ∈ T L d ( x , T ) + ∑ x ∈ T R d ( x , T ) = ∑ x ∈ T L ( d ( x , T L ) + 1 ) + ∑ x ∈ T R ( d ( x , T R ) + 1 ) = ∑ x ∈ T L d ( x , T L ) + ∑ x ∈ T R d ( x , T R ) + N − 1 = D ( i ) + D ( N − i + 1 ) + N − 1 \begin{align*} D(N) &= \sum_{x\in{T}}{d(x,T)} \\ & =\sum_{x\in{T_L}}{d(x,T)} + \sum_{x\in{T_R}}{d(x,T)} \\ & =\sum_{x\in{T_L}}{(d(x,T_L)+1)} + \sum_{x\in{T_R}}{(d(x,T_R)+1)} \\ & =\sum_{x\in{T_L}}{d(x,T_L)} + \sum_{x\in{T_R}}{d(x,T_R)} + N-1 \\ & =D(i) + D(N-i+1) + N-1\end{align*} D(N)=xTd(x,T)=xTLd(x,T)+xTRd(x,T)=xTL(d(x,TL)+1)+xTR(d(x,TR)+1)=xTLd(x,TL)+xTRd(x,TR)+N1=D(i)+D(Ni+1)+N1
<5> 由题意,所有子树随机等概率出现,那么: T L T_L TL T R T_R TR的数目也是随机等概的,用 ( i , N − i + 1 ) (i,N-i+1) (i,Ni+1)表示左右子树的节点分配情况 ( i = 1 , 2 , . . . . , n − 1 ) (i=1,2,....,n-1) (i=1,2,....,n1),则这样的组合有: ( 0 , N − 1 ) , ( 1 , N − 2 ) , ( 2 , N − 3 ) , . . . , ( N − 1 ) (0,N-1),(1,N-2),(2,N-3),...,(N-1) (0,N1),(1,N2),(2,N3),...,(N1),共N种情况; T L T_L TL T R T_R TR都需要考虑使用其中一种,易有出现概率 p j = 1 N , j = 0 , 1 , 2 , . . . , N − 1 p_j=\frac{1}{N}, j=0,1,2,...,N-1 pj=N1,j=0,1,2,...,N1,于是对 T L T_L TL数学期望(平均值)
D ( i ) = ∑ j = 0 N − 1 p j D ( j ) = 1 N ∑ j = 0 N − 1 D ( j ) \begin{align*} D(i) &= \sum_{j=0}^{N-1}{p_jD(j)} \\ & =\frac{1}{N}\sum_{j=0}^{N-1}{D(j)} \end{align*} D(i)=j=0N1pjD(j)=N1j=0N1D(j)
同理,对 T R T_R TR,可以调整角标。令 k = N − j + 1 k=N-j+1 k=Nj+1,则 j = 0 j=0 j=0 k = N − 1 k=N-1 k=N1 j = N − 1 j=N-1 j=N1 k = 0 k=0 k=0
同理, p j = p k = 1 N p_j=p_k=\frac{1}{N} pj=pk=N1有(其实这步熟悉级数运算的都不用写出来,但写出来也是方便基础不好的朋友理解嘛)
D ( N − i + 1 ) = ∑ j = 0 N − j + 1 p j D ( j ) = ∑ k = N − 1 0 p k D ( k ) = 1 N ∑ k = 0 N − 1 D ( k ) = 1 N ∑ j = 0 N − 1 D ( j ) \begin{align*} D(N-i+1) &= \sum_{j=0}^{N-j+1}{p_jD(j)} \\ &= \sum_{k=N-1}^{0}{p_kD(k)} \\ &= \frac{1}{N}\sum_{k=0}^{N-1}{D(k)} \\ & =\frac{1}{N}\sum_{j=0}^{N-1}{D(j)} \end{align*} D(Ni+1)=j=0Nj+1pjD(j)=k=N10pkD(k)=N1k=0N1D(k)=N1j=0N1D(j)

D ( i ) = D ( N − i + 1 ) = 1 N ∑ j = 0 N − 1 D ( j ) D(i)=D(N-i+1) =\frac{1}{N}\sum_{j=0}^{N-1}{D(j)} D(i)=D(Ni+1)=N1j=0N1D(j)
于是
D ( N ) = D ( i ) + D ( N − i + 1 ) + N − 1 = 2 N ∑ j = 0 N − 1 D ( j ) + N − 1 \begin{align*} D(N) &= D(i) + D(N-i+1) + N-1 \\ & =\frac{2}{N}\sum_{j=0}^{N-1}{D(j)} + N-1\end{align*} D(N)=D(i)+D(Ni+1)+N1=N2j=0N1D(j)+N1
至此,黑皮书中P85的表达式得证,接下来继续分析。
【命题1证明】第2部分:迭代递推平均路径长度
<6>
D ( N ) = 2 N ∑ j = 0 N − 1 D ( j ) + N − 1 N D ( N ) = 2 ∑ j = 0 N − 1 D ( j ) + N ( N − 1 ) \begin{align*} D(N) &= \frac{2}{N}\sum_{j=0}^{N-1}{D(j)} + N-1 \\ ND(N) & =2\sum_{j=0}^{N-1}{D(j)} +N(N-1)\end{align*} D(N)ND(N)=N2j=0N1D(j)+N1=2j=0N1D(j)+N(N1)
换N为N-1
( N − 1 ) D ( N − 1 ) = 2 ∑ j = 0 N − 2 D ( j ) + ( N − 1 ) ( N − 2 ) \begin{align*} (N-1)D(N-1) & =2\sum_{j=0}^{N-2}{D(j)} +(N-1)(N-2)\end{align*} (N1)D(N1)=2j=0N2D(j)+(N1)(N2)
两式相减
N D ( N ) − ( N − 1 ) D ( N − 1 ) = 2 D ( N − 1 ) + 2 ( N − 1 ) N D ( N ) = ( N + 1 ) D ( N − 1 ) + 2 ( N − 1 ) \begin{align*} ND(N)-(N-1)D(N-1) & =2D(N-1) +2(N-1)\\ ND(N)& =(N+1)D(N-1) +2(N-1)\end{align*} ND(N)(N1)D(N1)ND(N)=2D(N1)+2(N1)=(N+1)D(N1)+2(N1)
近似分析,舍弃常数项(不影响 O ( ? ) O(?) O(?)分析)
N D ( N ) = ( N + 1 ) D ( N − 1 ) + 2 N D ( N ) N + 1 = D ( N − 1 ) N + 2 N + 1 \begin{align*} ND(N)& =(N+1)D(N-1) +2N \\ \frac{D(N)}{N+1}& =\frac{D(N-1)}{N} +\frac{2}{N+1}\end{align*} ND(N)N+1D(N)=(N+1)D(N1)+2N=ND(N1)+N+12
通过以上迭代式有
D ( N ) N + 1 = D ( N − 1 ) N + 2 N + 1 \begin{align*} \frac{D(N)}{N+1}& =\frac{D(N-1)}{N} +\frac{2}{N+1}\end{align*} N+1D(N)=ND(N1)+N+12
D ( N − 1 ) N = D ( N − 2 ) N − 1 + 2 N \begin{align*} \frac{D(N-1)}{N}& =\frac{D(N-2)}{N-1} +\frac{2}{N}\end{align*} ND(N1)=N1D(N2)+N2
D ( N − 2 ) N − 1 = D ( N − 3 ) N − 2 + 2 N − 1 \begin{align*} \frac{D(N-2)}{N-1}& =\frac{D(N-3)}{N-2} +\frac{2}{N-1}\end{align*} N1D(N2)=N2D(N3)+N12
. . . . ... ...
D ( 2 ) 3 = D ( 1 ) 2 + 2 3 \begin{align*} \frac{D(2)}{3}& =\frac{D(1)}{2} +\frac{2}{3}\end{align*} 3D(2)=2D(1)+32
累加以上式子,错位相消
D ( N ) N + 1 = D ( 1 ) 2 + 2 ∑ i = 3 N + 1 1 i = 2 ∑ i = 3 N + 1 1 i \begin{align*} \frac{D(N)}{N+1}& =\frac{D(1)}{2} +2\sum_{i=3}^{N+1}{\frac{1}{i}}=2\sum_{i=3}^{N+1}{\frac{1}{i}}\end{align*} N+1D(N)=2D(1)+2i=3N+1i1=2i=3N+1i1
最后一项为转换为调和级数,近似有 D ( N ) N + 1 = O ( l o g N ) \frac{D(N)}{N+1}=O(logN) N+1D(N)=O(logN)
从而所求
d A v g = 1 N D ( N ) d_{Avg}=\frac{1}{N}{D(N)} dAvg=N1D(N)
近似计算有
d A v g = O ( l o g N ) d_{Avg}=O(logN) dAvg=O(logN)
【证毕】

【注】以上推导过程中如果不知晓调和级数的话,请参考帖子[4][5],其实黑皮书在第1章和《计算机程序设计艺术》中也曾提及,在此不做展开。

命题2(《黑皮书》P70 4.2):二叉树的平均深度为 O ( N ) O(\sqrt{N}) O(N )

这个问题我们单开了一个帖子[6],完成后可以点击。

命题3(《黑皮书》P79 4.3.6):对二叉查找树,若交替插入和删除 Θ ( N 2 ) \Theta(N^2) Θ(N2)次,则树的期望深度是 Θ ( N 2 ) \Theta(N^2) Θ(N2)

【TODO】懵,还在证明

命题4(《黑皮书》P80 4.3.6):对二叉查找树,任意连续M次操作在最坏情况下的时间开销为 O ( M l o g N ) O(MlogN) O(MlogN)

【TODO】懵,还在证明

通过以上数学分析和实践感受我们可以发现:如果对删除和插入操作不加限制,那么一棵二叉树很可能会失去平衡或者退化为单链表,这是我们不希望看到的,因此前人又设计了新的数据结构,添加了“平衡”(balance)操作,其中典型的代表是下一节会介绍的AVL树。

参考资料

[1] 《数据结构与算分析—C语言描述(原书第2版)》(黑皮书,主线教材)
[2] 《计算机程序设计艺术》第1卷、第3卷
[3] 《算法导论》
[4] 欧拉常数——最神秘的数字,调和级数的产物,至今看不清它的面貌
[5] 极限.调和级数和谁最亲近
[6] 小肥柴慢慢手写数据结构(C篇)(5-1-1 更复杂的数学证明) 【TODO】
[7] linux BST树算法实现

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值