本文详细描述了排序中常用的七大算法,均基于Java数据结构及代码实现,不熟悉Java也可以看看,毕竟思想是一样的,稍微改动即可。(此文均以顺序为最终排序结果)
目录
一、直接插入排序
一、图文解释
故名思意,直接插入排序即指将要插入的数字在已经排好的数组中找到应插入有的位置进行插入。例:(红色数字代表当前要插入的数字,蓝色数字代表当前要与红色数字进行比较的数字,绿色代表已经排好的数字)
原数组:
5 | 1 | 3 | 2 | 4 |
很明显,当原数组中只含有一个元素的时候,不用进行任何操作其就是有序的,所以我们从第二个元素开始对其进行排序。
5 | 1 | 3 | 2 | 4 |
那如何找出其在已经排好的数组中的位置呢?
则我们需要不断地与之前已经排好中的数组一个一个进行比较,遇到比其大的,说明此数应该放在其前面,直到遇到比其小的,则找到了此数在原已经排好的数组中的位置。
5 | 1 | 3 | 2 | 4 |
1 | 5 | 3 | 2 | 4 |
1 | 5 | 3 | 2 | 4 |
1 | 3 | 5 | 2 | 4 |
1 | 3 | 5 | 2 | 4 |
1 | 3 | 2 | 5 | 4 |
1 | 2 | 3 | 5 | 4 |
1 | 2 | 3 | 5 | 4 |
1 | 2 | 3 | 4 | 5 |
二、代码
代码:
public void insertSort(int[] arr){
for(int i=1;i<arr.length;i++){
int tmp=arr[i];
//找出tmp在原数组中的位置
int j=i-1;
for(;j>=0;j--){
if(arr[j]>tmp){
arr[j+1]=arr[j];
}else{
break;
}
}
arr[j+1]=tmp;
}
}
三、时间复杂度
从代码中也不难发现,最坏情况即为原数组为倒序的情况,则几乎每一个数字与已经排好的数组的每一个数字都要进行比较,从而导致其时间复杂度最终为 ,并且其不会打乱原来相同元素的相对位置,为稳定排序
二、希尔排序
从上文来看,当原数组为倒序时,会导致直接插入排序时间复杂度极大,而希尔排序则可以认为是直接插入排序在极端情况下对直接插入排序的一种优化。
一、图文解释
希尔排序可以看成将原数组进行分组,每一组再进行直接插入排序。那应该怎么进行分组呢?
可能有人的第一思路和我一样,那无非是将原数组左右等分成两组进行两组分开排序,然后再等分成三组,再分开进行排序,再等分成四组......从理论上来说,这样也行,但是如果当左右等分原数组后,原数组两半刚好就是倒序的呢?例:
5 | 4 | 3 | 2 | 1 | 10 | 9 | 8 | 7 | 6 |
那这样分组进行排序那不是并没有优化直接插入排序所带来的弊端吗?所以这种分组方式显然不是希尔大佬选择的分组方式。
希尔大佬的分组方式:
(相同颜色的数字为一组,gap代表每一组每个数字之间的间隔)
gap=5
10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
排序后:
5 | 4 | 3 | 2 | 1 | 10 | 9 | 8 | 7 | 6 |
gap=2
5 | 4 | 3 | 2 | 1 | 10 | 9 | 8 | 7 | 6 |
排序后:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
可以看到,每一组就算碰到最坏情况,其每一组的数字量都比较少。
二、代码
有些书上gap可能会采用gap/=3的处理方法,此处采用gap/=2的处理方法
public void shellSort(int[] arr){
int gap=arr.length/2;
while(gap>0){
for(int i=0;i<gap;i++){
for(int j=i+gap;j<arr.length;j+=gap){
int tmp=arr[j];
int k=j-gap;
for(;k>=0;k-=gap){
if(arr[k]>tmp){
arr[k+gap]=arr[k];
}else{
break;
}
}
arr[k+gap]=tmp;
}
}
gap/=2;
}
}
三、时间复杂度
关于希尔排序的时间复杂度,并没有人研究出一个具体的数,大概是~
,很明显,为非稳定排序
三、选择排序
选择排序故名思意就是不断遍历还未排序的数组,不断从未排序的数组中挑出最小的元素,放到已排序数组的末尾。
一、代码
public static void selectSort(int[] arr){
int i=0;
while(i<arr.length){
int min=arr[i];
int minIndex=i;
for(int j=i;j<arr.length;j++){
if(arr[j]<min){
min=arr[j];
minIndex=j;
}
}
int tmp=arr[i];
arr[i++]=min;
arr[minIndex]=tmp;
}
}
二、时间复杂度
由于排每一个元素都要将剩下的元素进行遍历,故其时间复杂度为 ,其为不稳定排序,例当原数组为7(1) 2 3 4 7(2) 1时,交换顺序后,无法保证原来“7”的前后相对顺序,因为交换顺序后,第一个7会出现在第二个7后面。
四、堆排序
一、图文解释
堆的底层使用到的即是数组,而堆排序相当于是对选择排序中遍历未排序数组的一种优化。其将二叉树的思想完美结合于数组中,利用向下调整,向上调整,挑出最大值(最小值)。
如果结果要求顺序排序,那应该创建大根堆,如果结果要求逆序排序,那应该创建小根堆。
大根堆:
我们要时刻记得,此树非真正的二叉树,而是我们结合数组的一种思考方式,其本质上还是数组,最终我们挑出来的元素还应存在于此数组(树)中。
将最大的元素与未排序的最后一个元素交换位置,不断重复此过程,直到所有元素被排完。
例:
则此时未排序的最后一个元素为2,接下来应该将1向下调整,将树根结点也就是数组0下标的元素与2交换位置,之后不断重复此过程。
(ps:“建树”的时候,我们应使用到向上调整)
二、代码
//向上调整
private void createTree(int[] arr){
int parent=(arr.length-1)/2;
while(parent>=0){
downChange(arr,parent,arr.length);
parent--;
}
}
//向下调整
private void downChange(int[] arr,int parent,int end){
int child=2*parent+1;
while(child<end) {
if(child+1<end&&arr[child+1]>arr[child]){
child++;
}
if(arr[parent]<arr[child]){
int tmp=arr[parent];
arr[parent]=arr[child];
arr[child]=tmp;
parent=child;
}else{
break;
}
child=2*parent+1;
}
}
public void heapSort(int[] arr){
//建树
createTree(arr);
int end=arr.length;
while(end>1){
int tmp=arr[0];
arr[0]=arr[end-1];
arr[end-1]=tmp;
end--;
downChange(arr,0,end);
}
}
三、时间复杂度
在不断进行向下调整的过程中,大概每一个结点都要从顶走到树底,即其时间复杂度为O(N*logN),为不稳定排序。
五、冒泡排序
冒泡排序即不断交换数组中元素的位置,将数值较大的元素向数组尾移动,将数值较小的元素向数组头移动。
一、代码
public void bubbleSort(int[] arr){
for(int i=0;i<arr.length;i++){
boolean isSwap=false;
for(int j=0;j<arr.length-i-1;j++){
if(arr[j]>arr[j+1]){
int tmp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=tmp;
isSwap=true;
}
}
if(!isSwap){
break;
}
}
}
二、时间复杂度
在最坏情况下,每一个元素都要与未排序的每一个元素进行比较,导致其时间复杂度为O(N^2),其为稳定排序。
六、快速排序
快排简单来说就是任取一个原数组中的元素作为基准值,然后将原数组中的元素分为比基准值小的元素和比基准值大的元素,再对分出来的这两组元素,重复上述操作,直到所有元素到达已经排序好的状态。
通常来说,我们常常选取数组中第一个元素作为我们的基准值,那么我们应该如何将原数组中的元素分成比基准值大和比基准值小的两组呢?有三种办法:
一、Hoare法
简单来说,Hoare法就是确定左右两个指针,分别指向数组的头和尾,右指针寻找比基准值小的元素,左指针寻找比基准值大的元素,然后交换。
6(key) | 1(left) | 2 | 7 | 9 | 3 | 4 | 5 | 10 | 8(right) |
右指针向左走,直到找到比基准值(key值)要更小的元素(注意先走的右指针,若先走左指针,则最终左右指针相遇时所在元素比基准值大,代码需要稍微做出改变):
6(key) | 1(left) | 2 | 7 | 9 | 3 | 4 | 5(right) | 10 | 8 |
随后左指针向右走,直到找到比基准值(key值)要更大的元素:
6(key) | 1 | 2 | 7(left) | 9 | 3 | 4 | 5(right) | 10 | 8 |
左右指针元素进行交换:
6(key) | 1 | 2 | 5(left) | 9 | 3 | 4 | 7(right) | 10 | 8 |
重复上述过程,直到左右指针相遇:
6(key) | 1 | 2 | 5(left) | 9 | 3 | 4(right) | 7 | 10 | 8 |
6(key) | 1 | 2 | 5 | 9(left) | 3 | 4(right) | 7 | 10 | 8 |
6(key) | 1 | 2 | 5 | 4(left) | 3 | 9(right) | 7 | 10 | 8 |
6(key) | 1 | 2 | 5 | 4(left) | 3(right) | 9 | 7 | 10 | 8 |
6(key) | 1 | 2 | 5 | 4 | 3(left)(right) | 9 | 7 | 10 | 8 |
最后将右指针指向元素与基准值元素交换:
3 | 1 | 2 | 5 | 4 | 6(key) | 9 | 7 | 10 | 8 |
此时,基准值左边均为小于基准值元素,基准值右边均为大于基准值元素。
二、挖坑法
先将基准值拿出来,key=6:
(空缺)(left) | 1 | 2 | 7 | 9 | 3 | 4 | 5 | 10 | 8(right) |
然后右指针往左走,直到找到比基准值小的元素,并将其放入空缺位置(left)中:
(空缺)(left) | 1 | 2 | 7 | 9 | 3 | 4 | 5(right) | 10 | 8 |
5(left) | 1 | 2 | 7 | 9 | 3 | 4 | (空缺)(right) | 10 | 8 |
左指针往右走,直到找到比基准值大的元素,并将其放入空缺位置(right)中:
5 | 1 | 2 | 7(left) | 9 | 3 | 4 | (空缺)(right) | 10 | 8 |
5 | 1 | 2 | (空缺)(left) | 9 | 3 | 4 | 7(right) | 10 | 8 |
不断重复上述过程,直到左右指针相遇:
5 | 1 | 2 | (空缺)(left) | 9 | 3 | 4(right) | 7 | 10 | 8 |
5 | 1 | 2 | 4(left) | 9 | 3 | (空缺)(right) | 7 | 10 | 8 |
5 | 1 | 2 | 4 | 9(left) | 3 | (空缺)(right) | 7 | 10 | 8 |
5 | 1 | 2 | 4 | (空缺)(left) | 3 | 9(right) | 7 | 10 | 8 |
5 | 1 | 2 | 4 | (空缺)(left) | 3(right) | 9 | 7 | 10 | 8 |
5 | 1 | 2 | 4 | 3(left) | (空缺)(right) | 9 | 7 | 10 | 8 |
5 | 1 | 2 | 4 | 3 | (空缺)(left)(right) | 9 | 7 | 10 | 8 |
当左右指针相遇时,再将一开始的基准值放入空缺位置中:
5 | 1 | 2 | 4 | 3 | 6 | 9 | 7 | 10 | 8 |
三、前后指针法
创建fast和low指针:
6(key) | 1(low)(fast) | 2 | 7 | 9 | 3 | 4 | 5 | 10 | 8 |
当fast指针指向的元素小于基准值时,fast和low指针指向的元素交换,交换一次之后,low指针向右走,fast指针向右走:
6(key) | 1 | 2 | 7(fast)(low) | 9 | 3 | 4 | 5 | 10 | 8 |
当fast指针指向的元素大于基准值时,不进行元素交换,fast指针继续向右走,直到fast指向的元素小于基准值,重复上述过程,直到fast超出数组范围:
6(key) | 1 | 2 | 7(low) | 9 | 3(fast) | 4 | 5 | 10 | 8 |
6(key) | 1 | 2 | 3 | 9(low) | 7 | 4(fast) | 5 | 10 | 8 |
6(key) | 1 | 2 | 3 | 4 | 7(low) | 9 | 5(fast) | 10 | 8 |
6(key) | 1 | 2 | 3 | 4 | 5 | 9(low) | 7 | 10 | 8 |
最后将low-1位置上的元素与基准值进行交换:
5 | 1 | 2 | 3 | 4 | 6(key) | 9(low) | 7 | 10 | 8 |
四、代码
//Hoare法
public static void quickSort1(int[] arr,int L,int R){
if(L>=R){
return ;
}
int left=L;
int right=R;
int key=arr[L];
while(left<right){
while(left<right&&arr[right]>key){
right--;
}
while(left<right&&arr[left]<=key){
left++;
}
int tmp=arr[left];
arr[left]=arr[right];
arr[right]=tmp;
}
arr[L]=arr[left];
arr[left]=key;
quickSort1(arr,L,left-1);
quickSort1(arr,left+1,R);
}
//挖坑法
public static void quickSort2(int[] arr,int L,int R){
if(L>=R){
return ;
}
int key=arr[L];
int left=L;
int right=R;
while(left<right){
while(left<right&&arr[right]>key){
right--;
}
arr[left]=arr[right];
while(left<right&&arr[left]<=key){
left++;
}
arr[right]=arr[left];
}
arr[left]=key;
quickSort2(arr,L,left-1);
quickSort2(arr,left+1,R);
}
//前后指针法
public static void quickSort3(int[] arr,int L,int R){
if(L>R){
return ;
}
int low=L+1;
int fast=L+1;
int key=arr[L];
while(fast<=R){
if(arr[fast]<key){
int tmp=arr[low];
arr[low]=arr[fast];
arr[fast]=tmp;
low++;
}
fast++;
}
arr[L]=arr[low-1];
arr[low-1]=key;
quickSort3(arr,L,low-2);
quickSort3(arr,low,R);
}
五、时间复杂度
最坏情况下,即最终结果要求顺序而原数组为逆序的情况下,时间复杂度为O(N^2),一般情况下,时间复杂度为O(N*logN)。而为了避免第一种情况的发生,可以对快排进行优化——三数取中法,找出最左边,中间,最右边的中间大的数字,与最左边的数字进行交换,让其作为基准数(实际上就是减少递归树的高度)。其为不稳定排序。
七、归并排序
一、图文解释
大家应该听过或者做过,合并两个有序数组这道题,这道题用到的思想就是归并在做的事情。
归并先把一个数组不断地切分:
6 | 1 | 2 | 7 | 9 | 3 | 4 | 5 | 10 | 8 |
6 | 1 | 2 | 7 | 9 |
3 | 4 | 5 | 10 | 8 |
6 | 1 |
3 | 4 |
2 | 7 | 9 |
5 | 10 | 8 |
.....
切分成一个一个数字后,两个两个数字合并成一段有序数组,不断向上合并,最终将整段数组排好序。
二、代码
public static void mergeSort(int[] arr,int L,int R){
int mid=L+((R-L)>>1);
if(R-L>1){
mergeSort(arr,L,mid);
mergeSort(arr,mid+1,R);
}
int left=L;
int right=mid+1;
int[] num=new int[R-L+1];
int i=0;
while(left<=mid&&right<=R){
if(arr[left]<=arr[right]){
num[i++]=arr[left++];
}else{
num[i++]=arr[right++];
}
}
while(i<num.length&&left==mid+1){
num[i++]=arr[right++];
}
while(i<num.length&&right==R+1){
num[i++]=arr[left++];
}
if (R + 1 - L >= 0) System.arraycopy(num, 0, arr, L, R + 1 - L);
}
三、时间复杂度
时间复杂度为O(N*logN),但其空间复杂度较大,为O(N),其为稳定排序。