所谓排序,就是要整理文件中的记录,使之按关键字递增(或递减)次序排列起来。当待排序记录的关键字都不相同时,排序结果是惟一的,否则排序结果不惟一。 在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的;若具有相同关键字的记录之间的相对次序发生改变,则称这种排序方法是不稳定的。 要注意的是,排序算法的稳定性是针对所有输入实例而言的。即在所有可能的输入实例中,只要有一个实例使得算法不满足稳定性要求,则该排序算法就是不稳定的。
一.插入排序 插入排序的基本思想是每步将一个待排序的记录按其排序码值的大小,插到前面已经排好的文件中的适当位置,直到全部插入完为止。插入排序方法主要有直接插入排序和希尔排序。
①.直接插入排序(稳定) 接插入排序的过程为:在插入第i个记录时,R1,R2,..Ri-1已经排好序,将第i个记录的排序码Ki依次和R1,R2,..,Ri-1的排序码逐个进行比较,找到适当的位置。使用直接插入排序,对于具有n个记录的文件,要进行n-1趟排序。
代码如下:
void Dir_Insert(int A[],int N) //直接插入排序 { int j,t; for(int i=1;i<N;i++) { t=A[i]; j=i-1; while(A[j]>t) { A[j+1]=A[j]; j--; } A[j+1]=t; } }
②.希尔排序(不稳定): 希尔(Shell)排序的基本思想是:先取一个小于n的整数d1作为第一个增量把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取得第二个增量d2<d1重复上述的分组和排序,直至所取的增量di=1,即所有记录放在同一组中进行直接插入排序为止。该方法实质上是一种分组插入方法。 一般取d1=n/2,di+1=di/2。如果结果为偶数,则加1,保证di为奇数。 希尔排序是不稳定的,希尔排序的执行时间依赖于增量序列,其平均时间复杂度为O(n^1.3).
代码如下:
void Shell(int A[],int n) //Shell排序 { int i,j,k,t; (n/2)%2 == 0 ? k = n/2+1 : k = n/2; //保证增量为奇数 while(k > 0) { for(j=k;j<n; j++) { t = A[j]; i = j - k; while(i>=0 && A[i]>t) { A[i+k]=A[i]; i=i-k; } A[i+k]=t; } if(k == 1) break; (k/2)%2 ==0 ? k=k/2+1 : k=k/2; } }
二.选择排序 选择排序的基本思想是每步从待排序的记录中选出排序码最小的记录,顺序存放在已排序的记录序列的后面,直到全部排完。选择排序中主要使用直接选择排序和堆排序。 ①.直接选择排序(不稳定) 直接选择排序的过程是:首先在所有记录中选出序码最小的记录,把它与第1个记录交换,然后在其余的记录内选出排序码最小的记录,与第2个记录交换......依次类推,直到所有记录排完为止。 无论文件初始状态如何,在第i趟排序中选出最小关键字的记录,需要做n-i次比较,因此,总的比较次数为n(n-1)/2=O(n^2)。当初始文件为正序时,移动次数为0;文件初态为反序时,每趟排序均要执行交换操作,总的移动次数取最大值3(n-1)。直接选择排序的平均时间复杂度为O(n^2)。直接选择排序是不稳定的。
代码如下:
void Dir_Choose(int A[],int n) //直接选择排序 { int k,t; for(int i=0;i<n-1;i++) { k=i; for(int j=i+1;j<n;j++) { if(A[j]<A[k]) k=j; } if(k!=i) { t=A[i]; A[i]=A[k]; A[k]=t; } } }
②.堆排序(不稳定) 堆排序是一种树形选择排序,是对直接选择排序的有效改进。n个关键字序列 K1,K2,...,Kn称为堆,当且仅当该序列满足(Ki<=K2i且Ki<=K2i+1)或(Ki>=K2i且Ki>=K2i+1),(1<=i<=n/2)。根结点(堆顶)的关键字是堆里所有结点关键字中最小者,称为小根堆;根结点的关键字是堆里所有结点关键字中最大者,称为大根堆。 若将此序列所存储的向量R[1..n]看作是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。 堆排序的关键步骤有两个:一是如何建立初始堆;二是当堆的根结点与堆的最后一个结点交换后,如何对少了一个结点后的结点序列做调整,使之重新成为堆。堆排序的最坏时间复杂度为O(nlog2n),堆排序的平均性能较接近于最坏性能。由于建初始堆所需的比较 次数较多,所以堆排序不适宜于记录较少的文件。堆排序是就地排序,辅助空间为O(1),它是不稳定的排序方法。
代码略..
三.交换排序 交换排序的基本思想是:两两比较待排序记录的排序码,并交换不满足顺序要求的那写偶对,直到满足条件为止。交换排序的主要方法有冒泡排序和快速排序.
①.冒泡排序(稳定的) 冒泡排序将被排序的记录数组R[1..n]垂直排列,每个记录R[i]看作是重量为ki的气泡。根据轻气泡不能在重气泡之下的原则,从下往上扫描数组R;凡扫描到违反本原则的轻气泡,就使其向上"漂浮"。如此反复进行,直到最后任何两个气泡都是轻者在上,重者在下为止。 冒泡排序的具体过程如下: 第一步,先比较k1和k2,若k1>k2,则交换k1和k2所在的记录,否则不交换。继续对k2和k3重复上述过程,直到处理完kn-1和kn。这时最大的排序码记录转到了最后位置,称第1次起泡,共执行n-1次比较。 与第一步类似,从k1和k2开始比较,到kn-2和kn-1为止,共执行n-2次比较。 依次类推,共做n-1次起泡,完成整个排序过程。 若文件的初始状态是正序的,一趟扫描即可完成排序。所需关键字比较次数为n-1次,记录移动次数为0。因此,冒泡排序最好的时间复杂度为O(n)。 若初始文件是反序的,需要进行n-1趟排序。每趟排序要进行n-i次关键字的比较(1<=i<=n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较次数达到最大值n(n-1)/2=O(n^2),移动次数也达到最大值3n(n-1)/2=O(n^2)。因此,冒泡排序的最坏时间复杂度为O(n^2)。 虽然冒泡排序不一定要进行n-1趟,但由于它的记录移动次数较多,故平均性能比直接插入排序要差得多。冒泡排序是就地排序,且它是稳定的。
代码如下:
void QP(int A[],int n) //优化的冒泡排序 { int count=0,t,flag; for(int i=0;i<n-1;i++) { flag=0; for(int j=0;j<n-i;j++) { if(A[j+1]<A[j]) { t=A[j]; A[j]=A[j+1]; A[j+1]=t; flag=1; count+=3; } } if(flag==0) break; } }
②.快速排序:(不稳定的) 快速排序采用了一种分治的策略,通常称其为分治法,其基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。 快速排序的具体过程如下: 第一步,在待排序的n个记录中任取一个记录,以该记录的排序码为准,将所有记录分成两组,第1组各记录的排序码都小于等于该排序码,第2组各记录的排序码都大于该排序码,并把该记录排在这两组中间。 第二步,采用同样的方法,对左边的组和右边的组进行排序,直到所有记录都排到相应的位置为止。
代码如下:
void Quick_Sort(int A[],int low,int high) //low和high是数组的下标 { if(low<high) { int temp,t=A[low]; int l=low,h=high; while(l<h) { while(A[l]<t) l++; while(A[h]>=t) h--; if(h>l) { temp=A[l]; A[l]=A[h]; A[h]=temp; } } Quick_Sort(A,low,l-1); Quick_Sort(A,l+1,high); } }
四.归并排序 归并排序是将两个或两个以上的有序子表合并成一个新的有序表。初始时,把含有n个结点的待排序序列看作由n个长度都为1的有序子表组成,将它们依次两两归并得到长度为2的若干有序子表,再对它们两两合并。直到得到长度为n的有序表,排序结束。 归并排序是一种稳定的排序,可用顺序存储结构,也易于在链表上实现,对长度为n的文件,需进行log2n趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlog2n)。归并排序需要一个辅助向量来暂存两个有序子文件归并的结果,故其辅助空间复杂度为O(n),显然它不是就地排序。
代码略...
五.基数排序 设单关键字的每个分量的取值范围均是C0<=Kj<=Crd-1(0<=j<=rd),可能的取值个数rd称为基数.基数的选择和关键字的分解因关键字的类型而异. (1).若关键字是十进制整数,则按个、十等位进行分解,基数rd=10,C0=0,C9=9,d为最长整数的位数. (2).若关键字是小写的英文字符串,则rd=26,C0='a',C25='z',d为最长字符串的长度. 基数排序的基本思想是:从低位到高位依次对待排序的关键码进行分配和收集,经过d趟分配和收集,就可以得到一个有序序列.
按平均时间将排序分为四类:
(1)平方阶(O(n2))排序 一般称为简单排序,例如直接插入、直接选择和冒泡排序;
(2)线性对数阶(O(nlgn))排序 如快速、堆和归并排序;
(3)O(n1+£)阶排序 £是介于0和1之间的常数,即0<£<1,如希尔排序;
(4)线性阶(O(n))排序 如基数排序。
各种排序方法比较 简单排序中直接插入最好,快速排序最快,当文件为正序时,直接插入和冒泡均最佳。
影响排序效果的因素 因为不同的排序方法适应不同的应用环境和要求,所以选择合适的排序方法应综合考虑下列因素: ①待排序的记录数目n; ②记录的大小(规模); ③关键字的结构及其初始状态; ④对稳定性的要求; ⑤语言工具的条件; ⑥存储结构; ⑦时间和辅助空间复杂度等。
不同条件下,排序方法的选择
(1)若n较小(如n≤50),可采用直接插入或直接选择排序。 当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。 (2)若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜; (3)若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或 归并排序。 快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短; 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。 若要求排序稳定,则可选用归并排序。但从单个记录起进行两两归并的 排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
稳定的算法:
冒泡排序(bubble sort) — O(n2) 插入排序 (insertion sort)— O(n2) 桶排序 (bucket sort)— O(n); 需要 O(k) 额外 记忆体 计数排序 (counting sort) — O(n+k); 需要 O(n+k) 额外 记忆体 归并排序 (merge sort)— O(n log n); 需要 O(n) 额外记忆体 原地归并排序 — O(n2) 二叉树排序 (Binary tree sort) — O(n log n); 需要 O(n) 额外记忆体 鸽巢排序 (Pigeonhole sort) — O(n+k); 需要 O(k) 额外记忆体 基数排序 (radix sort)— O(n·k); 需要 O(n) 额外记忆体
|
分享到:
相关推荐
编译原理LL(1)算法的基本概念 在编译原理LL(1)算法中,存在两个重要的概念:文法和预测分析表。 * 文法:文法是指编译器设计中的一种语法规则的集合,用于描述程序设计语言的语法结构。 * 预测分析表:预测分析表...
本文将深入探讨算符优先算法的基本概念、工作原理以及它如何与文法、终结符集、非终结符集、firstvt集、lastvt集和优先表等相关知识相互关联。 首先,我们来看算符优先算法的核心思想。这种算法基于算符的优先级和...
LMS算法是一种广泛应用于信号处理和通信系统的在线学习算法,其基本原理是通过最小化均方误差来更新滤波器的权重。自适应均衡器利用LMS算法来校正传输通道引起的失真,确保信号在传输过程中的质量。 描述中提到的...
3. **三个基本步骤**: - **扩展(Extension)**:对于每个非终结符A和输入串的子串w,查找是否存在规则A → BC,如果存在,那么在表格中填入解析树。 - **合并(Completion)**:对于每个非终结符A和输入串的子串...
运算符优先分析算法是编译原理实验中的一种重要算法,它的实现可以帮助我们更好地理解编译原理的基本概念和算法思想。在这个算法实现中,我们将详细介绍运算符优先分析算法的实现过程和设计思想。 一、实验目的 ...
首先,从文法的基本规则出发,初始化所有非终结符的FirstVT和LastVT集合为空。然后,通过文法规则逐步更新这些集合,直到它们不再改变,这标志着算法完成。在这个过程中,我们需要处理ε产生子(产生空串的规则)...
非终结符代表更高级别的抽象结构,终结符则是语言的基本构建块,如变量、关键字等。产生式则定义了非终结符如何转换成终结符或非终结符的组合。 2. **LL1分析表**:LL1分析表是算法的核心,它包含两部分:First集合...
在本文中,我们还讨论了算符优先分析算法的基本思想,包括自底向上分析、移近——规约分析和算符优先关系的确定。我们还讨论了FirstVT集和LastVT集的生成算法,以及如何使用这些集合来生成算符优先关系表。 本文...
### 算符优先算法详解 #### 一、实验背景及目标 在计算机科学领域,尤其是...综上所述,算符优先分析算法是一种实用的语法分析技术,通过本次实验的学习和实践,可以深入理解自下而上语法分析的基本原理和技术实现。
尽管如此,简单优先算法作为编译原理的基础,对于理解和掌握编译器设计的基本原理至关重要。 在学习和实践中,可以参考《编译原理》等经典教材,通过实例深入理解简单优先算法的工作原理,并通过编写简单的编译器或...
2. **基本规则**:对于长度为1的子串,检查其是否等于文法中的某个终结符,如果是,则对应的表格格子设置为真。 3. **递归规则**:对于长度大于1的子串,遍历所有可能的划分方式,如将子串分为前后两部分,然后检查...
首先,我们要理解算符优先分析法的基本原理。它基于一种自底向上的策略,从输入符号串的末端开始构造语法树,直到找到一个完整的句型为止。在这个过程中,我们使用算符优先表来决定何时进行归约操作。算符优先表通常...
- **示例**:“I saw a boy with a telescope”这个句子的句法分析过程可以被表示成一个有向单调超图,其中每个非终结符(如S, VP, NP等)以及每个词都代表一个顶点,而从一个非终结符到另一个非终结符的转换则由超边...
首先,我们来看算符优先算法的基本概念。这个算法基于算符的优先级和结合性来决定如何处理表达式。在给定的代码中,算符优先算法被用于处理符号串,判断何时进行归约(reduce)或移进(shift)操作。归约是将当前的...
在这个报告和程序中,我们将深入探讨算符优先分析的基本原理和实现过程。 首先,我们要理解什么是算符优先分析。在编程语言中,算符具有不同的优先级,例如乘法和除法的优先级高于加法和减法。算符优先分析就是依据...
首先,我们要理解LL1解析器的基本概念。LL1代表“Left-to-Right扫描,Leftmost derivation,使用1个查看符号(Lookahead)”。这种解析方法从输入串的左侧开始,每次决定一个最左推导,同时仅使用一个查看符号来决定...
一个上下文无关文法(Context-Free Grammar, CFG)由四个部分组成:一组非终结符(变量)、一组终结符(基本符号)、一个起始符号(通常是非终结符)和一组产生式规则。产生式规则描述了非终结符如何转换为其他非...
总的来说,first集和follow集是编译器设计中进行语法分析的重要数据结构,它们帮助我们理解文法的结构,识别和解决语法解析过程中的冲突,从而构建有效的解析算法。通过掌握这两个概念,开发者可以更好地设计和实现...
- FIREST FOLLOW 算法是计算非终结符的FOLLOW集和FIRST集的算法。它用于构建LL(1)解析表,其中“L”代表自左至右扫描输入,“L”代表最左推导,“1”代表使用下一个输入符号的最左信息。 7. **LL(1)解析器** - LL...
当遇到优先级较低的运算符或终结符(如数字或括号)时,我们会从栈中弹出一个或多个运算符,执行相应的运算,并将结果压回栈中。这一过程将持续到输入符号序列结束,最终栈顶的元素就是表达式的计算结果。 在郑州...