常用排序算法总结
- 排序的基本概念
所谓排序,就是整理表中的元素,使之按递增或者递减的顺序排列,下列仅仅介绍递增的情况
- 排序的稳定性:
如果待排序的表中有多个关键字相同的字段,经过排序之后这些具有相同关键字的元素之间的相对次序保持不变,则称这种排序为稳定排序;反之,如果具有相同关键字的元素之间的相对次序发生变化则称为不稳定排序。
对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。
例如,对于冒泡排序,原本是稳定的排序算法,如果将记录交换的条件改成A[i] >= A[i + 1],则两个相等的记录就会交换位置,从而变成不稳定的排序算法。
其次,说一下排序算法稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的。
- 时间复杂度
计算时间复杂度的方法:
- 用常数1代替运行时间中的所有加法常数
- 修改后的运行次数函数中,只保留最高阶项
- 去除最高阶项的系数
按数量级递增排列,常见的时间复杂度有:
常数阶O(1)
,对数阶O(log2n)
,线性阶O(n)
,
线性对数阶O(nlog2n)
,平方阶O(n^2)
,立方阶O(n^3)
,…,
k次方阶O(n^k)
,指数阶O(2^n)
。
随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
- 空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。
计算方法:
①忽略常数,用O(1)表示
②递归算法的空间复杂度=递归深度N*每次递归所要的辅助空间
③对于单线程来说,递归有运行时堆栈,求的是递归最深的那一次压栈所耗费的空间的个数,因为递归最深的那一次所耗费的空间足以容纳它所有递归过程。
- 排序分类:
我们通常所说的排序算法往往指的是内部排序算法,即数据记录在内存中进行排序,不涉及内存、外存之间的数据交换。外部排序算法要进行内存、外存之间的数据交换。
排序算法大体可分为两种:
一种是比较排序,时间复杂度O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。
另一种是非比较排序,时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。
下表给出了常见比较排序算法的性能:
一、 插入排序:
基本思想是:每次将一个待排序的元素,按其关键字大小插入到已经排好序的子表中的适当位置,直到全部元素插入完成为止。
1.直接插入排序:
1.1思路:
假设待排序的数组放在数组R[0...n-1]中,排序过过程中的某一时刻,R被划分成两个子区间R[0..i-1]和R[i...n-1](刚开始i=1,有序区间只有R[0]一个元素),其中前一个区间是有序区间,后一个区间是一个乱序区间。直接插入排序的一趟操作是将当前无序区的开头元素R[i]( 1 <= i <= n-1)插入到有序去,R[0..i-1]的合适位置,使R[0..i]变成新的有序区间,这种方法通常称为增量法,因为它每趟归并一个元素。
1.2算法实现:
package arithmeticTest; /** * 直接插入排序算法实现 * * @author: qlq * @date : 2018年4月2日下午6:13:15 */ public class StraightInsert { public static void main(String a[]) { int array[] = { 5, 4, 9, 8, 6, 6 }; straightInsertImpl(array); // 打印排序后的数组 for (int i : array) { System.out.print(i + " "); } } /** * 主要算法实现 * * @param arr * 需要排序的数组 */ public static void straightInsertImpl(int[] arr) { for (int i = 1; i < arr.length; i++) {// 遍历右边的无序区间 int temp = arr[i]; if (arr[i] >= arr[i - 1]) { // 如果无序区的第一个元素大于或者等于有序区的第一个元素,结束本次循环进行下一次 continue; } for (int j = 0; j < i; j++) {// 遍历左边的有序区间 if (temp < arr[j]) {// 找到位置后元素后移 for (; i > j; i--) { arr[i] = arr[i - 1]; } arr[j] = temp; break; // 找到合适的位置插入后结束这层循环即可 } } } } }
结果:
4 5 6 6 8 9
解释执行过程:
原数组: 5 4 9 8 6 6
第一趟: 4 5 9 8 6 6
第二趟: 4 5 9 8 6 6
第三趟: 4 5 8 9 6 6
第四趟: 4 5 6 8 9 6
第五趟: 4 5 6 6 8 9
上面加底色的是有序区
上面的最后一趟排序的时候最后一个6插入在8前面,因此是稳定的。最坏是数组是反序,元素移动次数和比较次数最多;最好的情况就是数组就是正序,数组移动次数和比较次数最少。
1.3 算法效率
时间复杂度:O(n^2),空间复杂度:O(1)。
2.折半插入排序或者二分插入排序
2.1思路:
基本的排序思路同直接插入排序,将数组分为左右无序区间和有序区间,只是在向有序区间插入元素的时候现在R[0..i-1]找到合适位置,再通过移动元素插入元素。
在R[low..high](初始时low=0,high=i-1)采用折半查找方法找到插入R[i]元素的位置为R[high+1],再将R[high+1..i-1]中元素后移一个位置,并置R[high+1]=R[i]。
一句话就是先插入一个元素的时候先用折半查找到元素的位置,然后将有序区中此位置后面的元素后移一位,最后将此元素插入这个位置。
例如:
4 6 7 5 9 6 3
对于下一个将要安置的元素是5,思路如下:
2.2算法实现
package arithmeticTest; /** * 折半插入算法实现 * @author: qlq * @date : 2018年4月2日下午7:24:36 */ public class HalfInsert { public static void main(String a[]) { int array[] = { 4,6,7,5,9,6,3}; straightInsertImpl(array); // 打印排序后的数组 for (int i : array) { System.out.print(i + " "); } } /** * 主要算法实现 * * @param arr * 需要排序的数组 */ public static void straightInsertImpl(int[] arr) { for (int i = 1; i < arr.length; i++) {// 遍历右边的无序区间 //1.查找元素位置 int low=0,high=i-1,temp=arr[i]; while(low <= high){//查找元素应该插入的位置 int mid=(low+high)/2; if(temp<arr[mid])//说明在low-mid区间 high=mid-1; else //在左边区间 low=mid+1; } //2.移动元素 for(int j=i-1;j>=high+1;j--) arr[j+1]=arr[j];//元素后移 //3.将需要排序的元素插进去 arr[high+1]=temp; } } }
2.3算法分析:
时间复杂度:O(n^2),空间复杂度:O(1)。折半查找由于顺序查找,所以折半插入排序也要由于直接插入排序。 也是稳定排序。
3.希尔排序
希尔排序,也叫递减增量排序,是插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法。
3.1思路:
实际是一组分组插入算法。基本思想是:先取定一个小于n的证书d1作为第一个增量,把表的全部元素分成d1个组,所有相互之间距离为d1的倍数的数组在一个组中,在这组内直接进行插入排序;然后去第二个增量d2(d2<d1),重复上述的分组和排序过程,直至所取的增量dt=1(dt<dt-1<..<d2<d1),即所有元素在同一个组内进行排序。
3.2算法实现;
9 8 7 6 5 4 3 2 1 0
第一次分为5组: (9,4) (8,3) (7,2) (6,1) (5,0)并将同组内元素进行排序 4 3 2 1 0 9 8 7 6 5 (9和4交换位置,8和3交换位置)
第二次分为2组: (4,2,0,8,6),(3,1,9,7,5) 结果;0 1 2 3 4 5 6 7 8 9
第三次分为1组: 0 1 2 3 4 5 6 7 8 9 (最终结果)
package arithmeticTest; /** * 希尔排序实现 * * @author: qlq * @date : 2018年4月2日下午9:10:25 */ public class XierInsert { public static void main(String a[]) { int array[] = { 9, 8, 7, 6, 4, 5, 3, 2, 1, 0 }; xierInsertImpl(array); // 打印排序后的数组 for (int i : array) { System.out.print(i + " "); } } /** * 主要算法实现 * * @param arr * 需要排序的数组 */ public static void xierInsertImpl(int[] arr) { int gap = arr.length / 2, temp, j;// 增量置初值 while (gap > 0) { for (int i = gap; i < arr.length; i++) {// 对所有相距gap的元素进行直接插入排序 temp = arr[i]; j = i - gap; while (j >= 0 && temp < arr[j]) { arr[j + gap] = arr[j]; j = j - gap; } arr[j + gap] = temp; } gap = gap / 2; // 减小增量 } } }
3.3算法分析:
时间复杂度:O(n^1.3),空间复杂度:O(1)。
二、交换排序
交换排序的思想:两两比较待排序元素的关键字,发现两个元素的次序相反时即进行交换,直到没有反序的元素位置,在这里介绍两种:冒泡排序和快速排序。
1.冒泡排序:(气泡排序)
1.1思路(小数上浮,大数下沉)
通过无序区中,相邻元素关键字之间的比较和交换位置,使关键字最小的元素如气泡一般逐渐上漂。整个算法是从元素的最下面开始逐渐比较,且使关键字小的元素逐渐跑到上面,经过一趟排序之后关键字最小的元素跑到最上端,接着在再剩下的元素中找次小的元素。
1.2算法实现:
package arithmeticTest; import javax.xml.transform.Templates; /** * 冒泡排序 * * @author: qlq * @date : 2018年4月2日下午10:03:13 */ public class Bubble { public static void main(String a[]) { int array[] = { 4, 6, 7, 5, 9, 6, 3 }; bubbleSortImpl(array); // 打印排序后的数组 for (int i : array) { System.out.print(i + " "); } } /** * 主要算法实现 * * @param arr * 需要排序的数组 */ public static void bubbleSortImpl(int[] arr) { for(int i=0,length_1=arr.length;i<length_1;i++){//从后往前扫描 for(int j=length_1-1;j>=i+1;j--){ if(arr[j]<arr[j-1]){ //如果小于就交换数据 int temp = arr[j]; arr[j]=arr[j-1]; arr[j-1]=temp; } } } } }
1.3算法分析:
时间复杂度:O(n^2),空间复杂度:O(1)。 稳定排序。
2.快速排序:
快速排序是不稳定的排序算法,不稳定发生在基准元素与A[tail+1]交换的时刻。
比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。
Java系统提供的Arrays.sort函数。对于基础类型,底层使用快速排序。对于非基础类型,底层使用归并排序。请问是为什么?
答:这是考虑到排序算法的稳定性。对于基础类型,相同值是无差别的,排序前后相同值的相对位置并不重要,所以选择更为高效的快速排序,尽管它是不稳定的排序算法;而对于非基础类型,排序前后相等实例的相对位置不宜改变,所以选择稳定的归并排序。
2.1思路:
数轴,左小右大。由冒泡排序改进而得,在待排序的n个元素任务一个作为基准后(通常取第一个元素作为基准),把该元素放入适当位置后,数据序列被此元素划分成两部分,比该元素小的在左边,大的在右边,并把该元素放入这两部分中间,称该元素归位,这个过程为一趟快速排序。(每趟排序仅归位一个元素)
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。 快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。步骤为: 从序列中挑出一个元素,作为"基准"(pivot). 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。
第二种理解:
2.2算法实现:
递归排序
package arithmeticTest; import java.util.Arrays; /** * 快速排序 * * @author: qlq * @date : 2018年4月2日下午10:33:22 */ public class KuaisuSort { public static void main(String[] args) { int[] a = {6,5,7,8,9,4}; System.out.println(Arrays.toString(a)); quickSort(a); System.out.println(Arrays.toString(a)); } public static void quickSort(int[] a) { if(a.length>0) { quickSort(a, 0 , a.length-1); } } private static void quickSort(int[] a, int low, int high) { //1,找到递归算法的出口 if( low > high) { return; } //2, 存 int i = low; int j = high; //3,key int key = a[ low ]; //4,完成一趟排序 while( i< j) { //4.1 ,从右往左找到第一个小于key的数 while(i<j && a[j] > key){ j--; } // 4.2 从左往右找到第一个大于key的数 while( i<j && a[i] <= key) { i++; } //4.3 交换 if(i<j) { int p = a[i]; a[i] = a[j]; a[j] = p; } } // 4.4,调整key的位置 int p = a[i]; a[i] = a[low]; a[low] = p; //5, 对key左边的数快排 quickSort(a, low, i-1 ); //6, 对key右边的数快排 quickSort(a, i+1, high); } }
2.3算法分析:
时间复杂度:O(nlog2n),空间复杂度:O(log2n),不稳定排序
三、选择排序
选择排序的思想是:每一趟从待排序的元素中选择一个最小的元素,顺序放到已经排好的序的子表的最后,直到全部元素排序完毕。适用于从大量的元素中选择一部分排序元素。包括直接选择(简单选择)排序和堆排序。
1.直接选择排序
1.1思路:
第i趟排序开始时,当前有序区和无序区分别为R[0..i-1]和R[i..n-1]。该趟排序是从当前无序区选择关键字最小的元素R[k],将它与无序区的第一个元素R[i]交换,使R[0..i],R[i+1..n-1]变为新的有序区和无序区。
1.2算法实现:
package arithmeticTest; import java.util.Arrays; import javax.xml.transform.Templates; /** * 快速选择排序 * * @author: qlq * @date : 2018年4月4日下午2:30:28 */ public class QuickSelectSort { public static void main(String[] args) { int a[] = { 5, 9, 8, 7, 4, 6, 5 }; sort(a); System.out.println(Arrays.toString(a)); } /** * 主要算法 * * @param arr */ public static void sort(int arr[]) { for (int i = 0, length_1 = arr.length - 1; i < length_1; i++) { for (int j = i + 1; j < length_1 + 1; j++) { if (arr[j] < arr[i]) { int temp = arr[j]; arr[j] = arr[i]; arr[i] = temp; } } } } }
1.3算法分析:
时间复杂度是O(n2),空间复杂度是O(1),不稳定排序。
参考;http://www.cnblogs.com/eniac12/p/5329396.html