目录
写篇博客自己回忆一下,如果也能帮助到其他人那更好了。
标题中的时间复杂度均指是是平均时间复杂度。
表格
排序算法 | 平均时间复杂度 | 最坏 | 最好 | 空间 | 稳不稳 |
---|---|---|---|---|---|
选择排序 | n2 | n2 | n2 | 1 | 不稳 |
冒泡排序 | n2 | n2 | n | 1 | 稳 |
插入排序 | n2 | n2 | n | 1 | 稳 |
二分插入排序 | n2 | n2 | n | 1 | 稳 |
希尔排序 | n1.3 | n2 | n | 1 | 不稳 |
归并排序 | nlogn | nlogn | nlogn | n | 稳 |
TimSort | nlogn | nlogn | n | n | 稳 |
快速排序 | nlogn | n2 | nlogn | logn | 不稳 |
堆排序 | nlogn | nlogn | nlogn | 1 | 不稳 |
桶排序 | n+k | n2 | n | n+k | 稳 |
计数排序 | n+k | n+k | n+k | n+k | 稳 |
基数排序 | n*k | n*k | n*k | n+k | 稳 |
💗🧡💛💚💙💜💗🧡💛💚💙💜💗🧡💛💚💙💜
【选择排序】 O(n2) 不稳
📃思想描述
- 每轮找最小的,记录下标往前放
- 最小的记录下标,一轮结束交换位置
最简单但是最没用的排序算法。
思路就是每趟都找剩下的当中最小的那一个。
就是每次都选择最小的那个往前放。
简单排序的时间复杂度为O(n2),这种排序算法不稳定。
## 0->length-1
### i+1->length
#### 比较更新minPos
public class SelectionSort {
public static void main(String[] args) {
int[] arr = {5,3,6,8,1,7,9,4,2};
int minPos;//记录当前轮次最小值的下标
for(int i=0;i<arr.length-1;i++) {
minPos = i;
for(int j=i+1;j<arr.length;j++) {
minPos = arr[j] < arr[minPos] ? j : minPos;
}
int temp = arr[i];
arr[i] = arr[minPos];
arr[minPos] = temp;
}
for(int i=0;i<arr.length;i++) {
System.out.print(arr[i]+" ");
}
}
}
【冒泡排序】 O(n2) 稳
其实我感觉冒泡和选择有点像
不过冒泡是每轮找最大的那个。
但是选择是每轮找最小的那个,并且不用边找边交换位置,而是记录下标,一轮结束了再交换。
📃思想描述
- 每轮都找最大的往后放
- 边找边换位置,让大的冒泡到最后
## 0->length-1
### 0->length-i-1
#### 比较交换位置
public static void main(String[] args) {
int[] arr = {5,3,6,8,1,7,9,4,2};
for(int i=0;i<arr.length-1;i++) {
for(int j=0;j<arr.length-i-1;j++) {
if(arr[j]>arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
for(int i=0;i<arr.length;i++) {
System.out.print(arr[i]+" ");
}
}
时间复杂度为O(n2),
冒泡排序是稳定的排序方法。
优化写法
冒泡排序最好的情况是O(n),改进的写法如下:
public void bubbleSort(int arr[]) {
boolean didSwap;
for(int i = 0, len = arr.length; i < len - 1; i++) {
didSwap = false;
for(int j = 0; j < len - i - 1; j++) {
if(arr[j + 1] < arr[j]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
didSwap = true;
}
}
if(didSwap == false) //如果是false,说明剩下的全部是有序的
return;
}
}
【插入排序】 O(n2) 稳
对于基本有序的数组最好用
📃思想描述
- 往已经有序的部分中插入下一个数
- 通过交换位置每轮保证当前部分有序
## 1->length
### i->0
#### 比较交换位置
public static void main(String[] args) {
int[] arr = {5,3,6,8,1,7,9,4,2};
for(int i=1;i<arr.length;i++) {
for(int j=i;j>0;j--) {
if(arr[j]<arr[j-1]) {
int temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
}
}
}
for(int i=0;i<arr.length;i++) {
System.out.print(arr[i]+" ");
}
}
插排最好情况的和冒泡是一致的,改进算法也可以按照冒泡的那个写。
【二分插入排序】
插入排序还可以改进为二分插入排序,在有序部分中找到正确位置这一步骤可以使用二分查找来完成,找到位置再将后面的元素后移。这样主要是节省比较的时间,虽然依然要移动相同数量的元素,但是数组平移比元素一个一个交换还是要快一点点。
public static void main(String[] args) {
int[] arr = {7,4,3,6,5,2,1,8,12,11,9,10};
for(int i=1;i<arr.length;i++) {
int des = binarySert(0,i-1,arr,arr[i]);
int temp = arr[i]; //记录待插入的数
for(int j=i;j>des;j--) { //这里必须逆序,temp记录的是后一个
arr[j]=arr[j-1];
}
arr[des]=temp;
}
for(int i=0;i<arr.length;i++) {
System.out.print(arr[i]+" ");
}
}
public static int binarySert(int start,int end, int[] arr, int a) {
int mid = start+(end-start)/2;
//这里end是有可能会小于start的,循环终止条件不能写start!=end
while(start<=end) {
if(arr[mid]>a) {
end=mid-1;
}else {
start=mid+1;
}
mid = start+(end-start)/2;
}
return start;
}
【希尔排序】 O(n1.3) 不稳
插排的改进,为什么效率比插排好,因为比如像1,通过三步就可以到最前面,而普通排序需要比对很多次。
一次排下来,小的数基本上都放到前面去了,大的很多都在后面了。
📃思想描述
- 每轮按照指定间隔插入排序
- 不断缩小间隔直到1,完成全部排序
## gap->0 (gap-1)/3
### gap->length
#### i->gap-1 (-=gap)
##### 比较交换位置
public static void main(String[] args) {
int[] arr = {7,4,27,6,5,2,1,8,12,9,10,11,34,15,23,20,3};
int gap = 1;
while(gap<=arr.length/3) {
gap = gap*3+1; //这样划分是因为某数学家研究出来这样最优
}
for(;gap>0;gap = (gap-1)/3) {
for(int i=gap;i<arr.length;i++) {
for(int j=i;j>gap-1;j-=gap) {
if(arr[j]<arr[j-gap]) {
int temp = arr[j];
arr[j] = arr[j-gap];
arr[j-gap] = temp;
}
}
}
}
for(int i=0;i<arr.length;i++) {
System.out.print(arr[i]+" ");
}
}
【归并排序】 O(n*logn) 稳
📃思想描述
- 将数组排序不断划分为更小的子问题
- 解决子问题再将子问题合并
首先让最小子问题有序,在下面的代码里,递归的终止是只有一个元素。
然后从两个元素合并开始。
问题就是先不断二分,再合并。
合并的问题是怎样将两个有序数组合并。
## Sort()
### start==end return
### mid = (end-start)/2 + start+1
### sort (start,mid-1) ;sort (mid,end)
### merge (start,mid,end)
## merge()
### 三个指针,两个有序数组,比较放入temp
#### arr[i]<arr[j] i++;k++; else j++;k++;
### 剩余放入temp
### arr[start:end] = temp[start:end]
public static void main(String[] args) {
int[] arr = {7,4,3,19,6,5,2,13,1,8,12,9,10,11,23};
int[] temp = new int[arr.length];
sort(arr,0,arr.length-1,temp);
for(int i=0;i<arr.length;i++) {
System.out.print(arr[i]+" ");
}
}
static void sort(int[] arr,int start,int end,int[] temp) {
if(start==end) return;
//分成两半
int mid = (end-start)/2 + start+1; //防止中间两int相加越界
sort(arr,start,mid-1,temp);
sort(arr,mid,end,temp);
merge(arr,start,mid,end,temp);
}
static void merge(int[] arr,int start,int mid,int end,int[] temp) {
int i=start;
int j=mid;
int k=start;
while(i<mid && j<=end) {
if(arr[i]<arr[j]) {
temp[k]=arr[i];
i++;
k++;
}else {
temp[k]=arr[j];
j++;
k++;
}
}
while(i<mid) temp[k++] = arr[i++];
while(j<=end) temp[k++] = arr[j++];
for(int a=start;a<=end;a++) {
arr[a]=temp[a];//这里注意一定要还给arr
}
}
Java里的对象排序
对象排序一般要求稳定,所以归并排序是一个比较好的选择。
TimSort也是归并排序,不过不是两个两分,而是一下分很多组,然后再将很多组两两归并。
【TimSort】
TimSort是一个自适应的、混合的、稳定的排序算法,融合了归并算法和二分插入排序算法的精髓。
在Java中是选择当元素数据少于32时,二分插入排序,如果大于32进行归并排序。
TimSort中有一个run的概念,run是单调增或者单调减的一段,并且run必须要保证至少有多少个元素:minrun。
在执行排序算法之前,会计算出这个minrun的值(所以说这个算法是自适应的,会根据数据的特点来进行自我调整),minrun会从32到64(包括)选择一个数字,使得数组的长度除以minrun等于或者略小于2的幂次方。比如长度是65,那么minrun的值就是33;如果长度是165,minrun就是42(注意这里的Java的minrun的取值会在16到32之间)。
不断去找run,遇到递减的run需要反转,反转后压栈。
最终合并栈中相邻的两个run。
【快速排序】O(nlogn) 不稳
就是让某元素左边的元素都小于它,右边的元素都大于它。
快排有很多解法,速度最快的是双指针,如下图。
可以理解为指针指在空的位置上就不动,然后遇到大小需要调换的时候,原本空位置的指针将不空,会让另一个指针不动。
📃思想描述
- 使用双指针找到temp值的目标位置
- 目标位置左右继续双指针递归找位置
## quickSort()
### left>=right return
### pos = Partition
### quickSort(left:pos-1)
### quickSort(pos+1:right)
## Partition()
### temp = arr[left]
### left<right
#### arr[right]>temp right--;
#### arr[left] = arr[right]
#### arr[left]<temp left++;
#### arr[right] = arr[left]
### arr[left] = temp;
### return temp
public static void main(String[] args) {
int[] arr = {7,4,3,19,6,5,2,13,1,8,12,9,10,11,23};
quickSort(arr,0,arr.length-1);
for(int i=0;i<arr.length;i++) {
System.out.print(arr[i]+" ");
}
}
public static void quickSort(int[] arr,int left,int right) {
if(left>=right) return;
int pos = Partition(arr,left,right);
quickSort(arr,left,pos-1); //对左子区间递归进行快速排序
quickSort(arr,pos+1,right); //对右子区间递归进行快速排序
}
public static int Partition(int[] arr,int left,int right) {
int temp = arr[left];
while(left<right) {
while(left<right&&arr[right]>temp)
right--;
arr[left] = arr[right];
while(left<right&&arr[left]<=temp)
left++;
arr[right] = arr[left];
}
arr[left] = temp;
return left;
}
避免最坏的情况(就是数组已经排好了),可以选择随机取一个数,而不是每次都从最左边取数,或者先判断是不是顺序增长或者顺序降低的,如果是就不用快排了。
Java里Arrays类的这个sort(int[] a)方法是使用双轴快排实现的。