写在前面:纸上得来终觉浅。基本排序算法的思想,可能不少人都说的头头是到,但能说和能写出来,真的仍是有很大区别的。面试
今天整理了一下各类经常使用排序算法,固然还不全,后面会继续补充。代码中可能有累赘或错误的地方,欢迎指正。算法
冒泡排序是最简单的排序算法之一,其具体思想就是将相邻两个元素进行比较,大的元素交换到最后面(升序),最大的元素移动的过程就像水冒泡同样。冒泡排序中,须要对n个元素进行冒泡,每次冒泡又须要进行n的数量级次比较,因此冒泡排序的时间复杂度为O(n^2)shell
/** * 冒泡排序 */ public void bubbleSort(int[] array) { if(array == null || array.length <= 0) { return ; } for(int i = 0; i < array.length - 1; i++) { for(int j = 0; j < array.length - i - 1; j++) { if(array[j] > array[j + 1]) { int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; } } } }
固然,比较理想的状态下,咱们想,若是在待排序数组已是有序的状况下,咱们冒泡一次,发现交换次数为零,那么后面的比较与交换就没必要再进行,因此,这种状况下,冒泡排序的最好状况时间复杂度是O(n),咱们所须要作的就是在每次冒泡过程当中添加一个记录交换次数的计数器。 以下:数组
/** * 冒泡排序2 */ public void bubbleSort2(int[] array) { if(array == null || array.length <= 0) { return ; } int swapCount = 0; //天加计数器记录每趟交换次数 for(int i = 0; i < array.length - 1; i++) { swapCount = 0; //清零 for(int j = 0; j < array.length - i - 1; j++) { if(array[j] > array[j + 1]) { int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; swapCount++; } } if(swapCount == 0) { //这一趟冒泡过程没有发生交换,待排数组已经有序,直接退出循环 break; } } }
选择排序集本思想和冒牌排序类似,都是经过一次遍历比较后获得最值。与冒泡排序不一样的是,冒泡排序须要相邻元素每次比较而后交换。而选择排序是通过一次遍历比较记录下最值,也就是通过总体选择,而后将选出的元素放在合适的位置。选择排序对冒泡排序进行了必定的优化,比较的次数没有发生变化,但省去了相邻元素频繁的交换。在时间复杂度上,依然是O(n^2);函数
public void selectSort(int[] array) { if(array == null || array.length <= 0) { return; } for(int i = 0; i < array.length - 1; i++) { int minIndex = i; int minNum = array[i]; for(int j = i; j < array.length; j++) { if(array[j] < minNum) { minNum = array[j]; minIndex = j; //记录最小值的下标 } } if(minIndex != i) { //若最小值不是在i处,将最小值交换到前面 int temp = array[i]; array[i] = array[minIndex]; array[minIndex] = temp; } } }
插入排序与冒泡,选择排序不一样。冒泡与选择都是经过一次遍历比较肯定出一个最值,而后放在合适的位置。而插入排序是经过比较来找到合适的位置进行插入,例如待排数组:3,2,6,4,8;第一个数3,已经有序,而后是2,与3比较,发现比3小,插到3前面:2,3,6,4,8,而后是6,6比3大,插到3后面,2,3,6,4,8,而后是4,4比6小而且比3大,则插到3后面6前面:2,3,4,6,8,最后是8,比6大,插到6的后面,完成插入排序。冒泡排序和选择排序在进行一次排序后,就能惟一肯定一个元素的位置,而插入排序却不行。最好状况,若待排序列已经有序,则插入排序的时间复杂度为O(n)。插入排序的平均时间复杂度依然是O(n^2)。学习
/** *插入排序 */ public void insertSort(int[] array) { if(array == null || array.length <= 0) { return; } for(int i = 0; i < array.length; i++) { int temp = array[i]; int j = i; while(j > 0 && temp < array[j - 1]) { //向后移动 array[j] = array[j - 1]; j--; } array[j] = temp; } }
我这里讲希尔排序放在直接插入排序算法后面,是由于希尔排序是插入排序的一种高效实现方法。希尔排序将整个待排序列经过划分红若干子序列来分别进行插入,分割子序列的方法是经过一个增量来达到。在插入排序中,若是待排序数组是有序的,那么,插入排序的只须要遍历一次数组,不用移动任何元素,就能完成排序,且时间复杂度为O(n)。因此利用插入排序,若数组是基本有序的,那么直接插入排序效率将会提升。希尔排序就是利用这个特色。希尔排序因为前面的插入排序中记录的关键字是和同一子序列中的前一个记录的关键字进行比较,所以关键字较小的记录就不是一步一步地向前挪动,而是跳跃式地往前移,从而使得进行最后一趟排序时,增量为1,至关于直接插入排序,可是这个时候,数组已经基本有序,只要做记录的少许比较和移动便可。所以希尔排序的效率要比直接插入排序高。希尔排序的时间复杂度取决于初始增量的选取,致使其时间复杂度很难计算,合理的选取n,时间复杂度能够达到O(n^1.3)大数据
/** * 一次增量为n的插入排序,能够对比直接插入排序 * @param array 待排序数组 * @param n 增量 */ public void oneShellSort(int[] array,int n) { if(array == null || array.length <= 0 || n <1) { return; } for(int i = n; i < array.length; i++) { int temp = array[i]; int j = i; while(j >= n && temp < array[j - n]) { array[j] = array[j - n]; j = j - n; } array[j] = temp; } } /** * 希尔排序,n递减 * @param array待排数组 * @param n 初始增量 */ public void shellSort(int[] array,int n) { if(array == null || array.length <= 0 || n <1) { return; } for(int i = n; i >=1; i--) { oneShellSort(array,i); //i等于1的时候至关于直接插入排序,不过这时已经基本有序了,因此快 } }
快速排序多是排序中经典排序了,咱们平时常常会看到快速排序的字眼,无论是你刚学习的考试中, 仍是面试题中。那么,快速排序究竟是什么样?一种思想实际上是这样,快速排序是取一个基数做为对比基准,在把大的数向后移的同时将比较小的数向前移。快速排序通过一次排序后就能惟一肯定一个元素的位置,这个元素将待排序序列分为两个部分,一部分全部元素小于等于该数,另外一部分全部元素大于等于该数。这两部分的元素又能够递归进行快速排序。快速排序不是一种稳定的排序,时间复杂度和待排序列的初始状况有很大关系。最坏状况下(待排序数组已经有序),时间复杂度为O(n^2);平均时间复杂度为O(nlog2n)。优化
快速排序分为两个步骤,一是要实现将待排序数组按照基数分为两部分,二是要递归实现排序。ui
/** * 快速排序每次获得一个肯定的位置,再根据这个位置将数组分为两部分 * @param array 待排序数组 * @param left 左边界 * @param right 右边界 * @return 肯定位置的元素的下标 */ public int getMiddleIndex(int[] array,int left,int right) { if(array == null || array.length <= 0) { return 0; } if(left >= right) { return 0; } int pivotNum = array[left]; //选取最左边的值为基准值 while(left < right) { while(left < right && array[right] >= pivotNum) { //从后开始找到小于基准值的数移到前面 right--; } array[left] = array[right]; while(left < right && array[left] <= pivotNum) { //从前开始找到大于基准值的数移到后面 left++; } array[right] = array[left]; } array[left] = pivotNum;//基准值的肯定位置 return left; } /** * 快速排序 */ public void quickSort(int[] array, int left, int right) { if(array == null || array.length <= 0 || left >= right) { return; } int minIndex = getMiddleIndex(array,left,right); //将待排序数组分为两部分 quickSort(array,left, minIndex - 1);//左边递归 quickSort(array,minIndex + 1,right);//右边递归 }
堆排序实质上是借助堆这一结构来实现排序,堆实际上是一种彻底二叉树结构,父亲节值点大于子节点的堆称为大顶堆,父节点值小于子节点的值称为小顶堆,很形象的名字。那么,对于待排序数组,如何经过堆来进行排序呢? 这里用大顶堆来举例。知足大顶堆的结构,那么堆的根确定就是最大值,假如咱们要按照升序排列,当咱们将最大值和最后一个值交换后,剩余元素如何从新构建一个新的大顶堆?只要又能构建出一个大顶堆,就能够将堆顶元素和倒数第二个元素进行交换,接下来就是迭代的过程了。因此,咱们须要
解决的问题就有两个:
一、如何将给的待排序数组构建成一个知足条件的堆?
二、将堆顶元素和最后一个元素交换后,如何调整成一个新的知足条件的堆。
spa
堆排序实例过程,参考:http://blog.csdn.net/xiaoxiaoxuewen/article/details/7570621/
咱们能够直接使用线性数组来表示一个堆,由初始的无序序列建成一个堆就须要自底向上从第一个非叶元素开始挨个调整成一个堆。比较当前堆顶元素的左右孩子节点,由于除了当前的堆顶元素,左右孩子堆均知足条件,这时须要选择当前堆顶元素与左右孩子节点的较大者(大顶堆)交换,直至叶子节点。咱们称这个自堆顶自叶子的调整成为筛选。每次调整堆的时间为logn,一共n个数,须要n次调整,因此堆排序的时间复杂度为:O(nlogn)
/** * 调整一个节点,使自该节点之后知足堆的要求 * 通过对构建后,若一个节点有左右子树,则其左右子树已经知足堆的要求 * @param array * @param begin * @param end */ public void headAdjust(int[] array, int begin, int end) { if(array == null || array.length <= 0 || begin >= end || begin >= array.length) { return; } int temp = array[begin]; for(int i = 2 * begin + 1; i <= end; i = i * 2 + 1) { if(i < end && array[i] < array[i + 1]) { i++; } if(temp > array[i]) { break; } array[begin] = array[i]; begin = i; } array[begin] = temp; } /** * 利用大顶堆,实现数组升序排序 * @param array */ public void heapSort(int[] array) { if(array == null || array.length <= 0) { return; } //第一次构建堆,从第一个非叶子节点开始调整,一直到根节点(数组中第一个数) for(int i = array.length / 2; i >= 0; i--) { headAdjust(array,i,array.length - 1); } //每次交换堆顶元素到数组后面,前面剩余数组以堆顶元素进行一次调整堆 for(int i = 1; i <= array.length - 1; i++) { int temp = array[0]; array[0] = array[array.length - i]; array[array.length - i] = temp; headAdjust(array,0,array.length - 1 - i); } }
归并排序使用了递归分治的思想,其基本思想是,先递归划分子问题,而后合并结果。把待排序列当作由两个有序的子序列,而后合并两个子序列,而后把子序列当作由两个有序序列。倒着来看,其实就是先两两合并,而后四四合并。最终造成有序序列。归并排序须要额外的空间来存储合并时的中间数组,空间复杂度为O(n),时间复杂度为O(nlogn)
归并排序过程:
/** * 归并排序 * @param array * @param left * @param right */ public void mergeSort(int[] array, int left, int right) { if(array == null || array.length == 0 || right >= array.length ) { return ; } if(left >= right) { return; } int mid = (left + right) / 2; //分红两部分 mergeSort(array, left, mid); //左右递归 mergeSort(array, mid + 1, right); merge(array,mid,left,right);//在原数组上进行合并 } /** * 归并排序的合并操做 * @param array * @param mid * @param left * @param right */ public void merge(int[] array, int mid, int left, int right) { if(array == null || array.length == 0) { return; } int[] temp = new int[right - left + 1]; int i = left; int j = mid + 1; int k = 0; while(i <= mid && j <= right) { if(array[i] < array[j]) { temp[k] = array[i]; i++; } else { temp[k] = array[j]; j++; } k++; } while(i <= mid) {//若左边有剩余 temp[k++] = array[i++]; } while(j <= right) {//若右边有剩余 temp[k++] = array[j++]; } k = 0; while(left <= right) {//有序数组复制到原数组相应位置 array[left++] = temp[k++]; } }
计数排序是利用空间换时间的一种排序方式,通常来讲,基于比较的排序方式(前面的冒泡排序,选择排序,直接插入排序,快速排序,归并排序等)时间复杂度最低是O(n^2)。可是计数排序在利用较多空间后的时间复杂度能够达到O(n);
计数排序基本思想:
将待排序列的数字做为排序数组的下标,遍历一次待排序列,排序数组统计每一个位置出现的次数。而后一次输出便可。固然,由于是待排数据做为数组下标,若待排序列存在负数,则须要找到最小的负数,全部数加上一个值转换成正数,最后的输出再转换回去。
/** * 计数排序 * @param array */ public void countSort(int[] array) { if(array == null || array.length == 0) { return; } int maxNum = array[0]; for(int i = 0; i < array.length; i++) { //找出待排序列的最大值 if(array[i] > maxNum) { maxNum = array[i]; } } int[] sortArray = new int[maxNum + 1]; //新建一个排序数组,加1 为了让最大值下标能放排序数组 for(int i = 0; i < array.length; i++) { //按照下标放入排序数组中 sortArray[array[i]] += 1; } int k = 0; for(int i = 0; i < sortArray.length; i++) { if(sortArray[i] == 0) { continue; } while(sortArray[i] > 0) { array[k++] = i; //sortArray[i]>0的状况是存在相同的值 sortArray[i]--; } } }
这里其实计数排序能够有一个小的改进,就是,咱们其实不须要建立maxNum长的排序数组,而只须要建立(maxNum - minNum + 1)长的数组就足够。
例如给定无序数组 { 2, 6, 3, 4, 5, 10, 9 },处理过程以下:
实现代码以下:
/** * 计数排序小改进 * @param array */ public void countSort2(int[] array) { if(array == null || array.length == 0) { return; } int maxNum = array[0]; int minNum = array[0]; for(int i = 0; i < array.length; i++) { //找出待排序列的最大和最小值 if(array[i] > maxNum) { maxNum = array[i]; } if(array[i] < minNum) { minNum = array[i]; } } //新建一个排序数组,最大值减去最小值加1,节省了必定的空间 int[] sortArray = new int[maxNum - minNum + 1]; for(int i = 0; i < array.length; i++) { //按照下标放入排序数组中,注意减去minNum sortArray[array[i] - minNum] += 1; } int k = 0; for(int i = 0; i < sortArray.length; i++) { if(sortArray[i] == 0) { continue; } while(sortArray[i] > 0) { array[k++] = i + minNum; //sortArray[i]>0的状况是存在相同的值,注意加上minNum sortArray[i]--; } } }
上面说到计数排序的小改进,真的只是小改进,由于若是出现:3,4,100,10000。这样的待排序序列,依然会消耗大量的空间。那么有没有更进一步的改进呢? 那就是桶排序啦。。
桶排序比较复杂,但核心思路来自计数排序,将待排序数组的maxNum - minNum按区间分红n个桶,如下分析来自:http并按照必定的映射函数将待排序序列每一个值映射到相应的桶中,而后对每一个桶排序,最后一次输出桶中的数据。如下分析来自:http://hxraid.iteye.com/blog/647759。
桶排序基本思想: 假设有一组长度为N的待排关键字序列K[1....n]。首先将这个序列划分红M个的子区间(桶) 。而后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就做为B[i]中的元素(每一个桶B[i]都是一组大小为N/M的序列)。接着对每一个桶B[i]中的全部元素进行比较排序(可使用快排)。而后依次枚举输出B[0]....B[M]中的所有内容便是一个有序序列。
桶排序的映射:
bindex=f(key) 其中,bindex 为桶数组B的下标(即第bindex个桶), k为待排序列的关键字。桶排序之因此可以高效,其关键在于这个映射函数,它必须作到:若是关键字k1<k2,那么f(k1)<=f(k2)。
也就是说B(i)中的最小数据都要大于B(i-1)中最大数据。很显然,映射函数的肯定与数据自己的特色有很大的关系,咱们下面举个例子:假如待排序列K= {4九、 38 、 3五、 97 、 7六、 73 、 2七、 49 }。这些数据所有在1—100之间。所以咱们定制10个桶,而后肯定映射函数f(k)=k/10。则第一个关键字49将定位到第4个桶中(49/10=4)。依次将全部关键字所有堆入桶中,并在每一个非空的桶中进行快速排序后获得以下图所示:
/** * 桶排序 * @param array */ public void bucketSort(int[] array) { if(array == null || array.length <= 0) { return; } int maxNum = array[0]; int minNum = array[0]; for(int i = 0; i < array.length; i++) { //找出待排序列的最大和最小值 if(array[i] > maxNum) { maxNum = array[i]; } if(array[i] < minNum) { minNum = array[i]; } } int bucketNum = (maxNum - minNum) / array.length; //bucketNum是桶的个数 ArrayList<List<Integer>> buckets = new ArrayList<>(); //桶的链表,每一个节点使一个桶 for(int i = 0; i <= bucketNum; i++) { buckets.add(new ArrayList<Integer>()); //初始化桶 } for(int i = 0; i < array.length; i++) { int num = (array[i] - minNum) / array.length; buckets.get(num).add(array[i]); } for(int i = 0; i <= bucketNum; i++) { Collections.sort(buckets.get(i)); //偷个懒,jdk1.8该方法采用二分插入法排序 } int k = 0; for(int i = 0; i <= bucketNum; i++) {//全部桶的元素顺序输出 for(int num: buckets.get(i)) { array[k++] = num; } } }
又到了分析各类算法时间复杂度,空间复杂度的时候了,万能的表格出来!-------……&……&……>>>>>>>:
排序方法 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 须要的辅助存储 | 算法的稳定性 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | -- | -- | O(1) | 不稳定 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(logn) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
计数排序 | O(n) | O(n) | O(n) | O(n) | |
桶排序 | 0(n) |
只有一个桶,取决 于桶内排序算法 |
O(N+C),其中C=N*(logN-logM) M是桶的个数 |
O(N+M) m为桶的个数 |
|
另外,关于算法的稳定性:
假定在待排序的记录序列中,存在多个具备相同的关键字的记录,若通过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj以前,而在排序后的序列中,ri仍在rj以前,则称这种排序算法是稳定的;不然称为不稳定的。
其中直接插入排序是比较简单的,在序列基本有序或者n较小时,直接插入排序是好的方法,所以常将它和其余的排序方法,如快速排序、归并排序等结合在一块儿使用。
参考连接:
http://www.cnblogs.com/wxisme/ 桶排序分析:http://hxraid.iteye.com/blog/647759
2017-03-14 17:25:07 Gonjan