数据结构基础:P3.1-树(一)--->树与树的表示

本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表
数据结构基础:P2.2-线性结构—>堆栈
数据结构基础:P2.3-线性结构—>队列
数据结构基础:P2.4-应用实例—>多项式加法运算
数据结构基础:P2.5-应用实例—>多项式乘法与加法运算-C实现


一、引子(顺序查找)

1.1 树与查找的定义

在我们日常生活工作中,事物跟事物之间的都存在一种叫层次关系,这种层次关系就是我们所说的

①家庭关系。要想表示人类的家谱,就形成了我们所说的这样的一个叫家谱树
在这里插入图片描述
②社会组织关系。比方说一个国家,它分成若干个省、市。省、市下面又分成若干个地级市,然后再下面再有县级市等等。
③管理信息。最典型来讲的话,我们管理硬盘。在硬盘上面有很多很多的文件,它的组织方法是比方说C盘下面或者D盘下面分成若干个子目录,子目录里面再分若干个子目录。

分层次组织在管理上具有更高的效率。对数据管理里面经常涉及到三个典型的操作,叫做 插入删除查找。我们首先来看看查找操作。

查找(Searching)

查找:根据某个给定关键字K ,从集合R中找出关键字与K相同的记录
静态查找:集合中记录是固定的
没有插入和删除操作,只有查找
动态查找:集合中记录是动态变化的
除查找,还可能发生插入和删除


1.2 顺序查找

所谓顺序查找,我们可以这样来进行实现

我们把元素放在一个数组里面。在这个数组之外,我们用一个结构来指向这个数组。这个结构有两个分量,一个分量是指针指向数组的头,另外一个分量就是代表这个数组里面我放的元素总共有多少个。
在这里插入图片描述

对应代码如下:

typedef struct LNode *List
struct LNode {
	ElementType Element[MAXSIZE];
	int Length;
};

哨兵

①从这里我们可以看到,我们现在的存储方法不是从0开始存储,是从1开始存储。这就是我们的程序设计里面有时候会用到的一种技巧,叫做哨兵。那么哨兵是怎么回事情呢?就是我们在数组里面循环的时候,经常从一个元素开始不断地循环。那么在循环过程当中,我们什么时候会退出循环有两个条件。一个条件就判别这个元素相等不相等,如果不相等那么我循环就不断地做。另外一个条件就是判断有没有到我的边界,所以每一轮循环都要对边界进行判断。
②哨兵的作用是我们可以在数组的最后或者边界上面设一个值,我们不需要每次去判断它的下标是不是达到我的边界,而只要按照循环的一般的原则:当你碰到我们放置那个值的时候循环就该退出来了。这样的话我们在写循环的时候就可以少写一个判断的分支。

顺序查找的代码如下:

int SequentialSearch (StaticTable *Tbl, ElementType K)
{ /*在表Tbl[1]~Tbl[n]中查找关键字为K的数据元素*/
	int i;
	Tbl->Element[0] = K; /*建立哨兵*/
	for(i = Tbl->Length; Tbl->Element[i]!= K; i--);
	return i; /*查找成功返回所在单元下标;不成功返回0*/
}

分析

这种方法我们可以知道它的时间复杂性呢是 O ( n ) O(n) O(n),运气好第一个就是,运气不好最后一个是,平均是 n / 2 n/2 n/2。这样的一种查找方法显然效率不是很高,比方说1000个数,那你平均要比较500次,100万个要比较50万次。有没有更好的查找方法?有,这个方法就叫做二分查找。


二、引子(二分查找)

2.1 二分查找的思想

二分查找它是种什么思想,我们举一个例子。

在以前使用电话需要布设电话线,一般的话是在路边设电线杆,一般电线杆是每50米一根电线杆。打个比方说杭州到上海某天电话打过去不通了,那么这个时候怎么找故障出现在哪一段。
顺序查找的操作:先爬上杭州第一根电线杆往上海打电话,看看有没有打通。打不通的话,这个时候你到第二个电线杆再往上海打电话。如果这个时候打通了,我们就断定这跟线是断在了第一根跟第二根之间。如果还没打通。你就爬到了第三根电线杆上去。再往上海打电话 …这工作量太巨大了。
二分查找的操作:开车开到杭州跟上海中间的位置,爬上电线杆分别往杭州和上海打个电话。如果往杭州的电话通了,往上海打电话不通,那就说明我们的故障是发生在现在这个位置到上海之间。这样我们把车开到现在位置与上海中间的地方去,再次向两边进行打电话检查…
效率对比:如果有100万个电线杆,按原来顺序查找操作的话,我们平均要爬50万次。按二分查找操作的话,要 l o g 2 1000000 ≈ 20 {\rm{lo}}{{\rm{g}}_2}1000000 \approx 20 log2100000020次。所以20次的工作量跟50万次的工作量差距,这就是二分查找的效率。

什么时候可以使用二分查找

①假设n个数据元素的关键字满足有序(比如:小到大)
k 1 < k 2 < . . . < k n {{\rm{k}}_{\rm{1}}}{\rm{ < }}{{\rm{k}}_{\rm{2}}}{\rm{ < }}...{\rm{ < }}{{\rm{k}}_{\rm{n}}} k1<k2<...<kn
②必须是连续存放(数组),那么可以进行二分查找。


2.2 示例

假设有13个数据元素,按关键字由小到大顺序存放在这里插入图片描述
使用二分查找关健字为444的数据元素查找过程如下:

1、left = 1, right = 13; mid = (1+13)/2 = 7: 100 < 444;
2、left = mid+1=8, right = 13; mid = (8+13)/2 = 10: 321 < 444;
3、left = mid+1=11, right = 13; mid = (11+13)/2 = 12: 查找结束;

使用二分查找关健字为43的数据元素过程如下:

1、left = 1, right = 13; mid = (1+13)/2 = 7: 100 > 43;
2、left = 1, right = mid-1= 6; mid = (1+6)/2 = 3: 39 < 43;
3、left = mid+1=4, right = 6; mid = (4+6)/2 = 5: 51 > 43;
4、left = 4, right = mid-1= 4; mid = (4+4)/2 = 4: 45 > 43;
5、left = 4, right = mid-1= 3;    left > right ? 查找失败,结束;

三、引子(二分查找实现)

所以根据以上的这个思路,我们可以很容易地设计出我们的二分查找算法,我们设计成一个函数 BinarySearch。二分查找算法具有对数的时间复杂度 O ( l o g N ) O(logN) O(logN).

int BinarySearch ( StaticTable * Tbl, ElementType K)
{ /*在表Tbl中查找关键字为K的数据元素*/
	int left, right, mid, NoFound=-1;
	left = 1; /*初始左边界*/
	right = Tbl->Length; /*初始右边界*/
	while ( left <= right )
	{
		mid = (left+right)/2; /*计算中间元素坐标*/
		if( K < Tbl->Element[mid]) right = mid-1; /*调整右边界*/
		else if( K > Tbl->Element[mid]) left = mid+1; /*调整左边界*/
		else return mid; /*查找成功,返回数据元素的下标*/
}
	return NotFound; /*查找不成功,返回-1*/
}

二分查找判定树

为什么二分查找会产生那么好的效率呢?当然从这里面我们可以看得到,它的基本前提是我对我要查找的这个集合事先进行了有序化的处理,就是从小到大把它排好了。通过排序使得数据有序化之后,我们查找的过程就事先知道它的顺序。而这样的查找顺序,我们可以构造出这样的一个东西来表述。
在这里插入图片描述
我们的查找过程是这样的:我第一次去比较的就是我要查找的值跟6之间的大小,以此来决定我是往前半段还是往后半段继续查找。如果往前半段,下一次比较一定是在3号这个位置。如果是后半段,我一定是在9号这个位置。如果当我比较到9号这个位置的时候,我又把我要找的这个值跟9号位置的值再做比较。如果比它小,下一次比较我一定就是在7号位置。如果比它大,我比较的是10号位置。所以这就形成了不管你这11个元素值是什么样的,具体是什么数值,我整个的查找过程就是按这个顺序进行的。这个比较的顺序呢,我们看到形成了一个层次结构,它就是一棵树,我们称之为判定树

判定树的特点:

①判定树上每个结点需要的查找次数刚好为该结点所在的层数。
----比如我要查找4这个元素,需要比较3次。
②查找成功时查找次数不会超过判定树的深度。
n n n个结点的判定树的深度为 ⌊ l o g 2 n ⌋ + 1 \left\lfloor {{\rm{lo}}{{\rm{g}}_{\rm{2}}}{\rm{n}}} \right\rfloor {\rm{ + 1}} log2n+1
④ASL(平均查找次数) = (4*4+4*3+2*2+1)/11 = 3


四、树的定义和术语

树的定义:

树(Tree) n ( n ≥ 0 ) {\rm{n(n}} \ge {\rm{0)}} n(n0)个结点构成的有限集合。
----当 n = 0 {\rm{n = 0}} n=0时,称为空树
----对于任一棵非空树 ( n > 0 ) ({\rm{n > 0}}) (n>0),它具备以下性质:
------树中有一个称为"(Root)“的特殊结点,用 r {\rm{r}} r 表示;
------其余结点可分为 m ( m > 0 ) {\rm{m(m > 0)}} m(m>0) 个互不相交的有限集 T 1 , T 2 , . . . , T m {{\rm{T}}_{\rm{1}}}{\rm{,}}{{\rm{T}}_{\rm{2}}}{\rm{,}}...{\rm{,}}{{\rm{T}}_{\rm{m}}} T1,T2,...,Tm ,其中每个集合本身又是一棵树,称为原来树的"子树(SubTree)”。
在这里插入图片描述

树与非树

以下三种情况不是树:
在这里插入图片描述
我们可以得出结论
子树是不相交的;
除了根结点外,每个结点有且仅有一个父结点;
一棵 N \rm{N} N个结点的树有 N − 1 \rm{N-1} N1条边。

树的一些基本术语

现在我们有以下一棵树。
在这里插入图片描述
1、结点的度(Degree):结点的子树个数
----A的度是3,B的度是2,G的度是1,F的度是0。
2、树的度:树的所有结点中最大的度数
3、叶结点(Leaf):度为0的结点
4、父结点(Parent):有子树的结点是其子树的根结点的父结点
5、子结点(Child):若A结点是B结点的父结点,则称B结点是A结点的子结点;子结点也称孩子结点。
6、兄弟结点(Sibling):具有同一父结点的各结点彼此是兄弟结点。
7、路径和路径长度:从结点 n 1 \rm{n_1} n1 n k \rm{n_k} nk 的路径为一个结点序列 n 1 , n 2 , . . . , n k {{\rm{n}}_{\rm{1}}}{\rm{,}}{{\rm{n}}_{\rm{2}}}{\rm{,}}...{\rm{,}}{{\rm{n}}_{\rm{k}}} n1,n2,...,nk, n i {{\rm{n}}_{\rm{i}}} ni n i + 1 {{\rm{n}}_{\rm{i+1}}} ni+1 的父结点。路径所包含边的个数为路径的长度。
8、祖先结点(Ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点。
9、子孙结点(Descendant):某一结点的子树中的所有结点是这个结点的子孙。
10、结点的层次(Level):规定根结点在1层,其它任一结点的层数是其父结点的层数加1。
11、树的深度(Depth):树中所有结点中的最大层次是这棵树的深度


五、树的表示

现在我有这样一棵树,如何来表示它呢?
在这里插入图片描述
思考

数组实现
所谓用数组来实现就是把这些结点按顺序存储在数组里面 。看起来这样难度还是比较大的,因为如果我们只看到了一个结点的一个顺序,我们很难判别一个结点它的父亲是谁,儿子是谁。因为我们树的结构它存在着很多种形式,任何一个结点都可能有一个儿子或者是多个儿子,很难判别。
链表实现
如果用链表来实现那就是每个结点用一个结构来表示。就像我们现在看到的A这个结点有3个儿子,所以我们可以用个结构包含了3个指针域,分别指向BCD。B这个结点有2个儿子,所以我们可以构造两个指针分别指向E跟F。 C这个结点只有一个儿子,所以我们用一个指针指向G。看起来我们好像可以用结构加链表的方式来表示树,但是我们再仔细看看就可以发现:这样的一种树的结构每个结点的结构的样子是不一样的。有的结点有3个指针,有的结点有1个指针,有的有2个指针,有的没有指针。所以的话这样的一种树的结构它的这种结构形式不一样会给我们后面的程序实现带来困难,因为我们访问到一个结点的时候,我们事先是没法知道它到底有多少个儿子的,所以给程序的设计会带来困难。
在这里插入图片描述


儿子-兄弟表示法:

它采用的一种方法是:
这个树上的每个结点的结构是统一的,就是两个指针域一个叫FirstChild,一个叫NextSibling。这两个指针域,第一个指针指向的是它的第一个儿子,第二个指针指向的是它的下一个兄弟。所有的结点都以这种方式来指向儿子跟兄弟,这样就可以把整棵树的结点把它串起来了。
在这里插入图片描述
针对我们前面看到这样的一棵树,对应的实现方法就是这样:
在这里插入图片描述
这样的一个链表实现方法把它旋转45度,就变成这样。
在这里插入图片描述
这个时候我们看到的是一棵树,这个树特点是每一个节点都有两个指针,一个指向左边,一个指向右边。每个结点最多是两个儿子。这种树我们叫做二叉树。二叉树就是度为2的这样的一种树,就是每个结点指针最多是两个。所以从这里面我们可以看到:一般的树我们都可以用儿子-兄弟这样的一种表示方法,把它用二叉树这种形式来实现。如果我们搞清楚了二叉树是怎么表示,怎么实现的,实际上就解决了一般树的许多问题。所以二叉树是在树结构研究里面是最重要的,也是最主要的内容。


六、小测验

1、在分量1~11的数组中按从小到大顺序存放11个元素,如果用顺序查找和二分查找分别查找这11个元素,哪个位置的元素在这两种方法的查找中总次数最少?

A. 1
B. 2
C. 3
D. 6

答案:A

2、在分量1~11的数组中按从小到大顺序存放11个元素,如果进行二分查找,查找次数最少的元素位于什么位置?

A. 1
B. 5
C. 6
D. 11

答案:C

3、一棵度为 m 的树有n个节点。若每个节点直接用m个链指向相应的儿子,则表示这个树所需要的总空间是n*(m+1) (假定每个链以及表示节点的数据域都是一个单位空间)。当采用儿子/兄弟(First Child/Next Sibling)表示法时,所需的总空间是:

A. 3n
B. 2n
C. n*m
D. n*(m-1)

答案:A

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知初与修一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值