目录
🌷1. 排序的概念及引用
1.1 排序的概念
1.2 衡量指标
1.2 十个排序算法
1.3 十个排序性能对比
🌷2. 冒泡排序
2.1 算法描述
2.2 动图
⭐️代码优化
🌷3. 选择排序
3.1 算法描述
3.2 动图
3.3 代码
🌷4. 插入排序
4.1 算法描述
4.2 动图
4.3 代码
🌷5 希尔排序
5.1 描述
5.2 动图
5.3 代码
🌷6. 归并排序
6.1 描述
6.2 动图
6.3 代码
⭐️优化代码(优化部分)
🌷7.堆排序
7.1 算法描述
7.2 动图
7.3 代码
🌷8. 快速排序(挖坑法)
8.1 描述
8.2 动图
8.3 代码
(1)递归版
(2)递归版优化
(3)非递归版
🌷9. 快速排序(二路快排)
代码
🌷10. 三路快排
🌷11. 测试排序后的数组是否正确排序?
🌷12. 全部代码
(1)生成数组(渐进有序,乱序)
(2)排序代码
(3)测试结果
🌷1. 排序的概念及引用
1.1 排序的概念
排序:所谓排序,就是对一组任意数据元素(或记录)经过指定关键字排序操作后,就可以把它们变成一组按照关键字排序的有序序列。通常排序的目的是快速查找。
1.2 衡量指标
对于一个排序算法来说,一般从以下3个方面来衡量算法的优劣:
- 排序算法的执行效率(时间复杂度)
- 排序算法的内存消耗(空间复杂度)
- 排序算法的稳定性
⭐️ 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
1.2 十个排序算法
内部排序:数据元素全部放在内存中的排序。外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.3 十个排序性能对比
【注意】以下排序算法全部为升序排序
为了避免代码太多的重复,这里提供一个swap方法(交换两个值),后面多个排序方法会用到:
// 交换两个值
private static void swap(int[] arr,int j, int i) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
🌷2. 冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
2.1 算法描述
冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。
2.2 动图
2.3 代码
public static void bubbleSort(int[] arr){
if(arr.length < 2){
return;
}
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
// j + 1保证索引不越界,因此,j < arr.length - i - 1
if(arr[j] > arr[j + 1]){
swap(arr,j,j + 1);
}
}
}
}
⭐️代码优化
当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。
// 冒泡排序
public static void bubbleSort(int[] arr){
if(arr.length < 2){
return;
}
for (int i = 0; i < arr.length - 1; i++) {
boolean isSwap = false;
for (int j = 0; j < arr.length - i - 1; j++) {
// j + 1保证索引不越界,因此,j < arr.length - i - 1
if(arr[j] > arr[j + 1]){
isSwap = true;
swap(arr,j,j + 1);
}
}
if(!isSwap){
break;
}
}
}
🌷3. 选择排序
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
3.1 算法描述
- n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为R[1..n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了。
3.2 动图
3.3 代码
public static void selectionSort(int[] arr){
// 起始状态 : 有序区间[0..i)
// 无序区间[i....n)
for (int i = 0; i < arr.length - 1; i++) {
// min指向的当前无序区间的最小值,min是下标
int min = i;
for (int j = i + 1; j < arr.length; j++) {
if(arr[j] < arr[min]){
min = j;
}
}
// 此时min一定指向无序区间最小值下标,换到无序区间的最开始位置
swap(arr,i,min);
}
}
🌷4. 插入排序
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
4.1 算法描述
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
4.2 动图
4.3 代码
public static void insertionSort(int[] arr){
// 无序区间[i...n)
// 有序区间[0...i)
// i指向当前无序区间的第一个元素
for (int i = 1; i < arr.length; i++) {
for (int j = i; j - 1 >= 0 && arr[j] < arr[j - 1]; j--) {
// 简化写法
swap(arr,j,j - 1);
// j - 1 要保证不为负数 因此j > 0,而不是 j >= 0
// if(arr[j] < arr[j - 1]){
// swap(arr,j,j - 1);
// }
}
}
}
🌷5 希尔排序
1959年Shell发明,第一个突破O(n^2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
希尔(Shell)排序又称缩小增量排序,是对直接插入排序的优化。
5.1 描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
5.2 动图
5.3 代码
// 希尔排序(插入排序的优化),不稳定
public static void shellSort(int[] arr) {
int gap = arr.length >> 1;
while (gap > 1) {
// 先按照gap分组,组内使用插入排序
insertionSortByGap(arr,gap);
gap = gap >> 1;
}
// 当gap == 1时,整个数组接近于有序,此时来一个插入排序
insertionSortByGap(arr,1);
}
// 按照gap分组,组内的插入排序
private static void insertionSortByGap(int[] arr, int gap) {
for (int i = gap; i < arr.length; i++) {
for (int j = i; j - gap >= 0 && arr[j] < arr[j - gap] ; j -= gap) {
swap(arr,j,j - gap);
}
}
}
🌷6. 归并排序
归并排序的核心思想还是蛮简单的。如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。归并排序使用的就是分治思想。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。
6.1 描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
6.2 动图
6.3 代码
public static void mergeSort(int[] arr){
mergeSortInternal(arr,0,arr.length - 1);
}
// 在arr[l...r]进行归并排序
private static void mergeSortInternal(int[] arr, int l, int r) {
// base case
// if(l > r){
// return;
// }
// mid = (l + r) / 2
int mid = l + ((r - l) >> 1);
// 先将原数组一分为二,在子数组上先进行归并排序
mergeSortInternal(arr,l,mid);
mergeSortInternal(arr,mid + 1,r);
// 此时两个子数组已经有序,将这两个子数组合并为原数组
merge(arr,l,mid,r);
}
private static void merge(int[] arr, int l, int mid, int r) {
// 本身就是插入排序
// 创建一个大小为r - l + 1的与原数组长度一样的临时数组aux
int[] aux = new int[r - l + 1];
// arraycopy:原数组,原数组开始copy的第一个下标,目标数组,目标数组copy的第一个下标,copy的长度
System.arraycopy(arr,l,aux,0,r - l + 1);
// 两个子数组的开始索引
int i = l,j = mid + 1;
// k表示当前原数组合并到哪个位置
// 时间复杂度O(n)
for (int k = l; k <= r; k++) {
if (i > mid) {
// 子数组1全部拷贝完毕,将子数组2的所有内容写回arr
arr[k] = aux[j - l];
j ++;
}else if (j > r) {
// 子数组2全部拷贝完毕,将子数组1的剩余内容写回arr
arr[k] = aux[i - l];
i ++;
}else if (aux[i - l] <= aux[j - l]) {
// 稳定性 <=
arr[k] = aux[i - l];
i ++;
}else {
arr[k] = aux[j - l];
j ++;
}
}
}
⭐️优化代码(优化部分)
public static void mergeSort(int[] arr){
mergeSortInternal(arr,0,arr.length - 1);
}
// 在arr[l...r]进行归并排序
private static void mergeSortInternal(int[] arr, int l, int r) {
// base case
// if(l > r){
// return;
// }
// 优化2.小数组(64个元素以内)直接使用插入排序
if (r - l <= 64) {
insertionSort(arr,l,r);
return;
}
// mid = (l + r) / 2
int mid = l + ((r - l) >> 1);
// 先将原数组一分为二,在子数组上先进行归并排序
mergeSortInternal(arr,l,mid);
mergeSortInternal(arr,mid + 1,r);
// 此时两个子数组已经有序,将这两个子数组合并为原数组
// merge(arr,l,mid,r);
if (arr[mid] > arr[mid + 1]) {
// 优化1.只有子数组1和子数组2存在元素的乱序才需要合并
merge(arr,l,mid,r);
}
}
// 在数组arr[l..r]上进行插入排序
private static void insertionSort(int[] arr, int l, int r) {
for (int i = l + 1; i <= r; i++) {
for (int j = i; j > l && arr[j] < arr[j - 1]; j--) {
swap(arr,j,j - 1);
}
}
}
🌷7.堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
7.1 算法描述
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
7.2 动图
7.3 代码
// 原地堆排序 升序最大堆
// 1.堆化,不断将堆顶和无序数组最后一个交换
// 2.再siftDown
// 3.重复12
public static void heapSort(int[] arr){
// 堆化
for (int i = (arr.length - 2)/2; i >= 0; i--) {
siftDown(arr,i, arr.length);
}
// 不断的将当前无序数组的最大值(堆顶) 和 最后一个元素进行交换~
for (int i = arr.length - 1; i >= 0 ; i--) {
// 将堆顶(最大值) 和 i交换
swap(arr,0,i);
// 进行元素的下沉操作
siftDown(arr,0,i);
}
}
// 元素下沉
private static void siftDown(int[] arr, int i, int length) {
int j = 2 * i + 1;
while (j < length){
if(j + 1 < length && arr[j] < arr[j + 1]){
j = j + 1;
}
if(arr[j] <= arr[i]){
// 下沉结束
break;
}else {
swap(arr,i,j);
i = j;
j = 2 * i + 1;//更新孩子节点
}
}
}
🌷8. 快速排序(挖坑法)
快排的思想是这样的:如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。
我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。
8.1 描述
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
8.2 动图
8.3 代码
(1)递归版
// 挖坑法快速排序 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
// 时间复杂度:O(N*logN)
public static void quickSortHole(int[] arr){
quickSortHoleInternal(arr,0, arr.length - 1);
}
// 默认挖坑法快速排序
private static void quickSortHoleInternal(int[] arr,int left,int right){
// base case
if(left > right){
return;
}
int p = partitionByHole(arr,left,right);
// 继续在两个子区间上进行快速排序
quickSortHoleInternal(arr,left,p - 1);
quickSortHoleInternal(arr,p + 1,right);
}
// 时间复杂度n
private static int partitionByHole(int[] arr, int left, int right) {
// 渐进有序(极端情况完全有序)的数组,快速排序性能会退化为O(n*n)
// 原因:每次取最左边的值,假设这个值是最小的,那么j要一直减到left(二叉树会变成单支树)
// 解决:①三数取中法(课本中的方法),②随机数法
int randomIndex = random.nextInt(left,right);
swap(arr,left,randomIndex);
int pivot = arr[left];
int i = left,j = right;
while (i < j) {
// 先让j从后向前扫描碰到第一个 < pivot的元素终止
while (i < j && arr[j] >= pivot) {
j --;
}
arr[i] = arr[j];
// 再让i从前向后扫描碰到第一个 > pivot的元素终止,
while (i < j && arr[i] <= pivot) {
i ++;
}
arr[j] = arr[i];
}
// 回填分区点
arr[j] = pivot;
return j;
}
(2)递归版优化
private static void qiuckSortHoleInternal(int[] arr,int left,int right){
// base case
// if(left > right){
// return;
// }
// 优化1.小数组使用插入排序
if (right - left <= 64) {
insertionSort(arr,left,right);
return;
}
int p = partitionByHole(arr,left,right);
// 继续在两个子区间上进行快速排序
qiuckSortHoleInternal(arr,left,p - 1);
qiuckSortHoleInternal(arr,p + 1,right);
}
(3)非递归版
// 非递归挖坑法快速排序
public static void quickSortNonRecursion(int[] arr){
Deque<Integer> stack = new ArrayDeque<>();
stack.push(arr.length - 1);
stack.push(0);
while (!stack.isEmpty()){
int l = stack.pop();
int r = stack.pop();
if (l >= r) {
// 当前这个子数组已经处理完毕
continue;
}
int p = partitionByHole(arr,l,r);
// 先将右半区间压入栈中
stack.push(r);
stack.push(p + 1);
// 继续处理左半区间
stack.push(p - 1);
stack.push(l);
}
}
🌷9. 快速排序(二路快排)
代码
public static void quickSort(int[] arr){
quickSortInternal(arr,0,arr.length - 1);
}
private static void quickSortInternal(int[] arr, int left, int right) {
if (right - left <= 64) {
insertionSort(arr,left,right);
return;
}
int p = partition(arr,left,right);
quickSortInternal(arr,left,p - 1);
quickSortInternal(arr,p + 1,right);
}
private static int partition(int[] arr, int left, int right) {
int randomIndex = random.nextInt(left,right);
swap(arr,left,randomIndex);
int v = arr[left];
// arr[l + 1..j] < v
int j = left;
// arr[j + 1..i) >= v
for (int i = left + 1; i < right; i++) {
if(arr[i] < v){
swap(arr,i,j + 1);
j ++;
}
}
swap(arr,left,j);
return j;
}
🌷10. 三路快排
public static void quickSort3(int[] arr) {
quickSortInternal3(arr,0,arr.length - 1);
}
// 三路快排
private static void quickSortInternal3(int[] arr, int l, int r) {
if (r - l <= 64) {
insertionSort(arr,l,r);
return;
}
int randomIndex = (int) (Math.random() * (r - l + 1)) + l;
swap(arr,l,randomIndex);
int v = arr[l];
// arr[l + 1...lt] < v
int lt = l;
// arr[gt...r] > v
int gt = r + 1;
// arr[lt + 1..i) == v,i指向当前要处理的元素
int i = l + 1;
// 终止条件i和gt重合
while (i < gt) {
if (arr[i] < v) {
swap(arr,lt + 1,i);
lt ++;
i ++;
}else if (arr[i] > v) {
// 此时不需要i++,因为gt-1这个未处理的元素换到i位置
swap(arr,gt - 1,i);
gt --;
}else {
// 此处arr[i] == v
i ++;
}
}
swap(arr,l,lt);
// 交换后arr[l..lt - 1] < v ; arr[gt..r] >v
quickSortInternal3(arr,l,lt - 1);
quickSortInternal3(arr,gt,r);
}
🌷11. 测试排序后的数组是否正确排序?
// 测试是否排序正确
public static boolean isSorted(int[] data){
for (int i = 0; i < data.length - 1; i++) {
if(data[i] > data[i + 1]){
// 反例
return false;
}
}
return true;
}
🌷12. 全部代码
(1)生成数组(渐进有序,乱序)
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
public class ArrayUtil {
private static ThreadLocalRandom random = ThreadLocalRandom.current();
// 生成一个大小为n的近乎有序的数组
// 数组的有序与否取决于元素的交互次数
public static int[] generateSortedArray(int n,int swapTimes){
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
for (int i = 0; i < swapTimes; i++) {
int x = random.nextInt(0,n);
int y = random.nextInt(0,n);
swap(arr,x,y);
}
return arr;
}
private static void swap(int[] arr,int x, int y) {
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
// 生成一个大小为n的随机数数组
// 随机数的取值范围为[l..r)
public static int[] generateRandomArray(int n,int l,int r){
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = random.nextInt(l,r);
}
return arr;
}
// 深拷贝数组
public static int[] arrCopy(int[] arr) {
return Arrays.copyOf(arr,arr.length);
}
}
(2)排序代码
import util.ArrayUtil;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.concurrent.ThreadLocalRandom;
public class SevenSort {
private static ThreadLocalRandom random = ThreadLocalRandom.current();
private static int[] arr6;
public static void main(String[] args) throws Exception{
int n = 10_0000;
// 渐进有序的数组
// int[] arr = ArrayUtil.generateSortedArray(n,10);
// 无序数组
int[] arr = ArrayUtil.generateRandomArray(n,0,Integer.MAX_VALUE);
int[] arr1 = ArrayUtil.arrCopy(arr);
int[] arr2 = ArrayUtil.arrCopy(arr);
int[] arr3 = ArrayUtil.arrCopy(arr);
int[] arr4 = ArrayUtil.arrCopy(arr);
int[] arr5 = ArrayUtil.arrCopy(arr);
int[] arr6 = ArrayUtil.arrCopy(arr);
int[] arr7 = ArrayUtil.arrCopy(arr);
int[] arr8 = ArrayUtil.arrCopy(arr);
// --------------------------------
long start = System.nanoTime();
qiuckSort(arr8);
long end = System.nanoTime();
if (isSorted(arr8)) {
System.out.println("快速排序共耗时 : " + (end - start) / 1000000.0 + "ms");
}
//--------------------------------
start = System.nanoTime();
qiuckSortNonRecursion(arr7);
end = System.nanoTime();
if (isSorted(arr7)) {
System.out.println("快速排序(挖坑法,非递归法)共耗时 : " + (end - start) / 1000000.0 + "ms");
}
//---------------------------------
start = System.nanoTime();
qiuckSortHole(arr6);
end = System.nanoTime();
if (isSorted(arr6)) {
System.out.println("快速排序(挖坑法,递归法)共耗时 : " + (end - start) / 1000000.0 + "ms");
}
//----------------------------------
start = System.nanoTime();
mergeSort(arr5);
end = System.nanoTime();
if (isSorted(arr5)) {
System.out.println("归并排序共耗时 : " + (end - start) / 1000000.0 + "ms");
}
//---------------------------------
start = System.nanoTime();
shellSort(arr4);
end = System.nanoTime();
if (isSorted(arr4)) {
System.out.println("希尔排序共耗时 : " + (end - start) / 1000000.0 + "ms");
}
// ------------------------------
start = System.nanoTime();
insertionSort(arr3);
end = System.nanoTime();
if (isSorted(arr3)) {
System.out.println("插入排序共耗时 : " + (end - start) / 1000000.0 + "ms");
}
// -------------------------------
start = System.nanoTime();
selectionSort(arr2);
end = System.nanoTime();
if (isSorted(arr2)) {
System.out.println("选择排序共耗时 : " + (end - start) / 1000000.0 + "ms");
}
// ------------------------------
start = System.nanoTime();
bubbleSort(arr1);
end = System.nanoTime();
if (isSorted(arr1)) {
System.out.println("冒泡排序共耗时 : " + (end - start) / 1000000.0 + "ms");
}
// ------------------------------
start = System.nanoTime();
heapSort(arr);
end = System.nanoTime();
if (isSorted(arr)) {
System.out.println("堆排序共耗时 : " + (end - start) / 1000000.0 + "ms");
}
}
// 测试是否排序正确
public static boolean isSorted(int[] data){
for (int i = 0; i < data.length - 1; i++) {
if(data[i] > data[i + 1]){
// 反例
return false;
}
}
return true;
}
// 交换两个值
private static void swap(int[] arr,int j, int i) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void quickSort3(int[] arr) {
quickSortInternal3(arr,0,arr.length - 1);
}
// 三路快排
private static void quickSortInternal3(int[] arr, int l, int r) {
if (r - l <= 64) {
insertionSort(arr,l,r);
return;
}
int randomIndex = (int) (Math.random() * (r - l + 1)) + l;
swap(arr,l,randomIndex);
int v = arr[l];
// arr[l + 1...lt] < v
int lt = l;
// arr[gt...r] > v
int gt = r + 1;
// arr[lt + 1..i) == v,i指向当前要处理的元素
int i = l + 1;
// 终止条件i和gt重合
while (i < gt) {
if (arr[i] < v) {
swap(arr,lt + 1,i);
lt ++;
i ++;
}else if (arr[i] > v) {
// 此时不需要i++,因为gt-1这个未处理的元素换到i位置
swap(arr,gt - 1,i);
gt --;
}else {
// 此处arr[i] == v
i ++;
}
}
swap(arr,l,lt);
// 交换后arr[l..lt - 1] < v ; arr[gt..r] >v
quickSortInternal3(arr,l,lt - 1);
quickSortInternal3(arr,gt,r);
}
// 算法4的分区快排(二路快排)
public static void quickSort(int[] arr){
quickSortInternal(arr,0,arr.length - 1);
}
private static void quickSortInternal(int[] arr, int left, int right) {
if (right - left <= 64) {
insertionSort(arr,left,right);
return;
}
int p = partition(arr,left,right);
quickSortInternal(arr,left,p - 1);
quickSortInternal(arr,p + 1,right);
}
private static int partition(int[] arr, int left, int right) {
int randomIndex = random.nextInt(left,right);
swap(arr,left,randomIndex);
int v = arr[left];
// arr[l + 1..j] < v
int j = left;
// arr[j + 1..i) >= v
for (int i = left + 1; i < right; i++) {
if(arr[i] < v){
swap(arr,i,j + 1);
j ++;
}
}
swap(arr,left,j);
return j;
}
// 挖坑法快速排序 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
// 时间复杂度:O(N*logN)
public static void quickSortHole(int[] arr){
quickSortHoleInternal(arr,0, arr.length - 1);
}
// 默认挖坑法快速排序
private static void quickSortHoleInternal(int[] arr,int left,int right){
// base case
// if(left > right){
// return;
// }
// 优化1.小数组使用插入排序
if (right - left <= 64) {
insertionSort(arr,left,right);
return;
}
int p = partitionByHole(arr,left,right);
// 继续在两个子区间上进行快速排序
quickSortHoleInternal(arr,left,p - 1);
quickSortHoleInternal(arr,p + 1,right);
}
// 非递归挖坑法快速排序
public static void quickSortNonRecursion(int[] arr){
Deque<Integer> stack = new ArrayDeque<>();
stack.push(arr.length - 1);
stack.push(0);
while (!stack.isEmpty()){
int l = stack.pop();
int r = stack.pop();
if (l >= r) {
// 当前这个子数组已经处理完毕
continue;
}
int p = partitionByHole(arr,l,r);
// 先将右半区间压入栈中
stack.push(r);
stack.push(p + 1);
// 继续处理左半区间
stack.push(p - 1);
stack.push(l);
}
}
// 时间复杂度n
private static int partitionByHole(int[] arr, int left, int right) {
// 渐进有序(极端情况完全有序)的数组,快速排序性能会退化为O(n*n)
// 原因:每次取最左边的值,假设这个值是最小的,那么j要一直减到left(二叉树会变成单支树)
// 解决:①三数取中法(课本中的方法),②随机数法
int randomIndex = random.nextInt(left,right);
swap(arr,left,randomIndex);
int pivot = arr[left];
int i = left,j = right;
while (i < j) {
// 先让j从后向前扫描碰到第一个 < pivot的元素终止
while (i < j && arr[j] >= pivot) {
j --;
}
arr[i] = arr[j];
// 再让i从前向后扫描碰到第一个 > pivot的元素终止,
while (i < j && arr[i] <= pivot) {
i ++;
}
arr[j] = arr[i];
}
// 回填分区点
arr[j] = pivot;
return j;
}
// 归并排序是一种稳定的排序方法。
// 和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,
// 因为始终都是O(n * logn)的时间复杂度。代价是需要额外的内存空间。
// 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
// 空间复杂度:O(N)
// 稳定性:稳定
public static void mergeSort(int[] arr){
mergeSortInternal(arr,0,arr.length - 1);
}
// 在arr[l...r]进行归并排序
private static void mergeSortInternal(int[] arr, int l, int r) {
// base case
// if(l > r){
// return;
// }
// 优化2.小数组(64个元素以内)直接使用插入排序
if (r - l <= 64) {
insertionSort(arr,l,r);
return;
}
// mid = (l + r) / 2
int mid = l + ((r - l) >> 1);
// 先将原数组一分为二,在子数组上先进行归并排序
mergeSortInternal(arr,l,mid);
mergeSortInternal(arr,mid + 1,r);
// 此时两个子数组已经有序,将这两个子数组合并为原数组
// merge(arr,l,mid,r);
if (arr[mid] > arr[mid + 1]) {
// 优化1.只有子数组1和子数组2存在元素的乱序才需要合并
merge(arr,l,mid,r);
}
}
// 在数组arr[l..r]上进行插入排序
private static void insertionSort(int[] arr, int l, int r) {
for (int i = l + 1; i <= r; i++) {
for (int j = i; j > l && arr[j] < arr[j - 1]; j--) {
swap(arr,j,j - 1);
}
}
}
private static void merge(int[] arr, int l, int mid, int r) {
// 本身就是插入排序
// 创建一个大小为r - l + 1的与原数组长度一样的临时数组aux
int[] aux = new int[r - l + 1];
// arraycopy:原数组,原数组开始copy的第一个下标,目标数组,目标数组copy的第一个下标,copy的长度
System.arraycopy(arr,l,aux,0,r - l + 1);
// 两个子数组的开始索引
int i = l,j = mid + 1;
// k表示当前原数组合并到哪个位置
// 时间复杂度O(n)
for (int k = l; k <= r; k++) {
if (i > mid) {
// 子数组1全部拷贝完毕,将子数组2的所有内容写回arr
arr[k] = aux[j - l];
j ++;
}else if (j > r) {
// 子数组2全部拷贝完毕,将子数组1的剩余内容写回arr
arr[k] = aux[i - l];
i ++;
}else if (aux[i - l] <= aux[j - l]) {
// 稳定性 <=
arr[k] = aux[i - l];
i ++;
}else {
arr[k] = aux[j - l];
j ++;
}
}
}
// 希尔排序(插入排序的优化),不稳定
public static void shellSort(int[] arr) {
int gap = arr.length >> 1;
while (gap > 1) {
// 先按照gap分组,组内使用插入排序
insertionSortByGap(arr,gap);
gap = gap >> 1;
}
// 当gap == 1时,整个数组接近于有序,此时来一个插入排序
insertionSortByGap(arr,1);
}
// 按照gap分组,组内的插入排序
private static void insertionSortByGap(int[] arr, int gap) {
for (int i = gap; i < arr.length; i++) {
for (int j = i; j - gap >= 0 && arr[j] < arr[j - gap] ; j -= gap) {
swap(arr,j,j - gap);
}
}
}
// 插入排序,稳定!近乎有序的集合上性能非常好,经常作为其他高阶排序的优化手段
public static void insertionSort(int[] arr){
// 无序区间[i...n)
// 有序区间[0...i)
// i指向当前无序区间的第一个元素
for (int i = 1; i < arr.length; i++) {
for (int j = i; j - 1 >= 0 && arr[j] < arr[j - 1]; j--) {
// 简化写法
swap(arr,j,j - 1);
// j - 1 要保证不为负数 因此j > 0,而不是 j >= 0
// if(arr[j] < arr[j - 1]){
// swap(arr,j,j - 1);
// }
}
}
}
// 选择排序:对数据不敏感,无论是否有序,都会进行比较
public static void selectionSort(int[] arr){
// 起始状态 : 有序区间[0..i)
// 无序区间[i....n)
for (int i = 0; i < arr.length - 1; i++) {
// min指向的当前无序区间的最小值,min是下标
int min = i;
for (int j = i + 1; j < arr.length; j++) {
if(arr[j] < arr[min]){
min = j;
}
}
// 此时min一定指向无序区间最小值下标,换到无序区间的最开始位置
swap(arr,i,min);
}
}
// 冒泡排序
public static void bubbleSort(int[] arr){
if(arr.length < 2){
return;
}
for (int i = 0; i < arr.length - 1; i++) {
boolean isSwap = false;
for (int j = 0; j < arr.length - i - 1; j++) {
// j + 1保证索引不越界,因此,j < arr.length - i - 1
if(arr[j] > arr[j + 1]){
isSwap = true;
swap(arr,j,j + 1);
}
}
if(!isSwap){
break;
}
}
}
// 原地堆排序 升序最大堆
// 1.堆化,不断将堆顶和无序数组最后一个交换
// 2.再siftDown
// 3.重复12
public static void heapSort(int[] arr){
// 堆化
for (int i = (arr.length - 2)/2; i >= 0; i--) {
siftDown(arr,i, arr.length);
}
// 不断的将当前无序数组的最大值(堆顶) 和 最后一个元素进行交换~
for (int i = arr.length - 1; i >= 0 ; i--) {
// 将堆顶(最大值) 和 i交换
swap(arr,0,i);
// 进行元素的下沉操作
siftDown(arr,0,i);
}
}
// 元素下沉
private static void siftDown(int[] arr, int i, int length) {
int j = 2 * i + 1;
while (j < length){
if(j + 1 < length && arr[j] < arr[j + 1]){
j = j + 1;
}
if(arr[j] <= arr[i]){
// 下沉结束
break;
}else {
swap(arr,i,j);
i = j;
j = 2 * i + 1;//更新孩子节点
}
}
}
}
(3)测试结果
参考文章:https://www.cnblogs.com/onepixel/p/7674659.html