算法及数据结构(下)

来源:互联网 发布:晓风软件 编辑:程序博客网 时间:2024/05/22 23:59

一、排序算法 

      所谓排序,就是按照某种次序,重新排列某一序列中的所有元素。为此,任意一对元素之间都应该能够比较大小,即在所有元素之间可以定义一个全序关系。排序算法种类繁多,根据其处理数据的规模与存储特点,可分为内部排序和外部排序算法,前者处理的数据规模不大,内存足以容纳,后者处理的数据规模很大,必须将数据存放于外部存储器中,根据输入不同的形式,排序算法可以划分为脱机算法与在线算法,在前一种情况下,待排序的数据是以批处理的形式给出的,而在网络计算之类的环境中,待排序的数据则是实时生成的,在排序算法开始运行时,数据并未完全就绪,而是随着排序算法本身的进行而逐步给出的,针对不同的体系结构,也需要采用不同的排序算法,由此又可以划分为串行和并行两大类排序算法,另外,根据排序算法是否采用随机策略,还有确定式和随机式之分。

      1、冒泡排序
      2、选择排序
      3、插入排序
      4、堆排序
      5、归并排序

       ①分治策略:为了解决一个规模较大的问题,我们可以将其分解为两个子问题,并借助递归分别得到它们的解,然后将子问题的解合并成原问题的解,这就是分治策略。为了保证分治策略的效率,首先必须保证子问题的划分及其解的合并都能快速完成,通常,都要求这两部分计算可以在线性时间内完成,另外,划分出来的子问题应该是相互独立的,也就是说,每个子问题的解不受其它子问题的影响,最后,子问题的规模不能相差悬殊,最后能够相等或者接近,事实上,归并排序算法完全满足上述要求。首先,以居中的位置为界,只需O(n)时间即可将待排序的序列均匀地划分为左、右两个子序列,而且各子序列的排序不受另一子序列的影响。更重要的是,根据这两个子序列各自的排序结果,可以在线性时间内获得整个序列的排序结果。 
       ②归并算法:所谓归并操作,就是将两个有序子序列合并为一个整体有序的序列。

      6、快速排序

       快速排序是分治策略的又一典型引用,归并排序算法的主要计算量集中于有序子序列的归并,而快速排序算法正好相反,它可以在常数的时间复杂度内由子问题的解直接得到原问题的解,但为了将原问题划分为两个子问题,快速排序算法却需要线性的时间复杂度,同时,虽然快速排序算法可以确保子任务的相互独立性,但并不能保证子任务的规模大体相当,甚至有可能极不平衡,但该算法易于实现,而且其平均时间复杂度足够低,在实际应用中往往成为首选的排序算法。
       ①轴点:在每个长度不小于 3 的序列S[lo..hi]中,对于任何lo < mi < hi,以每一个元素p = S[mi]为界,都可以将该序列分割为前、后两个子序列S1= S[lo..mi-1]和S2= S[mi+1..hi] ,若S1中元素均不大于 p,S2中元素均不小于 p,则元素 p 称作序列 S 的一个轴点(pivot) 。
       ②划分算法:获取轴点的算法:
{  while (lo < hi) {  while ((lo < hi) && (S[lo] ≤ S[hi]) hi--; while ((lo < hi) && (S[lo] ≤ S[hi]) lo++;  swap(S[lo], S[hi]);  swap(S[lo], S[hi]);}  return lo;}
       当然也可以随机获取三个元素,挑选其中大小居中的元素。

二、数据结构之串结构

       串是由有限个字符组成的一种线性结构,它的两个突出特点是结构简单,规模庞大。

      1、串模式匹配

       由 n 个字符构成的串记作 S = "aa1... an-1",其中 ai∈Σ。这里的Σ是所有可用字符的集合,称作字母表,n 称为 S 的长度,记作|S| = n ,长度为零的串称为空串 。所谓 S 的子串(Sub-string),就是起始于任一位置 i 的连续 k 个字符,记作 substr(S, i, k)="aiai+1...ai+k-1",0≤i<n,0≤k。起始于位置 0、长度为 k 的子串称为前缀(Prefix),记作 prefix(S, k) = substr(S, 0, k),终止于位置 n-1、长度为 k 的子串称为后缀(suffix),记作 suffix(S, k) = substr(S, n-k, k)。空串是任何串的子串,也是任何串的前缀、后缀。任何串都是自己的子串,也是自己的前缀、后缀。空串以及串本身亦称作平凡子串(前缀、后缀)。空串以及非平凡子串(前缀、后缀)称作真子串(前缀、后缀)。串 S = "aa1a2... an-1"和 T = "b0b1b2... bm-1"称作相等,当且仅当二者长度相等,且对应的字符分别相同,即 n = m 且对任何 0 ≤ i < n 都有 ai= bi 
   给定串T(称作主串)和串P(称作模式串),T中是否存在的某个子串与P相同?如果存在,找到该子串在T中的起始位置,这类操作,都属于串模式匹配的范畴,简称串匹配
      串匹配是一个经典的问题,有名字的算法不下三十余种。由于串结构自身的特点,在设计和分析此类算法时也需做特殊的考虑,可行的一种策略是,随机选取主串 T,但仅仅测试那些匹配成功的情况。为此,可以从 T 中随机取出长度为 m 的子串作为 P。

      2、蛮力算法

       蛮力匹配算法是最直接、直观的方法:

      3、Knuth-Morris-Pratt算法

       在最坏情况下蛮力算法的运行时间为主串、模式串长度的乘积,因此只适用于小规模的串匹配应用。对最坏情况稍加观察即可发现,之所以需要大量的时间,是因为存在大量的局部匹配(每一轮的 m 次比较中,只有最后一次是失败的),实际上,绝大部分的这类字符比较操作都是不必要的,因为关于主串中此前曾经比较成功过的字符,我们已经掌握了它们的所有信息。只要充分利用这些信息,就可以大大提高匹配算法的效率。 

       如上图所示,用T[i]和P[j]分别表示当前正在接受比较的一对字符。当轮比较进行到最后一对字符并发现失配后,蛮力算法将会让这两个字符指针回退(即令i = i-j+1j = 0 ),然后从这一位置继续比较。事实上,指针i完全不必回退⎯⎯因为通过前一轮比较我们已经清楚地知道,主串的子串T[i-j..i-1]完全是由字符'0'组成的。因此,在回退之后紧接下来的一轮迭代中,j-1次比较将注定会成功。既然如此,完全可以让指针i保持不动并且令j = j-1,然后继续比较。请注意,如此将可以省去j-1次比较!

       上述“i保持不动并且令 j = j-1”的含义,可以理解为 P相对于 T 向右移动一个单元,然后从刚才失配的位置继续比较。实际上,利用以往的成功比较所提供的信息,不仅可以避免主串字符指针的回退,而且有可能使模式串尽可能大跨度地右移。

       当本轮比较中发现T[i] ≠ P[7]失配后,应该将模式串P右移多少个单元呢?有必要逐个单元地右移吗?稍加观察即可发现,在这一情况下,移动一个、两个或三个单元都是徒劳的。事实上,根据以往的比较结果,必然有T[i-7..i] = P[0..7] = "CHINCHI" 。如果在此局部能够实现匹配,则至少在 T[i]左侧的那些字符应该是匹配的⎯⎯比如, P[0] T[i-3]对齐时,就属于这样的情况。如果再注意到i-3是能够如此匹配的最靠左位置,就可以放心地将模式串右移 7-3 = 4个单元,使 i保持不变, j= 3,然后继续比较。

       相对于蛮力算法,KMP算法可以避免很多不必要的比较操作,KMP算法的运行时间为O(n+m),其中 n 和 m 分别文本串和模式串的长度。

      4、BM算法

       KMP算法的思想可以总结为:不断将模式串与文本串比较,一旦局部失配,则利用此前比较给出的信息,尽可能长距离的移动模式串,在比较模式串与文本串时,扫描方向是自左向右,实际上,很多模式匹配算法采用了其他的扫描方向,比如从右向左或者从中间向两边,BM算法采用的就是从右向左的扫描次序,该算法的构思是不断自右向左地比较模式串P与主串T,一旦发现失配,则利用此前扫描所提供的信息,将P右移一定距离,然后重新自右向左扫描比较,该算法有两种启发式策略:借助坏字符和好后缀确定移动的距离,也可将二者结合起来,同时采用。
       ①坏字符

        如 图九.7 所示,在自右向左比较模式串P[0..m-1]与主串的子串T[i..i+m-1]的过程中,假设在P[j]处首次发现失配:T[i+j] = 'b' ≠ 'a' = P[j]。此时,我们应该用P中的哪个字符对准T[i+j]并重新自右向左比较呢?我们注意到,若 P 能够与 T 的某一(包括 T[i+j]在内的)子串匹配,则必然也应在 T[i+j] = 'b'处匹配;反之,若与 T[i+j]对准的字符不是'b',则不可能匹配。因此,只需将 P 中的每一字符'b'对准T[i+j],然后重新自右向左比较。为了避免 P 的左移,我们可以选用 P 中最靠右的字符'b'(如果存在的话),将其与 T[i+j]对齐,然后重新做一遍自右向左的扫描比较。具体来说,若 P 中最靠右的字符'b'为 P[k] = 'b',则 P 的右移量为 j - k。

  特殊情况: 

       如 图九.8 所示,P串中不含字符'b',此时可以直接将该串整体移过,用P[0]对准T[i+j+1],然后自右向左继续比较。 另外,即使 P 串中含有字符'b',却也有可能出现的位置太靠右,使得 k = BC['b'] ≥ j。在这一情况下,j-k 将不再是正数,若以此距离进行右移,实际效果将是左移⎯⎯显然,这是不必要的。因此,为处理这一情况,只需简单地将 P 串右移一个字符,然后重新自右向左扫描比较。

       ②好后缀策略

       BM算法的思想,是尽可能的利用此前已进行过的比较所提供的信息,以加速模式串的移动,上述坏字符策略,就很好的体现了这一构思,然而,仔细分析后我们发现,坏字符策略只利用了此前失败的比较所提供的信息,实际上,在失败之前往往还会有一系列成功的比较,他们也能提供大量的信息,我们利用这些信息加速模式串右移,称之为好后缀策略。

三、数据结构之图

       在众多的实际应用中,图结构都是描述与解决应用问题的一个基本而强有力的工具,这里的图可以表示为G=(V,E),其中集合V中的对象称作顶点,而集合E中的每一个元素对应于V中的某一对顶点,说明这两个顶点之间存在某种关系,称作边,这里的图,并非通常所指的图形、报表或者数学上的函数图像之类。
       ①无边图、混合图及有向图
       图中的边可以使没有方向的,也可以是有方向的,如果边e=(u,v)所对应的顶点u和v不是对等的,或者存在某种次序,就称e为有向边,如果u和v的次序无所谓,e就是一条无向边,注意无向边(u,v)也可以记作(v,u),而有向边(u,v)和(v,u)则是不同的两条边若E中所有的边都没有方向,则称G为无向图,若E中同时包含无向边和有向边,则称G为混合图,若E中只含有有向边,则称G为有向图,相对而言,有向图的通用性更强,因为无向图和混合图都可以转化为有向图。
       ②度:

       若边e = (u, v),则顶点u和v也称作e的端点(End vertices或Endpoints)。如果e是从u指向v的有向边,则u称作起点(Origin)或尾端点(Tail),v称作终点(Destination)或头端点(Head)。我们也称u和v是相邻的(Adjacent),称e与v、u是相关联的(Incident)。顶点v的关联边的总数,称为v的度数(Degree),记作deg(v)。以 图十.1(a)为例,有deg(a) = deg(c) = 3。在有向图中,以u为起点的有向边称作u的出边(Outgoing edge),以v为终点的边则称作v的入边(Incoming edge )。v的出边总数称作v的出度(Out-degree),记作outdeg(v);入边总数称作入度(In-degree),记作indeg(v)。以 图十.1(c)为例,有outdeg(a) = indeg(a) = outdeg(c) = indeg(c) =3。 



       ③简单图

       图中所含的边并不见得能构成一个集合(Set),准确地说它们构成了一个复集(Multiset)⎯⎯其中允许出现重复的元素。比如,若在某对顶点之间有多条无向边,或者两条有向边的起点和终点完全一样,就属于这种情况。这类重复的边也称作平行边(Parallel e dges)或多重边(Multipleedges)。例如,要是用顶点表示城市,用边表示城市之间的飞机航线,则有可能在某一对城市之间存在多条航线。无论是无向图还是有向图,还有另一种特殊情况:与某条边关联的是同一个顶点(如 图十.1 中的顶点a)⎯⎯这样的边称作自环(Self-loop)。在某些特定的应用问题中,这类边的确是有意义的⎯⎯比如在城市交通图中,沿着某条街道,有可能会不经过任何交叉路口而直接返回原处。不过,这些特殊情况通常并不多见。不含上述特殊边的图,称作简单图(Simple graph)。对简单图而言,其中的边必然构成一个集合,而且每条边只能联接于不同的顶点之间。

      ④图的复杂度

      设简单图G包含n个顶点和m条边。若 G 是无向图,则有 m ≤ n(n-1)/2;若 G 是有向图,则有 m ≤ n(n-1)。

      ⑤子图、生成子图、限制子图

      设G=(V,E)和G‘=(V',E'),如果 V' ⊆ V 且 E' ⊆ E,则称 G'是 G 的一个子图,如果 V' = V 且 E' ⊆ E,则称 G'是 G 的一个生成子图,若 U ⊆ V,则在删除 V\U 中的顶点及其关联边之后所得到的 G 的子图,称为 G 限制在 U上的子图,记做 G|U= (U, E|U) 
 
       ⑥通路、环路及可达分量

       所谓图中的一条通路或路径,就是由(不一定互异的)m+1个顶点与m条边交替构成的一个序列ρ = {v0, e1, v1, e2, v2, ..., em, vm},m ≥ 0,而且 ei= (vi-1, vi),1 ≤ i ≤ m,m称作该通路的长度,长度m≥1的路径,若第一个顶点与最后一个顶点相同,则称之为环路如果组成通路ρ的所有顶点各不相同,则称之为简单通路,如果在组成环路的所有顶点中,除v0= vm外均各不相同,则称之为简单环路,如果组成通路ρ的所有边都是有向边,而且每一ei都是从vi-1指向vi,1 ≤ i ≤ m,则称ρ为有向通路,类似的也可定义有向环路。在图G=(V,E)中,若记n=|V|,则简单路径长度不超过 n-1;长度为 k 的简单路径的总数不超过 n!/(n-k-1)!,1 ≤ k ≤ n-1,G中简单路径的数目是有限的。

       在有向图G中,若从顶点s到v有一条通路,则称v是“从s可达的”,对于指定的顶点s,从s可达的所有顶点所组成的集合,称作s在G中对应的可达分量,记作Vr(G, s)。 

       ⑦连通性、等价类与连通分量

       考察图 G = ( V, E)。在顶点 u, v ∈ V 之间,如果既存在一条从 u 到 v 的通路ρ(u, v),也存在一条从v到u的通路ρ(v, u),则称u和v是连通的,记作u ~ v。实际上,若 G 是无向图,则“ρ(u, v)存在”当且仅当“ρ(v, u)存在”。而对于有向图 G 来说,“ρ(u,v)和ρ(v, u)同时存在”即意味着“存在一条同时经过 u 和 v 的环路”。故此,有向图中相互连通的顶点也被称为是互相“强连通的”。在有向图中,由一组相互强连通的顶点构成的极大集合,称作一个强连通分量。若u~v,则必然存在一条经过u和v的环路,而且对于该环路上的任一顶点w,都有w~u和w~v。连通关系“~”满足如下性质:反身性:对于任何顶点 v,都有 v ~ v 成立;对称性:u~v仅当v~u;传递性:对于任何顶点u、v和w,只要u~v且v~w,则必有u~w。 

       ⑧森林、树及无向图的生成树

       考察无向图G=(V,E),若G中不含任何环路,则称之为森林,连通的森林(任何两个顶点都是连通的)称作树。若G为由n个顶点与m条边组成一幅无向图,若G是连通的,则m≥n-1,若G是一棵树,则m=n-1,若G是森林,则m≤n-1。若G的某一生成子图G‘为一棵树,则称G’为G的一颗生成树。

       ⑨有向图的生成树

       在有向图G=(V,E)中,若存在某个顶点s满足Vr(G, s) = V(即从s可到达所有顶点)且s到任一顶点的通路唯一,同时G中不含回路,则称G为一颗以s为根的有向树。

       ⑩带权网络

       有些时候,图不仅需要表示元素之间是否存在某种关系,而且还需要表示这一关系的某一细节,以铁路运输为例,可以用顶点表示城市,用顶点之间的边表示城市之间是否有铁路联接,然而,为了更为细致地描述铁路运输网,还需要记录每段铁路的长度、运输成本等信息,为适应这类应用的需求,我们需要为每条边设置相应的数据域,以记录对应的信息,对于任一边 e ∈ E,weight(e)称作 e 的权重 ,引入权重函数之后的图G(V,E,weight()),称作带权图或带权网络,有时也简称为网络。


 

1 0