算法 七大基于比较的排序算法
所谓的排序,就是令一串记录的数据按照其中的某个或者某些关键字的大小进行递增或者递减排列的操作。排序有稳定性这一说法,稳定性指的是:假定在待排序的记录数据中,存在多个具有相同关键字的数据,如果经过排序后,这些数据的相对次序保持不变,即在原序列中,r[i] = r[j],且r[i]在r[j]之前,排序之后,r[i]还在r[j]之前,那么就称这种排序算法是稳定的,否则是不稳定的。比如说:
排序还有内部排序和外部排序之说,内部排序:数据元素全部放在内存中的排序;外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内存之间移动数据的排序。不仅如此,有的排序基于比较,有点不基于比较,本文章所列举的都是基于比较的排序算法。
我们生活中排序的应用
上网购物:
无聊时去看大学的排名:
可以说我们的生活中到处充满了排序!那么常见的排序算法有哪些呢?
1.常见的排序算法
常见的排序算法可以分为四大类:插入排序、选择排序、交换排序和归并排序,而选择排序又可以分为直接插入排序和希尔排序,选择排序可以分为选择排序和堆排序,交换排序可以分为冒泡排序和快速排序。
2.常见排序算法的实现
2.1 插入排序
虽然说插入排序细分为直接插入排序和希尔排序,但是希尔排序实际上是直接插入排序的一种优化,因此当我们弄明白直接插入排序后,对于掌握希尔排序也就有了一定的基础。
基本思想:
直接插入排序是一种简单的排序算法,其基本思想是:
把待排序的数据按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的数据插入为止,得到一个新的有序序列。就像是我们打牌一样,我们会将每一张得到的牌插入已经有序的序列中,以达到整理手牌的目的。
直接插入排序
假设现在有一个待排序列arr,当插入arr的第i(i >= 1)个数据时,前面的arr[0],arr[1],...,arr[i - 1]已经排好序了,那么此时用arr[i]与前面已经排好的数据进行比较,找到合适的位置将arr[i]插入即可,原来位置上的数据按照原来的顺序后移。
假设现在arr = {3,1,6,7,4},现在要从小到大排序,那么具体过程如下图所示:
了解思路以后,代码就很容易实现了:
public static void insertSort(int[] arr) {for (int i = 0; i < arr.length; i++) {int j = i - 1;int tmp = arr[i];for (; j >= 0; j--) {if (arr[j] > tmp) {arr[j + 1] = arr[j];}else {break;}}arr[j + 1] = tmp;}}
进行测试:
import java.util.Arrays;public class Test {public static void main(String[] args) {int[] arr = {3,1,6,7,4};Sort.insertSort(arr);System.out.println(Arrays.toString(arr));}
}//运行结果
[1, 3, 4, 6, 7]
符合预期!
对于直接插入排序,有以下总结:
- 元素集合越是接近有序,直接插入排序算法的时间效率越高,最快能达到O(N)
- 时间复杂度:O(N^2)
- 时间复杂度:O(1)
- 稳定性:稳定
希尔排序(缩小增量排序)
希尔排序,又称“缩小增量法”,是对直接插入排序的优化升级,其核心逻辑围绕“增量调控、分组排序、逐步收敛”展开,具体可梳理为:
首先设定一个初始增量(通常取待排序序列长度的一半,记为`gap`),依据该增量将序列划分为若干个独立子组——即把下标间隔为`gap`的元素归为一组。接着,对每个子组分别执行直接插入排序,让组内元素先实现局部有序;这一步的关键作用是通过“大跨度分组”,让元素快速向自己的最终有序位置靠近,避免直接插入排序中“小元素在末尾需逐个前移”的低效问题。
随后按照固定规则缩小增量(例如每次将`gap`减半),重复“按新增量分组→对各组执行直接插入排序”的流程:随着增量逐渐减小,子组的数量会变少、每组包含的元素会增多,但此时整个序列已在多轮分组排序中逐渐趋近有序,后续的插入排序操作会越来越高效。
最终当增量缩小至`1`时,整个序列会合并成一个唯一的子组(即完整序列本身)。此时对这个“已基本有序”的序列执行最后一次直接插入排序,仅需少量调整就能完成最终排序,高效得到全局有序的结果。
操作如下图所示:
通过代码实现主要有两个部分,一个是分组,另一个是直接插入排序,但希尔排序的直接插入排序需要将普通的直接插入排序进行略微修改。代码实现如下:
public static void shellSort(int[] arr) {//进行分组int gap = arr.length/2;while (gap >= 1) {shellInsertSort(arr,gap);gap /= 2;}}//每次分组后进行的直接插入排序public static void shellInsertSort(int[] arr,int gap) {for (int i = gap; i < arr.length; i++) {int j = i - gap;int tmp = arr[i];for (; j >= 0; j -= gap) {if (arr[j] > tmp) {arr[j + gap] = arr[j];}else {break;}}arr[j + gap] = tmp;}}
进行测试:
import java.util.Arrays;public class Test {public static void main(String[] args) {int[] arr = {9,1,2,5,7,4,8,6,3,5};Sort.shellSort(arr);System.out.println(Arrays.toString(arr));}
}//运行结果
[1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
符合预期!代码解读:
- 首先是分组部分,一开始分的组数为数组长度的一半,接着每次都缩小一半,直到分为1组。
- 接着是直接插入部分,这里i初始值为gap,即第一个子数组的第二个元素的位置,i每次+1就好,因为分组是跳跃式的,i+1会进入下一个子数组,而j通过i取值,从而达到对每个子数组进行交替直接插入排序。j的值为i - gap(步长),j每次减步长,保证在子数组内遍历,其他的和普通的直接插入排序差不多。
希尔排序的特性总结:
- 希尔排序是直接插入排序的优化
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此很多专家在书中给出的希尔排序的时间复杂度都不统一,但是因为我们gap的取值是按照Knuth提出的方式进行的,而Knuth进行了大量的试验统计,因此希尔排序的时间复杂度暂且就按照:O(n^1.25)到O(1.6n^1.25)来算
- 稳定性:不稳定
2.2 选择排序
基本思想:
每一次从待排序的数据中选出最小(或最大)的一个数据,存放在序列的起始位置,直到全部待排序的数据排完。
选择排序
当我们知道了选择排序的基本思想以后,它的步骤通常如下:
- 在数据集合arr[i]到arr[n-1]中找到关键码最小(或最大)的数据元素
- 如果它不是待排数据中的第一个元素,则将它与数据中的第一个元素交换
- 接着在剩余的待排序数据中,继续采取上述的两个步骤,直到待排序的数据只剩一个
假设现在待排数据集合arr = {5,,3,8,4,2},对它进行升序排序,则:
那么,代码实现如下:
public class Sort {//选择排序public static void selectSort(int[] arr) {for (int i = 0; i < arr.length; i++) {//最小值的下标int minIndex = i;for (int j = i + 1; j < arr.length; j++) {//如果待排数据中有小于minIndex下标的,minIndex更新if (arr[j] < arr[minIndex]) {minIndex = j;}}//交换minIndex与待排数据的第一个元素swap(arr,i,minIndex);}}//交换数组中的两个数private static void swap(int[] arr,int i,int j) {int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp;}
}
进行测试:
import java.util.Arrays;public class Test {public static void main(String[] args) {int[] arr = {5,3,8,4,2};Sort.selectSort(arr);System.out.println(Arrays.toString(arr));}
}//运行结果:
[2, 3, 4, 5, 8]
符合预期!
选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)(内外两个循环)
- 空间复杂度:O(1)
- 稳定性:不稳定
对于选择排序,其实有一种优化方案,就是使用双指针的思想,简单来说,就是两头都进行排序,左端排小的值,右边排的大的值,从而将内循环的时间复杂度减少一半。虽然时间效率提高了,但是整个算法的时间复杂度还是O(N^2)。它的代码实现如下:
//选择排序——优化public static void selectSortOptimize(int[] arr) {//定义左边界int left = 0;//定义右边界int right = arr.length - 1;//左右边界一起移动,但他们相遇时,说明数组排好了while (left < right) {//最小元素的下标int minIndex = left;//最大元素的下标int maxIndex = left;for (int i = left; i <= right; i++) {if (arr[i] < arr[minIndex]) {//更新最小值下标minIndex = i;}if (arr[i] > arr[maxIndex]) {//更新最大值下标maxIndex = i;}}//将最小值放到左边界swap(arr,left,minIndex);//处理特殊情况:当最大值在left处时,进行最小值的放置时,会把在left处的最大值移到minIndex处if (maxIndex == left) {maxIndex = minIndex;}//将最大值放到右边界swap(arr,right,maxIndex);//缩小左右边界left++;right--;}}
进行测试:
import java.util.Arrays;public class Test {public static void main(String[] args) {int[] arr = {5,3,8,4,2};Sort.selectSortOptimize(arr);System.out.println(Arrays.toString(arr));}
}//运行结果
[2, 3, 4, 5, 8]
堆排序
对于堆排序,在优先级队列的学习中,只要掌握了优先级队列的底层堆,那么实现堆排序易如反掌。堆排序是通过堆来进行选择排序的,需要注意如果要升序排序,就建大堆,如果要降序排序,就建小堆!
堆排序的步骤:
- 根据选择建堆
- 利用交换方法和向下调整进行排序
不知道如何建堆和向下调整,请看:https://blog.csdn.net/2301_80037974/article/details/151122539?spm=1001.2014.3001.5502https://blog.csdn.net/2301_80037974/article/details/151122539?spm=1001.2014.3001.5502
假设现在有一个数据集合:arr = {5,3,6,7,1,2},对它进行升序排序,使用代码实现如下:
//堆排序public static void heapSort(int[] arr) {//建堆createHeap(arr);//进行排序int end = arr.length - 1;while (end > 0) {swap(arr,0,end);end--;siftDown(arr,0,end);}}//建大堆private static void createHeap(int[] arr) {for (int parent = (arr.length - 1 -1)/2; parent >= 0; parent--) {siftDown(arr,parent,arr.length-1);}}//向下调整private static void siftDown(int[] arr,int parent,int size) {int child = parent*2 + 1;while (child <= size) {if (child + 1 <= size && arr[child + 1] > arr[child]) {child++;}if (arr[child] > arr[parent]) {swap(arr,child,parent);}else {break;}parent = child;child = parent*2 + 1;}}
进行测试:
import java.util.Arrays;public class Test {public static void main(String[] args) {int[] arr = {5,3,6,7,1,2};Sort.heapSort(arr);System.out.println(Arrays.toString(arr));}
}//运行结果:
[1, 2, 3, 5, 6, 7]
符合预期!
堆排序的特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
2.3 交换排序
基本思想:
所谓的交换,就是根据序列中两个数据的比较结果来决定是否对换它们在序列中的位置,交换排序的特点:把关键码值较大的数据向序列的尾部移动,关键码值较小的数据向序列的首部移动。
冒泡排序
冒泡排序是最基础的交换排序,通过相邻元素的逐一比较,让 “较大数据” 像水泡一样逐步 “浮” 到序列尾部。它的核心流程如下(以升序排序为例):
- 从序列头部开始,依次比较两个相邻的元素
- 如果“前元素 > 后元素”,那么就交换这个两个元素,保证大的元素后移
- 经过一遍遍历以后,最大的待排元素会“冒泡”到序列尾部
- 缩小待排序列,重复以上操作,直到待排序列长度为1即可
代码实现也非常简单:
public class Sort {//冒泡排序public static void bubbleSort(int[] arr) {for (int i = 0; i < arr.length - 1; i++) {//每遍历一遍,有i个元素已经冒泡到尾部了for (int j = 0; j < arr.length - 1 - i; j++) {if (arr[j] > arr[j + 1]) {swap(arr,j,j+1);}}}}//交换数组中的两个数private static void swap(int[] arr,int i,int j) {int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp;}
}
进行测试:
import java.util.Arrays;public class Test {public static void main(String[] args) {int[] arr = {6,8,4,2,3,5};System.out.println("排序前:" + Arrays.toString(arr));Sort.bubbleSort(arr);System.out.println("排序后:" + Arrays.toString(arr));}
}//运行结果:
排序前:[6, 8, 4, 2, 3, 5]
排序后:[2, 3, 4, 5, 6, 8]
符合预期!
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
对于冒泡排序,我们发现:当在一轮遍历中没有发生元素的交换的话,此时序列已经有序了,那么后面的遍历就没有必要了。了解这个以后,冒泡排序的优化版本就来了:
//冒泡排序_优化public static void bubbleSortOptimize(int[] arr) {for (int i = 0; i < arr.length - 1; i++) {//添加标记boolean mark = true;//每遍历一遍,有i个元素已经冒泡到尾部了for (int j = 0; j < arr.length - 1 - i; j++) {if (arr[j] > arr[j + 1]) {swap(arr,j,j+1);//发生交换标记发生改变mark = false;}}//如果一轮遍历标记无变化,说明序列已经有序if (mark) {return;}}}
进行测试:
import java.util.Arrays;public class Test {public static void main(String[] args) {int[] arr = {6,8,4,2,3,5};System.out.println("排序前:" + Arrays.toString(arr));Sort.bubbleSortOptimize(arr);System.out.println("排序后:" + Arrays.toString(arr));}
}//
排序前:[6, 8, 4, 2, 3, 5]
排序后:[2, 3, 4, 5, 6, 8]
符合预期!
快速排序
快速排序是Hoare与1962年提出的一种二叉树结构的交换排序方法,其基本思想:
任取待排序元素序列中的某元素作为基准值,按照该基准值将待排序集合分为两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程,直到所有元素都排列到相应位置上为止。像这样:
快速排序的操作类似于二叉树,那么我们想要用代码实现,可以通过二叉树的递归思想,每一次根据基准值分好左右子序列以后,先处理左子序列,处理完了再处理右子序列即可。代码实现如下:
//快速排序public static void quickSort(int[] arr) {quick(arr,0,arr.length-1);}//快速排序的核心操作private static void quick(int[] arr,int i,int j) {if (i >= j) {return;}int k = find(arr,i,j);//处理左子序列quick(arr,i,k-1);//处理右子序列quick(arr,k+1,j);}//找基准值的下标private static int find(int[] arr,int i,int j) {//基准值下标int k = i;//基准值int pivot = arr[k];while (i < j) {while (i < j && arr[j] >= pivot) {j--;}while (i < j && arr[i] <= pivot) {i++;}swap(arr,i,j);}swap(arr,k,i);return i;}
进行测试:
import java.util.Arrays;public class Test {public static void main(String[] args) {int[] arr = {6,1,2,7,9,3,4,5,10,8};System.out.println("排序前:" + Arrays.toString(arr));Sort.quickSort(arr);System.out.println("排序后:" + Arrays.toString(arr));}
}//运行结果
排序前:[6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
排序后:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
符合预期!但其实实现快速排序的方法不止这一种,上述的实现方式称为Hoare版快速排序,常用的还有挖坑法快速排序和前后指针法快速排序,它们的核心思想都是将待排序列按照基准值划分为左右两部分处理。这三种方法不同的地方在于找基准值最终位置的操作不同。
挖坑法:
在找基准值最终位置的操作中,挖坑法的操作是这样的:现将基准值存放在一个临时变量key中,那么原来基准值的位置就形成了一个“坑位”,依旧是i从左往右走,遇到比基准值小的就走,大的就停,j从右往左走,遇到比基准值大的就走,小的就停。j先走,停下后,将此时j位置的值给坑位,接着i走,停下后,把此时i位置的值给空出来的新坑位,当i<j时,j和i继续走。当i>=j后,将key放入最终的坑位中,返回基准值最终位置i。
代码实现:
//找基准值的下标_挖坑法private static int find_Dig(int[] arr,int i,int j) {//基准值int pivot = arr[i];while (i < j) {while (i < j && arr[j] >= pivot) {j--;}arr[i] = arr[j];while (i < j && arr[i] <= pivot) {i++;}arr[j] = arr[i];}arr[i] = pivot;return i;}
前后指针法:
在找基准值最终位置的操作中,前后指针法的操作是这样的:定义两个指针,分别是cur(遍历指针),prev(小于基准值的序列边界),通过cur去遍历这个序列,当遇到小于基准值的值是,扩大小于基准值的序列边界prev,并且当prev位置的元素与cur位置的元素不是同一个时,进行交换,如果遇到大于基准值的元素时,cur继续向后走,直到遍历结束。
代码实现:
//找基准值的下标_双指针法private static int find_TwoPointers(int[] arr,int i,int j) {//遍历指针int cur = i + 1;//指向小于基准值的序列的边界int prev = i;//基准值int pivot = arr[i];while (cur <= j) {//当遍历到的元素小于基准值时if (arr[cur] < pivot) {//小于基准值的序列边界扩大prev++;//当边界的元素和遍历元素是同一个时,不交换if (prev != cur) {swap(arr,prev,cur);}}cur++;}swap(arr,prev,i);return prev;}
快速排序的问题与其优化
在实现快速排序的过程中,我们难免发现两个问题:
- 有时候序列已经趋于有序时,所去的基准值可能会导致快速排序退化为冒泡排序,比如说arr = {1,2,3,5,6},取基准值1,那么序列按照基准值分为两个子序列时,我们会发现它没有左子序列,只有右子序列,再分,情况还是一样,这就导致快速排序退化为冒泡排序,使得代码时间效率降低了。
- 当子序列被分的很短时,再接着分其实就没有必要了,能不能在子序列很短的情况下使用别的排序算法呢?
对于这两个痛点,我们的解决方案如下:
- 使用三数取中法(在序列的左端、中端和右端的三个数据将关键码值位于中间的值放到基准值的位置)去降低基准值取到极端值的情况
- 在子序列划分足够小的情况下,使用插入排序
代码实现如下(Hoare版本):
//交换数组中的两个数private static void swap(int[] arr,int i,int j) {int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp;}//快速排序public static void quickSort(int[] arr) {quick(arr,0,arr.length-1);}//快速排序的核心操作private static void quick(int[] arr,int i,int j) {//当子序列长度小于5时,递归结束,进行插入排序if (j - i + 1<= 5) {insertSort(arr,i,j);return;}//使用三数取中法selectPivotByMedianOfThree(arr,i,j);int k = find(arr,i,j);//处理左子序列quick(arr,i,k-1);//处理右子序列quick(arr,k+1,j);}//找基准值的下标private static int find(int[] arr,int i,int j) {//基准值下标int k = i;//基准值int pivot = arr[k];while (i < j) {while (i < j && arr[j] >= pivot) {j--;}while (i < j && arr[i] <= pivot) {i++;}swap(arr,i,j);}swap(arr,k,i);return i;}//快速排序专用插入排序private static void insertSort(int[] arr,int left,int right) {for (int i = left + 1; i <= right; i++) {int j = i - 1;int tmp = arr[i];for (; j >= left; j--) {if (arr[j] > tmp) {arr[j + 1] = arr[j];}else {break;}}arr[j + 1] = tmp;}}//三数取中法private static void selectPivotByMedianOfThree(int[] arr,int left,int right) {//中间元素int mid = (right + left)/2;if (arr[left] < arr[right]) {// 情况1:左 < 右if (arr[mid] < arr[left]) {// 中 < 左 < 右 → 左是中值,不用动return;} else if (arr[mid] > arr[right]) {// 左 < 右 < 中 → 右是中值,交换左和右swap(arr, left, right);} else {// 左 < 中 < 右 → 中是中值,交换左和中swap(arr, left, mid);}} else {// 情况2:左 >= 右if (arr[mid] < arr[right]) {// 中 < 右 <= 左 → 右是中值,交换左和右swap(arr, left, right);} else if (arr[mid] > arr[left]) {// 右 <= 左 < 中 → 左是中值,不用动return;} else {// 右 <= 中 <= 左 → 中是中值,交换左和中swap(arr, left, mid);}}}
快速排序的特性总结(优化版本):
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
对于二叉树的一些操作,我们也不一定要使用递归的思路,也可以使用非递归的思路,既然快速排序基于二叉树的结构,那么它同样也能用非递归的思想实现。
非递归思路:可以用栈来模拟递归的实现(未进行优化):
//快速排序_非递归实现public static void quickSortNor(int[] arr) {quick(arr,0,arr.length-1);}//核心操作private static void quickNor(int[] arr,int start,int end) {Stack<Integer> stack = new Stack<>();//第一层划分基准值的位置int pivot = find(arr,start,end);//确保左子序列长度大于1if (pivot > start + 1) {stack.push(start);stack.push(pivot - 1);}//确保右子序列长度大于1if (pivot < end - 1) {stack.push(pivot + 1);stack.push(end);}while (!stack.empty()) {//取出栈储存的子序列end = stack.pop();start = stack.pop();//接着划分pivot = find(arr,start,end);//确保左子序列长度大于1if (pivot > start + 1) {stack.push(start);stack.push(pivot - 1);}//确保右子序列长度大于1if (pivot < end - 1) {stack.push(pivot + 1);stack.push(end);}}}
代码运行举例:
2.4 归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
其实现思路主要有两点:1.进行分解;2,进行合并。
- 对于分解,我们发现,归并排序在分解阶段的效果和快速排序的分子序列有异曲同工之处,那么我们也可以像快速排序那样使用递归的思路,获取序列长度的中间位置,将序列分成两个差不多相等的部分,接着继续分解,直到子序列只有一个元素。
- 对于合并,当分解完成后,此时我们会获得两个子序列,接着只要把这两个子序列拼接在一起形成一个新的序列,然后用这个新序列把旧序列对应位置上的值覆盖即可,随着一次合并的完成,方法会开始递归回去,将每个子序列合并,最终实现排序!
代码实现如下:
//归并排序public static void mergeSort(int[] arr) {mergeSortTmp(arr,0,arr.length-1);}//核心操作private static void mergeSortTmp(int[] arr,int left,int right) {//开始分解if (left >= right) {return;}//求中间位置int mid = (left + right)/2;//分解左边mergeSortTmp(arr,left,mid);//分解右边mergeSortTmp(arr,mid+1,right);//开始合并merge(arr,left,mid,right);}//合并private static void merge(int[] arr,int left,int mid,int right) {//定义新序列int[] tmp = new int[right - left + 1];int k = 0;//将两个子序列的元素排序后放入新序列(经典的拼接数组问题)int s1 = left;int e1 = mid;int s2 = mid+1;int e2 = right;while (s1 <= e1 && s2 <= e2) {if (arr[s1] <= arr[s2]) {tmp[k] = arr[s1];k++;s1++;}else {tmp[k] = arr[s2];k++;s2++;}}//假设第一个子序列比第二个子序列长while (s1 <= e1) {tmp[k] = arr[s1];k++;s1++;}//假设第二个子序列比第一个子序列长while (s2 <= e2) {tmp[k] = arr[s2];k++;s2++;}/*这几个循环结束后,两个子序列已经在新序列中排好序现在把新序列的元素覆盖到旧序列的对应位置上*/for (int i = 0; i < k; i++) {arr[i+left] = tmp[i];}}
进行测试:
import java.util.Arrays;public class Test {public static void main(String[] args) {int[] arr = {10,6,7,1,3,9,4,2};System.out.println("排序前" + Arrays.toString(arr));Sort.mergeSort(arr);System.out.println("排序后" + Arrays.toString(arr));}
}//运行结果
排序前[10, 6, 7, 1, 3, 9, 4, 2]
排序后[1, 2, 3, 4, 6, 7, 9, 10]
与我们的推算一样!
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
前面我们说过,归并排序和快速排序有异曲同工之处,既然快速排序能够使用非递归的方式实现,那么归并排序是否也可以呢?答案是肯定的。它的核心思路是放弃递归的分解过程,改用循环来控制子序列的合并顺序,从最小的子序列(长度为 1)开始,然后每次子序列的长度翻倍,直到最终完成整个数组的排序。
代码实现如下:
//归并排序_非递归实现public static void mergeSortNor(int[] arr) {//子序列的长度设置int gap = 1;//这层循环控制gap的增长while (gap < arr.length) {//这层循环控制子序列的合并,每次处理两个相邻的子序列,因此i+2*gapfor (int i = 0; i < arr.length; i += 2*gap) {//左子序列起始int left = i;//左子序列结束int mid = left + gap - 1;//防止越界if (mid >= arr.length) {mid = arr.length - 1;}//右子序列结束int right = mid + gap;//防止越界if (right >= arr.length) {right = arr.length-1;}//开始合并merge(arr,left,mid,right);}gap = 2*gap;}}
3.排序算法复杂度及稳定性分析
排序算法 | 最好时间复杂度 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | 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) | 稳定 |
那么,到此基于比较的七大算法已经全部介绍到,感谢您的阅读,如有错误还请指出,谢谢!