算法分析.docx

上传人:b****6 文档编号:8855515 上传时间:2023-02-02 格式:DOCX 页数:25 大小:309.06KB
下载 相关 举报
算法分析.docx_第1页
第1页 / 共25页
算法分析.docx_第2页
第2页 / 共25页
算法分析.docx_第3页
第3页 / 共25页
算法分析.docx_第4页
第4页 / 共25页
算法分析.docx_第5页
第5页 / 共25页
点击查看更多>>
下载资源
资源描述

算法分析.docx

《算法分析.docx》由会员分享,可在线阅读,更多相关《算法分析.docx(25页珍藏版)》请在冰豆网上搜索。

算法分析.docx

算法分析

■算法的定义(举例:

求N个整数的和)

对于计算机科学来说,算法指的是对特定问题求解步骤的一种描述,是若干条指令的有穷序列,且满足算法的特性。

■算法的特性:

输入、输出、确定性、有限性、可行性

■描述方式:

自然语言、图形(流程图)、伪代码、程序设计语言

■算法与程序的区别:

算法是解决特定问题步骤的描述,程序是解决特定问题的功能代码;算法一般不能直接到计算机上执行,程序是可以直接在计算机上执行的;程序是用某种计算机程序设计语言对算法翻译的结果,算法是程序的精髓、灵魂。

程序设计的实质就是构造解决问题的算法。

■算法设计的一般过程:

充分理解要解决的问题、数学模型拟制、算法详细设计、算法描述、算法思路的正确性验证、算法分析、算法的计算机实现和测试、文档资料的编制

■递归:

子程序(或函数)直接调用自己或通过一系列调用语句间接调用自已,称为递归。

直接或间接调用自身的算法称为递归算法。

■算法复杂性:

算法运行时所需要的计算机资源的量

™时间复杂性、空间复杂性

■时间复杂性(T(n))分析方法:

事后统计法、事前分析估算法

■影响时间复杂性的因素:

问题规模n、输入序列I、算法本身A

■影响空间复杂性的因素:

算法本身、输入输出数据、辅助变量

常见几类时间复杂性

O

(1):

常数阶时间复杂性

O(n)、O(n2)、O(n3)、…:

多项式阶时间复杂性

O(2n)、O(n!

)和O(nn):

指数阶时间复杂性

O(nlogn)和O(logn):

对数阶时间复杂性

贪心法:

■基本思想

™从问题的某一个初始解出发,在每一个阶段都根据贪心策略来做出当前最优的决策,逐步逼近给定的目标,尽可能快地求得更好的解。

当达到算法中的某一步不能再继续前进时,算法终止。

■得出的结论

™每个阶段面临选择时,贪心法都做出对眼前来讲是最有利的选择

™选择一旦做出不可更改,即不允许回溯

™根据贪心策略来逐步构造问题的解

基本要素

■最优子结构性质

™一个问题的最优解一定包含其子问题的最优解

™采用反证法证明

■贪心选择性质

™所求问题的整体最优解可以通过一系列局部最优的选择获得,即通过一系列的逐步局部最优选择使得最终的选择方案是全局最优的

例题:

会场安排:

设有11个会议等待安排,用贪心法找出满足目标要求的会议集合。

这些会议按结束时间的非减序排列如表2-1所示。

11个会议按结束时间的非减序排列表:

会议i

1

2

3

4

5

6

7

8

9

10

11

开始时间bi

1

3

0

5

3

5

6

8

8

2

12

结束时间ei

4

5

6

7

8

9

10

11

12

13

14

会议集合为{1,4,8,11}

最短路:

Dijkstra算法思想:

■按各个顶点与源点之间路径长度的递增次序,生成源点到各个顶点的最短路径的方法,即先求出长度最短的一条路径,再参照它求出长度次短的一条路径,依此类推,直到从源点到其它各个顶点的最短路径全部求出为止。

步骤:

■步骤1:

设计合适的数据结构。

带权邻接矩阵C,即如果E,令C[u][x]=的权值,否则,C[u][x]=无穷;采用数组dist来记录从源点到其它顶点的最短路径长度;采用数组p来记录最短路径;

■步骤2:

初始化。

令集合S={u},对于集合V-S中的所有顶点x,设置dist[x]=C[u][x];如果顶点i与源点相邻,设置p[i]=u,否则p[i]=-1;

■步骤3:

在集合V-S中依照贪心策略来寻找使得dist[x]具有最小值的顶点t,t就是集合V-S中距离源点u最近的顶点。

■步骤4:

将顶点t加入集合S中,同时更新集合V-S;

■步骤5:

如果集合V-S为空,算法结束;否则,转步骤6;

■步骤6:

对集合V-S中的所有与顶点t相邻的顶点x,如果dist[x]>dist[t]+C[t][x],则dist[x]=dist[t]+C[t][x]并设置p[x]=t。

转步骤3。

//Dijkstra算法描述

//n:

顶点个数;u:

源点、C[n][n]:

带权邻接矩阵;dist[]:

记录某顶点与源点u的最短

//路径长度;p[]:

记录某顶点到源点的最短路径上的该顶点的前驱顶点。

voidDijkstra(intn,intu,floatdist[],intp[],intC[][])

{

bools[n];//如果s[i]等于true,说明顶点i已加入集合S;否则,顶点i属于集合V-S

for(inti=1;i<=n;i++)

{

dist[i]=C[u][i];//初始化源点u到其它各个顶点的最短路径长度

s[i]=false;

if(dist[i]==)p[i]=-1;//满足条件,说明顶点i与源点u不相邻,设置p[i]=-1

elsep[i]=u;//说明顶点i与源点u相邻,设置p[i]=u

}//for循环结束

dist[u]=0;s[u]=true;//初始时,集合S中只有一个元素:

源点u

for(i=1;i<=n;i++)

{

inttemp=;intt=u;

for(intj=1;j<=n;j++)//在集合V-S中寻找距离源点u最近的顶点t

if((!

s[j])&&(dist[j]

{

t=j;

temp=dist[j];

}

if(t==u)break;//找不到t,跳出循环

s[t]=true;//否则,将t加入集合S

for(j=1;j<=n;j++)//更新与t相邻接的顶点到源点u的距离

if((!

s[j])&&(C[t][j]<)

if(dist[j]>(dist[t]+C[t][j]))

{

dist[j]=dist[t]+C[t][j];

p[j]=t;

}

}

}

哈夫曼编码:

算法的构造思想

■以字符的使用频率做权构建一棵哈夫曼树,然后利用哈夫曼树对字符进行编码,俗称哈夫曼编码。

具体来讲,是将所要编码的字符作为叶子结点,该字符在文件中的使用频率作为叶子结点的权值,以自底向上的方式、通过执行n-1次的“合并”运算后构造出最终所要求的树,即哈夫曼树,它的核心思想是让权值大的叶子离根最近。

■采取的贪心策略:

每次从树的集合中取出双亲为0且权值最小的两棵树作为左、右子树,构造一棵新树,新树根结点的权值为其左右孩子结点权之和,将新树插入到树的集合中。

步骤:

■步骤1:

确定合适的数据结构。

■步骤2:

初始化。

构造n棵结点为n个字符的单结点树集合F={T1,T2,…,Tn},每棵树中只有一个带权的根结点,权值为该字符的使用频率;

■步骤3:

如果F中只剩下一棵树,则哈夫曼树构造成功,转步骤6;否则,从集合F中取出双亲为0且权值最小的两棵树Ti和Tj,将它们合并成一棵新树Zk,新树以Ti为左儿子,Tj为右儿子(反之也可以)。

新树Zk的根结点的权值为Ti与Tj的权值之和;

■步骤4:

从集合F中删去Ti、Tj,加入Zk;

■步骤5:

重复步骤3和4;

■步骤6:

从叶子结点到根结点逆向求出每个字符的哈夫曼编码(约定左分支表示字符“0”,右分支表示字符“1”)。

则从根结点到叶子结点路径上的分支字符组成的字符串即为叶子字符的哈夫曼编码。

算法结束。

//采用极小堆实现的哈夫曼改进算法描述如下:

PHtTreeHuffmane(intn,doublew[n])//构造具有n个叶子结点的哈夫曼树

{

PHtTreepht;PRIORITY_QUEUEpq;inti,p1,p2;

if(n<=1)return;

pht=(PHtTree)malloc(sizeof(structPHtTree));//动态分配哈夫曼树的空间

pht->ht=(structHtNode*)malloc(sizeof(structHtNode)*(2*n-1));

for(i=0;i<2*n-1;i++)//初始化,设置ht数组的初始值

{

pht->ht[i].parent=0;

if(i

pht->ht[i].weight=w[i];

else

pht->ht[i].weight=-1;

}//endfor

pq=creat_pq(n);//创建优先队列pq

for(i=0;i

insert(pht->ht[i].weight,pq);//用n个字符的使用频率来初始化优先队列pq

for(i=0;i

{

p1=p2=0;//结点在优先队列中的下标

p1=delete_min(pq);p2=delete_min(pq);//从优先队列pq中取出权值最小的两个

pht->ht[p1].parent=n+i;pht->ht[p2].parent=n+i;

pht->ht[n+i].weight=pht->ht[p1].weight+pht->ht[p2].weight;

pht->ht[n+i].lchild=p1;pht->ht[n+i].rchild=p2;

insert(pht->ht[n+i].weight,pq);//将新构造的结点插入优先队列pq中

}

pht->root=2*n-2;returnpht;//pht指向根结点

}

 

最小生成树:

Prim:

■算法思想

™设G=(V,E)是无向连通带权图,V={1,2,…,n};设最小生成树T=(U,TE),算法结束时U=V,TEE。

™首先,令U={u0},TE={}。

然后,只要U是V的真子集,就做如下贪心选择:

选取满足条件iU,jV-U,且边(i,j)是连接U和V-U的所有边中的最短边,即该边的权值最小。

然后,将顶点j加入集合U,边(i,j)加入集合TE。

继续上面的贪心选择一直进行到U=V为止,此时,选取到的所有边恰好构成G的一棵最小生成树T。

需要注意的是,贪心选择这一步骤在算法中应执行多次,每执行一次,集合TE和U都将发生变化,即分别增加一条边和一个顶点。

■实现Prim算法的关键

™找到连接U和V-U的所有边中的最短边!

为此必须知道V-U中的每一个顶点与它所连接的U中的每一个顶点的边的信息。

™该信息可通过设置两个数组closest和lowcost来体现。

其中,closest[j]表示V-U中的顶点j在集合U中的最近邻接顶点,lowcost[j]表示V-U中的顶点j到U中的所有顶点的最短边的权值,即边(j,closest[j])的权值。

■步骤:

■步骤1:

确定合适的数据结构。

设置带权邻接矩阵C,bool数组s[],如果s[i]=true,说明顶点i已加入集合U;设置两个数组closest[]和lowcost[];

■步骤2:

初始化。

令集合U={u0},TE={},并初始化数组closest、lowcost和s;

■步骤3:

在集合V-U中寻找使得lowcost具有最小值的顶点t,t就是集合V-U中连接集合U中的所有顶点中最近的邻接顶点;

■步骤4:

将顶点t加入集合U,边(t,closest[t])加入集合TE;

■步骤5:

如果集合V=U,算法结束,否则,转步骤6;

■步骤6:

对集合V-U中的所有顶点k,更新其lowcost和closest,用下面的公式更新:

if(C[t][k]

voidPrim(intn,intu0,intC[n][n])//顶点个数n、开始顶点u0、带权邻接矩阵C[n][n]

{//如果s[i]=true,说明顶点i已加入最小生成树的顶点集合U;否则顶点i属于集合V-U

bools[n];intclosest[n];doublelowcost[n];

s[u0]=1;//初始时,集合U中只有一个元素,即顶点u0

for(inti=0;i

if(i!

=u0)

{

lowcost[i]=C[u0][i];

closest[i]=u0;

s[i]=false;

}

for(i=0;i

{

doubletemp=(1<<30);intt=u0;

for(intj=0;j

if((!

s[j])&&(lowcost[j]

{

t=j;

temp=lowcost[j];

}

if(t==u0)

break;//找不到t,跳出循环

s[t]=true;//否则,将t加入集合U

for(j=0;j

if((!

s[j])&&(C[t][j]

{

lowcost[j]=C[t][j];

closest[j]=t;

}

}

}

Kruskal算法:

■算法思想

™设G=(V,E)是无向连通带权图,V={1,2,…,n};设最小生成树T=(V,TE),该树的初始状态为只有n个顶点而无边的非连通图T=(V,{}),Kruskal算法将这n个顶点看成是n个孤立的连通分支。

它首先将所有的边按权从小到大排序。

然后,只要T中的连通分支数目不为1,就做如下的贪心选择:

在边集E中选取权值最小的边(i,j),如果将边(i,j)加入集合TE中不产生回路(或环),则将边(i,j)加入边集TE中,即用边(i,j)将这两个连通分支合并连接成一个连通分支;否则继续选择下一条最短边。

在这两种情况下,都把边(i,j)从集合E中删去。

继续上面的贪心选择直到T中所有顶点都在同一个连通分支上为止。

此时,选取到的n-1条边恰好构成G的一棵最小生成树T。

步骤:

■步骤1:

初始化。

将图G的边集E中的所有边按权从小到大排序,边集TE={},把每个顶点都初始化为一个孤立的分支,即一个顶点对应一个集合;

■步骤2:

在E中寻找权值最小的边(i,j);

■步骤3:

如果顶点i和j位于两个不同连通分支,则将边(i,j)加入边集TE,并执合并操作将两个连通分支进行合并;

■步骤4:

将边(i,j)从集合E中删去,即E=E-{(i,j)};

■步骤5:

如果连通分支数目不为1,转步骤2;否则,算法结束,生成最小生成树T。

voidKruskal(intn,structedgebian[],doubleC[n][n])//顶点个数n、带权邻接矩阵C[n][n]

{

intnodeset[n];//顶点所属的集合

intcount=1;boolflag[n+1];

if(n==1)return;

for(inti=1;i<=n;i++)

{

nodeset[i]=i;flag[i]=false;

for(intj=1;j<=n;j++)//将图中所有边存于数组bian中

if(C[i][j]<)

{

bian[count].u=i;

bian[count].v=j;

bian[count].weight=C[i][j];

count++;

}

}

sort(bian+1,bian+count);//sort函数将数组bian中的元素按weight的大小进行排列

count=1;intedgeset=0;//存储最小生成树中边的个数

intw=0;//最小生成树的耗费

while(edgeset<(n-1))//不足n-1条边

{

if(!

flag[bian[count].u])&&(flag[bian[count].v]))//u未加入任何集合,v已加入某集合

{

w+=bian[count].weight;edgeset++;

flag[bian[count].u]=true;//将u加入某一集合

nodeset[bian[count].u]=nodeset[bian[count].v];

}

//记录u点所加入的集合

elseif((flag[bian[count].u])&&(!

flag[bian[count].v]))

{

w+=bian[count].weight;edgeset++;flag[bian[count].v]=true;

nodeset[bian[count].v]=nodeset[bian[count].u];

}

elseif((!

flag[bian[count].u])&&(!

flag[bian[count].v]))//u、v均未加入任何集合

{

w+=bian[count].weight;edgeset++;flag[bian[count].u]=true;

flag[bian[count].v]=true;nodeset[bian[count].u]=nodeset[bian[count].v];

}

else//两端顶点都加入了set,判断新加入的边是否使结点形成了环

if(nodeset[bian[count].u]!

=nodeset[bian[count].v])//若无环

{

w+=bian[count].weight;edgeset++;inttmp=nodeset[bian[count].v];

for(inti=1;i<=n;i++)//将两个集合中的元素合到一个集合中

if(nodeset[i]==tmp)nodeset[i]=nodeset[bian[count].u];

}

count++;

}//endwhile

}//endKruskal

Prim和Kruskal算法的比较:

(1)从算法的思想可以看出,如果图G中的边数较小时,可以采用Kruskal,因为Kruskal算法每次查找最短的边;边数较多可以用Prim算法,因为它是每次加一个顶点。

可见,Kruskal适用于稀疏图,而Prim适用于稠密图。

(2)从时间上讲,Prim算法的时间复杂度为O(n2),Kruskal算法的时间复杂度为O(eloge)。

(3)从空间上讲,显然在Prim算法中,只需要很小的空间就可以完成算法,因为每一次都是从个别点开始出发进行扫描的,而且每一次扫描也只扫描与当前顶点集对应的边。

但在Kruskal算法中,因为时刻都得知道当前边集中权值最小的边在哪里,这就需要对所有的边进行排序,对于很大的图而言,Kruskal算法需要占用比Prim算法大得多的空间。

分治法:

■基本思想

™将一个难以直接解决的大问题,分解成一些规模较小的相同问题,以便各个击破,分而治之。

■步骤1:

分解——即将问题分解为若干个规模较小、相互独立、与原问题形式相同的子问题;

■步骤2:

治理

■步骤2-1:

求解各个子问题

■步骤2-2:

合并

例题:

二分查找:

算法思想:

假定元素序列已经由小到大排好序,将有序序列分成规模大致相等的两部分,然后取中间元素与特定查找元素x进行比较,如果x等于中间元素,则算法终止;如果x小于中间元素,则在序列的左半部继续查找,即在序列的左半部重复分解和治理操作;否则,在序列的右半部继续查找,即在序列的右半部重复分解和治理操作。

可见,二分查找算法重复利用了元素间的次序关系。

步骤1:

确定合适的数据结构。

设置数组s[n]来存放n个已排好序的元素;变量low和high表示查找范围在数组中的下界和上界;middle表示查找范围的中间位置;x为特定元素;

步骤2:

初始化。

令low=0;high=n-1;

步骤3:

middle=(low+high)/2,即指示中间元素;

步骤4:

判定low小于等于high是否成立,如果成立,转步骤5;否则,算法结束;

步骤5:

判断x与s[middle]的关系。

如果x==s[middle],算法结束;如果x>s[middle],则令low=middle+1;否则令high=middle-1,转步骤3。

归并排序(合并排序):

算法思想:

合并排序是采用分治策略实现对n个元素进行排序的算法,是分治法的一个典型应用和完美体现。

它是一种平衡、简单的二分分治策略,其计算过程分为三大步:

(1)分解:

将待排序元素分成大小大致相同的两个子序列。

(2)求解子问题:

用合并排序法分别对两个子序列递归地进行排序。

(3)合并:

将排好序的有序子序列进行合并,得到符合要求的有序序列。

voidMerge(intA[],intlow,intmiddle,inthigh){

inti,j,k;

int*B=newint[high-low+1];

i=low;j=middle+1;k=0;

while(i<=middle&&j<=high){

if(A[i]<=A[j])B[k++]=A[i++];

else{

B[k++]=A[j++];

//sum+=(middle-i+1);//求逆序数用这行

}

}

while(i<=middle)

B[k++]=A[i++];

while(j<=high)

B[k++]=A[j++];

for(i=low;i<=high;i++)

A[i]=B[i-low];

deleteB;

}

voidMergeSort(intA[],intlow,inthigh){

intmiddle;

if(low

middle=(low+high)/2;

MergeSort(A,low,middle);

MergeSort(A,middle+1,high);

Merge(A,low,middle,high);

}

}

快速排序:

算法思想:

通过一趟扫描将待排序的元素分割成独立的三个序列:

第一个序列中所有元素均不大于基准元素、第二个

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 考试认证 > 财会金融考试

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1