3.78k likes | 3.9k Views
高级数据结构. 教材: 《 数据结构( C++ 描述) 》 (金远平编著,清华大学出版社) 讲课教师: 金远平,软件学院 ypjin@seu.edu.cn. 将孤立地分析一次算法调用得出的结论应用于一个 ADT 的相关操作序列会产生过于悲观的结果。 例 1.12 整数容器 Bag 。 class Bag { public: Bag ( int MaxSize = DefaultSize ) ; // 假设 DefaultSize 已定义 int Add ( const int x ) ; // 将整数 x 加入容器中
E N D
高级数据结构 教材:《数据结构(C++描述)》(金远平编著,清华大学出版社)讲课教师: 金远平,软件学院ypjin@seu.edu.cn JYP
将孤立地分析一次算法调用得出的结论应用于一个ADT的相关操作序列会产生过于悲观的结果。将孤立地分析一次算法调用得出的结论应用于一个ADT的相关操作序列会产生过于悲观的结果。 例1.12整数容器Bag。 class Bag { public: Bag ( int MaxSize = DefaultSize ); // 假设DefaultSize已定义 int Add (const int x ); // 将整数x加入容器中 int Delete (const int k ); // 从容器中删除并打印k 个整数 private: int top; // 指示已用空间 int *b; // 用数组b存放整数 int n; // 容量 }; 代价分摊(1.5.4) JYP
各操作的实现如下: Bag::Bag ( int MaxSize = DefaultSize ):n(MaxSize) { b = new int[n]; top = -1; } int Bag::Add (const int x) { if (top = = n-1) return 0; // 返回0表示加入失败 else { b[++top] = x; return 1; } } JYP
int Bag::Delete (const int k) { if (top + 1 < k ) return 0; //容器内元素不足k个,删除失败 else { for (int i = 0; i < k; i++) cout << b[top – i] << “ ” ; top = top - k; return 1; } } 先分析操作成功的情况:Add(x)的时间复杂性是O(1);Delete(k)需要k个程序步,且k可能等于n,在最坏情况下其时间复杂性是O(n);一个调用Add操作 m1次,Delete操作m2次的序列的总代价则为O(m1+ m2n)。 JYP
前面是常规分析的结论。进一步观察:如果一开始容器为空,则删除的元素不可能多于加入的元素,即 m2次Delete操作的总代价不可能大于m1次Add操作的总代价。因此,在最坏情况下,一个调用Add操作 m1次,Delete操作m2次的序列的总代价为O(m1)。 操作失败时,Add(x)和Delete(k) 的时间复杂性都是O(1)。因此,在操作可能失败的情况下,一个调用Add操作 m1次,Delete操作m2次的序列的总代价为O(m1+ m2)。 JYP
常规分析并没有错,只是其推导出的总代价上界远大于实际可得的上界。其原因是这种分析法没有注意到连续的最坏情况删除是不可能的。常规分析并没有错,只是其推导出的总代价上界远大于实际可得的上界。其原因是这种分析法没有注意到连续的最坏情况删除是不可能的。 为了取得更准确的结果,还应该度量ADT数据结构的状态。对于每一个可能的状态S,赋予一个实数(S)。(S)称为S的势能,其选择应使得(S)越高,对处于S状态的数据结构成功进行高代价操作的可能越大。 例如,将容器元素个数作为容器状态的势能就很合理,因为元素越多,对容器成功进行高代价操作的可能越大。 JYP
考虑一个由m个对ADT操作的调用构成的序列,并设ti是第i次调用的实际代价,定义第i次调用的分摊代价ai为考虑一个由m个对ADT操作的调用构成的序列,并设ti是第i次调用的实际代价,定义第i次调用的分摊代价ai为 ai = ti + (Si) – (Si-1) Si-1是第i次调用开始前ADT数据结构的状态,Si是第i次调用结束后ADT数据结构的状态。设的选择使得(Sm) ≥ (S0),则 JYP
即,分摊代价的总和是实际代价总和的上界。 例1.12将容器元素个数作为(S)。若操作序列始于空容器,则(Sm) ≥ (S0)总是成立。下表反映了容器(S)的典型变化情况。 JYP
对于Add操作,ti=1,(Si)–(Si-1)=1,所以ai=2;对于Delete操作,ti=k,(Si)–(Si-1)= –k,所以ai=0。 任何一个调用Add操作 m1次,Delete操作m2次的序列的总代价为O(m12 + m20) = O(m1)。 JYP
可见,分摊分析法将偶尔出现的高价操作调用的代价分摊到邻近的其它调用上,故而得名。可见,分摊分析法将偶尔出现的高价操作调用的代价分摊到邻近的其它调用上,故而得名。 而且,当用分摊分析法得到的一个操作调用序列的代价总和比用常规分析法得到的代价总和小时,人们就得到了更接近实际代价的分析结果,或者说对算法时间复杂性的判断更准确了。 JYP
一个字符串的子序列通过从字符串中删除零或多个任意位置的字符得到。一个字符串的子序列通过从字符串中删除零或多个任意位置的字符得到。 两个字符串x和y的最长公共子序列记为lcs(x, y)。 例如,x = abdebcbb,y = adacbcb,则lcs(x, y)是adcbb和adbcb,如下所示: 两个字符串的最长公共子序列(2.4.3) JYP
问题的基本求解方法: 用标记空串,则lcs(x, )= lcs(, y) = 。 lcs(xa, ya) = lcs(x, y)a,即xa和ya的最长公共子序列由x和y的最长公共子序列后接a构成。 若xa和yb的最后一个字符不相等,则当lcs(xa, yb)不以a结尾时一定等于lcs(x, yb),当lcs(xa, yb)不以b结尾时一定等于lcs(xa, y)。因此lcs(xa, yb)等于 lcs(x, yb)与 lcs(xa, y)中较长者。 JYP
由此可得计算两个字符串最长公共子序列长度的递归算法lcs:由此可得计算两个字符串最长公共子序列长度的递归算法lcs: int String::lcs ( String y ) { // 驱动器 int n = Length( ), m = y.Length( ); return lcs( n, m, y.str ); } int String::lcs (int i, int j, char *y ) { // 递归核心 if ( i == 0 | | j == 0) return 0; if ( str[i-1] ==y[j-1] ) return ( lcs( i-1, j-1, y) + 1); return max( lcs( i-1, j, y), lcs( i, j-1, y)); } JYP
设x的长度为n,y的长度为m,在最坏情况下lcs的时间复杂性为w(n, m)。 w(n, m) = c (c为常数) n = 0或m = 0 w(n, m-1) + w(n-1, m) 否则 因此,w(n, m)≥2 w(n-1, m-1)≥…≥2min(n, m)c,即lcs的时间复杂性是指数型的。 进一步可发现,lcs(i, 0)=0(0≤i≤n),lcs(0, j) =0(0≤j≤m)。lcs(i, j)的计算依赖于lcs(i–1, j–1)、lcs(i–1, j)和lcs(i, j–1),如下图所示: JYP
根据以上拓扑关系,可以在不用递归的情况下计算lcs(i, j)。算法Lcs实现了上述优化策略,这种策略体现了动态规划的思想。算法Lcs的时间复杂性显然是O(nm),这比其递归版有很大改进。 JYP
int String::Lcs ( String y ) { int n = Length( ), m = y.Length( ); int lcs[MaxN][MaxM]; // MaxN和MaxM 是已定义的常数 int i, j; for ( i = 0; i <= n; i++) lcs[i][0] = 0; // 初始值 for ( j = 0; j <= m; j++) lcs[0][j] = 0; // 初始值 for ( i = 1; i <= n; i++) for ( j = 1; j <= m; j++) if ( str[i-1] ==y.str[j-1] ) lcs[i][j] = lcs[i-1][j-1] + 1; else lcs[i][j] = max(lcs[i-1][j], lcs[i][j-1]); return lcs[n][m]; } JYP
例如,x = abdebcbb,y = adacbcb,lcs(x, y) = adbcb,改进算法的计算如下所示: JYP
计算机模拟(simulation): • 用软件模仿另一个系统的行为。 • 将研究对象表示为数据结构,对象动作表示为对数据的操作,控制动作的规则转换为算法。 • 通过更改数据的值或改变算法设置,可以观察到计算机模拟的变化,从而使用户能够推导出关于实际系统行为的有用结论。 • 在计算机处理一个对象的动作期间,其它对象和动作需等待。 • 队列在计算机模拟中具有重要应用。 机场模拟(2.9) JYP
简单机场模拟: • 只有一个跑道。 • 在每个时间单元,可起飞或降落一架飞机,但不可同时起降。 • 飞机准备降落或起飞的时间是随机的,在任一时间单元,跑道可能处于空闲、降落或起飞状态,并且可能有一些飞机在等待降落或起飞。 • 飞机在地上等待的代价比在空中等待的小,只有在没有飞机等待降落的情况下才允许飞机起飞。 • 当出现队列满的情况时,则拒绝为新到达的飞机服务。 JYP
需要两个队列landing和takeoff。 飞机可描述为: struct plane { int id; // 编号 int tm; // 到达队列时间 }; 飞机的动作为: enum action { ARRIVE, DEPART }; JYP
模拟运行: • 时间单元:1 — endtime,并产生关于机场行为的重要统计信息,如处理的飞机数量,平均等待时间,被拒绝服务飞机的数量等。 • 采用基于泊松分布的随机整数决定在每个时间单元有多少架新飞机需要降落或起飞。 • 假设在10个时间单元中到达的飞机数分别是:2,0,0,1,4,1,0,0,0,1,那么每个时间单元的平均到达数是0.9。 JYP
一个非负整数序列满足给定期望值v的泊松分布意味着,对于该序列的一段足够长的子序列,其中整数的平均值接近v。一个非负整数序列满足给定期望值v的泊松分布意味着,对于该序列的一段足够长的子序列,其中整数的平均值接近v。 • 在模拟中还需要建立新到达飞机的数据,处理被拒绝服务的飞机,起飞、降落飞机,处理机场空闲和总结模拟结果。 • 下面是机场模拟类定义: JYP
class AirportSimulation { // 机场模拟。一个时间单元 = 起飞或降落的时间 public: AirportSimulation( ); // 构造函数 void RunSimulation( ); // 模拟运行 private: Queue<plane> landing(6); // 等待降落飞机队列,假设用环 // 型队列,实际长度为5 Queue<plane> takeoff(6); // 等待起飞飞机队列,同上 double expectarrive; //一个时间单元内期望到达降落飞机数 double expectdepart; //一个时间单元内期望到达起飞飞机数 int curtime; // 当前时间 int endtime; // 模拟时间单元数 int idletime ; // 跑道空闲时间单元数 int landwait ; // 降落飞机的总等待时间 JYP
int nland ; // 降落的飞机数 int nplanes; // 处理的飞机数 int nrefuse; // 拒绝服务的飞机数 int ntakeoff; // 起飞的飞机数 void Randomize( ); // 设置随机数种子 int PoissionRandom(double& expectvalue); // 根据泊松分布和给定期望值生成随机非负整数 plane* NewPlane(plane& p, action kind); // 建立新飞机的数据项 void Refuse(plane& p, action kind); // 拒绝服务 void Land(plane& p); // 降落飞机 void Fly(plane& p); // 起飞飞机 void Idle( ); // 处理空闲时间单元 void Conclude( ); // 总结模拟结果 }; JYP
构造函数初始化各变量,如下所示: AirportSimulation::AirportSimulation( ) { // 构造函数 Boolean ok; cout << “请输入模拟时间单元数:”; cin >> endtime; idletime = landwait = nland = nplanes = 0; nrefuse = ntakeoff = takoffwait = 0; // 初值 Randomize( ); // 设置随机数种子 do { cout << “请输入一个时间单元内期望到达降落飞机数:”; cin >> expectarrive; cout << “请输入一个时间单元内期望到达起飞飞机数:”; cin >> expectdepart; JYP
if (expectarrive < 0.0 || expectdepart < 0.0) { cout << “这些数不能为负!请重新输入。”<< endl; ok = FALSE; } else if (expectarrive + expectdepart > 1.0) { cout << “机场将饱和!请重新输入。”<< endl; ok = FALSE; } else ok = TRUE; } while (ok == FALSE); } JYP
RunSimulation( )是模拟运行的主控程序: void AirportSimulation::RunSimulation( ) { int pri; // 伪随机整数 plane p; for (curtime = 1; curtime <= endtime; curtime++) { cout << “时间单元” << curtime << “:”; pri = PoissionRandom(expectarrive); for (int i =1; i <= pri; i++) { //处理新到达准备降落的飞机 p = *NewPlane(p, ARRIVE); if (landing.IsFull( )) Refuse(p, ARRIVE); else landing.Add(p); } pri = PoissionRandom(expectdepart); JYP
for (int i =1; i <= pri; i++) { //处理新到达准备起飞的飞机 p = *NewPlane(p, DEPART); if (takeoff.IsFull( )) Refuse(p, DEPART); else takeoff.Add(p); } if (!landing.IsEmpty( )) { // 降落飞机 p = *landing.Delete(p); Land(p); } else if (!takeoff.IsEmpty( )) { // 起飞飞机 p = *takeoff.Delete(p); Fly(p); } else Idle( ); // 处理空闲时间单元 } Conclude( ); // 总结模拟结果 } JYP
用库函数srand和rand生成随机数,并用时钟设置随机种子,以增强随机性:用库函数srand和rand生成随机数,并用时钟设置随机种子,以增强随机性: void AirportSimulation::Randomize( ) { srand((unsigned int) (time(NULL)%10000)); } 库函数time返回自格林威治时间1970年1月1日00:00:00 至今经历的秒数。这使得每次模拟运行随机数起点都不同。 rand按照均匀分布生成随机数,还需要转化为适合机场模拟的泊松分布随机数。下面直接给出根据泊松分布和给定期望值生成伪随机整数的算法(其数学推导略) : JYP
int AirportSimulation::PoissionRandom(double& expectvalue) { int n = 0; // 循环计数 double limit; // e-v, 其中,v是期望值 double x; // 伪随机数 limit = exp(-expectvalue); x = rand( ) / (double) INT_MAX; // rand( )生成0到INT_MAX之间的整数, x在0和1之间 while (x > limit) { n++; x *= rand( ) / (double) INT_MAX; } return n; } JYP
建立新飞机的数据项由函数NewPlane实现: plane* AirportSimulation::NewPlane(plane& p, action kind) { nplanes++; // 飞机总数加1 p.id = nplanes; p.tm = curtime; switch (kind) { case ARRIVE: cout << “飞机” << nplanes << “准备降落。” << endl; break; case DEPART: cout << “飞机” << nplanes << “准备起飞。” << endl; break; } return &p; } JYP
处理被拒绝的飞机由函数Refuse实现: void AirportSimulation::Refuse(plane& p, action kind) { switch (kind) { case ARRIVE: cout << “引导飞机” << p.id << “到其它机场降落。” << endl; break; case DEPART: cout << “告诉飞机” << p.id << “等一会儿再试。” << endl; break; } nrefuse++; // 被拒绝飞机总数加1 } JYP
处理飞机降落由函数Land实现: void AirportSimulation::Land(plane& p) { int wait; wait = curtime – p.tm; cout << “飞机” << p.id << “降落,该机等待时间:” << wait << “。”<< endl; nland++; // 降落飞机总数加1 landwait += wait; // 累加总降落等待时间 } JYP
处理飞机起飞由函数Fly实现: void AirportSimulation::Fly(plane& p) { int wait = curtime – p.tm; cout << “飞机” << p.id << “起飞,该机等待时间:” << wait << “。”<< endl; ntakeoff++; // 起飞飞机总数加1 takeoffwait += wait; // 累加总起飞等待时间 } JYP
处理机场空闲由函数Idle实现: void AirportSimulation::Idle( ) { cout << “跑道空闲。” << endl; idletime++; // 跑道空闲时间加1 } 总结模拟结果由函数Conclude实现: void AirportSimulation::Conclude( ) { cout << “总模拟时间单元数:” << endtime << endl; cout << “总共处理的飞机数:” << nplanes << endl; cout << “降落飞机总数:” << nland << endl; cout << “起飞飞机总数:” << ntakeoff << endl; JYP
cout << “拒绝服务的飞机总数:” << nrefuse << endl; cout << “队列中剩余的准备降落飞机数:” << landing.Size( ) << endl; // 假设队列成员函数Size( )返回队列中元素个数 cout << “队列中剩余的准备起飞飞机数:” << takeoff.Size( ) << endl; if (endtime > 0) cout << “跑道空闲时间百分比:” << ((double) idletime / endtime) * 100.0 << endl; if (nland > 0) cout << “降落平均等待时间:” << (double) landwait / nland << endl; if (ntakeoff > 0) cout << “起飞平均等待时间:” << (double) takeoffwait / ntakeoff << endl; } JYP
可通过下列程序模拟运行: #include “common.h” #include “simdefs.h” // 存放模拟类定义及相关函数实现 void main( ) { AirportSimulation s; s.RunSimulation( ); } JYP
模拟过程产生的数据如下: 请输入模拟时间单元数:30 请输入一个时间单元内期望到达降落飞机数:0.47 请输入一个时间单元内期望到达起飞飞机数:0.47 时间单元1:飞机1准备降落。 飞机1降落,该机等待时间:0。 时间单元2:跑道空闲。 时间单元3:飞机2准备降落。 飞机3准备降落。 飞机2降落,该机等待时间:0。 时间单元4: 飞机3降落,该机等待时间:1。 JYP
时间单元5:飞机4准备降落。 飞机5准备降落。 飞机6准备起飞。 飞机7准备起飞。 飞机4降落,该机等待时间:0。 时间单元6:飞机8准备起飞。 飞机5降落,该机等待时间:1。 时间单元7:飞机9准备起飞。 飞机10准备起飞。 飞机6起飞,该机等待时间:2。 时间单元8: 飞机7起飞,该机等待时间:3。 时间单元9: 飞机8起飞,该机等待时间:3。 JYP
时间单元10:飞机11准备降落。 飞机11降落,该机等待时间:0。 时间单元11:飞机12准备起飞。 飞机9起飞,该机等待时间:4。 时间单元12:飞机13准备降落。 飞机14准备降落。 飞机13降落,该机等待时间:0。 时间单元13: 飞机14降落,该机等待时间:1。 时间单元14: 飞机10起飞,该机等待时间:7。 时间单元15: 飞机15准备降落。 飞机16准备起飞。 飞机17准备起飞。 飞机15降落,该机等待时间:0。 JYP
时间单元16:飞机18准备降落。 飞机19准备降落。 飞机20准备起飞。 飞机21准备起飞。 飞机18降落,该机等待时间:0。 时间单元17: 飞机22准备降落。 飞机19降落,该机等待时间:1。 时间单元18: 飞机23准备起飞。 告诉飞机23等一会儿再试。 飞机22降落,该机等待时间:1。 JYP
时间单元19: 飞机24准备降落。 飞机25准备降落。 飞机26准备降落。 飞机27准备起飞。 告诉飞机27等一会儿再试。 飞机24降落,该机等待时间:0。 时间单元20: 飞机28准备降落。 飞机29准备降落。 飞机30准备降落。 飞机31准备降落。 引导飞机31到其它机场降落。 飞机25降落,该机等待时间:1。 JYP
时间单元21:飞机32准备降落。 飞机33准备起飞。 告诉飞机33等一会儿再试。 飞机26降落,该机等待时间:2。 时间单元22:飞机28降落,该机等待时间:2。 时间单元23:飞机29降落,该机等待时间:3。 时间单元24:飞机34准备起飞。 告诉飞机34等一会儿再试。 飞机30降落,该机等待时间:4。 JYP
时间单元25:飞机35准备起飞。 告诉飞机35等一会儿再试。 飞机36准备起飞。 告诉飞机36等一会儿再试。 飞机32降落,该机等待时间:4。 时间单元26:飞机37准备起飞。 告诉飞机37等一会儿再试。 飞机12起飞,该机等待时间:15。 时间单元27:飞机16起飞,该机等待时间:12。 时间单元28:飞机17起飞,该机等待时间:13。 时间单元29:飞机20起飞,该机等待时间:13。 JYP
时间单元30:飞机38准备起飞。 飞机21起飞,该机等待时间:14。 总模拟时间单元数:30 总共处理的飞机数:38 降落飞机总数:19 起飞飞机总数:10 拒绝服务的飞机总数:8 队列中剩余的准备降落飞机数:0 队列中剩余的准备起飞飞机数:1 跑道空闲时间百分比:3.33 降落平均等待时间:1.11 起飞平均等待时间:8.60 JYP
当n = 0或1时,只有一棵二叉树。 当n = 2,存在2棵不同(结构)的二叉树: 二叉树计数(4.9) JYP
而当n = 3,则存在5棵不同的二叉树: 那么,具有n个结点的不同二叉树究竟有多少呢? JYP
不失一般性,将树的n个结点编号为1到n。假设一棵二叉树的前序序列为1 2 3 4 5 6 7 8 9且其中序序列为2 3 1 5 4 7 8 6 9,则通过这对序列可以唯一确定一棵二叉树。 为了构造相应的二叉树,可找出前序第一个结点,即1。于是,结点1是树根,中序序列中所有在1之前的结点(2 3)属于左子树,其余结点(5 4 7 8 6 9)属于右子树。 JYP
这一步构造如下所示: JYP
接着,可根据前序序列2 3和中序序列2 3构造左子树。显然,结点2是树根。由于在中序序列中,结点2之前无结点,所以其左子树为空,结点3是其右子树,如下图所示: JYP