完整教程:数据结构**排序** 超越Arrays.sort() 探索Java排序算法的奥秘与魅力
文章目录
“跬步积累,终成千里路!”
1. 前言
本文主要围绕Java中七大基于比较的排序展开,内容很干,有点难度!请大家跟进小编的步伐!!
本文的全部代码在这!
2. 正文
1. 排序
1.1 排序的相关概念
排序:顾名思义,排序就是按照数据大小进行递增或递减排列起来的操作。
稳定性: 稳定性这个词可能是我们之前没有接触过的,下面看小编的图解

内部排序:数据元素全部放在内存中的排序。例如电脑的内存可能为8GB,适用于数据量相对较小,内存可以完全容纳,这里我们所学到的七大排序全是在内存上完成的
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。这里的数据放在磁盘上,也就是我们电脑中的C盘,D盘
1.2 排序的运用
首先我们要明白为什么需要排序,在我们的生活中,处处充满了排序,例如价格排序,成绩排序… 所以我们一定要学习好排序这个知识
1.3 排序的算法
我们这里主要介绍基于比较的排序,还有一些非基于比较的排序,小编会单独出一期,那些仅限于了解即可!
我们一点点攻克这些主要的排序算法!!
2. 插入排序
2.1 直接插入排序
我们首先要介绍的就是直接插入排序,也是很简单的一种排序
反之如果 array[j]<tmp,则直接让array[j+1]=tmp即可
下面我们完善一下代码
public static void insertSort(int[]array){
for (int i = 1; i < array.length ; i++) {
int tmp=array[i];
int j=i-1;
while(j>=0){
if(array[j]>tmp){
array[j+1]=array[j];
j--;
}else {
array[j+1]=tmp;
break;
}
}
array[j+1]=tmp;
}
}
这个代码的完成也是非常简单,我们看一下测试的对不对
结构正确!
下面我们关注一下这个排序的稳定性:直接插入排序是一种稳定的的排序,我们根据代码画图即可得知,但有的小伙伴会说,如果在代码中的第一行if语句中把 " > " 改成 ">= "则不稳定了,这里小编想说,如果一个排序是一个稳定的排序那么它可以变成不稳定的排序,如果一个排序是一个不稳定的排序,则不能变成稳定的排序!
我们再来关注一下时间复杂度:要根据算法的思想来计算时间复杂度,这里 j 分别走了1,2,3…N-1, 所以是一个等差数列,时间复杂度为O(N^2)
最后关注一下空间复杂度:这个排序没有开辟新的空间,仅仅是在array数组上进行操作,所以空间复杂度为O(1)
这里小编想强调,如果这个数据本身是有序的例如1,2,3,4,5,6,用直接插入排序时间复杂度就会达到O(N),但是数据是逆序,时间复杂度就很大,所以直接插入排序多应用于这个数据本身是比较有序的,越有序插入越快
2.2 希尔排序
希尔排序可以理解为是直接插入排序的一种优化,代码本身是不难的,但是思路很新颖
希尔排序就是采用如上的思路,先把数据进行分组,再进行分组排序,这样使得数据变得更有序!最后进行整体排序。我们接下来看一下希尔排序是如何分组的
我们这里一共有8个数据,希尔排序的分组分别为4,2,1,这里如果分的组越多数据则会越少,数据更无序,分的组越少,数据就越多但越有序!
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定。
因为希尔排序的公式不唯一,小编这里就用gap/=2来实现我们的代码
为了使我们的代码整体看上去美观我们shellSort只传array这一个参数,内部进行调用shell方法传两个参数。
首先我们要确定一点,在我们完成的直接插入排序中 i 是从1位置出发的,这里的 i 还是从1位置出发吗? 显然不是的,上面的图也告诉我们 i 应该从gap位置出发的!
其次,我们的for循环中 i 是应该++ 还是应该+=gap呢?显然是++的,如果+=gap则很多数据会被跳跃排序不到了!
最后我们确定 j 每次是- - 还是 - =gap,应该为 - = gap,因为要将array[j+gap]赋值为array[i]
下面我们完成代码,这与直接插入排序的思路是一样的
public static void shellSort(int[]array){
int gap=array.length;
while(gap>1){
gap/=2;
shell(array,gap);
}
}
private static void shell(int[] array, int gap) {
for (int i = gap; i <array.length ; i++) {
int tmp=array[i];
int j=i-gap;
while(j>=0){
if(array[j]>tmp){
array[j+gap]=array[j];
j-=gap;
}else{
array[j+gap]=tmp;
break;
}
}
array[j+gap]=tmp;
}
}
我们再来看看运行结果
确实没有任何问题!
下面我们关注一下
稳定性: 不稳定,可以自己尝试画图
时间复杂度: 没有准确的值 n^1.3 – n^1.5
空间复杂度: O(1)
3. 选择排序
3.1 选择排序
对于选择排序的实现我们有两种思路:
首先我们了解一下基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。这里小编以最小的一个元素来实现选择排序举例

根据上图我们实现代码就非常容易首先我们需要一个交换的方法来实现交换值的操作
然后我们来完成selectSort
也是非常简单的private static void shell(int[] array, int gap) { for (int i = gap; i <array.length ; i++) { int tmp=array[i]; int j=i-gap; while(j>=0){ if(array[j]>tmp){ array[j+gap]=array[j]; j-=gap; }else{ array[j+gap]=tmp; break; } } array[j+gap]=tmp; } }
结果也是没问题的稳定性: 不稳定
时间复杂度: O(N^2)
空间复杂度: O(1)选择排序的时间复杂度为O(N^2),与直接插入排序不同,它的时间复杂度不管数据是否有序都不变!
我们提供另一种思路来完成选择排序,也是对选择排序的优化! 上述代码遍历数组一次只寻找了一个最小值,如果遍历数组一次能找到最大值和最小值,那么效率就会提高!
我们可以根据图先完成代码!
完成这部分代码后,当我们运行时会出现问题
排序成这样了,大家这里是不是满头问号??下面小编再列举一种情况
所以我们的代码需要完善一种情况,就是swap(array,left,minIndex)后,如果maxIndex此时==left 那么将maxIndex赋值minIndex 即可!
public static void selectSort2(int[]array) {
int left = 0;
int right=array.length-1;
while (left < right) {
int minIndex=left;
int maxIndex=left;
for (int i = left+1; i <=right; i++) {
if (array[i] < array[minIndex]) {
minIndex = i;
}
if (array[i] > array[maxIndex]) {
maxIndex = i;
}
}
swap(array, left, minIndex);
if(maxIndex==left) {
maxIndex = minIndex;
}
swap(array,right,maxIndex);
left++;
right--;
}
}
结果这次没问题了!
这是我们对选择排序进行的优化,但是优化后的时间复杂度仍然为O(N^2),只是效率提升。可见选择排序的时间效率很低很低。
3.2 堆排序
在上篇博客我们着重介绍了堆和堆的排序,这里我们复习一下如何进行堆排序
首先我们要先创造一个堆,这里我们创建大根堆,因为是从小到大的排序

下面我们完成向下建堆的操作
这里小编不过多赘述了交换0下标与堆尾下标元素,再进行向下建堆调整

最后代码就完成了
public static void heapSort(int[]array){
creatHeap(array);
int end=array.length-1;
while (end>0) {
swap(array,0,end);
siftDown(array,0,end);
end--;
}
}
private static void creatHeap(int[] array) {
for (int parent = (array.length-1-1)/2; parent >=0 ; parent--) {
siftDown(array,parent,array.length);
}
}
private static void siftDown(int[]array,int parent, int length) {
int child=parent*2+1;
while (child <length){
if(child+1 < length && array[child+1] > array[child]){
child++;
}
if(array[parent]<array[child]){
swap(array,parent,child);
parent=child;
child=parent*2+1;
}else{
break;
}
}
}
结果也没问题。
稳定性: 不稳定
时间复杂度: O(NlogN)
空间复杂度: O(1)
堆排序是一个非常快的排序,且空间复杂度为O(1) ! 它又快又不占空间, 很难让人不爱❤️
4. 交换排序
4.1 冒泡排序
这是我们熟悉不能再熟悉的排序方法,见到它如同见到故人般!!
我们直接来实现代码
非常简单,i 代表趟数 ; j 代表遍历元素的下标,这里讲解一下冒泡排序的优化
首先我们可以在趟数中优化,我们知道 j 从头到尾走一次后最大值已经放在末端,则末端的元素不需要进行下一次的排序!
这样就优化了冒泡排序我们可以定一个布尔类型的变量,赋值为false,j 走一圈如果发生交换了则把boolean变量赋值为true , 跳出循环后判断boolean类型的变量是否为true 如果为false则代表数据已经有序没有发生任何排序

所以最终的冒泡排序代码如下
public static void bubbleSort(int[]array){
for (int i = 0; i < array.length ; i++) {
boolean flg=false;
for (int j = 0; j < array.length-1-i; j++) {
if(array[j+1]<array[j]){
swap(array,j,j+1);
flg=true;
}
}
if(!flg){
break;
}
}
}
稳定性: 稳定
时间复杂度: O(N^2)
空间复杂度: O(1)
注意:我们这里讨论的时间复杂度是没有优化过的冒泡排序,如果有题目问冒泡排序的时间复杂度也要回答为 O(N^2) 如果进行优化,可能会达到 O(N) !
4.2 快速排序
在经历了前面五种排序后,有的小伙伴可能会觉得非常简单,那么大的来了
快速排序是我们从未接触过的全新的一种思想,有三种快速排序的方法,小编依次介绍!
挖坑法(最常用的方法)
(图一)这样操作后我们能确定元素6左边均为小于6的元素, 右边均为大于6的元素!!我们可以把当前元素6的下标定义为pivot
我们分别对pivot的左边进行如上操作,再对pivot右边进行如上操作,最终就会变为有序的排列!! 这也是二叉树递归的思想,我们可以把快速排序与二叉树相联系!!下面我们一点点完成代码
我们先搭好框架,quickSort调用quick含三个参数的方法,quick中调用patition方法,来对我们的数组完成图一操作,并返回pivot我们先来完成patition方法
这是我们根据图一可以完成的代码,但是!!这个代码有一个很小的问题也很隐蔽
如果元素是以上情况,right会走到left的左边,我要让left与right相遇就停止!所以要完善代码
只需要再加一个条件在内层循环即可下面我们完成quick方法

这与二叉树的思路是一样的,分别递归左边和右边,左边的right要更新,右边的left要更新!如果left>=right则停止!
public static void quickSort(int[]array){
quick(array,0,array.length-1);
}
private static void quick(int[] array, int left, int right) {
if(left>=right){
return;
}
int pivot=patition(array,left,right);
quick(array,left,pivot-1);
quick(array,pivot+1,right);
}
private static int patition(int[] array, int left, int right) {
int tmp=array[left];
while(left<right){
while(array[right]>=tmp && left<right){
right--;
}
array[left]=array[right];
while(array[left]<=tmp && left<right){
left++;
}
array[right]=array[left];
}
array[left]=tmp;
return left;
}
这里注意一点 : 在patition方法中内循环array[right/left]与tmp相比一定要取等号,如果不取等号两边遇到一样的数字就会发生死循环!!
结果也是没问题的!!
稳定性: 不稳定
时间复杂度: O(NlogN)
空间复杂度: O(logN)
注意: 这里的时间复杂度和空间复杂度我们都只讨论最好的情况,也就是数据是分散开的,排序后会呈现一个完全二叉树! 如果数据是 1,2,3,4,5,6 或 6,5,4,3,2,1最坏情况 那么时间复杂度和空间复杂度就会有变化
这样就会形成一个单分支的二叉树,时间复杂度甚至达到了O(N^2) 空间复杂度也达到了O(N) ! !
但是,我们在讨论快速排序的时间复杂度和空间复杂度都要说最好的情况,等讲解完快速排序的三种方法小编会把单分支二叉树这一特殊情况进行优化!
Hoare法
这是一个以名字来命名的算法,我们先看一下这个伟大人物的简介

可见hoare的伟大之处! 下面我们讲解hoare法的思路
我们hoare法的思路与挖坑法基本一致,都是要将头元素的左边都是比头元素小的,右边都是比头元素大的然后再进行递归! 递归的思路与挖坑法一致再走一遍当前图的操作!
这里我们观察hoare法得到的头元素左右元素序列与挖坑法得到的左右元素序列是不同的!
下面我们来完成以下代码,我们要知道挖坑法与hoare法的本质就是patition方法内容不同,其余在quick方法的递归的操作都是相同的!
我们多定义了一个leftTmp用来存放起始left的值
public static void quickSort(int[]array){
quickHoare(array,0,array.length-1);
}
private static void quickHoare(int[] array, int left, int right) {
if(left>=right){
return;
}
int pivot=patition2(array,left,right);
quickHoare(array,left,pivot-1);
quickHoare(array,pivot+1,right);
}
private static int patition2(int[] array, int left, int right) {
int leftTmp=left;
while(left<right){
int tmp=array[left];
while(array[right]>=tmp&& left<right){
right--;
}
while(array[left]<=tmp&& left<right){
left++;
}
swap(array,left,right);
}
swap(array,left,leftTmp);
return left;
}
结果·也是没问题的,这样hoare法的快速排序我们就介绍完了!
- 前后指针法(不常用了解为主)
与挖坑法和hoare法一样,只是patition方法改变,我们直接来实现一下
private static void quickPointer(int[] array, int left, int right) {
if (left >= right) {
return;
}
int pivot = patition3(array, left, right);
quickPointer(array, left, pivot - 1);
quickPointer(array, pivot + 1, right);
}
private static int patition3(int[] array, int left, int right) {
int prev = left;
int cur = left + 1;
while (cur <= right) {
if (array[cur] < array[left] && array[++prev] != array[cur]) {
swap(array, cur, prev);
}
cur++;
}
swap(array, prev, left);
return prev;
}
自此,快速排序的三种方法我们都讲解完毕,下面我们看一下快速排序的优化!!
4.3 快速排序优化
三数取中法
首先我们要先对单分支二叉树这种情况(1,2,3,4,5 / 5,4,3,2,1)进行优化
下面我们完善一下代码
我们新定义一个方法用来寻找中间大小的值,三数取中法主要就是1,2,3,4,5,6和 6,5,4,3,2,1单分支树的优化,所以这里找中值分为两种情况,一种是left下标的元素小于right下标,另一种就是大于。再细分为mid left right 三者的关系
分别各有三种情况,这样这个方法就完成了
再进行交换,三数取中法的优化就完成了!
递归到小的子区间时,使用插入排序
如果快速排序的序列呈现一个完全二叉树的形状
所以我们可以自己规定,当递归到某一部分时,采用直接插入排序进行剩余的操作!
我们这里设置的区间是当排序的数据的长度小于5采用直接插入排序!!记住一定要加return!! 不然代码会继续往下走重新再排序。并且,这里用的直接插入排序方法不能套用之前写过的insertSort,因为此时的 i 是从left+1下标开始走,走到right下标 不是从1下标开始走一整个数组长度
这样对快速排序的优化就完成了,执行结果也是没有问题的
4.4 快速排序的非递归实现
在前面我们学习二叉树的时候多次使用了递归这一操作,最后都会用非递归的方式去实现,这次也不例外!!
下面思考应该如何做呢??
这个思路有点绕,但是仔细看是可以看懂的!也就是我们利用了栈的入栈出栈去实现了递归这个操作,结束入栈的条件有两个,当栈为空排序就完成了!
下面完成代码
这是我们入栈的操作!我们对比图刚开始部分就可以充分理解
这是我们出栈的操作并且判断下一次的pivot与left+1和right-1进行比较,如果达到条件继续入栈,未达到条件代表当前元素只有一个已经有序!
这样非递归实现的快速排序我们就完成了!
5. 归并排序
5.1 归并排序递归实现
上面我们经过快速排序的洗礼相信我们对排序的理解又上了一个层次,下面我们介绍最后一种基本排序“归并排序”,下面我们讲解归并排序的思路
归并排序主要包含分解和合并两个步骤,首先我们看分解
我们不断从数组中间位置平均分为两部分,分解到只剩唯一一个元素,下面开始合并
这就是合并的过程!下面我们一点点完成代码!
首先我们调用mergeTmp(分解)这个方法,分解就是不断递归最后只剩唯一的元素
下面我们思考如何完成合并这个操作,如果是两个独立的元素,那么谁小谁放在前面即可,但是如果是两个数组进行排序,那么该如何去做呢!
其中 s1 是新组成数组的left e1是mid s2是mid+1 e2是right
根据图完成我们的代码
我们按着图的思路走,i代表tmp数组的各个位置,提前定义就不用使用for循环了!
最后把tmp数组的每一个值传给array数组! 但是这个代码有一个小小的问题!
运行发现结果不对,这是因为我们上面举的例子是0,1,2,3下标进行合并,如果是4,5,6,7下标进行合并,array[j] 还是从0下标开始,这样就会覆盖原来的值,所以我们这里可以利用一个小技巧进行调整
如果这里改为j+left 那么代码就没有问题了,j每次从0开始,left每次是初始位置!
public static void mergeSort(int[] array) {
mergeTmp(array, 0, array.length - 1);
}
private static void mergeTmp(int[] array, int left, int right) {
if (left >= right) {
return;
}
int mid = (left + right) / 2;
mergeTmp(array, left, mid );
mergeTmp(array, mid + 1, right);
merge(array, left, mid, right);
}
private 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 i=0;
int[] tmp = new int[left + right + 1];
while (s1 <=e1 && s2 <= e2) {
if (array[s1] < array[s2]) {
tmp[i++] = array[s1++];
} else {
tmp[i++] = array[s2++];
}
}
while (s1 <=e1) {
tmp[i++] = array[s1++];
}
while(s2<=e2){
tmp[i++] = array[s2++];
}
for (int j = 0; j < i ; j++) {
array[j+left]=tmp[j];
}
}
稳定性: 稳定
时间复杂度: O(NlogN)
空间复杂度: O(N)
这里归并排序的空间复杂度由于申请了一个新的数组所以为O(N),我们可以理解为归并排序牺牲了空间来创造时间
5.2 归并排序的非递归实现
归并排序也类似于一个二叉树的结构进行的排序,那么肯定也可以使用非递归来实现!下面我们看看思路
非递归实现的归并排序的思路如上图,我们只需要把每一个数组的元素作为独立的个体也就是独立的left,然后一次次合并即可
下面我们看看每一个left和right下标应该如何取
我们可以发现每一次mid和right下标都发生了变化!(这里right=2*gap+left-1)!!
下面我们来完成代码
我们根据思路完成代码,但是运行还是有问题
问题是数组越界异常,这里我们传入6个元素,gap=4的时候会出现问题!
同样思考如果传入的是5个元素,mid也会越界,只有left不会越界,我们是以left为基准!
完善这个问题后,排序还是错误的,还错在哪里呢!?
我们代码的这里不应为i++,下一次left的位置应该是right+1 也就应该为i+2*gap!!
这样排序就不会有问题了
public static void mergeSortNor(int[] array) {
int gap = 1;
while (gap < array.length) {
mergeTmp2(array, 0, gap, array.length - 1);
gap *= 2;
}
}
private static void mergeTmp2(int[] array, int left, int gap, int right) {
for (int i = 0; i < array.length; i=i+2*gap) {
left=i;
int mid = left + gap - 1;
if(mid>=array.length){
mid=array.length-1;
}
right = left + 2 * gap - 1;
if(right>=array.length){
right=array.length-1;
}
merge(array, left, mid, right);
}
}
这样非递归的归并排序我们就完成了!
以上我们完成了基本的七大排序!!相信你一定很有收获
以下是小编对这些排序的总结!
| 排序算法 | 平均时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|
| 插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
| 冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
| 选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
| 希尔排序 | O ( n 1.3 ) O(n^{1.3}) O(n1.3) ~ O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
| 快速排序 | O ( n log n ) O(n \log n) O(nlogn) | O ( log n ) O(\log n) O(logn) | 不稳定 |
| 归并排序 | O ( n log n ) O(n \log n) O(nlogn) | O ( n ) O(n) O(n) | 稳定 |
| 堆排序 | O ( n log n ) O(n \log n) O(nlogn) | O ( 1 ) O(1) O(1) | 不稳定 |
3. 结语
以上就是本文主要的内容,我们已经攻克了排序的所有内容,下面小编会讲解七大排序,数据结构的知识就剩下一个尾巴了!有不明白的地方可以留言小编会回复,希望读者们多提建议,小编会改正,共同进步!谢谢大家。

浙公网安备 33010602011771号