1、算法复杂度概念 线性表(上),2008/02/19,2,关于浮点数四则运算的定义,3,算法复杂度问题:,一般是指问题随规模的增长算法所需消耗的运算时间和内存空间的增长趋势。 因此不考虑计算机本身硬件的特质,一般也忽略算法所消耗的与问题规模无关的固定量的计算与空间。,4,如何描述增长趋势的高低?,不考虑不变量:C + f(n) f(n) 忽略不能与时俱进的因素k * f(n) f(n) 区分同类型的增长方式的不同量级f(n) = nk f(n) = nk+c 专注增长趋势中最本质的区别C log n n nk kn (k 1),5,算法复杂度的考察方法,考察一个算法的复杂度,一般考察的是当问题复
2、杂度n的增加时,运算所需时间、空间代价f(n)的上下界。(Asymptotic upper or lower bound) 进一步而言,又分为最好情况、平均情况、最坏情况三种情况。通常最坏情况往往是我们最关注的。,6,算法复杂度的上界(大O表示法),大O表示法是用一个函数f(n)来描写算法复杂度的上界的表示方式。记为:O(f(n)),7,大表示法,如果能同时找到算法复杂度的上下确界函数g(n),f(n)。 且g(n) = f(n),则算法复杂度能更精确的表达为(f(n),8,算法复杂度的优化(fibonacci数列问题),刚开始(零年)1对 一年后1对 二年后2对 三年后3对 今年的兔子对数
3、= 前年已经有的兔子总数 + 去年的兔子数,01234,Fibonacci 数列,9,采用展开递推公式的方法,fib(5),fib(3),fib(4),+,fib(1),fib(2),+,fib(0),fib(1),+,fib(2),fib(3),+,fib(1),fib(2),+,从fib(n)开始,自顶向下逐层展开。 直到fib(0) 或 fib(1)为止。 然后逐层计算上去。,= 1,= 1,= 1,= 1,10,递归算法的运算复杂度分析,fib(5),fib(3),fib(4),+,T(n) = T(n-1) + T(n-2) +1 T(1) = T(0) = 1,fib(1),fib
4、(2),+,fib(0),fib(1),+,fib(2),fib(3),+,fib(1),fib(2),+,找出与n相关的函数,= 1,= 1,= 1,= 1,2n/2 T(n) - 1 = 2n,11,奇妙的黄金分割数,A B,AB BC = BC AB + BC,1,0.618,AB AB + BC + 1 = BC BC,BC,AB,+1,Xn+1 = Xn + Xn-1,12,使用双子星递归函数实现该算法:,#include void f2(int x,int y); void f1( int x, int y) if (x 200)return;elsex = x +y; y = x
5、 -y; x = x - y;printf(“%d,“, x);f2(x,y); ,void f2(int x,int y) y = x + y;f1(x,y); void main() f1(0,1); ,13,Fibonacci 数列的通项公式,1/1 = 1, 2/1 = 2, 3/2 = 15, 5/3 = 1666., 8/5 = 16, 13/8 = 1625 21/13 = 161538.,黄金分割数,T(n) = O(1.618 n),14,Fibonacci 数列的矩阵表达,X,=,1+1 1+0 1+0 1+0,X,=,2+1 2+0 1+1 1+0,X,=,5 3 3 2
6、,n,=,fib(n+1) fib(n) fib(n) fib(n-1),How come?,15,Fibonacci 数列的矩阵表达,X,=,1+1 1+0 1+0 1+0,X,=,2+1 2+0 1+1 1+0,X,=,5 3 3 2,n,=,fib(n+1) fib(n) fib(n) fib(n-1),How come?,16,Fibonacci 数列的矩阵表达,X,=,X,+,=,+,F2 0 F1 0,BC,n,=,fib(n+1) fib(n) fib(n) fib(n-1),17,Recursive powering(递归乘方法),n,n次矩阵乘法,算法复杂度 = O (n),
7、=,n/2,n/2,x,+ ,n/2/2,n/2/2,x,+ ,总共要多少层递归?,=,log2n,算法复杂度 = O (logn),18,Recursive powering(递归乘方法),n,n次矩阵乘法,算法复杂度 = O (n),=,n/2,n/2,x,+ ,n/2/2,n/2/2,x,+ ,总共要多少层递归?,=,log2n,算法复杂度 = O (logn),19,Fibonacci 数列算法的优化 参考阅读:MIT教程第三讲,T(n) = O ( 1.618n)= O( n )= O( logn ),fib(110): 1022 次运算 111 次运算 7 次运算,20,算法设计技
8、术,贪心法:试图通过局部最优达到全局最优 分治法:划分成小问题,各个求解 回溯法:对多种可能性逐步试探,失败时回来选其它可能性 动态规划法:多路经的求解方式 分枝界限法:上下界+广度优先,21,算法与问题求解,问题:为贫困地区儿童募捐: 10,000元。 解决方案:A:向遇到的每一个人尽可能的鼓动募捐,直到完成任务。B:鼓动10个朋友,每人去募捐1000元。鼓动10个朋友,每人去募捐100元 鼓动10个朋友,每人捐10元。,22,分治算法要点:,要能够确定可以解决的 简单问题(base case) 要能够找到分解化简(recursive decomposition)问题的方法,23,线性表(顺
9、序表),定义 基本操作 算法复杂度分析,24,“线性表”简称为表,是零个或多个元素的有穷序列,表示成A =(a0, a2, . , ai -1, ai , ai+1, , an -1)(n1) 表中所含元素的个数称为表的“长度”。 以二元组表示:L= ,其中D= a1,a2, a3, . an 表示元素集合 S= R R=, , 表示关系集,a1,图 顶点:表示数据示 边:表示是数据间的顺序结构关系,an,线性表的形式化定义,25,说明,同一线性表中的元素必须是同一类型的; 在线性表中只有三类元素:第一个元素,最后一个元素(结尾标记),其他元素; 下面的声明语句是什么意思? char *str
10、 = “”; char a20 = “hello!”;,26,线性表的顺序存储实现,为了存储线性表,至少要保存下列信息: 1)线性表中的数据元素本身; 2)线性表中数据元素的顺序关系; 3)线性表中起始位置; 4)线性表的结尾标记;,在计算机内部可以采用不同的方式来存储一个线性表,其中最简单的方式就是采用顺序存储结构。即通过物理存储上的相邻关系来来表述元素间的顺序关系。 采用顺序存储结构存储的线性表称为顺序表,27,用数组存储线性表时元素的地址可以通过起始的基地址在加上偏移量来得到。,Offset = sizeof(e) * subscript,线性表的顺序存储实现(续),Base+Offse
11、t=location,28,an-1,Loc(a0)+(n-1)*k,an-1,n-1,ai,Loc(a0)+i*k,ai,i,a1,Loc(a0)+k,a1,1,a0,Loc(a0),a0,0,数据元素,存储地址,数据元素,下标变量(编号),直接存取,29,在C 语言中定义线性表,定义1:#define MAXNUM 100DataType elementMAXNUM;int n; /* 实际元素个数n MAXNUM */定义2:struct SeqListDataType elementMAXNUM; int n; /* 实际元素个数n MAXNUM */;定义3:链表,顺序表,30,顺序
12、表常用操作:,创建操作:静态声明:DataType buffMAXNUM;动态申请:DtatType *p = (DataType)malloc(MAXNUM * sizeof(DataType); 下标访问操作:pi; 插入、删除操作 查找与排序,31,顺序表的插入算法框架int insert_seq( SeqList *plist, int p, DataType x ) 判断表是否会溢出;判断 0pn; 从元素kn-1至 kp顺序下移一位;/*第n个元素的下标是n-1*/在位置p处插入x;,32,插入排序,基本思想:每步将一个待排序的记录,按其排序码大小插到前面已经排序的字序列的合适位置
13、,直到全部插入排序完为止。,x,顺次选取一个元素,插入到合适位置,33,插入排序的细分类,如何插入到已排好序的序列中?直接插入(从后向前找位置后插入)O(n2)二分法插入(按二分法找位置后插入)O(nlog2n)表插入排序(按链表查找位置后插入)O(n2),34,直接插入排序,基本思想: 假定前面m 个元素已经排序; 取第(m+1) 个元素,插入到前面的适当位置; 一直重复,到m=n 为止。 (初始情况下,m = 1),35,第一趟: 23, 起始只有一个记录11, 23 11第二趟: 11,23,11,23,55 55第三趟: 11,23,55,11,23,55,97 97第四趟: 11,2
14、3,55,97,11,19,23,55,97 19第五趟: 11,19,23,55,97,11,19,23,55,80,97 80,示例:23,11,55,97,19,80,36,数组数据插入,37,将操作分离成 独立的函数,38,在算法中使用结构体作为数据对象,typedef int KeyType; typedef int DataType; typedef struct KeyType key; /* 排序码字段 */DataType info; /* 记录的其他字段 */RecordNode; typedef struct int n; /* n为文件中的记录个数,nMAXNUM */
15、RecordNode *record; SortObject;,(周二上机题),39,直接插入排序算法复杂度评价,极端情况下: 最小比较次数每个记录仅比较一次最大比较次数每个记录比较已排好序的记录长度,40,直接插入排序算法评价2,最小移动次数 最大移动次数,41,直接插入排序算法评价3,初始数据状态相关: 文件初态不同时,直接插入排序所耗费的时间有很大差异。 若文件初态为正序,则算法的时间复杂度为O(n) 若初态为反序,则时间复杂度为O(n2),42,直接插入排序算法评价4 平均复杂度,插入记录Ri-1,有i种可能的插入位置,即插入到第0,1,i-1位置上,假设每种情况发生的概率是相等的,均
16、为 pj = 1/i (j=0,1,i-1) 比较次数为Cj=j+1(j=0,i-2,i-2),则插入记录Ri-1的平均比较次数为,43,直接插入排序算法评价5 平均复杂度,直接插入排序的总的比较次数为:,44,直接插入排序算法评价,直接插入排序算法的平均移动次数与平均比较次数同级,也是O(n2) 直接插入排序的平均时间复杂度为T(n) = O(n2) 算法中引入了一个附加的记录空间temp,因此辅助空间为S(n) = O(1) 直接插入排序是稳定的,45,二分法插入排序,特点:在直接插入排序的基础上减少比较的次数,即在插入Ri时改用二分法比较找插入位置,便得到二分法插入排序 限制:必须采用顺
17、序存储方式。,46,(highlow ,查找结束,插入位置为low 或high+1 ),( 4236 ),( 4253 ),47,二分法插入排序算法,void binSort(SortObject * pvector) int i, j, left, mid, right; RecordNode temp; for( i = 1; i n; i+ ) temp = pvector- recordi; left = 0; right = i 1; while (left recordmid.key) right = mid-1;,elseleft = mid+1;/while for(j=i-1
18、; j=left; j-)pvector-recordj+1 = pvector-recordj; if(left != i) pvector-recordleft = temp;/ for / binSort,48,二分法插入排序方法性能分析,当n较大时,比直接插入排序的最大比较次数少得多。但大于直接插入排序的最小比较次数 算法的移动次数与直接插入排序算法的相同 最坏的情况为n2/2 最好的情况为n 平均移动次数为O(n2) 二分法插入排序算法的平均时间复杂度为T(n)= O(n2) 二分插入排序法是稳定的排序算法,在检索时采用leftright结束,left、right的修改原则是:tem
19、p.key recordmid.key,保证排序是稳定的。,49,结论,移动次数与直接插入排序相同,最坏的情况为n2/2,最好的情况为n,平均移动次数为O(n2) 二分法插入排序算法的平均时间复杂度为T(n)= O(n2) 二分法插入排序是稳定的,50,线性表的抽象数据类型,ADT List is operations List createNullList ( void )创建并且返回一个空线性表。 int insertPre ( List list, position p, DataType x )在list中p位置前插入值为x的元素,并返回插入成功与否的标志。 int insertPos
20、t ( List list, position p, DataType x )在list中p位置后插入值为x的元素,并返回插入成功与否的标志。 int deleteV (List list, DataType x )在list中删除一个值为x的元素,并返回插入成功与否的标志。 int deleteP (List list, position p )在list中删除位置为p的元素,并返回插入成功与否的标志。 Position locate ( List list, DataType x )在list中查找值为x的元素的位置。 int isNull ( List list )判别list是否为空线
21、性表。 end ADT List,51,ListEmpty( L ) 判定线性表是否为空表。初始条件:线性表L已存在。操作结果:若 L 为空表,则返回 TRUE,否则返回 FALSE。 ListLength( L ) 求得线性表的长度,即线性表中所含数据元素的个数。 初始条件:线性表 L 已存在。操作结果:返回 L 中元素个数。PriorElem( L, cur_e, &pre_e )初始条件:线性表 L 已存在。操作结果:若 cur_e 是 L 中的数据元素,则用 pre_e 返回它的前驱,否则操作失败,pre_e 无定义。 若 cur_e 是线性表 L 中第一个数据元素,则它的前驱 pre
22、_e 为“空元素“。 NextElem( L, cur_e, &next_e )初始条件:线性表 L 已存在。操作结果:若 cur_e 是 L 中的数据元素,则用 next_e 返回它的后继,否则操作失败,next_e 无定义。若 cur_e 是线性表 L 中最后一个数据元素,则它的后继 next_e 为“空元素“。 GetElem( L, i, &e )初始条件:线性表 L 已存在,1iLengthList(L)。操作结果:用 e 返回 L 中第 i 个元素的值。此操作的结果是求得线性表 L 中和位序 i 相对应的数据元素,因此,只有当 i 的值在线性表的长度范围内才有意义。 LocateE
23、lem( L, e, compare( ) )初始条件:线性表 L 已存在,compare( ) 是元素判定函数。操作结果:返回 L 中第1个与 e 满足关系 compare( ) 的元素的位序。若这样的元素不存在,则返回值为0。,线性表的抽象数据类型(引用型操作),52,线性表的抽象数据类型(加工型操作),ClearList( &L )初始条件:线性表 L 已存在。操作结果:将 L 重置为空表。值得注意的是,在进行了 DestroyList(L) 操作之后,线性表 L 不再存在,即不能在以后的程序中再引用它,而在对线性表L进行 ClearList(L) 操作之后,仅是删除表中所有元素,在以后
24、的程序中仍可对它进行某些“合法“操作,如判空、插入等。PutElem( &L, i, &e )初始条件:线性表L已存在,1iLengthList(L)。操作结果:L 中第 i 个元素赋值同 e 的值。和GetElem操作相同,i 的值必须在线性表的长度范围内。 ListInsert( &L, i, e )初始条件:线性表 L 已存在,1iLengthList(L)+1。操作结果:在 L 的第 i 个元素之前插入新的元素 e,L 的长度增1。可在线性表中任意一个元素之前插入一个新的数据元素,i=1 意为在第一个元素之前插入一个新的数据元素,i=LengthList(L)+1 则为在最后一个元素之
25、后插入一个新的数据元素。换句话说,操作结果是使新插入的数据元素成为插入之后的线性表中第 i 个数据元素,显然,插入位置 i 的合法值应为1iLengthList(L)+1。ListDelete( &L, i, &e )初始条件:线性表 L 已存在且非空,1iLengthList(L)。操作结果:删除 L 的第 i 个元素,并用 e 返回其值,L 的长度减1。,53,线性表的抽象数据类型的要点,能体现线性表的逻辑结构所应表达的操作特性 所有操作不改变该抽象数据类型的基本特性 与具体实现方法无关,54,线性表类型定义(C+样例),55,线性表类型头文件声明(简版),56,线性表操作 函数定义,57,主函数应用,58,作业:,阅读理解p19 p39 阅读理解程序: 预备的上机题 扩展的思考题,