算法导论——lec 12 平摊分析与优先队列

在平摊分析中,执行一系列数据结构操作所需要的时间是通过对执行的所有操作求平均得出,反映在任何情况下(即最坏情况下),每个操作具有平均性能。掌握了平摊分析主要有三种方法,聚集分析、记账方法、势能方法。掌握了平摊分析的方法以后,我们就可以利用他来分析一些优先队列。

一、 平摊分析

【问题一】对作用于一个初始为空的栈上的n个PUSH、POP和MULTIPOP组成的序列进行分析。

【问题二】考虑一个由0开始向上计数的k位二进制计数器。作用于一个初始为零的计数器上的n个INCREMENT操作的时间分析。

1、 定义: 在平摊分析中,执行一系列数据结构操作所需要的时间是通过对执行的所有操作求平均得出,反映在任何情况下(即最坏情况下),每个操作具有平均性能。

2、 特点: 即使某些单一的操作具有较大的代价,平均之后每个操作的代价还可能是很小的;平摊分析不牵扯到概率,适用于任何输入。所以能得到更精确的界值并设计更精巧的数据结构。

3、 聚集分析: 用于确定一个n个操作序列的总代价的上界T(n)。每个操作的代价可以表示为T(n)/n。我们把平均代价当做每个操作的平摊代价,因此所有的操作具有相同的平摊代价。

a、 在平摊分析的聚集方法中,我们要证明对所有的n,n个操作构成的序列在最坏情况下总的时间为T(n);

b、 在最坏情况下,每个操作的平摊代价就为T(n)/n。

c、 这个平摊代价对每个操作都是成立的,即使当序列中存在几种类型的操作时也是一样的。

注: 在记账和势能分析中不同操作的平摊代价可能不同。

【问题一分析】一个对象在每次被压入栈后至多被弹出一次,所以在一个空栈上调用POP(包括在MULTIPOP内的调用)的次数至多等于PUSH操作的次数,即至多为n。因此,对包含n个PUSH、POP和MULTIPOP操作序列的总时间为O(n),每个操作的平摊代价为O(n)/n = O(1)。

【问题二分析】在每次调用INCREMENT时,A[0]都要发生翻转,下一个高位A[1]每隔一次翻转:当作用与初始为零的计数器时,n次操作会导致A[1]翻转?n/2?次。类似地,位A[2] 每四次翻转一次,或在n次INCREMENT 中翻转?n/4?次。一般地,对i = 0, 1, ..., k-1,在一个n 次INCREMENT操作的序列中,位A[i]要翻转?n/2^i?次,这个序列作用于初始为零的计数器上。对于i≥k,位A[i]始终不变。因此,在序列中发生的位翻转的总次数为

最坏情况下,作用于一个初始为零的计数器上的n次INCREMENT操作的时间为O(n)。每次操作的平均代价为O(n)/n=O(1)。

4、 记账方法: 要确定每个操作的平摊代价。当有一种以上的操作时,每种操作都可有一个不同的平摊代价。这种方法对操作序列中的某些操作先“多记账”,将多记的部分作为对数据结构中的特定对象上“预付的存款”存起来。在该序列中稍后将用到这些存款,以补偿哪些对它们记的“帐”少于实际代价的操作。

我们对一个操作收费的数量称为其平摊代价。

如果希望通过对平摊代价的分析来说明每次操作的最坏情况平均代价较小,则操作序列的总的平摊代价必须是实际代价的一个上界,与在聚集分析中一样,这种关系必须对所有的操作序列都成立。

a、 如果把第i个操作的实际代价用ci表示,第i个操作的平摊代价用c‘i表示,对n个操作的所有序列我们需要:

b、 总存款:(对任意n都成立)

【问题一分析】我们对栈的操作赋予以下的平摊代价

PUSH
2

POP 0

MULTIPOP
0

此处所有三个平摊代价都是O(1),假设用1元钱来表示代价的单位。开始时栈是空的。用餐厅中一堆迭放的盘子来类比栈数据结构。当把一个盘子压入栈时,我们用一元来支付压入动作的实际代价,还有1元存款,我们将其放在盘子顶上。因此在任何一个时间点,盘子上面都有一元存款。盘子上的存款是用来预付将盘子从栈中弹出所需代价的。当执行了一个POP操作,对该操作不收取任何费用,只要用盘中缩放的存款来支付代价即可。对MULTIPOP操作也无需收取任何费用。

对任意的包含n次PUSH、POP和MULTIPOP操作的序列,总的平摊代价就是其总的实际代价的一个上界。因为总的平摊代价为O(n),所以总的实际代价也是O(n)。

【问题二分析】我们规定把某一位置置1的操作收取2元的平摊费用。当某数位被置1后,我们用2元中的1元来支付置位操作的实际代价,而将另1元存在该位上作为余款。在任何时间,计数器中每个1上都有1元余款,这样在将某位复位成0是不用付任何费,只要取出1元余款即可。因为计数器中为1的位数始终是非负的,因此余款永远是非负的。对n次INCREMENT操作,总的平摊代价为O(n)。

5、 势能方法:与记账方法的相似之处在于要确定每个操作的平摊代价,而且可能先对操作多记账以补偿以后的不足记账。这种方法将存款作为数据结构整体的“势能”来维护,而不是与数据结构中的单个对象联系起来。

势能方法开始时先对一个初始数据结构D0执行n个操作,对每个i=1,2,3,...,n,设ci为第i次操作的实际代价,Di为对数据结构Di-1作用第i个操作的结果,势函数Φ将每个数据结构Di映射为Φ(Di),即与数据结构Di相关的势。第i个操作的平摊代价为:

c‘i  = ci + Φ(Di) - Φ(Di-1)

从上式可以看出,每个操作的 平摊代价为实际代价加上该操作所增加的势。N个操作的平摊代价:

如果我们定义一个势函数Φ使得Φ(Dn)>= Φ(D0),则总的平摊代价就是总的实际代价的一个上界。

实际上,我们并不总是知道要执行多少操作,所以,如果要求对所有的i有Φ(Di)>= Φ(D0),则就像记账方法中一样,我们保证预先支付。

通常为了方便,定义Φ(D0)=0,然后证明对所有的i,有Φ(Di)>=0。

从直觉上看,如果第i操作的势差Φ(Di)>= Φ(Di-1)是正的,则平摊代价表示对第i个操作多收了费,同时数据结构的势也增加了。如果势差是负值,则平摊代价就表示对第i个操作的不足收费,这是通过减少势来支付该操作的实际代价。势函数Φ的增长为高代价操作做准备。

平摊代价由势函数决定。

【问题一分析】定义栈上的势函数为栈中对象的个数。开始为空栈, Φ(D0) = 0,栈中对象个数非负,即有  Φ(Di) >= 0 =  Φ(D0)。

Push操作作用于一个有s个对象的栈,则势差为 Φ(Di) -  Φ(Di-1) =
(s+1)-s = 1

那么,Push操作的代价为: c’i = ci + Φ(Di) -  Φ(Di-1) 
= 2

假设第i个操作是MultiPop(S, k),且弹出了k’ = mim(k, s)个对象,该操作的实际代价为k‘,则

Φ(Di) - Φ(Di-1)= - k′

MULTIPOP操作的平摊代价为

ci + Φ(Di) - Φ(Di-1) =k’-k’=0

类似的,POP操作的平摊代价也是0.三种栈操作中每一种平摊代价都是O(1),这样包含n个操作的序列的总平摊代价就是O(n)。

【问题二分析】我们定义在第i次INCREMENT操作后的计数器的势为bi ,即第i次操作后的计数器中1的个数。

我们来计算下一次INCREMENT操作的平摊代价。假设第i次INCREMENT操作对ti 个位进行了复位。该操作的实际代价至多为ti +1,因为除了将ti 个位复位外,它至多将1位置成1。所以,在第i次操作后计数器中1的个数为bi ≤ bi-1 - ti + 1,势差为

Φ(Di) - Φ(Di-1)≤ (bi-1 - ti + 1) - bi-1 =1 - ti

平摊代价为

ci + Φ(Di) - Φ(Di-1)≤ (ti + 1) + (1 - ti)=2

因为对于所有的i, Φ(Di)>= 0 ,故n次INCREMENT操作的序列的总平摊代价为总实际代价的一个上界,且n次INCREMENT操作的最坏代价为O(n)。

势能方法给我们一个简单的方法来分析计数器,即使它开始的时候不为0,开始的时候有b0个1, 此处0<=b0, bn<=k,我们有

故n次INCREMENT操作的实际代价为

因为b0 ,bn<=k,只要k=O(n),总的实际代价就是O(n)。换言之,如果我们执行了至少n=Ω(k)次INCREMENT操作,则无论计数器中包含什么样的初始值,总的实际代价都是O(n)。

二、 优先队列——二项堆

平摊分析多应用于分析复杂数据结构的基本操作的平摊代价。

优先级队列的基本操作:

a、 INSERT(S,x):把元素x插入集合S;

b、 MAXIMUM(S):返回S中具有最大关键字的元素;

c、 EXTRACT-MAX(S):去掉并返回S中的具有最大关键字的元素;

d、 INCREASE-KEY(S,x,k):将元素x的关键字的值增加到k,这里k值不能小于x的原关键字的值。

优先级队列 的应用: 最短路径、作业调度、分支定界。

【1】、 可合并堆的操作:

二叉堆在合并操作上是低效的,关键是要维护一个二叉树,为了使数据结构更为灵活,可以将二叉树变成森林。

Insert:将一个单节点的树加入森林。

Extract-Min:删除最小元素指针指向的元素,重置最小指针,可能需要树的重组。

Decrease-Key:将x剪下作为新的树,可能需要树的重组。

Union:合并两个森林的所有根,可能需要树的重组。

【2】、 Pairing Heap:仅在Extract-Min时进行树的重组。

1、 二项树

a、二项树的定义

B0:只包含一个节点。

Bk:由两个Bk-1连接而成,其中一棵树的根是另一棵树的根的最左孩子。

b、 二项树Bk的性质:

1)、 共有2^k个节点。

2)、 树的高度为k。

3)、 在深度i处,恰有C(k, i)个节点。

4)、 根的度为k,它大于任何其他节点的度,并且如果根的子女从右往左编号:0,1,2,3,......,k-1,则对应的第i个子女就是子树Bi的根。

5)、 在一个包含n个节点的二项树中,任意节点的最大度数为lgn

2、 二项堆:二项堆H由一组满足如下二项堆性质的二项树组成:

a、 H中每个树遵循最小堆性质:节点的关键字大于或等于其父节点的关键字——最小堆有序。

b、 对任意非负整数k,在H中至多有一棵二项树的根具有数k。

Note:由b可知,在n个节点的二项堆中,包含至多floor(lgn)+1棵二项树。

如图所示的二项堆:13个节点,3个二项树构成一个按照根的度数递增的链表。

更具体的表示:

3、 二项堆的操作:

a、 创建一个新的二项堆:

head[H] = NIL;

时间为O(1)

b、 寻找最小关键字:O(lg n)

Binomial-Heap-Minimum(H)
1 y<--nil
2 x<--head[H]
3 min<--INF
4 while x <> nil
5 	do if key[x] < min
6 		min<--key[x]
7 		y<--x
8		x<--sibling[x]
9 return y

c、 合并两个二项堆: O(lg n)

Binomial-Link(y, z)
1 p[y]<--z
2 sibling[u]<--child[z]
3 child[z]<--y
4 degree[z]<--degree[z]+1

以上操作的算法复杂度为O(1)。

Binomial-Heap-Union(H1, H2)
{
	H<--Make-Binomial-Heap()
	head[H]<--Binomial-Heap-Merge(H1, H2)
	free H1 and H2 but not the list they point to
	if head[H] = nil then return H
	prev-x<--nil
	x<--head[H]
	next-x<--sibling[H]

	while next-x != x do
	{
		if(degree[x] != degree[next-x]) or (sibling[next-x] != nil and degree[sibling[next-x]] = degree[x])  then
		{
			prev-x<--x
			x<--next-x
		}else if(key[x] < key[next-x]){
			sibling[x]<--sibling[next-x]
			Binomial-Link(next-x, x)
		}else{
			if(prev-x = nil) then
				head[H]<--next-x
			else
				sibling[prev-x] <-- next-x
			Binomial-Link(x, next-x)
			x<--next-x
		}
		next-x<--sibling[x]
	}
	return y
}

以上合并操作的算法复杂度为O(lg n)

d、 插入一个节点:O(lg n)

Binomial-Heap-Insert(H, x)
{
	H'<--Make-Binomial-Heap()
	p[x]<--nil
	child[x]<--nil
	sibling[x]<--nil
	head[H']--x
	H <-- Binomial-Heap-Union(H, H')
}

e、 抽取具有最小关键字的节点:O(lg n)

Binomial-Heap-Extract-Min(H)
{
	find and remove the root with the min key in the root list of H
	H' <-- Make-Binomial-Heap()
	reverse the order of the linked list of x' s children and set head[H'] to point to the head of the resulting list
	H <-- Binomial-Heap-Union(H, H')
	return x
}

f、 减小关键字的值:O(lg n)

Binomial-Heap-Decrease-Key(H, x, k)
{
	if k > key[x] then error "k>key[x]"
	key[x]<--k
	y<--x
	z<--p[y]
	while(z != nil and key[y] < key[z])
	{
		exchange key[y]<-->key[z] and other fields
		y<--z
		z<--p[y]
	}
}

g、 删除一个关键字:O(lg n)

Binomial-Heap-Delete(H, x)
{
	Binomial-Heap-Decrease-Key(H, x, -INF)
	Binomial-Heap-Extract-Min(H)
}

三、 优先队列——斐波那契堆

1、 斐波那契堆的特点:

a、 与二项堆非常相似,但不要求森林中的每棵树都是二项树。

b、如果没有DECREASE-KEY和DELETE,斐波那契堆里的每棵树都是二项树。

c、 只有EXTRACT-MIN和DELETE的平摊代价是O(logn),其他的都是Θ(1),包括DECREASE-KEY,使得频繁用到DECREASE-KEY的图上算法在渐进分析上有飞跃!

2、 斐波那契堆是一组遵循最小堆性质的树,满足如下条件:

a、 树的根存放在双向链表中(H的根链表);

b、 每棵树的根存放该树的最小元素;

c、 堆的最小值指针指向所有根中对应最小key值的那个;

d、对于每个节点x,记录x的度数rank(x),以及一个布尔变量mark(x),记录其失去子女的情况。

说明:

a、 双向链表表示树根及孩子节点,这样从中删除节点以及合并两个节点都可以在常数时间完成。

b、 树根列表是无序的。

c、 Mark规则:新生节点、节点成为另一个节点的孩子时,设为false;失去一个孩子时设为true。

d、 定义势能函数:t(H)为堆中树的个数,m(H)是堆中被标记的节点的个数。

3、 斐波那契堆是由无序的二项树组成,并且无序二项树Uk具有以下性质:

a、 共有2^k个节点;

b、 树的高度为k;

c、 在深度i处恰有C(k, i)个节点;

d、 根的度数为k,大于其他任何节点的度数;并且子女U0, U1, ..Uk-1按照某种次序排列。

n个节点的无序二项堆的最大度数D(n) = lg n

4、 关键: Insert和Union的时候,不调整结构,这样可能导致树的个数增多,导致Extract-Min耗时,所以在Extract-Min调整结构。

5、 斐波那契堆的操作:

a、 插入节点:Fib-Heap-Insert,生成一个单一节点的树并插入root-list中,O(1)。

Fib-Heap-Insert(H, x)
1 degree[x]<--0
2 p[x]<--nil
3 child[x]<--nil
4 left[x]<--x
5 right[x]<--x
6 mark[x]<--false
7 将包含x的根表和根表H连接
8 if min[x] = nil or key[x] < key[min[H]]
9 	then min[H]<--x
10n[H]<--n[H]+1

说明:1)新插入的树是原来min[H]的左兄弟;

2)不调整堆的结构,所以可能有度数相同的无序二项树;

3) 实际代价O(1);

4) 平摊代价:O(1) + 1 = O(1)。

b、 合并堆:FIB-HEAP-UNION(H1, H2) 创建并返回一个包含堆H1和H2中所有节点的新堆, O(1)。

Fib-Heap-Union(H1, H2)
1 H<--Make-Fib-Heap()
2 min[H]<--min[H1]
3 将H2的根表和H1的根表连接
4 if(min[H1] = nil) or (min[H2] != nill and min[H2] < min[H1])
5 	then min[H]<--min[H2]
6 n[H]<--n[H1]+n[H2]
7 free H1 and H2
8 return H

说明:不调整堆的结构,势能变化是0,平摊代价等于实际代价O(1).

c、 抽取最小节点:Fib-Heap-Extract-Min(H),抽取最小节点并调整结构。

Fib-Heap-Extract-Min(H)
1 z<--min[H]
2 if z != nil
3 	then for each child x of z
4 			do add x to the root list of H
5 				p[x]<--nil
6 		remove z from the root list of H
7 		if z = right[z]	//当右边节点为空的时候,right指针指向自己
8 			then min[H]<--nil
9 			else min[H]<--right[z]
10				Consolidate(H)
11		n[H]<--n[H]-1

说明: 下一步要调整H的根表,即减少斐波那契堆中树的数目,这由调用CONSOLIDATE(H)来完成。对根表的调整过程即反复执行下面的步骤,直到根表中的每个根都有一个不同的degree值。过程CONSOLIDATE使用了一个辅助数组A[0..D(n[H])],如果A[i]=y,则当前的y是个degree[y]=i的根。

1).在根表中找出两个具有相同度数的根x和y,且key[x]≤key[y]

2).将y与x链接:将y从根表中去掉,再使y成为x的一个孩子。这个操作由FIB-HEAP-LINK过程完成。域degree[x]被增值,且如果y上有标记的话也被去掉。

A[0..D(n[H])]:从当前min[H]指向的根开始,如果该度数的A[]为空,则填入;否则与A[]中记录的根链接。

以下是Consolidate的伪码:

Consolidata(H)
1 for i <-- 1 to D(n[H])
2 	do A[i]<--nil
3 for each node w in the root list of H
4 	do x<--w
5 		d<--degree[w]
6 		while A[d] != nil
7 			do y<--A[d]
8 				if key[x] > key[y]
9 					then exchange x<-->y
10				Fib-Heap-Link(H, y, x)
11				A[d]<--nil
12				d<--d+1
13		A[d]<--x
14min[H]<--nil
15for i<--0 to D(n[H])
16	do if A[i] != nil
17		then add A[i] to the root list of H
18				if min[H] = nil or key[A[i]] < key[min[H]]
19					then min[H]<--A[i]

FIB-HEAP-LINK(H, y, x):

1 从H的根表中去掉y

2 使y成为x的一个子结点,增加degree[x]

3 mark[y] ← FALSE

以下是Fib-Heap-Extract-Min的操作过程

说明:

1) 实际代价:因为在FIB-HEAP-EXTRACT-MIN中至多需要处理最小节点的D(n)个子女,除去CONSOLIDATEO(D(n));在调用CONSOLIDATE时根的大小至多为D(n)+t(H)-1。在6-12行的while循环的每一次执行中,有一个根要被链接到另一个上,则for循环中所做的工作总量至多与D(n)+t(H)成正比,总的实际工作量为O(D(n)+t(H))。

2) 势能变化:在抽取最小节点之前的势为t(H)+2m(H),而在此之后的势至多为(D(n)+1)+2m(H),因为该操作之后至多留下D(n)+1个根,且操作中没有任何节点被加标记。

3) 平摊代价: O(D(n) + t(H)) + ((D(n) + 1) + 2 m(H)) - (t(H) + 2 m(H)) = O(D(n))

d、 减小一个关键字:减小关键字的时候会将关键词剪除,如果在一棵树根下CUT过多的话,可能是有问题的。因此引入Mark的概念,保证不在一个位置上过度CUT。

Fib-Heap-Decrease-Key(H, x, k)
1  if k > key[x]
2  	then error "k > key[x]"
3  key[x]<--k
4  y<--p[x]
5  if y != nil and key[x] < key[y]
6  	then Cut(H, x, y)
7  <span style="white-space:pre">	</span>	Cascading-Cut(H, y)
8  if key[x] < key[min[H]]
9  	then min[H]<--x
CUT(H, x, y)
1 把x从y的子女表中删除,减小degree[y]
2 将x加入H的根表
3 p[x] ← NIL
4 mark[x] ← FALSE
Cascading-Cut(H, y)
1 z ← p[y]
2 if z ≠ NIL
3 	then if mark[y] = FALSE
4 		then mark[y] ← TRUE
5 	else CUT(H, y, z)
6 		Cascading-Cut(H, z)

说明: 1)先将46减少为15,再将35减少为5,此时Cascading-Cut发挥作用,因为Cut的节点的父节点Mark为true,所以父节点也要Cut,而父节点的父节点也Mark为True,所以也要被Cut。可以这么认为——每个节点如果被剪除了两次子节点,那么这个节点也要被剪除,同时其父节点的 剪除次数也增加了一次。

2) 实际代价: 假定在一次DECREASE-KEY的调用中要递归调用c次CASCADING-CUT。每次调用CASCADING-CUT的时间为O(1),这样, DECREASE-LEY的实际代价为O(c)。

3)势能变化: 对CASCADING-CUT的每次递归调用删除一个加了标记的节点并清除标记位。在此之后,共有t(H)+c棵树(原来的t(H)棵树,由连锁删除产生的c-1棵树,以及以x为根的树),和至多m(H)-c+2个加标记的节点(c-1)个节点被消除的标记,而最后一次调用CASCADING-CUT则可能给某一节点加上了标记。)因此,势的改变最多为:((t(H) + c) + 2(m(H) - c + 2)) - (t(H) +
2m(H)) = 4 – c。

4) 平摊代价:O(c)+4-c=O(1)

没有Decrease-Key的话,D(n) = O(log n);有的话,有些树不再是二项树,但是也差不多。

算法导论——lec 12 平摊分析与优先队列

时间: 2024-10-10 19:37:44

算法导论——lec 12 平摊分析与优先队列的相关文章

算法导论——lec 13 贪心算法与图上算法

之前我们介绍了用动态规划的方法来解决一些最优化的问题.但对于有些最优化问题来说,用动态规划就是"高射炮打蚊子",采用一些更加简单有效的方法就可以解决.贪心算法就是其中之一.贪心算法是使所做的选择看起来是当前最佳的,期望通过所做的局部最优选择来产生一个全局最优解. 一. 活动选择问题 [问题]对几个互相竞争的活动进行调度:活动集合S = {a1, a2, ..., an},它们都要求以独占的方式使用某一公共资源(如教室),每个活动ai有一个开始时间si和结束时间fi ,且0 ≤ si &

算法导论——lec 11 动态规划及应用

和分治法一样,动态规划也是通过组合子问题的解而解决整个问题的.分治法是指将问题划分为一个一个独立的子问题,递归地求解各个子问题然后合并子问题的解而得到原问题的解.与此不同,动态规划适用于子问题不是相互独立的情况,即各个子问题包含公共的子子问题.在这种情况下,如果用分治法会多做许多不必要的工作,重复求解相同的子子问题.而动态规划将每个子问题的解求解的结果放在一张表中,避免了重复求解. 一. 动态规划介绍 1. 动态规划方法介绍: 动态规划主要应用于最优化问题, 而这些问题通常有很多可行解,而我们希

算法导论——lec 07 快速排序

一. 快速排序的描述 1. 快速排序是一种原地排序的算法,最坏情况下的时间复杂度为Θ(n^2),期望的运行时间为Θ(n logn),且其中隐含的常数因子较小. 2. 快速排序分三个步骤: 分解:数组A[p...r]被划分成两个数组A[p...q-1]和A[q+1...r],使得A[p...q-1]中的元素都小于等于A[q],A[q+1...r]中的元素都大于等于A[q].下标q在这个划分过程中计算. 解决:递归调用快速排序,对子数组A[p...q-1]和A[q+1...r]进行排序. 合并:两个

算法导论——lec 08 线性时间排序

之前我们介绍了几种O(nlgn)的排序算法:快速排序.合并排序和堆排序,本节我们介绍基于比较的排序算法的下界以及几个线性时间的排序算法--计数排序.基数排序.桶排序. 一. 比较排序算法的下界 1. 决策树模型:比较排序可以被抽象的视为决策树.一棵决策树是一棵满二叉树,表示某排序算法 作用于给定输入所做的所有比较. 排序算法的执行对应于遍历一条从树的根到叶结点的路径.在每个内节结点处要做比较. 要使排序算法能正确的工作,其必要条件是,n 个元素的n!种排列中的每一种都要作为决策树 的一个叶子而出

算法导论——lec 06 堆排序

堆数据结构是一种数组对象,它可以被视为一颗完全二叉树,树中每个节点和数组中存放该节点值的那个元 素对应.如果表示堆的数组为A,那么树的根为A[1]. 一. 堆 1. 表示堆的数组A是一个具有两个属性的对象:length(A)是数组中的元素个数,heap-size(A)是存放在A中的堆的 元素个数:A[heap-size(A)]之后的元素都不属于相应的堆.也就是:Heap-size(A)<=length(A). 2. 给定某个节点的下标i,其父节点PARENT(i),左儿子LEFT(i)和右儿子R

算法导论 第12章 二叉查找树

二叉查找树是一种树数据结构,它与普通的二叉树最大的不同就是二叉查找树满足一个性质:对于树中的任意一个节点,均有其左子树中的所有节点的关键字值都不大于该节点的关键字值,其右子树中的任意一个节点的关键字值都不小于该节点的关键字值. 在二叉查找树上可以进行搜索.取最小值.取最大值.取指定节点的前驱.取指定节点的后继以及插入和删除节点操作,因此二叉查找树和堆(大顶堆和小顶堆)一样,也可以做优先队列,都能够在 O(lgn) 的时间内取得集合的最大值和最小值.一个二叉查找树的期望高度为O(lgn),因此在二

[算法导论]#1 摊还分析

目录 引言 聚集分析 记账方法 势能法 总结 引言 一个哈希表多大合适? 数据量为\(n?\),如果哈希表无限大(>=\(n?\)),那么时间复杂度是\(O(1)?\)的,不过很显然,虽然节省了时间,但是浪费了空间. 实际上在我们不知道数据量的情况下,我们无法确定哈希表的大小,这时我们有个很美丽的数据结构->动态表 动态表的工作原理 建立一个表,初始化大小为1. 如果表的容量不够,那么就把大小扩大为原来的两倍,将原来表的内容复制一遍 把原来表的内存释放,再执行插入 容量会以1,2,4,8,16

算法导论——lec 10 图的基本算法及应用

搜索一个图是有序地沿着图的边訪问全部定点, 图的搜索算法能够使我们发现非常多图的结构信息, 图的搜索技术是图算法邻域的核心. 一. 图的两种计算机表示 1. 邻接表: 这样的方法表示稀疏图比較简洁紧凑. typedef struct{ int adjvex;//邻接顶点的位置 struct ArcNode *next; int weight;//边的权重 }ArcNode; typedef struct{ VertexType data; ArcNode *firstarc; }VNode, A

算法导论5.1-2

题目摘要:利用random(0,1):提供一个random(a,b)的函数 分析:利用random(0,1)产生2进制数,这可以产生所有0,1,2……n的数,当这些数在[a,b]中时,我们就返回它,不在就不返回.实现时n只要包含b-a就可以了然后加上a后再判断. 1 #include<iostream> 2 #include<ctime> 3 4 using namespace std; 5 6 int RandomCopy(int a, int b) 7 { 8 srand(ti