01 | 简单而又复杂的排序算法
简单而复杂的排序算法
目前市面上常见的排序算法主要有两种:
- \(O(n^2)\):选择排序,冒泡排序,插入排序、希尔排序(有时能\(nlogn\))
- \(O(nlogn)\):快速排序、归并排序、堆排序、
- \(O(n+k)\):计数排序、桶排序
- \(O(n*k)\):基数排序
算法优化往往与排序思想一点而通,本文主要介绍十大排序算法,并对其中一些经典排序算法优化
\(O(n^2)\)算法
选择、冒泡、插入是时间复杂度为\(O(n^2)\)的算法
选择排序——排序之源
原始选择排序
核心思想:在一组数据中,每次选择一个最小值放到数组最前边
特点:
- 简单
- 效率低下
public void selectSort(Integer[] arr) {
int n = arr.length;
for (int i = 0; i < n; i++) {
/*初始化minIndex指向i*/
int minIndex = i;
/*循环不变量->[i+1,n)找到最小值的索引,i最终指向最小值*/
for (int j = i + 1; j < n; j++) {
if (arr[minIndex] > arr[j]) {
minIndex = j;
}
}
Community.swap(arr, i, minIndex);
}
}
优化
优化核心思想:每一轮找到当前未处理元素的最大值和最小值
public void selectSortOptimization(Integer[] arr) {
/*
l,r指向arr的左右两边(最大、最小值该填充的位置)
min,max分别指向最未检查元素的最大值索引和最小值索引
*/
int l = 0;
int r = arr.length - 1;
while (l <= r) {
int minIndex = l;
int maxIndex = r;
/*每一轮查询保证arr[maxIndex]>=arr[minIndex]*/
if (arr[maxIndex] < arr[minIndex]) {
Community.swap(arr, maxIndex, minIndex);
}
/*循环不变量:
l,r的位置不变:指向最小、最大值应该插入的位置
maxIndex、minIndex含义不必拿:找到最小最大值对应的索引*/
for (int i = l + 1; i < r; i++) {
if (arr[i] > arr[maxIndex]) {
maxIndex = i;
}
if (arr[i] < arr[minIndex]) {
minIndex = i;
}
}
/*交换位置*/
if (minIndex != l) {
Community.swap(arr, minIndex, l);
}
if (maxIndex != l) {
Community.swap(arr, maxIndex, r);
}
/*前进*/
l++;
r--;
}
每次找到未检索区域的最大值和最小值,并与指针指向的数组位置交换
冒泡排序——分角角逐
核心思想:顺序性的两两比较,相互角逐,每轮角逐出最大的值,就像是冒泡一样
public void bubbleSort(Integer[] arr) {
int n = arr.length - 1;
/*n-1轮即可,最后一次不需要冒泡*/
for (int i = 0; i < n - 1; i++) {
for (int j = 1; j < n - i; j++) {
if (arr[j] < arr[j - 1]) {
Community.swap(arr, j, j - 1);
}
}
}
}
冒泡排序优化
- 采用标志位提前中止冒泡排序
- 每次仅考虑未排好序的部分
public void bubbleSortOptimization(Integer[] arr) {
int n = arr.length - 1;
while (true) {
/*newN记录最近一次应该冒泡的位置*/
int newN = 0;
boolean isSwap = false;
for (int i = 1; i <= n; i++) {
if (arr[i] < arr[i - 1]) {
Community.swap(arr, i, i - 1);
isSwap = true;
newN = i;
}
}
/*newN之后不考虑冒泡了*/
n = newN;
/*没有冒泡就中止循环*/
if (!isSwap) {
break;
}
}
}
插入排序——我是赌王
核心思想:插入排序就像是赌王整理扑克牌一样,每次把一个数放到应该插入的地方
public void insertSort(Integer[] arr) {
int n = arr.length;
for (int i = 1; i < n; i++) {
/*j从i-1开始,找i的位置*/
for (int j = i; j > 0; j--) {
if (arr[j - 1] > arr[j]) {
Community.swap(arr, j, j - 1);
}else {
/*之前的都是有序的,一直找到i应该插入的位置,就没有插入排序的必要了*/
break;
}
}
}
}
实现方式:
- 循环不变量:约定j不断靠近i应该插入的位置(一步一步的)
- 找到即可退出此次循环
插入排序优化
核心思想:swap这个操作太费时啦,可以存储一份当前值arr[j],然后arr[j-1]之前的值全部往后覆盖,直到找到该插入的位置
public void insertSortAlgorithm(Integer[] arr) {
int n = arr.length;
for (int i = 1; i < n; i++) {
/*备份arr[i]*/
int temp = arr[i];
for (int j = i; j > 0; j--) {
if (temp < arr[j - 1]) {
arr[j] = arr[j - 1];
} else {
/*j依然是temp该插入的位置*/
arr[j] = temp;
break;
}
}
}
}
希尔排序——我是更牛逼的插入排序
核心思想:多路插入排序,将插入排序抽样为多个子序列,然后对各个子序列插入排序,使得数据越来越趋近有序化。
public void shellSort(Integer[] arr) {
int n = arr.length;
/*抽样率,每次抽样的间隔*/
int gap = n / 2;
while (gap > 0) {
/*找到每个小组的最后一个目标,对其插入排序*/
for (int i = gap; i < n; i++) {
/*插入排序开始了*/
int j = i;
int temp = arr[i];
while (j - gap >= 0 && temp < arr[j - gap]) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
gap /= 2;
}
}
\(O(n^2)\)算法特性说明
| 算法 | 优点 | 缺点 |
|---|---|---|
| 选择排序 | 简单 | 低效率,无论怎样都需要两次遍历 |
| 冒泡排序 | 比选择排序效率高 | 比插入排序慢 |
| 插入排序 | 对近乎有序的数组排序性能好,甚至比\(O(nlogn)\)的算法块 | 无序的效率不如\(O(nlogn)\)族 |
\(O(nlogn)\)算法
快速排序——最伟大的发明
- 核心思想:递归,先大范围排序,逐渐缩小包围圈
- 重要基石:partition,最最最重要的实现
public void quickSort(Integer[] arr) {
int n = arr.length;
/*[0,n-1]快速排序*/
quickSort(arr, 0, n - 1);
}
/**
* @param arr arr
* @param l 左边界
* @param r 右边界
*/
private void quickSort(Integer[] arr, int l, int r) {
/*递归中止条件*/
if (l >= r) {
return;
}
int p = partition(arr, l, r);
quickSort(arr, l, p - 1);
quickSort(arr, p + 1, r);
}
/**
* @param arr arr[l,r]
* @param l left
* @param r right
* @return p
*/
private int partition(Integer[] arr, int l, int r) {
int p = arr[l];
int j = l;
/*
* arr[l+1,j]<p,arr[j+1,i-1]>=p
* j指向小于P的最后一个数*/
for (int i = l + 1; i <= r; i++) {
if (arr[i] < p) {
/*找到比P小的数,J挪动一位,然后交换位置*/
j++;
Community.swap(arr, i, j);
}
}
Community.swap(arr, l, j);
return j;
}
优化一:使用插入排序优化快排
- 近乎有序(数组较小)的情况下,插入排序比快排效率高
- 当\(r-l <= 15\),选用插入排序
private void quickSortOptimizationOne(Integer[] arr, int l, int r) {
if (r - l <= 15) {
new InsertSort().insertSortOptimization(arr);
return;
}
int p = partition(arr, l, r);
quickSort(arr, l, p - 1);
quickSort(arr, p + 1, r);
}
优化二:随机快排
-
对于大的近乎有序的数组,原始快排性能非常差,因为它选择第一个位置的数字(近乎最小的数字),非常不均衡(快排,快得就是把数组分成两个近似相同量级的数组排序,有点类似于二分查找的感觉)
-
改进核心思想:p值随机选择,不选取最左边的元素
private int partitionOptimizationOne(Integer[] arr, int l, int r) {
/*随机选取后边一个数,与l交换*/
Community.swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
int p = arr[l];
int j = l;
/*
* arr[l+1,j]<p,arr[j+1,i-1]>=p
* j指向小于P的最后一个数*/
for (int i = l + 1; i <= r; i++) {
if (arr[i] < p) {
/*找到比P小的数,J挪动一位,然后交换位置*/
j++;
Community.swap(arr, i, j);
}
}
Community.swap(arr, l, j);
return j;
}
优化三:双路快排
- 上述排序的缺点:对于数字重叠度高的数组,排序效率低(原因:重叠度高的数字会使拆分的数组分布不均)
- 解决方法双路快排或者三路快排

核心思想(@波波老师的思路):

private int partitionOptimizationTwo(Integer[] arr, int l, int r) {
/*随机选取后边一个数,与l交换*/
Community.swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
int p = arr[l];
int i = l + 1;
int j = r;
/*
* 循环不变量:[l+1,i-1]<=P [j+1,r]>=P
* */
while (true) {
/* arr[i]<p 防止连续出现的数字归在一方*/
while (i <= r && arr[i] < p) {
i++;
}
while ((j >= 0 && arr[j] > p)) {
j--;
}
/*检查一下是否过界*/
if(i>j){
break;
}
/*找到第一个不符合条件的两个位置*/
Community.swap(arr,i,j);
j--;
i++;
}
/*最后交换目标到期应该到的位置*/
Community.swap(arr,l,i);
return i;
}
优化四:三路快排
- 核心思想:设置lt、gt指针,分别指向小于目标和大于目标的最后一个值,而[lt,i-1]指向等于目标的值
public void quickSort3Ways(Integer[] arr, int l, int r) {
if(r-l<=15){
new InsertSort().insertSortOptimization(arr);
return;
}
/*随机选取后边一个数,与l交换*/
Community.swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
int p = arr[l];
/*lt [l+1,lt]<p
* gt [gt,r]>p*/
int lt = l;
int gt = r + 1;
/*
* 循环不变量:[lt+1,i-1]=p
* */
int i = l + 1;
while (i < gt) {
if (arr[i] > p) {
//此时不需要更新i的值由于,gt的值还没检测
gt--;
Community.swap(arr, i, gt);
} else if (arr[i] < p) {
lt++;
Community.swap(arr, lt, i);
//此时需要更新i的值,由于前边运算已经检测这个值等于p,所以不需要再检测一次i
i++;
} else {
i++;
}
}
Community.swap(arr, l, lt);
quickSort3Ways(arr, l, lt - 1);
quickSort3Ways(arr, gt, r);
归并排序——铁杵成针
核心思想:
- 先解决小问题,一点一点解决大问题
- 每次都是二分——这个舒服
public void mergeSort(Integer[] arr) {
int n = arr.length;
mergeSort(arr, 0, n - 1);
}
private void mergeSort(Integer[] arr, int l, int r) {
if (r - l <= 15) {
new InsertSort().insertSortOptimization(arr);
return;
}
/*找到中间点mid*/
int mid = l + (r - l) / 2;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
/**
* @param arr arr
* @param l l
* @param mid mid
* @param r r
* arr[l,mid] arr[mid+1,r]
*/
private void merge(Integer[] arr, int l, int mid, int r) {
Integer[] front = Arrays.copyOfRange(arr, l, mid);
Integer[] tail = Arrays.copyOfRange(arr, mid + 1, r);
/*front*/
int i = 0;
/*tail*/
int j = 0;
/*arr*/
int k = l;
while (i < front.length && j < tail.length) {
if (front[i] >= tail[j]) {
arr[k] = tail[j];
j++;
} else if (front[i] < tail[j]) {
arr[k] = front[i];
i++;
}
k++;
}
/*没弄完继续弄*/
while (j < tail.length) {
arr[k] = tail[j];
k++;
j++;
}
while (i < front.length) {
arr[k] = front[i];
k++;
i++;
}
}
优化
- 对近乎有序的数组效果不好
- 原因:递归到两个子部分后,不管他们是否有序都会进行merge操纵
- 改进思路:可以判断一下mid和mid+1,若他们俩是有序的不需要merge了,hiahiahia👀
private void mergeSort(Integer[] arr, int l, int r) {
if (r - l <= 15) {
new InsertSort().insertSortOptimization(arr);
return;
}
/*找到中间点mid*/
int mid = l + (r - l) / 2;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
if (arr[mid] > arr[mid + 1]) {
merge(arr, l, mid, r);
}
}
自底向上的归并排序
- 不用递归
- 不需要索引
- 就是这么牛逼
/*定义最小的归并尺寸SZ*/
for (int sz = 1; sz < n; sz += sz) {
/*i表示每一轮归并的起始位置*/
for (int i = 0; i < n-sz; i += sz + sz) {
/*i<n-sz:确保i+sz存在才merge,否则不需要merge*/
/*[i,i+sz-1] [i+sz,i+sz+sz-1]*/
if(arr[i+sz-1]>arr[i+sz]){
/*需要merge*/
mergeBu(arr,i,i+sz-1,Math.min(i+sz+sz-1,n-1));
}
}
}
堆排序——一个不太熟但很牛逼的排序
堆结构特性如下:
- 层级分明的大小关系
- 可以用数组表示

上代码:
public void heapSort(Integer[] arr) {
int n = arr.length;
heapify(arr);
int j = n - 1;
while (j >= 1) {
/*拿出最上方的值(最大值)
* 与最后的值交换位置(理应放到最后)*/
Community.swap(arr, j, 0);
/*边界[0,j-1],j个元素
* 整理成堆*/
siftDown(arr, 0, j);
/*走下一个j*/
j--;
}
}
private void heapify(Integer[] arr) {
int n = arr.length;
for (int i = parent(n - 1); i >= 0; i--) {
/*从有子节点的最后一个节点开始,
* 由下往上siftdown*/
siftDown(arr, i, n);
}
}
private int parent(int i) {
return (i - 1) / 2;
}
private void siftDown(Integer[] arr, int i, int n) {
/*有左孩子时会考虑siftdown操作*/
while (leftChild(i) < n) {
int j = leftChild(i);
/*找到最大的子节点*/
if (j + 1 < n && arr[j] < arr[j + 1]) {
j += 1;
}
if (arr[j] < arr[i]) {
break;
}
/*下沉*/
Community.swap(arr, j, i);
/*更新i,检查下一次下沉情况*/
i = j;
}
}
private int leftChild(int i) {
return 2 * i + 1;
}
二叉堆
索引堆
爱生活、爱编程

浙公网安备 33010602011771号