一. 排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持 不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳 定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
二. 插入排序
1. 直接插入排序
- 基本思想:
直接插入排序是一种简单的插入排序法,其基本思想是: 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到 一个新的有序序列; 实际中我们玩扑克牌时,就用了插入排序的思想。
实现过程:
首先只看序列当中的第一个元素, 那么这一个元素是有序的, 从第二个元素开始,将当前待排序元素的下标设为i, 并使用变量tmp储存该元素,设下标j,默认第一个值为i-1(从已经有序的部分最后一个位置开始比较);
如果下标j对应元素比tmp大,则将该元素则向右移动一个位置(即将这个元素放到j+1位置),并将j–,直到j<0或者j对应元素小于tmp,此时将tmp储存至j+1处。就能将tmp插入到一个合适的位置,使得前i+1个元素有序,然后继续向后遍历序列 , 直到完成所有元素的插入操作,数组完成了升序排列; 如果要降序,实现方式是相同的 , 只需要改变一下比较方式即可。
- 实现代码:
/** *直接插入排序 * 时间:最坏逆序情况下O(N^2);最好顺序情况下O(N) * 空间:O(1) * 稳定性:稳定 */ public static void insertSort(int[] array) { //把第一个元素当为有序的,从第二个元素开始,往前面的序列中插入 for (int i = 1; i < array.length; i++) { //先将arrar[i]中的值拿出来 int tmp = array[i]; //找到前面序列中第一个比tmp小的位置,然后插入 int j = 0; for (j = i-1; j >= 0; j--) { //比tmp大的元素向后挪动一个位置 if (array[j] > tmp) { array[j+1] = array[j]; }else { //此时j位置元素比tmp小,j+1就是要插入的位置; break; } } array[j+1] = tmp; } }
- 性能分析:
元素集合越接近有序,直接插入排序算法的时间效率越高
时间复杂度 | 空间复杂度 | 稳定性 |
最好:数组有序,O(N)最坏:O(N ^ 2) | O(1) | 稳定的排序 |
2. 希尔排序
- 基本思想及实现过程:
希尔排序法又称缩小增量法, 希尔排序是直接插入排序的改进,
把待排序文件中所有记录分成gap个组,距离为gap的记录分在同一组内,并对每一组内的记录进行插入排序。
希尔排序gap先大后小, gap大于1的状态,称为预排序 ; 这个过程是让数组不断接近有序, 直到最终gap等于1 , 此时数组已经接近有序的了, 所有数据在统一在一组内排序就会很快。
gap递减的规则不唯一 , 可以循环除3加1,也可以除2; 下面的代码中用的是除2实现。
假如有10个元素需要排序,首先将这10个元素为5组,对每组分别进行直接插入排序,再分为2组,并对每组分别进行直接插入排序,最后分为1组,进行直接插入排序;
- 实现代码:
/** *希尔排序(插入排序的优化) * 时间:记住 O(N^1.3) 就好 * 空间:O(1) * 稳定性:不稳定 */ public static void shellSort(int[] array) { int gap = array.length; //将数组中的元素分为gap组,每组元素之间间隔gap个元素, //将每组中的元素进行插入排序 while(gap > 1) { gap /= 2; shell(array, gap); } } private static void shell(int[] array, int gap) { //和插入排序的过程一样,只不过,每个元素的下一个元素与当前元素间隔为gap for (int i = gap; i < array.length; i++) { //这里循环的调整部分i++要注意去理解一下 int tmp = array[i]; int j = 0; for (j = i-gap; j >= 0; j -= gap) { if (array[j] > tmp) { array[j+gap] = array[j]; }else { break; } } array[j+gap] = tmp; } }
- 性能分析:
时间复杂度 | 空间复杂度 | 稳定性 |
O(N ^ 1.3) | O(1) | 不稳定的排序 |
希尔排序的效率与怎么分组息息相关,严蔚敏和殷人昆的《数据结构》书中中对希尔排序的效率问题是这样表示的:
《数据结构(C语言版)》— 严蔚敏
《数据结构-用面向对象方法与C++描述》— 殷人昆
二. 选择排序
1. 直接选择排序
- 基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
- 实现过程:
在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素
若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
- 实现代码:
/** *直接选择排序 *时间:O(N^2) *空间:O(1) *稳定性:不稳定 */ public static void selectSort(int[] array) { for (int i = 0; i < array.length; i++) { //每一次将最小的元素放到i位置 int minIndex = i;//记录最小值的下标 for (int j = i+1; j < array.length; j++) { if(array[minIndex] > array[j]) { minIndex = j;//更新 } } //如果i和minIndex相等就没必要去交换 if(i != minIndex) { swap(array, i, minIndex); } } } private static void swap(int[] array, int i, int j) { int tmp = array[i]; array[i] = array[j]; array[j] = tmp; }
- 性能分析:
直接选择排序思考非常好理解,但是效率不是很好 , 实际中很少使用
时间复杂度 | 空间复杂度 | 稳定性 |
O(N ^ 2) | O(1) | 不稳定的排序 |
2. 堆排序
- 基本思想及实现过程:
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。
需要注意的是排升序要建大堆,排降序建小堆。
升序排序步骤:
将数组转换成大根堆,使用索引end标记最后一个元素。
将堆顶元素与end处元素交换,end–。
对堆顶结点进行向下调整,调整的结点下标不大于end。
重复2,3步骤,直到end<=0。
大根堆堆顶存放最大元素,每次将最大元素与end出元素交换,再调整end-1部分的堆,这样就能依次把最大的元素放在后面 ; 同样的降序排列使用小根堆即可。
- 实现代码:
/** *堆排序 *时间:O(N*logN) *空间:,O(1) *稳定性:不稳定 */ public static void heapSort(int[] array) { //排升序,先将数组改成一个大根堆 createBigHeat(array); int end = array.length-1; //end=0时,只剩最后一个元素,位置也就自动确定了 while(end > 0) { //将堆顶元素(也就是在确定最大元素)与数组最后一个位置元素交换 swap(array, 0, end); //向下调整,注意此时不能把end位置处的元素算在调整范围内了 shiftDown(array, 0, end); end--; } } private static void createBigHeat(int[] array) { //从最后一个根节点开始向上调整 for (int parent = (array.length-1-1)/2; parent >= 0 ; parent--) { shiftDown(array, parent, array.length); } } private static void shiftDown(int[] array, int parent, int len) { int child = parent*2+1; //判断有左孩子 while(child < len) { //判断是否有右孩子,并比较左右孩子的大小 if(child+1 < len && array[child] < array[child+1]) { child++; } if(array[child] > array[parent]) { swap(array, child, parent); parent = child; child = parent*2+1; }else{ break; } } } private static void swap(int[] array, int i, int j) { int tmp = array[i]; array[i] = array[j]; array[j] = tmp; }
- 性能分析:
堆排序使用堆来选数,效率就高了很多
时间复杂度 | 空间复杂度 | 稳定性 |
O( N*logN ) | O(1) | 不稳定的排序 |
三. 交换排序
基本思想:
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特 点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
1. 冒泡排序
- 基本思想和实现过程:
从第二个元素开始,将该元素与前一个元素比较,如果前一个元素比较大,则交换。直到最后一个元素为最大元素,这一过程称为一趟冒泡 ; 每进行一趟冒泡排序,下次排序比较的范围就缩小一个位置,因为每进行一趟冒泡排序就会确定一个元素的最终位置。
如果数组在冒泡前就有序了呢 , 这时候不需要再进行冒泡比较了,所以基于上面的思想可以进行优化,基本思路就是当遇到一趟排序时并没有发生元素交换,这时候就说明数组已经有序了,下一趟就不用排了,所以代码中可以使用一个标记,如果在冒泡过程中发生了交换 , 就改变标记的状态 , 这样就可以在下次冒泡前根据这个标记来确定之后是否需要继续冒泡排序了。
- 实现代码:
/** *冒泡排序 *时间:不考虑优化的话O(N^2); *空间:O(1) *稳定性:稳定 */ public static void bubbleSort(int[] array) { boolean flag = true; for (int i = 0; i < array.length-1; i++) { for (int j = 0; j < array.length-1-i; j++) { if(array[j] > array[j+1]) { swap(array, j, j+1); flag = false; } } if(flag) { break; } } } private static void swap(int[] array, int i, int j) { int tmp = array[i]; array[i] = array[j]; array[j] = tmp; }
- 性能分析:
时间复杂度 | 空间复杂度 | 稳定性 |
不考虑优化,最坏 : O( N^2 )最好:O(N), 数组有序。 | O(1) | 稳定的排序 |
2. 快速排序
基本思想:
从数组中找一个基准值 ,然后以该基准值,将比基准值小的元素排在基准值左边,比基准值大的元素排在基准值右边,此时基准值在数组中的排序位置已经确定了,再对基准值左边和右边的序列以相同的方式重新找基准值并将比基准值小的排左边,大的排右边,直到只剩下一个元素,此时所有的元素排序位置都确定了,排序完成。
快速排序递归实现的主框架与二叉树前序遍历规则非常像,在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。
将区间按照基准值划分为左右两半部分的常见方式有:
Hoare版
挖坑法
前后指针
实现快速排序有三步,第一找基准值, 第二确定基准值位置,第三对基准值左右序列进行相同的找基准操作,最终会使所有的元素有序。
2.1 Hoare法
实现过程:
- 以范围内第一个元素为基准值。
- 先让right从右边界开始向前遍历,遇到比基准值小的元素为止 ; 再left从左边界开始向后遍历,遇到比基准值大的元素时停止。
- 然后交换这两个元素,继续2过程。
- 直到left与right相遇为止,将相遇位置(比基准值小的元素)与基准值原位置交换,此时基准值的位置确定,基准左边元素比基准值小,右边元素比基准值大。
分别对基准值左右序列进行上述相同的操作:取左边界元素为基准值,通过Hoare法确定基准值排序位置; 直到左右序列只有一个元素或没有元素为止。
注意:一定要先让right找比基准值小的元素,再让left找比基准值大的元素,这样保证最后相遇时的元素一定是比基准值小的元素。
实现代码:
/** * Hoare快速排序 * 时间:N*logN * 空间:O(logN) * 稳定性:不稳定 * * 当数据是有序的时候,这个快排的时间复杂度是O(n^2) * 空间复杂度:O(n) */ public static void quickSortHoare(int[] array) { quickHoare(array, 0, array.length-1); } private static void quickHoare(int[] array, int start, int end) { if(start >= end) {//这里必须写上>号,预防没有左树或者右树的情况 return; //比如: 1 2 3 4 5 或者 5 4 3 2 1 } int pivot = partitionHoare(array, start, end); quickHoare(array, start, pivot-1); quickHoare(array, pivot+1, end); } private static int partitionHoare(int[] array, int left, int right) { int i = left; //把范围内的第一个元素当作基准 int pivot = array[left]; while(left < right) { //先从右往左边找比pivot小的元素 //注意这里必须是先找右边较小的数字,保证在最后相遇位置的元素比基准小 while(left < right && array[right] >= pivot) {//这里的等号要理解,如果没有的话可能会出现死循环的情况,比如开头和结尾的元素相同 right--;//left<right条件预防其他元素都比基准大 } //然后从左往右找比pivot大的元素 while(left < right && array[left] <= pivot) { left++;//left<right条件预防其他元素都比基准小 } swap(array, left, right); } //相遇位置的元素和原来i位置(基准位置)元素交换 swap(array, i, left); return left;//返回基准元素所在位置 } private static void swap(int[] array, int i, int j) { int tmp = array[i]; array[i] = array[j]; array[j] = tmp; }
2.2 挖坑法
实现过程:
挖坑法与Hoare 法是很相似的,Hoare 法是交换,挖坑法不过是将交换改为了填坑:
以范围内第一个元素为基准值。
先让right从右边界开始向前遍历,遇到比基准值小的元素为止 ; 初始以基准值处为坑 , 将right位置的元素放到基准值处(填坑) , 此时right处就是新的坑位(挖坑) ; 然后left找比基准值小的元素 , 将left位置的元素放到right处(填坑) ; 此时left处就是新的坑位(挖坑) … 直到left与right相遇为止 , 此时再将基准值放到left位置(填最后一个坑)。
分别对基准值左右序列进行上述相同的操作:取左边界元素为基准值,通过挖坑法确定基准值排序位置。直到左右序列只有一个元素或没有元素为止。
注意:一定要先让right找比基准值小的元素。
实现代码:
/** *挖坑法快排 *和Hoare版的相同,只是将交换 换成了 填坑 */ public static void quickSortDigging(int[] array) { quickHoare(array, 0, array.length-1); } private static void quickDigging(int[] array, int start, int end) { if(start >= end) { return; } int pivot = partitionDigging(array, start, end); quickDigging(array, start, pivot-1); quickDigging(array, pivot+1, end); } private static int partitionDigging(int[] array, int left, int right) { int pivot = array[left];//将第一个元素作为基准保存起来,此时第一个位置就是一个坑位 while(left < right) { while(left < right && array[right] >= pivot) { right--; } //在右边找到比基准小的元素,然后赋值给左边的坑位,此时右边就空出一个坑位 array[left] = array[right]; while(left < right && array[left] <= pivot) { left++; } //在左边找到比基准大的元素,然后赋值给右边的坑位,此时左边又空出一个坑位 array[right] = array[left]; } //最后一次循环是left往右边走和right坑位相遇,将pivot放到最后一个坑位即可 array[left] = pivot; return left; }
2.3 前后指针法
实现过程:
以范围内第一个元素为基准值。
prev指向初始位置,cur指向prev的下一个位置。
prev先走 , cur后走 , 每次都是移动一个位置 , cur找比基准小的元素, 找到了prev移动一个位置并将prev和cur位置处的元素交换 。
直到cur>right,再交换基准值位置和prev的值即可
分别对基准值左右序列进行上述相同的操作:取左边界元素为基准值,通过前后指针法确定基准值排序位置。直到左右序列只有一个元素或没有元素为止。
看下面的动图看起来就像是prev和cur之间夹着比基准大的元素向后走。
上面的思路其实是可以优化的 , 由于prev先走 , cur后走 , 在cur碰到比基准大的元素前cur和prev总会指向同一个位置, 此时的交换是没有什么意义的, 所以可以加一个判断 , 当cur和prev不在同一个位置时再进行交换。
实现代码:
/** *前后指针快速排序 *时间和空间: 和Hoare版的相同,只是找基准的方式不同 */ public static void quickSortPointer(int[] array) { quickHoare(array, 0, array.length-1); } private static void quickPointer(int[] array, int start, int end) { if(start >= end) { return; } int pivot = partitionPointer1(array, start, end); quickPointer(array, start, pivot-1); quickPointer(array, pivot+1, end); } //写法一: private static int partitionPointer1(int[] array, int left, int right) { int d = left + 1; int pivot = array[left]; for (int i = left + 1; i <= right; i++) { //一开始i和d是在同步移动的,当i位置的元素大于基准时,此时d就不在移动, //当d下一次移动时,d指向的就是比基准大的元素,然后交换 if (array[i] < pivot) { if(i != d) { swap(array, i, d); } d++; } } swap(array, d - 1, left); return d - 1; } //写法二: private static int partitionPointer2(int[] array, int left, int right) { int prev = left; int cur = left+1; while(cur < right) { //当cur指向的元素小于基准且前一个元素比基准大,此时++prev使prev指向的是比基准大的元素,然后交换 //cur和prev之间会夹着比基准大的元素不断向后 if(array[cur] < array[left] && array[++prev] != array[cur]) { swap(array,cur,prev); } cur++; } //循环结束之后指向的是比基准小的元素,将其和array[left](基准)交换, swap(array, left,prev); return prev; } private static void swap(int[] array, int i, int j) { int tmp = array[i]; array[i] = array[j]; array[j] = tmp; }
2.4 性能分析及快速排序优化
假设数组中有N个待排序的元素,上述三种方法实现快速排序过程其实类似于二叉树的前序遍历,递归每层需要遍历数组元素的总个数为N ,递归的高度为logN,所以快速排序时间复杂度为O(N*logN) ,空间复杂度为O(logN),但此时是比较好的情况下的复杂度 , 递归结构类似于一棵完全二叉树。
上面的情况是我们直接选取第一个元素为基准值,但这个基准值可能是最大或最小的元素,如果基准值是数组中最大或最小的元素,此时如果还按上面的方式实现 , 递归下来就是一棵单分支型树,极端情况下,数组是有序或逆序情况下,每次找到的基准值都是最大或最小值,这时候时间复杂度为O(N^2),空间复杂度为O(N),为了解决这里的问题 , 我们就需要对上面的思路进行优化了。
上面三种方法的基本思路是不变的 , 主要需要优化的找基准值的方法 , 让基准值尽量向范围内的中位数靠拢 , 这里主要介绍下面的优化方法:
三数取中法
找到范围中最左边位置,最右边位置 , 中间位置中的三个元素 , 选择选择其中一个居中的数字作为基准值。
我们可以先判断左右边界值的大小 , 然后再与中间位置的元素比较 , 以此来确定出这三个元素的中间值为基准值 。
进行了这个优化就不会有单分支情况的出现了 , 时间复杂度为O(N*logN)。
递归到小的子区间时,可以考虑使用插入排序(减少递归次数)
快速排序的时间主要是在递归上 , 看上面分析复杂度的图 , 其实其实递归主要主要集中在最后几层 , 而最后几层 , 每次递归的范围内 , 所要排序的元素其实是很少的 , 而且由于元素少 , 所以这些元素是接近有序的 , 通过对上文中插入排序的分析 , 序列中元素少且接近有序的情况下使用插入排序是非常快的 , 我们可以自己去设置这个较小范围 , 当递归元素等于和小于这个范围时就采用插入排序来完成这个序列中的排序。
实现代码:
在挖坑法基础上实现
/** * 快速排序优化(三数取中法和在指定区间进行插入排序) * 数据有序的时候,使用三数取中方法,树呈完全二叉的状态,降低了时间和空间复杂度, * 不加插入优化: 时间,空间: 和Hoare版不考虑有序相同 * 递归的次数主要集中在树的后面几层,后面几层的每个查找区间都很小,数据接近于有序 * 在这些小区间中使用插排效率会很高 */ public static void quickSortOptimize(int[] array) { quickHoare(array, 0, array.length-1); } private static void quickOptimize(int[] array, int start, int end) { if(start >= end) { return; } //指定较小区间范围使用插排 if(end - start+1 <= 10) { insertSort(array, start, end); return; } //在找基准前尽量解决划分不均匀的问题 //找出第一个,最后一个,中间元素的的中间值,将中间值作为基准 int midIndex = findMidValOfIndex(array, start, end); swap(array, start, midIndex); int pivot = partitionOptimize(array, start, end); quickOptimize(array, start, pivot-1); quickOptimize(array, pivot+1, end); } private static void insertSort(int[] array, int left, int right) { for (int i = left+1; i < right; i++) { int tmp = array[i]; int j = 0; for (j = i-1; j >= left; j--) { if (array[j] > tmp) { array[j+1] = array[j]; }else { break; } } array[j+1] = tmp; } } private static int findMidValOfIndex(int[] array, int start, int end) { int midindex = (end+start) / 2; if(array[start] < array[end]) { if(array[midindex] < array[start]) { return start; }else if(array[midindex] > array[end]) { return end; }else{ return midindex; } }else { if(array[midindex] > array[start]) { return start; }else if(array[midindex] < array[end]) { return end; }else{ return midindex; } } } private static int partitionOptimize(int[] array, int left, int right) { int pivot = array[left];//将第一个元素作为基准保存起来,此时第一个位置就是一个坑位 while(left < right) { while(left < right && array[right] >= pivot) { right--; } //在右边找到比基准小的元素,然后赋值给左边的坑位,此时右边就空出一个坑位 array[left] = array[right]; while(left < right && array[left] <= pivot) { left++; } //在左边找到比基准大的元素,然后赋值给右边的坑位,此时左边又空出一个坑位 array[right] = array[left]; } //最后一次循环是left往右边走和right坑位相遇,将pivot放到最后一个坑位即可 array[left] = pivot; return left; }
没优化的版本:
时间复杂度 | 空间复杂度 | 稳定性 |
最坏O(N^2), 平均O(N*logN) | 最坏O(N),平均O(logN) | 不稳定的排序 |
三数取中法优化优化的版本:
时间复杂度 | 空间复杂度 | 稳定性 |
O(N*logN) | O(logN) | 不稳定的排序 |
2.4 非递归实现快速排序
非递归实现快速排序借助栈来实现,去观察用递归实现的快速排序,在第一次找到基准后,可以对左右区间进行递归操作,对左右区间再完成找基准的操作,不断进行这个过程,直至完成排序 ; 而非递归方法的实现就是使用栈来模拟递归的过程。
基本思路就是创建一个栈,在第一次找到基准后,分别把基准左边的左右边界压入栈中和基准右边的左右边界压入栈 ;
只要栈不为空 , 就取出右左边界 , 对该区间内的元素进行找基准再进行找基准操作 ; 然后再分别把基准左边的左右边界压入栈中和基准右边的左右边界压入栈。
在入栈前要判断基准左边(右边)是不是有两个以上的元素 , 当只有一个元素就不需要入栈了
直到栈为空,排序完成。
实现代码:
/** *非递归实现快速排序 *利用一个栈来实现 */ public static void quickSortNonRecursive(int[] array) { Stack<Integer> stack = new Stack<>(); int start = 0; int end = array.length-1; int pivot = partitionDigging(array, start, end); //判断基准左边是不是有两个以上的元素 if(pivot > start+1) { stack.push(start); stack.push(pivot-1); } //判断基准右边是不是还有两个以上的元素 if(pivot < end-1) { stack.push(pivot+1); stack.push(end); } while(!stack.isEmpty()) { end = stack.pop(); start = stack.pop(); pivot = partitionOptimize(array, start, end); //判断基准左边是不是有两个以上的元素 if(pivot > start+1) { stack.push(start); stack.push(pivot-1); } //判断基准右边是不是还有两个以上的元素 if(pivot > end-1) { stack.push(pivot+1); stack.push(end); } } } private static int partitionDigging(int[] array, int left, int right) { int pivot = array[left];//将第一个元素作为基准保存起来,此时第一个位置就是一个坑位 while(left < right) { while(left < right && array[right] >= pivot) { right--; } //在右边找到比基准小的元素,然后赋值给左边的坑位,此时右边就空出一个坑位 array[left] = array[right]; while(left < right && array[left] <= pivot) { left++; } //在左边找到比基准大的元素,然后赋值给右边的坑位,此时左边又空出一个坑位 array[right] = array[left]; } //最后一次循环是left往右边走和right坑位相遇,将pivot放到最后一个坑位即可 array[left] = pivot; return left; }
四. 归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并 。
1. 递归实现的归并排序
递归实现归并排序时,递过程分解数组,归过程合并数组 , 用解决子问题的思考方式 , 要整个数组有序, 那么就将将这个数组均分成左右两个部分 , 先让左边有序 , 再让右边有序 , 最终再合并两个有序序列 , 而此时的左右序列同样如此解决。
每次递归将范围内的数据均分成左右两个部分,最后范围内只有一个数据 , 递归结束。
递归结束返回后就是归并的过程 , 其实就是在合并两个有序数组 , 最终将所有范围的序列合并完成后 , 排序完成。
实现代码:
/** * 归并排序 * 时间:O(n*logN) * 空间:O(n) * 稳定性:稳定 */ public static void mergeSort(int[] array) { mergeSortChild(array, 0, array.length-1); } private static void mergeSortChild(int[] array, int left, int right) { if(left == right) { return; } int mid = (left+right) / 2; mergeSortChild(array, left, mid); mergeSortChild(array, mid+1, right); merge(array, left, mid, right); } public static void merge(int[] array, int left, int mid, int right) { int s1 = left; int e1 = mid; int s2 = mid+1; int e2 = right; int[] tmp = new int[right-left+1]; int k = 0; //相当于是在合并两个有序数组 while(s1<=e1 && s2<=e2) { if(array[s1] < array[s2]) { tmp[k++] = array[s1++]; }else { tmp[k++] = array[s2++]; } } while(s1 <= e1) { tmp[k++] = array[s1++]; } while(s2 <= e2) { tmp[k++] = array[s2++]; } for (int i = 0; i < tmp.length; i++) { array[i+left] = tmp[i]; } } /**
2. 非递归实现归并排序
非递归实现归并排序只需要完成归并操作即可 , 一一归并···两两归并···四四归并···直至整体有序 , 排序完成。
对数组进行分组,每组元素为gap,初始时为1,按照2组为一个单位进行有序合并,合并后每组元素为gap=2*gap,直到每组元素个数大于或等于排序数组元素个数len为止
合并过程中需要保证调整后的mid与right不能越界,如果越界需要调整为待排序序列的最后一个元素下标
实现代码:
/** * 非递归实现归并排序 */ public static void mergeSortNonRecursive(int[] array) { int gap = 1; while(gap < array.length) { for (int i = 0; i < array.length; i += 2*gap) { int left = i; int mid = left+gap-1; int right = mid+gap; if(mid > array.length-1) { mid = array.length-1; } if(right >= array.length-1) { right = array.length-1; } merge(array, left, mid, right); } gap *= 2; } } public static void merge(int[] array, int left, int mid, int right) { int s1 = left; int e1 = mid; int s2 = mid+1; int e2 = right; int[] tmp = new int[right-left+1]; int k = 0; //相当于是在合并两个有序数组 while(s1<=e1 && s2<=e2) { if(array[s1] < array[s2]) { tmp[k++] = array[s1++]; }else { tmp[k++] = array[s2++]; } } while(s1 <= e1) { tmp[k++] = array[s1++]; } while(s2 <= e2) { tmp[k++] = array[s2++]; } for (int i = 0; i < tmp.length; i++) { array[i+left] = tmp[i]; } }
3. 性能分析
归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
时间复杂度 | 空间复杂度 | 稳定性 |
O(N*logN) | O(N) | 稳定的排序 |
4. 海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
- 先把文件切分成 200 份,每个 512 M
- 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
- 进行 2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了
五. 常见排序算法性能分析总结
排序方法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
插入排序 | 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^1.3) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(1) | 不稳定 |
快速排序 | O(n * log(n)) | O(n * log(n)) | O(n^2) | O(log(n)) ~ O(n) | 不稳定 |
归并排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(n) | 稳定 |
六. 计数排序(非基于比较)
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
- 找出序列中最大值和最小值,开辟maxVal-minVal+1的辅助空间
- 最小的数对应下标为0的位置,遇到一个数就给对应下标处的值+1,。
- 遍历一遍辅助空间,就可以得到有序的一组序列。
实现代码:
/** * 计数排序 * 时间:O(范围+n) * 空间:O(范围) * 稳定性:不稳定 */ public static void countSort(int array[]) { int maxVal = array[0]; int minVal = array[0]; //遍历数组找到最大值和最小值 for (int i = 1; i < array.length; i++) {//n if(array[i] > maxVal) { maxVal = array[i]; } if(array[i] < minVal) { minVal = array[i]; } } int[] countArr = new int[maxVal-minVal+1]; //统计数组中每个元素出现的次数 for (int i = 0; i < array.length; i++) {//n countArr[array[i]-minVal]++; } int index = 0; for (int i = 0; i < countArr.length; i++) {//范围+n for (int j = 0; j < countArr[i]; j++) { array[index++] = i+minVal; } } }
性能分析:
计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
时间复杂度 | 空间复杂度 | 稳定性 |
O(MAX(N,范围)) | O(范围) | 稳定的排序 |