数据结构与算法 基本概念
数据(Data) :是客观事物的符号表示。在计算机科学中指的是所有能输入到计算机中并被计算机程序处理的符号的总称。
数据元素(Data Element) :是数据的基本单位,在程序中通常作为一个整体来进行考虑和处理。
一个数据元素可由若干个数据项(Data Item)组成。数据项是数据的不可分割的最小单位。数据项是对客观事物某一方面特性的数据描述。
数据对象(Data Object):是性质相同的数据元素的集合,是数据的一个子集。如字符集合C={’A’,’B’, ‘C’,…} 。
数据的逻辑结构 数据结构(Data Structure):是指相互之间具有(存在)一定联系(关系)的数据元素的集合。元素之间的相互联系(关系)称为逻辑结构。数据元素之间的 逻辑结构 有四种基本类型,
集合 :结构中的数据元素除了“同属于一个集合”外,没有其它关系。
线性结构 :结构中的数据元素之间存在一对一的关系。
树型结构 :结构中的数据元素之间存在一对多的关系。
图状结构 :结构中的数据元素之间存在多对多的关系。
数据的存储结构
顺序存储结构 :用数据元素在存储器中的相对位置来表示数据元素之间的逻辑结构(关系)。
链式存储结构 :在每一个数据元素中增加一个存放另一个元素地址的指针(pointer),用该指针来表示数据元素之间的逻辑结构(关系)。
抽象数据类型ADT
算法与数据分析 常量($c$) < 对数($\log_2n$) <$\log_2^2n$ < 线性($n$) < $nlogn$ < 平方($n^2$) <立方($n^3$) < 指数($2^n$) < 阶乘($n!$)
线性表 顺序表 顺序表定义 1 2 3 4 5 6 #define MAXSIZE 100 typedef struct { ElemType *elem; int length; int listsize; }SqList;
初始化顺序表 使用引用
1 2 3 4 5 6 7 Status InitList_Sq (SqList &L) { L.elem=new ElemType[MAXSIZE]; if (!L.elem) exit (OVERFLOW); L.length=0 ; L.listsize=MAXSIZE; return OK; }
使用指针
1 2 3 4 5 6 7 Status InitList_Sq (SqList *L) { L-> elem=new ElemType[MAXSIZE]; if (! L-> elem) exit (OVERFLOW); L-> length=0 ; L-> listsize=MAXSIZE; return OK; }
销毁顺序表 1 2 3 4 5 6 7 8 9 Status DestroyList (SqList &L) { if (L.elem) { delete L.elem; L.elem=NULL ; L.length=0 ; L.listsize=0 ; } return OK; }
清空顺序表 1 2 3 4 5 6 Status ClearList (SqList &L) { L.length=0 ; return OK; }
求顺序表长度 1 2 3 4 5 Status ClearList (SqList &L) { L.length=0 ; return OK; }
判断顺序表是否为空 1 2 3 4 5 int IsEmpty (SqList L) { if (L.length==0 ) return 1 ; else return 0 ; }
获取顺序表的指定元素位置 1 2 3 4 5 int IsEmpty (SqList L) { if (L.length==0 ) return 1 ; else return 0 ; }
查找指定值的元素位置 1 2 3 4 5 6 int LocateElem (SqList L,ElemType e) { for (i=0 ;i< L.length;i++) if (L.elem[i]==e) return i+1 ; return 0 ; }
在顺序表中插入元素 插入算法主要耗时在移动元素上
1 2 3 4 5 6 7 8 9 Status ListInsert_Sq (SqList &L,int i ,ElemType e) { if (i<1 || i>L.length+1 ) return ERROR; if (L.length==L.listsize) return ERROR; for (j=L.length-1 ;j>=i-1 ;j--) L.elem[j+1 ]=L.elem[j]; L.elem[i-1 ]=e; L.length++; return OK; }
在线性表中删除元素 同样耗时在移动元素上
1 2 3 4 5 6 7 8 Status ListDelete_Sq (SqList &L,int i,ElemType &e) { if ((i<1 )||(i>L.length)) return ERROR; e=L.elem[i-1 ]; for (j=i;j<=L.length-1 ;j++) L.elem[j-1 ]=L.elem[j]; --L.length; return OK; }
顺序表优缺点 优点:
存储密度大(结点本身所占存储量/结点结构所占存储量)
可以随机存取表中任一元素
缺点:
在插入、删除某一元素时,需要移动大量元素
浪费存储空间
属于静态存储形式,数据元素的个数不能自由扩充
随机存取法
链表 头结点
使用头节点的好处:
便于首元结点的处理 首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理;
便于空表和非空表的统一处理 无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。
顺序存储法
链表优缺点 优点:
数据元素的个数可以自由扩充
插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高
缺点:
存储密度小
存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问(顺藤摸瓜)
单链表定义 1 2 3 4 5 typedef struct LNode { ElemType data; struct LNode *next ; }LNode,*LinkList;
初始化 1 2 3 4 5 6 Status InitList_L (LinkList &L) { L=new LNode; if (!L) exit (OVERFLOW); L->next=NULL ; return OK; }
销毁 1 2 3 4 5 6 7 8 9 10 Status DestroyList_L (LinkList &L) { LinkList p; while (L) { p=L; L=L->next; delete p; } return OK; }
或者
1 2 3 4 5 6 7 8 9 LinkList p = L; while (p){ q = p->next; delete p; p=q; } L=NULL ; return OK; }
清空 1 2 3 4 5 6 7 8 9 Status ClearList (LinkList &L) { LinkList p; while (L->next) { p=L->next; L->next =p->next; delete p; } return OK; }
或者
1 2 3 4 5 6 7 8 LinkList p = L->next, q; while (p){ q=p->next; delete p; p=q;} L->next=NULL ; return OK; }
求表长 1 2 3 4 5 6 7 8 9 10 int ListLength_L (LinkList L) { LinkList p; p=L->next; j=0 ; while (p){ j++; p=p->next; } return j; }
判断表是否为空 1 2 3 4 5 6 7 int ListEmpty (LinkList L) { if (L->next) return 0 ; else return 1 ; }
获取表中第n个元素 1 2 3 4 5 6 7 8 9 Status GetElem_L (LinkList L,int i,ElemType &e) { p=L->next;j=1 ; while (p&&j<i){ p=p->next; ++j; } if (!p || j>i)return ERROR; e=p->data; return OK; }
获取表中值为e的元素 1 2 3 4 5 6 LNode *LocateElem_L (LinkList L,Elemtype e) { p=L->next; while (p &&p->data!=e) p=p->next; return p; }
插入元素 1 2 3 4 5 6 7 8 9 10 Status ListInsert_L (LinkList &L,int i,ElemType e) { p=L;j=0 ; while (p&&j<i−1 ){p=p->next;++j;} if (!p||j>i−1 )return ERROR; s=new LNode; s->data=e; s->next=p->next; p->next=s; return OK; }
若无头节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Status ListInsert_L (LinkList &L,int i,ElemType e) { if (!L&&i!=1 ) return ERROR; if (i==1 ) { s=new LNode; s->data=e; s->next=L; L=s; return OK;} p=L;j=1 ; while (p&&j<i−1 ){p=p->next;++j;} if (!p||j>i−1 )return ERROR; s=new LNode; s->data=e; s->next=p->next; p->next=s; return OK; }
删除元素 1 2 3 4 5 6 7 8 9 10 11 Status ListDelete_L (LinkList &L,int i,ElemType &e) { p=L;j=0 ; while (p->next &&j<i-1 ){ p=p->next; ++j; } if (!(p->next)||j>i-1 ) return ERROR; q=p->next; p->next=q->next; e=q->data; delete q; return OK; }
若无头结点
1 2 3 4 5 6 7 8 9 10 11 12 Status ListDelete_L (LinkList &L,int i,ElemType &e) { if (!L) return ERROR; if (i==1 ) {p=L; L=L->next; delete p; return OK;} p=L;j=1 ; while (p->next &&j<i-1 ){p=p->next; ++j; } if (!(p->next)||j>i-1 ) return ERROR; q=p->next; p->next=q->next; e=q->data; delete q; return OK; }
建立链表 前插法
1 2 3 4 5 6 7 8 9 void CreateList_F (LinkList &L,int n) { L=new LNode; L->next=NULL ; for (i=n;i>0 ;--i){ p=new LNode; cin >>p->data; p->next=L->next;L->next=p; } }
尾插法
1 2 3 4 5 6 7 8 9 10 11 12 void CreateList_L (LinkList &L,int n) { L=new LNode; L->next=NULL ; r=L; for (i=0 ;i<n;++i){ p=new LNode; cin >>p->data; p->next=NULL ; r->next=p; r=p; } }
有序链表的合并 1 2 3 4 5 6 7 8 void MergeList_L (LinkList &La,LinkList &Lb,LinkList &Lc) { pa=La->next; pb=Lb->next; pc=Lc=La; while (pa && pb){ if (pa->data<=pb->data){ pc->next=pa;pc=pa;pa=pa->next;} else {pc->next=pb; pc=pb; pb=pb->next;} pc->next=pa?pa:pb; delete Lb;
双链表 1 2 3 4 5 typedef struct DuLNode { ElemType data; struct DuLNode *prior ; struct DuLNode *next ; }DuLNode, *DuLinkList
双链表的插入 1 2 3 4 5 6 7 8 9 10 Status ListInsert_DuL (DuLinkList &L,int i,ElemType e) { if (!(p=GetElemP_DuL(L,i))) return ERROR; s=new DuLNode; s->data=e; s->prior=p->prior; p->prior->next=s; s->next=p; p->prior=s; return OK; }
双链表的删除 1 2 3 4 5 6 7 8 Status ListDelete_DuL (DuLinkList &L,int i,ElemType &e) { if (!(p=GetElemP_DuL(L,i))) return ERROR; e=p->data; p->prior->next=p->next; p->next->prior=p->prior; delete p; return OK; }
栈和队列 顺序栈
顺序栈的定义 1 2 3 4 5 6 7 #define MAXSIZE 100 typedef struct { SElemType *base; SElemType *top; int stacksize; }SqStack;
顺序栈的初始化 1 2 3 4 5 6 7 Status InitStack ( SqStack &S ) { S.base =new SElemType[MAXSIZE]; if ( !S.base ) return OVERFLOW; S.top = S.base; S.stackSize = MAXSIZE; return OK; }
判断是否为空 1 2 3 4 5 bool StackEmpty ( SqStack S ) { if (S.top == S.base) return true ; else return false ; }
求顺序栈的长度 1 2 3 4 int StackLength ( SqStack S ) { return S.top – S.base; }
清空顺序栈 1 2 3 4 5 Status ClearStack ( SqStack &S ) { if ( S.base ) S.top = S.base; return OK; }
销毁顺序栈 1 2 3 4 5 6 7 8 Status DestroyStack ( SqStack &S ) { if ( S.base ){ delete S.base ; S.stacksize = 0 ; S.base = S.top = NULL ; } return OK; }
进栈 1 2 3 4 5 6 7 Status Push ( SqStack &S, SElemType e) { if ( S.top - S.base== S.stacksize ) return ERROR; *S.top++=e; return OK; }
出栈 1 2 3 4 5 6 7 Status Pop ( SqStack &S, SElemType &e) { if ( S.top == S.base ) return ERROR; e= *--S.top; return OK; }
取顺序栈栈顶元素 1 2 3 4 5 6 Status GetTop ( SqStack S, SElemType &e) { if ( S.top == S.base ) return ERROR; e = *( S.top – 1 ); return OK; }
链栈 运算受限的单链表,只能在链表头部进行操作,故没有必要附加头结点。栈顶指针就是链表的头指针
定义 1 2 3 4 typedef struct StackNode { SElemType data; struct StackNode *next ; } StackNode, *LinkStack;
初始化 1 2 3 4 void InitStack (LinkStack &S ) { S=NULL ; }
判断是否为空 1 2 3 4 5 6 7 Status StackEmpty (LinkStack S) { if (S == NULL ) { return TRUE; } else { return FALSE; } }
进栈 1 2 3 4 5 6 7 8 9 10 Status Push (LinkStack &S, SElemType e) { p = new StackNode; if (!p) { exit (OVERFLOW); } p->data = e; p->next = S; S = p; return OK; }
出栈 1 2 3 4 5 6 7 8 9 Status Pop (LinkStack &S,SElemType &e) { if (S==NULL ) return ERROR;e = S-> data; p = S; S = S-> next; delete p; return OK; }
取栈顶元素 1 2 3 4 5 6 7 SElemType GetTop(LinkStack S) { if (S == NULL) { exit(1); } else { return S->data; } }
循环队列 定义 1 2 3 4 5 6 #define M 100 Typedef struct { QElemType *base; int front; int rear; }SqQueue;
解决假溢出问题
初始化 1 2 3 4 5 6 Status InitQueue (SqQueue &Q) { Q.base =new QElemType[MAXQSIZE] ; if (!Q.base) exit (OVERFLOW); Q.front=Q.rear=0 ; return OK; }
求循环队列的长度 1 2 3 int QueueLength (SqQueue Q) { return (Q.rear-Q.front+MAXQSIZE)%MAXQSIZE; }
入队 1 2 3 4 5 6 Status EnQueue (SqQueue &Q,QElemType e) { if ((Q.rear+1 )%MAXQSIZE==Q.front) return ERROR; Q.base[Q.rear]=e; Q.rear=(Q.rear+1 )%MAXQSIZE; return OK; }
出队 1 2 3 4 5 6 Status DeQueue (LinkQueue &Q,QElemType &e) { if (Q.front==Q.rear) return ERROR; e=Q.base[Q.front]; Q.front=(Q.front+1 )%MAXQSIZE; return OK; }
链队列
定义 1 2 3 4 5 6 7 8 typedef struct QNode { QElemType data; struct Qnode *next ; }Qnode, *QueuePtr; typedef struct { QueuePtr front; QueuePtr rear; }LinkQueue;
初始化 1 2 3 4 5 6 Status InitQueue (LinkQueue &Q) { Q.front=Q.rear=(QueuePtr) malloc (sizeof (QNode)); if (!Q.front) exit (OVERFLOW); Q.front->next=NULL ; return OK; }
销毁链队列 1 2 3 4 5 6 Status InitQueue (LinkQueue &Q) { Q.front=Q.rear=(QueuePtr) malloc (sizeof (QNode)); if (!Q.front) exit (OVERFLOW); Q.front->next=NULL ; return OK; }
判断链队列是否为空 1 2 3 Status QueueEmpty (LinkQueue Q) { return (Q.front==Q.rear); }
求链队列对头元素 1 2 3 4 5 Status GetHead (LinkQueue Q, QElemType &e) { if (Q.front==Q.rear) return ERROR; e=Q.front->next->data; return OK; }
链队列入队 1 2 3 4 5 6 7 8 Status EnQueue (LinkQueue &Q,QElemType e) { p=(QueuePtr)malloc (sizeof (QNode)); if (!p) exit (OVERFLOW); p->data=e; p->next=NULL ; Q.rear->next=p; Q.rear=p; return OK; }
不带头结点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Status EnQueue (LinkQueue &Q, QElemType e) { p = (QueuePtr)malloc (sizeof (QNode)); if (!p) { exit (OVERFLOW); } p->data = e; p->next = NULL ; if (Q.front == NULL ) { Q.front = p; Q.rear = p; return OK; } Q.rear->next = p; Q.rear = p; return OK; }
带头结点的链队的入队操作,只要把新生成的结点加到尾结点后即可
而不带头结点的操作则还要注意到边界操作,假如是第一次入队,需修改队头指针。
同样的道理,对于出队操作,假如是最后一个结点出队,需要注意修改队尾指针
由此,我们建议链式队列好采用带头结点的实现方式。
链队列出队 1 2 3 4 5 6 7 8 9 Status DeQueue (LinkQueue &Q,QElemType &e) { if (Q.front==Q.rear) return ERROR; p=Q.front->next; e=p->data; Q.front->next=p->next; if (Q.rear==p) Q.rear=Q.front; free (p); return OK; }
多维数组
分治算法
分治算法的适用条件
该问题规模缩小到一定的程度就可以容易地解决
该问题可以分解为若干个规模较小的相同问题,即该问题具有递归子结构性质
利用该问题分解出的子问题的解可以合并为该问题的解
该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题
分治算法的复杂性分析
排序和搜索 合并排序 主要思想:
将数组分成均匀两半(divide).
递归解决每个半部分(sort).
把两部分合并起来组成一个新的排序(merge).
算法实现
合并函数Merge的实现
合并排序的计算时间 递归树展开
直接展开
数学归纳
快速排序 基本思想
算法实现
快速排序的计算时间
二分搜索/折半查找 算法和时间复杂度
递归式求解 三种常用方法:
主方法
主定理
举例
特殊情况
查找 顺序查找
改进:把待查关键字key存入表头(“哨兵”),从后向前逐个比较,可免去查找过程中每一步都要检测是否查找完毕,加快速度。
1 2 3 4 5 6 7 8 int Search_Seq ( SSTable ST , KeyType key ) { ST.R[0 ].key =key; for ( i=ST.length; ST.R[ i ].key!=key; - - i ); return i; }
空间复杂度:一个辅助空间。
时间复杂度:
查找成功时的平均查找长度 设表中各记录查找概率相等 $ASL_s(n)=(1+2+ … +n)/n =(n+1)/2$
查找不成功时的平均查找长度 $ASL_f =n+1$
折半查找 1 2 3 4 5 6 7 8 9 10 int Search_Bin (SSTable ST,KeyType key) { low=1 ;high=ST.length; while (low<=high){ mid=(low+high)/2 ; if (key==ST.R[mid].key) return mid; else if (key<ST.R[mid].key) high=mid-1 ; else low=mid+1 ; } return 0 ; }
递归方式
1 2 3 4 5 6 7 8 9 int Search_Bin (SSTable ST, keyType key, int low, int high) { if (low>high) return 0 ; mid=(low+high)/2 ; if (key等于ST.elem[mid].key) return mid; else if (key小于ST.elem[mid].key) …….. else ……. }
查找过程:每次将待查记录所在区间缩小一半,比顺序查找效率高,时间复杂度$O(\log_2n)$
适用条件:采用顺序存储结构的有序表,不宜用于链式结构
索引表的查找
分块查找 分块有序,即分成若干子表,要求每个子表中的数值都比后一块中数值小(但子表内部未必有序 )。 然后将各子表中的最大关键字构成一个索引表,表中还要包含每个子表的起始地址(即头指针) 。
分块查找过程
对索引表使用折半查找法 (因为索引表是有序表);
确定了待查关键字所在的子表后,在子表内采用顺序查找法 (因为各子表内部是无序表);
分块查找性能分析
分块查找优缺点 优点: 插入和删除比较容易,无需进行大量移动。
缺点: 要增加一个索引表的存储空间并对初始索引表进行排序运算。
适用情况: 如果线性表既要快速查找又经常动态变化,则可采用分块查找。
哈希表查找 有关术语 哈希方法: 选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放; 查找时,由同一个函数对给定值k计算地址,将k与地址单元中元素关键码进行比较,确定查找是否成功。
哈希函数: 哈希方法中使用的转换函数
冲突不可避免,哈希方法一般采用除留余数法。
哈希表的查找
哈希表的查找效率分析
ASL与装填因子$\alpha$有关!既不是严格的$O(1)$,也不是$O(n)$
哈希表的平均查找长度是装填因子$\alpha$的函数,而不是 $n$ 的函数。
这说明,用哈希表构造查找表时,可以选择一个适当的装填因子 $\alpha$,使得平均查找长度限定在某个范围内。
几点结论
对哈希表技术具有很好的平均性能,优于一些传统的技术
链地址法优于开放地址法
除留余数法作哈希函数优于其它类型函数
排序 排序算法的好坏如何衡量?
时间效率——排序速度(比较次数与移动次数)
空间效率——占内存辅助空间的大小
稳定性——A和B的关键字相等,排序后A、B的先后次序保持不变,则称这种排序算法是稳定的。否则就是不稳定的。
插入排序
直接插入排序是最简单的排序法
直接插入排序
1 2 3 4 5 6 7 8 9 10 11 void InsertSort (SqList &L) {int i,j; for (i=2 ;i<=L.length;++i) if ( L.r[i].key<L.r[i-1 ].key) { L.r[0 ]=L.r[i]; L.r[i]=L.r[i-1 ]; for (j=i-2 ; L.r[0 ].key<L.r[j].key;--j) L.r[j+1 ]=L.r[j]; L.r[j+1 ]=L.r[0 ]; } }
时间复杂度为 $O(n^2)$ 空间复杂度为 $O(1)$ 是一种稳定 的排序方法
折半查找 前面的ppt好像讲过了?
1 2 3 4 5 6 7 8 9 10 11 12 13 Status BInsertSort ( SqList &L ) { for ( i = 2 ; i <= L.length ; ++i ) { L.r[0 ] = L.r[i]; low = 1 ; high = i-1 ; while ( low <= high ) { m = ( low + high ) / 2 ; if ( LT( L.r[0 ].key , L.r[m]. key ) ) high = m -1 ; else low = m + 1 ; } for ( j=i-1 ; j>=high+1 ; - - j ) L.r[j+1 ] = L.r[j]; L.r[high+1 ] = L.r[0 ]; } }
折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快
当 n 较大时,总关键码比较次数比直接插入排序的最坏情况要好得多,但比其最好情况要差 在对象的初始排列已经按关键码排好序或接近有序时,直接插入排序比折半插入排序执行的关键码比较次数要少 折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列
减少了比较次数,但没有减少移动次数 平均性能优于直接插入排序
时间复杂度为 $O(n^2)$ 空间复杂度为 $O(1)$ 是一种稳定 的排序方法
希尔排序 先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
1 2 3 4 5 6 void ShellSort (SqList &L,int delta[ ],int t) { for (k=0 ;k<t;++k) ShellInsert(L,delta[k]); }
交换排序 两两比较,如果发生逆序则交换,直到所有记录都排好序为止。
冒泡排序$O(n^2)$ 快速排序$O( n\log_2n )$
冒泡排序 1 2 3 4 5 6 7 8 9 10 11 12 13 void bubble_sort (SqList &L) { int m,i,j,flag=1 ; RedType x; m=n-1 ; while ((m>0 )&&(flag==1 )) { flag=0 ; for (j=1 ;j<=m;j++) if (L.r[j].key>L.r[j+1 ].key) { flag=1 ; x=L.r[j];L.r[j]=L.r[j+1 ];L.r[j+1 ]=x; } m--; } }
时间复杂度为 $O(n^2)$ 空间复杂度为 $O(1)$ 是一种稳定 的排序方法
快速排序 前面也讲过了
①每一趟的子表的形成是采用从两头向中间交替式逼近法;
②由于每趟中对各子表的操作都相似,可采用递归算法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void QSort ( SqList &L,int low, int high ) { if ( low < high ) { pivotloc = Partition(L, low, high ) ; Qsort (L, low, pivotloc-1 ) ; Qsort (L, pivotloc+1 , high ) } } int Partition ( SqList &L,int low, int high ) { L.r[0 ] = L.r[low]; pivotkey = L.r[low].key; while ( low < high ) { while ( low < high && L.r[high].key >= pivotkey ) --high; L.r[low] = L.r[high]; while ( low < high && L.r[low].key <= pivotkey ) ++low; L.r[high] = L.r[low]; } L.r[low]=L.r[0 ]; return low; }
可以证明,平均计算时间是$O(n\log_2n)$。 实验结果表明:就平均计算时间而言,快速排序是我们所讨论的所有内排序方法中最好的一个。 快速排序是递归的,需要有一个栈存放每层递归调用时参数(新的low和high)。 最大递归调用层次数与递归树的深度一致,因此,平均情况下空间复杂度为$O(\log_2n)$,最坏情况下为$O(n)$ 。
时间复杂度:$O(n^2) $ 平均时间效率:$O(n\log_2n) $ 空间效率:$O(n)$—递归要用到栈空间 稳 定 性: 不稳定 —可选任一元素为支点。
归并排序 排序过程 初始序列看成n个有序子序列,每个子序列长度为1 两两合并,得到$\lfloor$n/2$\rfloor$个长度为2或1的有序子序列 再两两合并,重复直至得到一个长度为n的有序序列为止
时间效率:$O(n\log_2n) $ 空间效率:$O(n)$ 稳 定 性:稳定
选择排序 前面讲过了
每一趟在后面 $n-i +1$个中选出关键码最小的对象, 作为有序序列的第$ i $个记录
1 2 3 4 5 6 7 8 9 10 void SelectSort (SqList &K) { for (i=1 ; i<L.length; ++i) { k=i; for ( j=i+1 ;j<=L.length ; j++) if ( L.r[j].key <L.r[k].key) k=j; if (k!=i)L.r[i]←→L.r[k]; } }
时间复杂度:$O(n²)$ 空间复杂度:$O(1)$ 不稳定
堆排序 堆的定义
基本思想
有n 个结点的完全二叉树,最后一个分支结点的标号是$\lfloor$$\frac{n}{2}$$\rfloor$
将根结点r[1]与左、右子树根结点比较,并与大者交换 重复直至叶子结点,得到新的堆
时间效率:$O(n\log_2n) $ 空间效率:$O(1)$ 稳 定 性:不稳定 适用于$n$ 较大的情况
树和二叉树 二叉树是有序树(子树有序,不能颠倒)
性质1: 在二叉树的第i层上至多有$2^i-1$个结点
性质2: 深度为k的二叉树至多有$2^k-1$个结点
性质3: 对于任何一棵二叉树,若2度的结点数有$n_2$个,则叶子数$n_0$必定为$n_2+1 $(即$n_0=n_2+1$)
性质4: 具有n个结点的完全二叉树的深度必为$⌊log2n⌋+1$
性质5: 对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,其右孩子编号必为2i+1;其双亲的编号必为i/2。
满二叉树和完全二叉树
满二叉树是叶子一个也不少的树,而完全二叉树虽然前n-1层是满的,但最底层却允许在右边缺少连续若干个结点。满二叉树是完全二叉树的一个特例。
对n个元素组成的有序顺序表进行折半查找时,查找成功时的比较次数最多为$⌊log2n⌋+1$
二叉链表 1 2 3 4 typedef struct BiNode { TElemType data; struct BiNode *lchild ,*rchild ; }BiNode,*BiTree;
1 2 typedef struct TriTNode { TelemType data; struct TriTNode *lchild ,*parent ,*rchild ; }TriTNode,*TriTree;
遍历二叉树 先序
1 2 3 4 5 6 7 8 Status PreOrderTraverse (BiTree T) { if (T==NULL ) return OK; else { cout <<T->data; PreOrderTraverse(T->lchild); PreOrderTraverse(T->rchild); } }
中序
1 2 3 4 5 6 7 8 Status InOrderTraverse (BiTree T) { if (T==NULL ) return OK; else { InOrderTraverse(T->lchild); cout <<T->data; InOrderTraverse(T->rchild); } }
后序
1 2 3 4 5 6 7 8 Status PostOrderTraverse (BiTree T) { if (T==NULL ) return OK; else { PostOrderTraverse(T->lchild); PostOrderTraverse(T->rchild); cout <<T->data; } }
效率
时间效率:O(n) //每个结点只访问一次
空间效率:O(n) //栈占用的最大辅助空间
重要结论 若二叉树中各结点的值均不相同,则: 由二叉树的前序序列+中序序列,或由其后序序列+中序序列均能唯一地确定一棵二叉树, 但由前序序列+后序序列却不一定能唯一地确定一棵二叉树。
应用 计算结点总数
1 2 3 4 5 6 7 8 int NodeCount (BiTree T) { if (T==NULL ) return 0 ; else return 1 + NodeCount(T- >lchild) + NodeCount(T->rchild); }
计算叶子结点总数
1 2 3 4 5 6 7 8 9 10 11 int LeafCount (BiTree T) { if (T==NULL ) return 0 ; else if (T->lchild==NULL &&T->rchild==NULL ) return 1 ; 右孩子都为空),若是则返回1 else return LeafCount(T->lchild) + LeafCount(T->rchild); }
计算树的高度
1 2 3 4 5 6 7 8 9 10 11 int Height (BiTree T) { if (T==NULL ) return 0 ; else { int hl = Height(T>lchild), int hr = Height(T->rchild); return (hl ≥ hr) ? (1 + hl): (1 + hr); } }
层次遍历算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void LayeredTraverse (BiTree T) { cout <<T->data; InitQueue(Q); EnQueue(Q, T); while (!QueueEmpty(Q)){ DeQueue(Q, p); if (p->lchild!=NULL ){ cout <<p->lchild->data; EnQueue(Q, p->lchild); } if (p->rchild!=NULL ){ cout <<p->rchild->data; EnQueue(Q, p->rchild); } } }
线索二叉树 LTag :若 LTag=0, lchild域指向左孩子;
若 LTag=1, lchild域指向其前驱。
RTag :若 RTag=0, rchild域指向右孩子;
若 RTag=1, rchild域指向其后继。
区分前驱后继分别为前序中序后序
线索:指向结点前驱和后继的指针 线索链表:加上线索二叉链表 线索二叉树:加上线索的二叉树(图形式样) 线索化:对二叉树以某种次序遍历使其变为线索二叉树的过程
避免指针域悬空,让空域指向根结点
树的存储结构 双亲表示法
1 2 3 4 5 6 7 8 9 typedef struct PTNode { DataType data; int parent; } PTNode; typedef struct PTree { PTNode nodes[MAXSIZE]; int r, n; } PTree;
孩子表示法
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct CTNode { int child; struct CTNode *next ; }CTNode; typedef struct CTBox { DataType data; CTNode *firstchild; }CTBox; typedef struct CTree { CTBox nodes[MAXSIZE]; int n,r; } CTree;
双亲孩子表示法
孩子兄弟表示法 1 2 3 4 typedef struct CSNode { ElemType data; struct CSNode *firstchild ,*nextsibling ; }CSNode,*CSTree;
树的存储结构——孩子兄弟表示法 也称为二叉树表示法、二叉链表表示法
树和森林 树、森林和二叉树的相互转换
二叉树的应用
二叉排序树 二叉排序树或是空树,或是满足如下性质的二叉树:
若其左子树非空,则左子树上所有结点的值均小于根结点的值;
若其右子树非空,则右子树上所有结点的值均大于等于根结点的值;
其左右子树本身又各是一棵二叉排序树
二叉排序树的查找 1 2 3 4 5 BSTree SearchBST (BSTree T, KeyType key) { if ((!T) || key==T->data.key) return T; else if (key<T->data.key) return SearchBST(T->lchild,key); else return SearchBST(T->rchild,key); }
平均查找长度和二叉树的形态有关,即, 最好:$\log_2n$(形态匀称,与二分查找的判定树相似) 最坏: $(n+1)/2$(单支树)
二叉排序树的生成就不讲了,主要讲讲删除
二叉排序树的删除
平衡二叉树 根结点的左、右子树深度之差的绝对值≤ 1 左、右子树是平衡二叉树;
平衡因子:该结点左子树与右子树的高度差
对于一棵有n个结点的AVL树,其高度保持在$O(\log_2n)$数量级,ASL也保持在$O(\log_2n)$量级。
结点插入的平衡调整
结点删除的平衡调整
练习
判断平衡二叉树 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 typedef struct BiNode { TElemType data; struct BiNode *lchild ,*rchild ; }BiNode,*BiTree; bool isBalanced (BiTree T) bool isBalanced (BiTree T) { return height(T) >= 0 ; } int dfs (TreeNode* root) { if (T == null) { return 0 ; } int left_height = dfs(T->lchild); int right_height = dfs(T->rchild); if (abs (left_height - right_height) > 1 || left_height == -1 || right_height == -1 ) { return -1 ; } return 1 + max(left_height, right_height); } }
霍夫曼树 关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀-前缀编码
一棵有n个叶子结点的Huffman树有 $2n-1$个结点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 typedef struct { int weight; int parent,lch,rch; } HuffmanNode, *HuffmanTree; 算法 void CreatHuffmanTree (HuffmanTree HT,int n){ if(n<=1)return; m=2*n-1; HT=new HTNode[m+1];//0号单元未用,HT[m]表示根结点 for(i=1;i<=m;++i) {HT[i].lch=0;HT[i].rch=0;HT[i].parent=0;} for(i=1;i<=n;++i)cin>>HT[i].weight; for( i=n+1;i<=m;++i) //构造 Huffman树 { Select(HT, i-1, &s1, &s2); //在HT[k](1≤k≤i-1)中选择两个其双亲域为0, // 且权值最小的结点, // 并返回它们在HT中的序号s1和s2 HT[s1].parent=i; HT[s2] .parent=i; //表示从F中删除s1,s2 HT[i].lch=s1; HT[i].rch=s2 ; //s1,s2分别作为i的左右孩子 HT[i].weight=HT[s1].weight + HT[s2].weight; //i 的权值为左右孩子权值之和 } }
生成霍夫曼编码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void CreatHuffmanCode (HuffmanTree HT, HuffmanCode &HC, int n) {HC=new char *[n+1 ]; cd=new char [n]; cd[n-1 ]=’\0 ’; for (i=1 ; i<=n; ++i){ start=n-1 ; c=i; f=HT[i].parent; while (f!=0 ){ --start; if (HT[f].lchild= =c) cd[start]=’0 ’; else cd[start]=’1 ’; c=f; f=HT[f].parent; } HC[i]= new char [n-start]; strcpy (HC[i], &cd[start]); } delete cd; }
图与贪心算法 矩阵表示 邻接矩阵表示 1 2 3 4 5 6 7 8 9 10 //用两个数组分别存储顶点表和邻接矩阵 #define MaxInt 32767 //表示极大值,即∞ #define MVNum 100 //最大顶点数 typedef char VerTexType; //假设顶点的数据类型为字符型 typedef int ArcType; //假设边的权值类型为整型 typedef struct{ VerTexType vexs[MVNum]; //顶点表 ArcType arcs[MVNum][MVNum]; //邻接矩阵 int vexnum,arcnum; //图的当前点数和边数 }AMGraph;
用邻接矩阵创建无向网络 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Status CreateUDN (AMGraph &G) { cin >>G.vexnum>>G.arcnum; for (i = 0 ; i<G.vexnum; ++i) cin >>G.vexs[i]; for (i = 0 ; i<G.vexnum;++i) for (j = 0 ; j<G.vexnum;++j) G.arcs[i][j] = MaxInt; for (k = 0 ; k<G.arcnum;++k){ cin >>v1>>v2>>w; i = LocateVex(G, v1); j = LocateVex(G, v2); G.arcs[j][i] = G.arcs[i][j]; } return OK; } int LocateVex (MGraph G,VertexType u) { int i; for (i=0 ;i<G.vexnum;++i) if (u==G.vexs[i]) return i; return -1 ; }
邻接表表示 空间效率为O(n+2e)。 若是稀疏图(e<<n2),比邻接矩阵表示法O(n2)省空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #define MVNum 100 typedef struct ArcNode { int adjvex; struct ArcNode * nextarc ; OtherInfo info; }ArcNode; ----------------------------------------------------- typedef struct VNode { VerTexType data; ArcNode * firstarc; }VNode; ----------------------------------------------------- typedef struct { VNode vertices[MVNum]; int vexnum, arcnum; }ALGraph;
利用邻接表创建无向网络 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Status CreateUDG (ALGraph &G) { cin >>G.vexnum>>G.arcnum; for (i = 0 ; i<G.vexnum; ++i){ cin >> G.vertices[i].data; G.vertices[i].firstarc=NULL ; } for (k = 0 ; k<G.arcnum;++k){ cin >>v1>>v2; i = LocateVex(G, v1); j = LocateVex(G, v2); p1=new ArcNode; p1->adjvex=j; p1->nextarc= G.vertices[i].firstarc; G.vertices[i].firstarc=p1; p2=new ArcNode; p2->adjvex=i; p2->nextarc= G.vertices[j].firstarc; G.vertices[j].firstarc=p2; } return OK; }
对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)。 邻接矩阵的空间复杂度为O(n2),而邻接表的空间复杂度为O(n+e)。
用途:邻接矩阵多用于稠密图;而邻接表多用于稀疏图
十字链表表示法 1 2 3 4 5 6 7 8 9 10 11 12 typedef struct arcnode //边结点{ int tailvex, headvex; struct arcnode *hlink ;//指向弧头相同的下一条弧 struct arcnode *tlink ; }DGraphEdge; typedef struct dnode //顶点结点{ int data; struct arcnode *firstin ;//指向以该顶点为弧头的第一个弧结点 struct arcnode *firstout ; }DGraph; DGraph g[M];
十字链表 (1) 用于表示有向图 (2) 邻接表和逆邻接表的组合
邻接多重表表示法 1 2 3 4 5 6 7 8 9 10 11 12 typedef struct arcnode //边结点{ int mark; int ivex, jvex; struct arcnode *ilink , *jlink ; 下一条边 }NDGraphEdge; typedef struct dnode //顶点结点{ int data; struct arcnode *firstedge ; }NDGraph; NDGraph G[M];
邻接多重表: (1) 用于表示无向图 (2) 每条边只表示一次的邻接表
图的遍历 DFS算法 邻接矩阵
1 2 3 4 5 6 7 8 9 10 11 12 void DFS (AMGraph G, int v) { cout <<v; visited[v] = true ; for (w = 0 ; w< G.vexnum; w++) if ((G.arcs[v][w]!=0 )&& (!visited[w])) DFS(G, w); } typedef struct { VerTexType vexs[MVNum]; ArcType arcs[MVNum][MVNum]; int vexnum,arcnum; }AMGraph;
邻接表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void DFS (ALGraph G, int v) { cout <<v; visited[v] = true ; p= G.vertices[v].firstarc; while (p!=NULL ){ w=p->adjvex; if (!visited[w]) DFS(G, w); p=p->nextarc; } } typedef struct ArcNode { int adjvex; struct ArcNode * nextarc ; OtherInfo info; }ArcNode; typedef struct VNode { VerTexType data; ArcNode * firstarc; }VNode; typedef struct { VNode vertices[MVNum]; int vexnum, arcnum; }ALGraph;
效率分析 用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在行,时间复杂度为O(n2)。 用邻接表来表示图,需要访问n个头结点和2e 个表结点,时间复杂度为O(n+e)。
结论: 稠密图适于在邻接矩阵上进行深度遍历; 稀疏图适于在邻接表上进行深度遍历
BFS 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void BFS (Graph G, int v) { cout <<v; visited[v] = true ; InitQueue(Q); EnQueue(Q, v); while (!QueueEmpty(Q)){ DeQueue(Q, u); for (w = FirstAdjVex(G, u); w>=0 ; w = NextAdjVex(G, u, w)) if (!visited[w]){ cout <<w; visited[w] = true ; EnQueue(Q, w); } } }
效率分析 如果使用邻接矩阵,则BFS对于每一个被访问到的顶点,都要循环检测矩阵中的整整一行( n 个元素),总时间代价为O(n2)。 用邻接表来表示图,需要访问 n个头结点和2e个表节点,时间复杂度为O(n+e)。
DFS和BFS效率比较 空间复杂度相同,都是O(n)(借用了堆栈或队列);
时间复杂度只与存储结构(邻接矩阵或邻接表)有关,而与搜索路径无关
AOV网和AOE网 ① AOV网(Activity On Vertices)—用顶点表示活动的网络 ② AOE网(Activity On Edges)—用边表示活动的网络
拓扑排序算法的实现 以邻接表作存储结构 把邻接表中所有入度为0的顶点进栈 栈非空时,输出栈顶元素Vj并退栈;在邻接表中查找Vj的直接后继Vk,把Vk的入度减1;若Vk的入度为0则进栈 重复上述操作直至栈空为止。若栈空时输出的顶点个数不是n,则有向图有环;否则,拓扑排序完毕
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 typedef struct node { int vex; struct node *next ; }GraphNode; typedef struct tnode { int vexdata; int in; struct node *link ; }Graph; void toposort (Graph g[],int n) { int top,m,k,j,s[M]; GraphNode *p; top=0 ; m=0 ; for (j=1 ;j<=n;j++) if (g[j].in==0 ) s[top++]=j; while (top>0 ) { j=s[--top]; printf ("%d " ,g[j].vexdata); m++; p=g[j].link; while (p!=NULL ) { k=p->vex; g[k].in--; if (g[k].in==0 ) s[top++]=k; p=p->next; } } printf ("\nm=%d\n" ,m); if (m<n) printf ("The network has a cycle\n" ); }
关键路径 AOE网(Activity On Edge)——也叫边表示活动的网。AOE网是一个带权的有向无环图,其中顶点表示事件,弧表示活动,权表示活动持续时间 起点——表示整个工程的开始点,也称源点 终点——表示整个工程的结束点,也称汇点 路径长度——路径上各活动持续时间之和 关键路径——起点到终点的最长路径 Ve(j)——表示事件Vj的最早发生时间 Vl(j)——表示事件Vj的最迟发生时间 e(k)——表示活动ak的最早开始时间 l(k)——表示活动ak的最迟开始时间 l(k) – e(k)——表示完成活动ak的时间余量 关键活动——关键路径上的活动,即l(k)=e(k)的活动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 Status TopoLogicalOrder ( Graph G, Stack &T) { FindInDegree(G, indegree); Stack S; InitStack(T); count=0 ; ve[0. .G,vexnum-1 ]=0 ; while (!stackEmpty(S)){ Pop(S,j); Push(T,j);++count; for (p=G.vertices[j].firstarc; p; p=p->nextarc){ k=p->adjvex; if (--indegree[k]==0 )Push(S,k); if (ve[j]+*(p->info)>ve[k]) ve[k]=ve[j]+*(p->info); } } if (count<G.vexnum) return ERROR; else return OK; } Status CriticalPath (Graph G) { Stack T; if (!TopoLogicalOrder(G,T) return ERROR; vl[0. .G.vexnum-1 ]=ve[G.vexnum-1 ]; while (!StackEmpty(T)){ for (Popo(T,j), p=G.vertices[j].firstarc; p; p=p->nextarc){ k=p->adjvex; dut=*(p->info); if (vl[k]-dut<vl[j]) vl[j]=vl[k]-dut } } for (j=0 ; j<G.vexnum; ++j){ for (p=G.vertices[j].firstarc; p; p=p->nextarc){ k=p->adjvex; dut=*(p->info); ee=ve[j]; el=vl[k]-dut; tag=(ee==el)?"*" :"" ; printf (j,k,dut,ee,el,tag); } } }
贪心算法 求解分数背包问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static float knapsack (float c, float [] w, float [] v, float [] x) { int n=v.length; Element [] d = new Element [n]; for (int i = 0 ; i < n; i++) d[i] = new Element(w[i],v[i],i); MergeSort.mergeSort(d); int i; float opt=0 ; for (i=0 ;i<n;i++) x[i]=0 ; for (i=0 ;i<n;i++) { if (d[i].w>c) break ; x[d[i].i]=1 ; opt+=d[i].v; c-=d[i].w; } if (i<n){ x[d[i].i]=c/d[i].w; opt+=x[d[i].i]*d[i].v; } return opt; }
对于0-1背包问题,贪心选择之所以不能得到最优解是因为在这种情况下,它无法保证最终能将背包装满,部分闲置的背包空间使每公斤背包空间的价值降低了。事实上,在考虑0-1背包问题时,应比较选择该物品和不选择该物品所导致的最终方案,然后再作出最好选择。由此就导出许多互相重叠的子问题。这正是该问题可用动态规划算法求解的另一重要特征。 实际上也是如此,动态规划算法的确可以有效地解0-1背包问题。
求解活动安排问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static int greedySelector (int [] s, int [] f, boolean a[]) { sort all the job in ascending order of finishing time, and relabel them. int n=s.length-1 ; a[1 ]=true ; int j=1 ; int count=1 ; for (int i=2 ;i<=n;i++) { if (s[i]>=f[j]) { a[i]=true ; j=i; count++; }\\if else a[i]=false ; }\\for return count; }
求解最小生成树 Prime算法: 归并顶点,与边数无关,适于稠密网 Kruskal算法:归并边,适于稀疏网
这个看图掌握一下吧。
效率分析
最短路径问题 一、 单源最短路径—用Dijkstra(迪杰斯特拉)算法 二、所有顶点间的最短路径—用Floyd(弗洛伊德)算法
迪杰斯特拉算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void ShortestPath_DIJ (AMGraph G, int v0) { n=G.vexnum; S[v0]=true ; D[v0]=0 ; for (v = 1 ; v<n; ++v){ S[v] = false ; D[v] = G.arcs[v0][v]; if (D[v]< MaxInt) Path [v]=v0; else Path [v]=-1 ; } for (i=1 ;i<n; ++i){ min= MaxInt; for (w=0 ;w<n; ++w) if (!S[w]&&D[w]<min) {v=w; min=D[w];} S[v]=true ; for (w=0 ;w<n; ++w) if (!S[w]&&(D[v]+G.arcs[v][w]<D[w])){ D[w]=D[v]+G.arcs[v][w]; Path [w]=v; } } }
弗洛伊德算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 采用图的邻接矩阵的存储结构 怎样构造一个图,在此不赘述,直接给出floyd-wallshall算法。 void MDNet::Floyd (CDC *pDC) { typedef vector <int > path; path p[max_vertex_num][max_vertex_num]; int D[max_vertex_num][max_vertex_num]; int i,j,k; for (i=1 ;i<=vex_num;i++) { for (j=1 ;j<=vex_num;j++) { p[i][j].push_back(i); p[i][j].push_back(j); D[i][j]=arcs[i][j]; } } for (k=1 ;k<=vex_num;k++) { for (i=1 ;i<=vex_num;i++) { for (j=1 ;j<=vex_num;j++) { if (i==j)continue ; if (D[i][k]+D[k][j]<D[i][j]) { D[i][j]=D[i][k]+D[k][j]; p[i][j]=p[i][k] for (int m=1 ;m<p[k][j].size();m++) p[i][j].push_back(p[k][j][m]); } } } } for (i=1 ;i<=vex_num;i++) { for (j=1 ;j<=vex_num;j++) { for (k=0 ;k<p[i][j].size();k++) cout <<p[i][j][k]<<" " ; cout <<D[i][j]; cout <<endl ; } } }
算法全局最优解 动态规划 动态规划与分治递归的区别
将待求解问题分解成若干个规模较小的相同类型的子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。
保存所有已解的子问题的答案以备后用。
基本步骤
找出最优解的性质,并刻划其结构特征。
递归地定义最优值。
以自底向上 的方式计算出最优值。
根据计算最优值时得到的信息,构造最优解。
带权重的活动安排问题 不能使用贪心算法求解
p(j)为与活动j兼容的最大活动序号
分为两种情形,$v_j$表示第j个活动的权重
递归求解和动态规划求解对比
时间复杂度为$O(1.618^n)$
因为如下图可算得$\frac{1+\sqrt{5}}{2}\approx1.618$
时间复杂度为$O(2n)$
因为完成两次递归调用填充一个$M[n]$,一共填充$2n$次
时间复杂度为$O(n\log_2n)$
因为快速排序的时间复杂度是$O(n\log_2n)$
动态规划算法的基本要素
最优子结构
重叠子问题
矩阵连乘问题 结构
矩阵乘法满足交换率,故矩阵连乘可能得到不同的结果
动态规划思路
得到递归的关系 $$ m[i,j]=\begin{cases} 0&&&&&&&i=j\ min{m[i,j]+m[k+1,j]+p_{i-1}p_kp_j}&&&&&&&i<j \end{cases} $$
但是递归算法的许多子问题被计算了多次
动态规划算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void matrixChain (int [] p, int [][] m, int [][] s) { int n=p.length-1 ; for (int i = 1 ; i <= n; i++) m[i][i] = 0 ; for (int r = 2 ; r <= n; r++) \\ r 为矩阵链的长度 for (int i = 1 ; i <= n - r+1 ; i++) { \\ i 为首矩阵的序号 int j=i+r-1 ; \\ j 为尾矩阵的序号 m[i][j] = m[i+1 ][j]+ p[i-1 ]*p[i]*p[j]; \\ 首先尝试在矩阵 i 处分开 s[i][j] = i; for (int k = i+1 ; k < j; k++) { \\ 然后尝试在矩阵 k 处分开 int t = m[i][k] + m[k+1 ][j] + p[i-1 ]*p[k]*p[j]; if (t < m[i][j]) { m[i][j] = t; s[i][j] = k; } } } } }
算法的时间空间复杂度 算法复杂度分析:
算法matrixChain的主要计算量取决于算法中对r,i和k的3重循环。循环体内的计算量为$O(1)$,而3重循环的总次数为$O(n^3)$。因此算法的计算时间上界为$O(n^3)$。算法所占用的空间显然为$O(n^2)$。
0-1背包问题 结构
贪心算法可能得不到最优解
正确和错误分析
添加一个w来记录重量限制即可
动态规划求解 1 2 3 4 5 6 7 8 9 10 Input: n, w1,…,wN, v1,…,vN for w = 0 to W M[0 , w] = 0 for i = 1 to n for w = 1 to W if (wi > w) M[i, w] = M[i-1 , w] else M[i, w] = max {M[i-1 , w], vi + M[i-1 , w-wi ]} return M[n, W]
时间复杂度 时间复杂度为 $\Theta(n W)$
最长子序列
结构
递归结构
算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Algorithm lcsLength (x,y,b) 1: m<-x.length; 2 : n<-y.length;3 : m[i][0 ]=0 ; m[0 ][i]=0 ;4 : for (int i = 1 ; i <= m; i++)5 : for (int j = 1 ; j <= n; j++) 6 : if (x[i]==y[j]) 7 : m[i][j]=m[i-1 ][j-1 ]+1 ;8 : b[i][j]=1 ;9 : else if (m[i-1 ][j]>=m[i][j-1 ]) 10 : m[i][j]=m[i-1 ][j];11 : b[i][j]=2 ;12 : else 13 : m[i][j]=c[i][j-1 ];14 : b[i][j]=3 ;
1 2 3 4 5 6 7 8 9 10 11 构造最长公共子序列 Algorithm lcs (int i,int j,char [] x,int [][] b) { if (i ==0 || j==0 ) return ; if (b[i][j]== 1 ){ lcs(i-1 ,j-1 ,x,b); System.out.print(x[i]); } else if (b[i][j]== 2 ) lcs(i-1 ,j,x,b); else lcs(i,j-1 ,x,b); }
时间复杂度 由于每个数组单元的计算耗费$O(1)$时间,算法LCSLength耗时$O(mn)$.
序列对齐 概念
编辑距离
问题结构
动态规划算法
时间复杂度 时间复杂度为$O(n^2)$
参考:
董强老师ppt