排序是计算机程序设计中的一种重要操做。若是数据可以根据某种规则排序,就能大大挺高数据处理的算法效率。html
排序就是整理文件中的记录,使之按关键字递增(或递减)的次序排列起来。前端
排序的对象是文件,它由一组记录组成。每条记录则由一个或若干个数据项(或域)组成。java
关键字项就是可用来标识一个记录的一个或多个组合的数据项。该数据项的值称为关键字(key)。须要注意的是,在不易产生混淆时,可将关键字项简称为关键字。算法
用来做为排序运算的关键字,能够是数字类型,也能够是字符类型。关键字的选取应根据问题的要求而定。shell
当待排序记录的关键字均不相同时,排序结果是唯一的,不然排序结果不唯一。后端
在待排序的文件中,若存在多个关键字相同的记录,通过排序后这些具备相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的;若具备相同关键字的记录之间的相对次序发生变化,则称这种排序方法是不稳定的。数组
根据排序时待排序的数据元素数量的不一样,使得排序过程当中涉及的存储器不一样,能够将排序方法分为两类。 一类是整个排序过程在内存储器中进行,称为内部排序,简称内排序。数据结构
另外一类是因为待排序元素数量太大,以致于内存储器没法容纳所有数据,排序须要借助外部存储设备才能完成,这类排序称为外部排序。app
通常状况下,内排序适宜在记录个数很少的小文件中使用,外排序则适用于记录个数太多,不能一次将其所有记录放入内存的大文件。ide
对于外排序,可进一步分为两种方法:
对于内排序,按策略进行划分,能够分为:
分析排序算法时,应该考虑比较的次数和数据移动的次数。
具体须要考虑如下3中状况的比较和移动次数:
不少排序方法在这3种状况下的性能是迥然不一样的,因此能够根据实际条件选择使用哪种算法。
插入排序的思想:
每次将一个待排序的记录按其关键字大小插入到前面已经排好序的子文件的适当位置,直到所有记录插入完成为止。
直接插入排序是一种最简单的排序方法,它的基本操做是将一个记录插入到已经排好序的有序表中,从而获得一个新的有序表。
假设待排序的记录存放在数组R[1……n]中,排序过程当中,R被分为两个子区间R[1……i]和R[i+1……n],其中R[1……i]是已经排好序的有序区;R[i+1……n]是当前未排序的部分。将当前无序区的第一个记录R[i+1]插入到有序区R[1……i]的适当位置,使R[1……i+1]变为新的有序区,每次插入一个数据,知道全部的数据有序为止。
通常来讲,插入排序都采用in-place在数组上实现。具体算法描述以下:
// 插入排序 public static int[] insertionSort(int[] arr) { int preIndex, temp; //比较的论数 for (int i = 1; i < arr.length; i++) { //当前下标的前一位下标 preIndex = i - 1; //当前位置的元素 temp = arr[i]; //前一位下标不越界而且前一位的元素值大于当前位置的元素值 while (preIndex >= 0 && arr[preIndex] > temp) { //移动元素位置 arr[preIndex + 1] = arr[preIndex]; //下标前移继续比较 preIndex--; } //插入相应位置 arr[preIndex + 1] = temp; } return arr; }
对于具备n个记录的文件,要进行n-1趟排序。 各类状态下的时间复杂度以下表:
算法所需的辅助空间是一个监视哨,辅助空间负责度S(n)=O(1),是一个就地排序。
直接插入排序是稳定的排序方法。
希尔排序是插入排序的一种,因D.L.Shell于1959年提出而得名。希尔排序也称做缩减增量排序(diminishing increment sort)
先取定一个小于n的整数d1做为第一个增量,把文件的所有记录分红d1个组,全部距离为d1的倍数的记录放在同一个组中,在各组内进行插入排序;而后,取第二个增量d2<d1,重复上述的分组和排序,直至所取的增量dt=1(dt<……<d2<d1),即全部记录放在同一组中进行直接插入排序为止。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
// 希尔排序 public static void shellSort2(int[] arr) { int j; // 初始值为数组长度的一半,而后按照一半的一半递减至1 for (int gap = arr.length / 2; gap > 0; gap /= 2) { // 初始取值为数组长度的一半,而后加一递增,不能超过数组长度 for (int i = gap; i < arr.length; i++) { int tmp = arr[i];// 取出数组的一半位置的元素。 // 比较步长两端的元素,若是后端小于前端 for (j = i; j >= gap && tmp - arr[j - gap] < 0; j -= gap) { arr[j] = arr[j - gap];// 将前端的值赋予后端 } // 此时j减去了gap则为前端的位置,将后端的值赋予前端。完成不一样位置的元素交换。 arr[j] = tmp; } } }
Shell排序的执行时间依赖于增量序列。
好的在增量序列的共同特征为:
希尔排序的时间性能优于直接插入排序,缘由以下:
希尔排序是一种不稳定的排序方法。
交换排序的基本思想:
两两比较待排序记录的关键字,发现两个记录的次序相反时,即进行交换,直到没有反序的记录为止。 应用交换排序基本思想的主要排序方法有冒泡和快速排序。
设想被排序的记录关键字保存在数组R[1……n]中,将每一个记录R[i]看作是重量为R[i].key的气泡。根据轻气泡不能在重气泡之下的原则,从下往上扫描数组R;凡扫描到违反本原则的轻气泡,就使其向上“漂浮”。如此反复进行,直到最后任何两个气泡都轻者在上,重者在下为止。
// 冒泡排序 public static void bubbleSort(int[] arr) { // 比较的轮数 for (int i = 0; i < arr.length - 1; i++) { // 每轮比较的次数 for (int j = 0; j < arr.length - 1 - i; j++) { // 从小到大排序 if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } } } }
若文件的初始状态是正序的,一趟扫描便可完成排序。所需的关键字比较次数C和记录移动次数M均达到最小值:
Cmin=n-1;Mmin=0
冒泡最好的时间复杂度为O(n)。
若成绩是文件是反序的,须要进行n-1趟排序。每趟排序要进行n-i此关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种状况下,比较和移动次数均达到最大值:
Cmax=n(n-1)/2=O(n^2);Mmax=3n(n-1)/2=O(n^2)
冒泡的最坏时间复杂度为O(n^2)。
冒泡算法的平均时间复杂度为O(n^2)。
冒泡排序是就地排序,且它是稳定的。
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,一般称其为分治法(Divide-and-ConquerMethod)。
将原有问题分解为若干个规模更小但结构与原问题类似的子问题,递归地解这些子问题。而后将这些子问题的解组合为原问题的解。而后将这些子问题的解组合为原问题的解。
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述以下:
// 快速排序 public static void fastSort(int[] arr, int left, int right) { // 最小位置 int minIndex = left; // 最大位置 int maxIndex = right; // 三方变量 int temp = 0; // 最大和最小位置不相等时 if (minIndex < maxIndex) { // 三方变量从最小位置开始取值比较 temp = arr[minIndex]; // 当最大和最小位置不重叠时 while (minIndex != maxIndex) { // 小位置没有越过大位置,而且大位置的值大于等于temp的取值 while (maxIndex > minIndex && arr[maxIndex] >= temp) { // 大位置向左移 maxIndex--; } // 不然,小位置的元素取大位置的元素 arr[minIndex] = arr[maxIndex]; // 小位置没有越过大位置,而且小位置的元素值小于等于三方变量 while (minIndex < maxIndex && arr[minIndex] <= temp) { // 小位置右移 minIndex++; } // 不然,大位置的元素取小位置的元素 arr[maxIndex] = arr[minIndex]; } // 中止处的小位置去三方变量的值。 arr[maxIndex] = temp; // 左半数组重复上面的操做 fastSort(arr, left, minIndex - 1); // 右半数组重复上面的操做 fastSort(arr, maxIndex + 1, right); } }
快速排序的时间主要耗费在划分操做上,对长度为k的区间进行划分,共需k-1次关键字的比较。
最坏状况是每次划分宣州区的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另外一个非空的子区间中记录数目仅仅比划分前的无序区中记录个数减小一个。
所以,快速排序必须作n-1此划分,第i此划分开始区间长度为n-i+1,所需的比较次数为n-i(1≤i≤n-1),故总的比较次数达到最大值:
Cmax=n(n-1)/2=O(n^2)
最好状况下,每次划分所取的基准都是当前无序区的“中值”记录,划分的结果是基准的左、右两个无序子区间的长度大体相等。总的关键字比较次数为:O(nlgn)。 平均时间复杂度为:O(nlgn)。
快速排序在系统内部须要一个栈来实现递归。若每次划分较为均匀,则其递归树的高度为O(lgn),故递归后须要栈空间为O(lgn)。最坏状况下,递归树的高度为O(n),所需的栈空间为O(n)。
快速排序是非稳定的。
选择排序的基本思想:
每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,直到所有记录排序完毕。
选择排序方法主要有一下两种:直接选择排序(或称简单选择排序)和堆排序。
第i趟排序开始时,当前有序区和无序区分别为R[1……i-1]和R[i……n](1≤i≤n-1),该趟排序则是从当前无序区中选出关键字最小的记录R[k],将它与无序区的第一个记录R[i]交换,是R[1……i]和R[i+1……n]分别变为新的有序区和新的无序区。由于每趟排序均使有序区中增长了一个记录,且有序区中的记录关键字均不大于无序区无序区轰炸过记录的关键字,即第i趟排序以后R[1……i].keys≤R[i+1……n].keys,因此进行n-1趟排序以后有R[1……n-1].keys≤R[n].key,即通过n-1趟排序以后,整个文件R[1……n]递增有序。
n个记录的直接选择排序可通过n-1趟直接选择排序获得有序结果。具体算法描述以下:
//直接选择排序 public static void SelectionSort(int[] arr) { int minIndex,temp; for(int i = 0;i<arr.length;i++) { minIndex=i; for(int j=i+1;j<arr.length;j++) { if(arr[j]<arr[minIndex]) { minIndex = j; } } temp = arr[i]; arr[i] =arr[minIndex]; arr[minIndex]=temp; } }
直接选择排序的平均时间复杂度为O(n^2)。
直接选择排序是一个就地排序,而且是不稳定的。
堆排序是利用彻底二叉树进行排序的方法。
堆有大根堆(根结点的关键字值最大的堆)和小根堆(根结点关键字值最小)之分。
堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。
大根堆排序思想:
首先将初始文件R[1……n]建成一个大根堆,此堆的初始的无序区;将关键字最大的记录R[1](即堆顶)堆顶和无序区的最后一个记录R[n]交换,由此获得新的无序区R[1……n-1]和有序区R[n],且知足R[1……n-1].keys≤R[n].keys,因为交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1……n-1]调整为堆;而后再次将R[1……n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此获得新的无序区R[1……n-2]和有序区R[n-1……n],且仍知足关系R[1……n-2].keys≤R[n-1……n].keys。一样要将R[1……n-2]调整为堆。重复以上步骤,直至按关键字有序。
//交换值 private static void swap(int[] arr,int biggerIndex,int rootIndex) { int temp=arr[rootIndex]; arr[rootIndex]=arr[biggerIndex]; arr[biggerIndex]=temp; } //调整大根堆 private static void adjustHeap(int[] arr,int rootIndex,int lastIndex) { //从根结点开始往下调整 int biggerIndex = rootIndex; int leftChildIndex=rootIndex * 2+1; int rightChildIndex=rootIndex*2+2; if(rightChildIndex<=lastIndex) {//若是右结点存在,则左结点必定存在 if(arr[rightChildIndex]> arr[rootIndex]||arr[leftChildIndex]>arr[rootIndex]) { //将子节点更大的元素下标赋值给biggerIndex biggerIndex = arr[rightChildIndex]>arr[leftChildIndex]?rightChildIndex:leftChildIndex; } }else if(leftChildIndex<=lastIndex) {//保证左结点存在,且不越界 if(arr[leftChildIndex]>arr[rootIndex]) { biggerIndex = leftChildIndex; } } if(biggerIndex!=rootIndex) { swap(arr, biggerIndex, rootIndex); adjustHeap(arr, biggerIndex, lastIndex); } } //构建大根堆 private static void buildMaxHeap(int[] arr,int lastIndex) { //从最后一个元素的父结点开始进行调整,一直调整到根结点结束 int j=(lastIndex-1)/2; while(j>=0) { int rootIndex = j; adjustHeap(arr, rootIndex, lastIndex); j--; } } //堆排序 public static void heapSort(int[] arr,int len) { int lastIndex = len -1; //构建最大堆 buildMaxHeap(arr, lastIndex); while(lastIndex>0) { swap(arr, 0, lastIndex); if(--lastIndex==0) {//剩余元素个数为1,排序结束,跳出循环 break; } adjustHeap(arr, 0, lastIndex); } }
堆排序的时间主要由创建初始堆和反复重建堆这两部分的时间开销构成,他们均是经过调用heapSort实现的,
堆排序的最坏时间复杂度为O(nlgn)。堆排序的平均性能较接近于最坏性能。
因为建初始堆岁序的比较次数较多,所欲堆排序不适宜与记录数较少的文件。
堆排序是就地排序,辅助空间为O(1)
堆排序是一种不稳定的排序方法。
归并排序是将两个或两个以上的有序表组合成一个新的有序表。
先将N个数据当作N个长度为1的表,将相邻的表成对合并,获得长度为2的N/2个有序表,进一步将相邻的合并,获得长度为4的N/4个有序表,一次类推,知道全部数据均合并成一个长度为N的有序表为止。
// 合并数据 private static void merge(int[] arr,int left,int center,int right) { int[] newArr = new int[arr.length]; int mid = center+1; int third = left; int tmp = left; while(left <=center&& mid<=right) { if(arr[left]<=arr[mid]) { newArr[third++]=arr[left++]; }else { newArr[third++]=arr[mid++]; } } while(mid<=right) { newArr[third++]=arr[mid++]; } while(left<=center) { newArr[third++]=arr[left++]; } while(tmp<=right) { arr[tmp]=newArr[tmp++]; } } //排序 private static void sort(int[] arr,int left,int right) { if(left>=right) { return; } int center = (left+right)/2; sort(arr, left, center); sort(arr, center+1, right); merge(arr, left, center, right); } //归并排序 public static void mergeSort(int[] arr) { sort(arr, 0, arr.length-1); }
归并排序的时间复杂度不管是在最好状况下,仍是在最坏状况下均是O(nlgn)。
须要一个辅助向量来暂存两个有序子文件归并的结果,故其辅助空间复杂度为O(n)。
归并排序是一种稳定的排序。
算法和数据结构的实现能够基于住存储器,也能够基于辅助存储器,但这会影响算法和数据结构的设计。
主存储器和辅助存储器的差异主要与存储介质中的访问速度、数据的存储量和数据的永久性有关。
访问辅助存储器比访问主存储器要慢不少,而外部排序的过程须要进行屡次的主存储器和辅助存储器之间的交换。
调整内部排序算法使之应用于外部排序,这种思路的更广泛的问题是这样作不可能比设计一个新的尽可能减小磁盘存取的算法更有效。
进行外部排序的一个更好的方法源于归并排序。
首先,按可用内存大小,将外存上喊n个记录的文件分红若干长度为l的子文件或段(segment),依次读入内存并利用有效的内部排序方法对它们进行排序,并将排序后获得的有序子文件从新写入外存。一般称这些有序子文件为归并段或顺串;而后对这些归并段进行逐趟归并,使归并段组件由小到大,直至整个有序文件为止。
置换选择其实是堆排序算法的一个微小变种。
多路归并与二路归并相似。若是有B个顺串须要归并,从每一个顺串中取出一个块放在主存中使用,那么B路归并算法仅仅查看B个值,而且选择最小的一个输出。把这个值从它的顺串中移出,而后重复这个过程。当任何顺串的当前块用完时,就从磁盘中独处这个顺串的下一个块。
通常来讲,简历大的初始顺串能够把运行时间减小到标准归并排序的四分之一,使用多路归并能够进一步把时间减半。
本博文,部分参考十大经典排序算法(动图演示),所涉及的动图,均出自此博文!
上一篇:图(graph)
下一篇: