目录
2.2.2希尔排序(O(n^1.2-n^1.3),不稳定)(缩小增量排序)
1.概念
1.1排序
排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。平时的上下文中。如果提到排序,通常指的是排升序(非降序)。通常意义上的排序,都是指的原地排序(in place sort)。
1.2稳定性
稳定性:在待排序的序列中,存在值相等的元素,经过排序之后,值相等的元素的先后顺序没有发生变化的排序算法称之为稳定的排序算法。
在选择排序算法时,除了时间复杂度和空间复杂度之外,某些场景下,排序的稳定性也是我们考虑的因素之一。如京东商城的订单系统,设每个订单的属性:单号、金额、下单时间;现要根据订单金额由小到大排序,选择稳定性的排序算法,排序之后,相同金额的订单彼此下单的顺序没有变化。
1.3七大排序总览
此处所涉及排序算法均为内部排序(待排序的数据都是放在内存中),根据排序的方式,可如下图分类:
快速排序:20世纪最伟大的算法之一;而归并排序,在21世纪发挥出重要价值,TimSort现在已经是主流编程语言默认的排序算法(Java、Python)。
而时间复杂度为O(n ^ 2)的基础排序算法常常作为高阶排序算法的优化手段(如插入排序经常用在高阶排序的优化中)。
2.七大排序
2.1选择排序
2.1.1直接选择排序(O(n^2),不稳定)
直接选择排序的原理十分简单:每一次从无序区间中选出最大(或最小)的一个元素,放在无序区间的最后(或最前),直到所有待排序的元素排序完成。如图:
而为何说直接选择排序是不稳定的算法呢?举个栗子:
有一组数据:9,2,5a,7,5b,4,3,6;
经过第一次排序,将无序区间中最小的元素2放在无序区间最前面:2、9、5a、7、5b、4、3、6;
经过第二次排序,将无序区间中最小的元素3放在无序区间最前面:2、3、5a、7、5b、4、9、6;
经过第三次排序,将无序区间中最小的元素4放在无序区间最前面:2、3、4、7、5b、5a、9、6;
第四次排序,将无序区间中最小的元素5b放在无序区间的最前面:2、3、4、5b、7、5a、9、6;
.... 2、3、4、5b、5a、7、9、6;
.... 2、3、4、5b、5a、6、9、7;
.... 2、3、4、5b、5a、6、7、9;
排序完成,可见,值相等的5a 与5b 在排序过后相对顺序已经发生了改变,所以说直接选择排序是不稳定的排序算法。
代码实现:只需要注意无序区间和有序区间就好;
public static void selectionSort(int[] arr) {
// 最开始无序区间[i...n)
// 有序区间[]
// 最外层的for循环指的循环走的趟数,每走一趟外层循环,就有一个最小值放在了正确的位置
for (int i = 0; i < arr.length - 1; i++) {
// min指的是最小元素的索引下标
int min = i;
// 内层循环在查找当前无序区间的最小值索引
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
// j对应的元素比当前最小值还小
min = j;
}
}
// 走出内层循环min这个变量一定保存了当前无序区间的最小值索引
// 有序区间[0..i) + 1
// 无序区间[i..n) - 1
swap(arr,i,min);
}
}
代码可见,使用了两次嵌套循环,所以时间复杂度为O(n^2);稳定性:不稳定
而直接选择排序其实还可进行优化,每一次从无序区间选择出两个(最大+最小)放在无序区间的最后和最前,这里仅做了解,直接看代码:
public static void selectionSortOP(int[] arr) {
int low = 0;
int high = arr.length - 1;
// low == high => 无序区间只剩下最后一个元素,其实整个数组已经有序了。
while (low < high) {
int min = low;
int max = low;
for (int i = low + 1; i <= high; i++) {
if (arr[i] < arr[min]) {
min = i;
}
if (arr[i] > arr[max]) {
max = i;
}
}
// 此时min对应了最小值索引,交换到无序区间的最前面
swap(arr,min,low);
// 边界条件 low == max
if (max == low) {
max = min;
}
swap(arr,max,high);
low ++;
high --;
}
}
2.1.2堆排序(O(nlogn),不稳定)
其基本原理也是选择排序,只是不再使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数。如下:
需要注意,排升序用大根堆,排降序用小根堆;
注意:堆中结点值得大小跟层高并没有必然联系;
堆排序的原理也并不复杂:
- 任意数组其实就可以看做是一个完全二叉树,就将这个数组调整为最大堆(heapify,所谓堆化,就是找到最后一个非叶子结点开始进行下沉siftDown操作,可见上方链接);
- 不断交换堆顶元素和无序区间最后一个位置的元素(最大值放在最终位置),将堆顶元素继续进行siftDown操作;
直接代码加注释:
public static void heapSort(int[] arr) {
// 1.先将任意数组进行heapify调整为最大堆
//找到最后一个非叶子节点,进行下沉操作
for (int i = (arr.length - 1) / 2; i >= 0; i--) {
siftDown(arr,i,arr.length);
}
// 2.不断交换堆顶元素到数组末尾,每交换一个元素,就有一个元素落在了最终位置
for (int i = arr.length - 1; i > 0; i--) {
// arr[i] 就是未排序数组的最大值,交换末尾
swap(arr,0,i);
siftDown(arr,0,i);
}
}
private static void siftDown(int[] arr,int i,int length) {
//还有左孩子
while (2 * i + 1 < length) {
int j = 2 * i + 1;//当前节点的左孩子
//判断是否有右孩子且比较左右孩子的值
if (j + 1 < length && arr[j + 1] > arr[j]) {
j = j + 1;//当前节点的右孩子
}
//此时的j一定是左右孩子的最大值索引
//有孩子且值比孩子大,无需下沉
if (arr[i] >arr[j]) {
break;
}else {
//交换当前节点和孩子的值
swap(arr,i,j);
i = j;
}
}
}
时间复杂度分析:调整堆的时间复杂度为O(n),而需要调整n - 1次,所定义堆排序总的时间复杂度为O(nlogn)。稳定性:不稳定
2.2插入排序
2.2.1直接插入排序(O(n^2),稳定)
可以将插入排序看做打扑克时码牌的过程,将待排序的集合看做两部分,已排序的区间[0...i),待排序的区间[i....n);每次选择无序区间的第一个元素插入到有序区间的合适位置,直到整个数组有序。初始化数据越接近有序,效率越高,经常作为高阶排序算法优化手段,原理如图:
来看代码实现:
public static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
// 已排序区间[0...1)
// 待排序区间[i ..n)
// 选择无序区间的第一个元素,不断向前看
// 注意看内层循环的终止条件 j >= 1而不是 j >= 0 ?
// 因为此时arr[j] 不断向前看一个元素 j - 1 要合法 j - 1 >= 0
for (int j = i; j >= 1 && arr[j] < arr[j - 1]; j--) {
swap(arr,j,j - 1);
}
}
}
时间复杂度分析:两次遍历,O(n^2)。稳定性:稳定
同样的,插入排序也有优化的空间,在选择无序区间的第一个元素插入到有序区间的合适位置时,优化他的插入位置的查询比较次数,将在有序区间的查找过程变为二分查找;于是将他的优化叫做:折半插入排序。
来看代码:
public static void insertionSortBS(int[] arr) {
// 有序区间[0..i)
// 无序区间[i..n)
for (int i = 0; i < arr.length; i++) {
int val = arr[i];
// 有序区间[left...right)
int left = 0;
int right = i;
while (left < right) {
int mid = (left + right) / 2;
if (val < arr[mid]) {
right = mid;
}else {
// val >= arr[mid]
left = mid + 1;
}
}
// 搬移[left..i)的元素
for (int j = i; j > left ; j--) {
arr[j] = arr[j - 1];
}
// left就是待插入的位置
arr[left] = val;
}
}
2.2.2希尔排序(O(n^1.2-n^1.3),不稳定)(缩小增量排序)
希尔排序其实也就是插入排序的优化,不断地将小数组调整的近乎有序,整个大数组就接近有序状态,这个时候使用插入排序的效率是很高的。
思路:
- 先选定一个整数(gap),将待排序的数组分成gap组,所有距离为gap的为同一组,对每一个小数组先进行插入排序。
- gap = gap/2(3),重复第一步。当gap == 1,说明此时整个数组已经近乎有序,只需要对整个数组来一次插入排序即可,效率一定是比较高的。
希尔排序是不稳定的,因为在按照gap对子数组进行排序时,有可能相等值的两个元素被分到了两个不同的数组,有可能交换他们的顺序。
代码如下:
public static void shellSort(int[] arr) {
int gap = arr.length >> 1;
// 预处理阶段
while (gap >= 1) {
// 按照gap分组之后,组内进行插入排序
insertionSortByGap(arr,gap);
gap = gap >> 1;
}
}
private static void insertionSortByGap(int[] arr, int gap) {
// [9,1,2,5,7,4,8,6,3,5] gap = 5
// i = 5
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);
}
}
}
时间复杂度分析:平均时间复杂度O(n^1.2-n^1.3),最坏情况下为O(n^2)。
2.3归并排序
2.3.1归并排序(O(nlogn),稳定)
归并排序(merge-sort),是建立在归并操作上的一种非常有效的排序算法,是采用分治思想的一个典型应用;思路如下:
- 归:不断地将原数组拆分为子数组(一拆为二),直到每个子数组只剩下一个元素(归过程已经结束)。
- 并:不断合并相邻的两个子数组为一个更大的子数组,合并的过程就是将两个已经有序的子数组合并为一个大的有序子数组,直到合并整个数组。
最核心的merge操作,需要开辟额外空间,空间大小就是合并后的数组大小:
- 先将两个子数组的所有内容复制到新数组中;
- 遍历两个子数组,将较小的值写回原数组;
代码实现:
public static void mergeSort(int[] arr) {
mergeSortInternal(arr,0,arr.length - 1);
}
public static void mergeSortInternal(int[] arr,int l,int r) {
//递归结束条件,只有一个元素时
// if(r == l) {
// return;
// }
//小数组直接使用插入排序
if(r - l <= 15) {
insertionSort(arr,l,r);
return;
}
int mid = l + (r - l) / 2;
mergeSortInternal(arr, l, mid);
mergeSortInternal(arr,mid + 1,r);
// arr[l..mid] 和 arr[mid + 1...r]有序 只需要合并这两个子数组即可
//当arr[mid]<arr[mid + 1]时,说明数组1的最大值已经小于数组2的最小值,无需合并
if(arr[mid] > arr[mid + 1]) {
//前后两个子数组还存在乱序,才需要合并
merge(arr,l,mid,r);
}
}
/**
* 为归并排序做优化的带参数插入排序
* @param arr
* @param l
* @param 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 + 1 && arr[j] < arr[j - 1]; j--) {
swap(arr,j,j - 1);
}
}
}
/**
* 合并两个有序的子数组
* @param arr
* @param l
* @param mid
* @param r
*/
public static void merge(int[] arr,int l,int mid,int r) {
// 先创建一个新的数组aux,将子数组的值复制给新数组
int[] aux = new int[r - l + 1];
// l = 2,r = 4
// arr[2..4]
// aux[0..2] 索引下标差了个l偏移量
for(int i = 0;i < aux.length;i++) {
aux[i] = arr[i + l];
}
//数组1和数组2的开始下标
int i = l,j = mid + 1;
for (int k = l; k <= r; k++) {
if (i > mid) {
// 第一个数组已经遍历完毕
arr[k] = aux[j - l];
j ++;
}else if (j > r) {
// 第二个子数组遍历完毕
arr[k] = aux[i - l];
i ++;
}else if (aux[i - l] <= aux[j - l]) {
// 将aux[i - l]写回arr[k]
arr[k] = aux[i - l];
i ++;
}else {
// aux[i - l] > aux[j - l] 写回aux[j - l]
arr[k] = aux[j - l];
j ++;
}
}
}
时间复杂度分析:拆分数组的过程使用递归拆分O(logn),合并子树组的过程遍历arr[l...r]进行合并O(n),所以总的时间复杂度为O(nlogn)。
2.4交换排序
2.4.1冒泡排序(O(n^2),稳定)
冒泡排序的原理比较简单:在无序区间,通过比较相邻的两个数,将最大的数交换(冒泡)到无序区间的最后,持续这个过程,直到整个数组有序。
思路比较简单啊,直接来看代码:
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
boolean isSwaped = false;
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr,j,j + 1);
isSwaped = true;
}
}
if (!isSwaped) {
// 内层没有元素交换,此时整个数组已经有序
break;
}
}
}
时间复杂度:O(n^2)。稳定性:稳定。
2.4.2快速排序(O(nlogn),不稳定)
快速排序的核心思路:分区。
先看基本思路:
- 分区值,默认选择最左侧元素pivot
- 从无序区间选择一个值作为分界点pivot开始扫描原集合,将数组所有 < 该pivot的元素放在分界点左侧,>= 该元素的值放在分区点的右侧。经过本轮交换,pivot放在了最终位置,pivot的左侧都是小于该值的元素,pivot右侧都是大于该值的元素。
- 在这两个子区间重复上述过程,直到整个集合有序
代码实现:
public static void quickSort(int[] arr) {
quickSortInternal(arr,0,arr.length - 1);
}
private static void quickSortInternal(int[] arr, int l, int r) {
if(l >= r) {
return;
}
int p = partition(arr,l,r);
//继续在左右两个子区间进行快速排序
//所有 < v的元素
quickSortInternal(arr,l,p - 1);
//所有 >= v的元素
quickSortInternal(arr,p + 1,r);
}
private static int partition(int[] arr, int l, int r) {
int v = arr[l];
// arr[l + 1..j] < v
// 最开始区间没有元素
int j = l;
// arr[j + 1..i) >= v
// 最开始大于区间也没有元素
for (int i = l + 1; i <= r; i++) {
if (arr[i] < v) {
swap(arr,i,j + 1);
j ++;
}
}
// 此时元素j就是最后一个 < v的元素,就把v换到j的位置
swap(arr,l,j);
return j;
}
但这样的快速排序算法有一个问题,就是他的性能严格受制于初始数据的情况而定:
显而易见分区值选择不能武断的就选择最左侧或者最右侧,所以我们可以针对分区选择来做优化:
- 三数取一:最左侧、最右侧、中间值选择其中之一
- 每次递归的时候选择数组中的任意一个元素作为分区点
这里以第二种举例:
private static int partition(int[] arr, int l, int r) {
int randomIndex = random.nextInt(l,r);
swap(arr,l,randomIndex);
int v = arr[l];
// arr[l + 1..j] < v
// 最开始区间没有元素
int j = l;
// arr[j + 1..i) >= v
// 最开始大于区间也没有元素
for (int i = l + 1; i <= r; i++) {
if (arr[i] < v) {
swap(arr,i,j + 1);
j ++;
}
}
// 此时元素j就是最后一个 < v的元素,就把v换到j的位置
swap(arr,l,j);
return j;
}
而其实,还可以使用非递归写法,借助栈来完成:
public static void quickSortNonRecursion(int[] arr) {
Deque<Integer> stack = new ArrayDeque<>();
// r
stack.push(arr.length - 1);
// l
stack.push(0);
// 每次从栈中取出两个元素,这辆个元素就是待排序区间的l..r
while (!stack.isEmpty()) {
int l = stack.pop();
int r = stack.pop();
if (l >= r) {
// 当前子数组已经处理完毕
continue;
}
int p = partition(arr,l,r);
// 继续入栈两个子区间
stack.push(p - 1);
stack.push(l);
stack.push(r);
stack.push(p + 1);
}
}
而快速排序出上述写法外,还可以选择一种常见的分区方法-Hoare挖坑法,原理如下:
这样,pivot左边就全是小于他的元素,右边就全是大于他的元素。
代码实现:
public static void quickSortHoare(int[] arr) {
quickSortInternalHoare(arr,0,arr.length - 1);
}
private static void quickSortInternalHoare(int[] arr, int l, int r) {
// 2.小区间上使用插入排序来优化,不用递归到底
if (r - l <= 15) {
insertionSort(arr,l,r);
return;
}
int p = partitionHoare(arr,l,r);
// 继续在左右两个子区间进行快速排序
// 所有 < v的元素
quickSortInternalHoare(arr,l,p - 1);
// 所有 >= v的元素
quickSortInternalHoare(arr,p + 1,r);
}
/**
* 挖坑分区法
* @param arr
* @param l
* @param r
* @return
*/
private static int partitionHoare(int[] arr, int l, int r) {
int randomIndex = random.nextInt(l,r);
swap(arr,l,randomIndex);
int pivot = arr[l];
int i = l;
int j = r;
while (i < j) {
// 先让j从后向前扫描到第一个 < v的元素停止
while (i < j && arr[j] >= pivot) {
j --;
}
arr[i] = arr[j];
// 再让i从前向后扫描到第一个 > v的元素停止
while (i < j && arr[i] <= pivot) {
i ++;
}
arr[j] = arr[i];
}
arr[i] = pivot;
return i;
}
3.完整代码与测试
package sort;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
/**
* 测试七大排序的性能以及产生辅助的测试数组
**/
public class SortHelper {
// 生成随机数的一个类
private static final ThreadLocalRandom random = ThreadLocalRandom.current();
/**
* 1.产生一个大小为n的随机整数数组,数组的取值范围为[l..r)
* @param n 数组的元素个数
* @param l 数组的取值最小值
* @param r 数组元素最大值
* @return
*/
public static int[] generaRandomArray(int n,int l,int r) {
int[] data = new int[n];
for (int i = 0; i < n; i++) {
// 生成一个[l..r)的随机数
data[i] = random.nextInt(l,r);
}
return data;
}
/**
* 2.生成一个大小为n的近乎有序的数组
* @param n 元素个数
* @param swapTimes 交换的次数,这个参数越大,数组越无序
* @return
*/
public static int[] generateSortedArray(int n,int swapTimes) {
int[] data = new int[n];
for (int i = 0; i < n; i++) {
data[i] = i;
}
for (int i = 0; i < swapTimes; i++) {
// 生成一个[0..n)随机数
int a = random.nextInt(n);
int b = random.nextInt(n);
int temp = data[a];
data[a] = data[b];
data[b] = temp;
}
return data;
}
/**
* 深拷贝原数组
* @param arr
* @return
*/
public static int[] arrCopy(int[] arr) {
return Arrays.copyOf(arr,arr.length);
}
/**
* 在指定的数组arr上测试排序名称为sortName的排序算法性能
* @param arr
* @param sortName
*/
public static void testSort(int[] arr,String sortName) {
Class<SevenSort> cls = SevenSort.class;
try {
Method method = cls.getDeclaredMethod(sortName,int[].class);
long start = System.nanoTime();
method.invoke(null,arr);
long end = System.nanoTime();
if (isSorted(arr)) {
System.out.println(sortName + "排序完成,共耗时:" + (end - start) / 1000000.0 + "ms");
}
}catch (Exception e) {
e.printStackTrace();
}
}
/**
* 检查当前的arr数组是否是一个非降序数组,前一个元素 <= 后一个元素
* @param arr
* @return
*/
private static boolean isSorted(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
if (arr[i] > arr[i + 1]) {
// 反例
System.err.println("排序算法有误!");
return false;
}
}
return true;
}
}
package sort;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
public class SevenSort {
private static ThreadLocalRandom random = ThreadLocalRandom.current();
public static void main(String[] args) {
int n = 50000;
int[] arr = SortHelper.generaRandomArray(n,0,Integer.MAX_VALUE);
int[] arr1 = SortHelper.arrCopy(arr);
int[] arr2 = SortHelper.arrCopy(arr);
int[] arr3 = SortHelper.arrCopy(arr);
int[] arr4 = SortHelper.arrCopy(arr);
int[] arr5 = SortHelper.arrCopy(arr);
// SortHelper.testSort(arr,"insertionSort");
// SortHelper.testSort(arr1,"mergeSort");
// SortHelper.testSort(arr2,"insertionSortBS");
// SortHelper.testSort(arr3,"heapSort");
// SortHelper.testSort(arr4,"shellSort");
SortHelper.testSort(arr5,"quickSort");
}
/**
* 选择排序--每次找出最小的放在最前面
* @param arr
*/
public static void selectionSort(int[] arr) {
// 最开始无序区间[i...n)
// 有序区间[]
// 最外层的for循环指的循环走的趟数,每走一趟外层循环,就有一个最小值放在了正确的位置
for (int i = 0; i < arr.length - 1; i++) {
// min指的是最小元素的索引下标
int min = i;
// 内层循环在查找当前无序区间的最小值索引
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
// j对应的元素比当前最小值还小
min = j;
}
}
// 走出内层循环min这个变量一定保存了当前无序区间的最小值索引
// 有序区间[0..i) + 1
// 无序区间[i..n) - 1
swap(arr,i,min);
}
}
/**
* 双向选择排序--每次选出最大的和最小的放在合适位置
* @param arr
*/
public static void selectionSortOP(int[] arr) {
int low = 0;
int high = arr.length - 1;
// low == high => 无序区间只剩下最后一个元素,其实整个数组已经有序了。
while (low < high) {
int min = low;
int max = low;
for (int i = low + 1; i <= high; i++) {
if (arr[i] < arr[min]) {
min = i;
}
if (arr[i] > arr[max]) {
max = i;
}
}
// 此时min对应了最小值索引,交换到无序区间的最前面
swap(arr,min,low);
// 边界条件 low == max
if (max == low) {
max = min;
}
swap(arr,max,high);
low ++;
high --;
}
}
/**
* 冒泡排序--比较相邻两个元素的大小,将大的元素向后交换
* @param arr
*/
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
boolean isSwaped = false;
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr,j,j + 1);
isSwaped = true;
}
}
if (!isSwaped) {
// 内层没有元素交换,此时整个数组已经有序
break;
}
}
}
public static void quickSort(int[] arr) {
quickSortInternal(arr,0,arr.length - 1);
}
private static void quickSortInternal(int[] arr, int l, int r) {
if(l >= r) {
return;
}
int p = partition(arr,l,r);
//继续在左右两个子区间进行快速排序
//所有 < v的元素
quickSortInternal(arr,l,p - 1);
//所有 >= v的元素
quickSortInternal(arr,p + 1,r);
}
private static int partition(int[] arr, int l, int r) {
int randomIndex = random.nextInt(l,r);
swap(arr,l,randomIndex);
int v = arr[l];
// arr[l + 1..j] < v
// 最开始区间没有元素
int j = l;
// arr[j + 1..i) >= v
// 最开始大于区间也没有元素
for (int i = l + 1; i <= r; i++) {
if (arr[i] < v) {
swap(arr,i,j + 1);
j ++;
}
}
// 此时元素j就是最后一个 < v的元素,就把v换到j的位置
swap(arr,l,j);
return j;
}
public static void quickSortNonRecursion(int[] arr) {
Deque<Integer> stack = new ArrayDeque<>();
// r
stack.push(arr.length - 1);
// l
stack.push(0);
// 每次从栈中取出两个元素,这辆个元素就是待排序区间的l..r
while (!stack.isEmpty()) {
int l = stack.pop();
int r = stack.pop();
if (l >= r) {
// 当前子数组已经处理完毕
continue;
}
int p = partition(arr,l,r);
// 继续入栈两个子区间
stack.push(p - 1);
stack.push(l);
stack.push(r);
stack.push(p + 1);
}
}
public static void quickSortHoare(int[] arr) {
quickSortInternalHoare(arr,0,arr.length - 1);
}
private static void quickSortInternalHoare(int[] arr, int l, int r) {
// 2.小区间上使用插入排序来优化,不用递归到底
if (r - l <= 15) {
insertionSort(arr,l,r);
return;
}
int p = partitionHoare(arr,l,r);
// 继续在左右两个子区间进行快速排序
// 所有 < v的元素
quickSortInternalHoare(arr,l,p - 1);
// 所有 >= v的元素
quickSortInternalHoare(arr,p + 1,r);
}
/**
* 挖坑分区法
* @param arr
* @param l
* @param r
* @return
*/
private static int partitionHoare(int[] arr, int l, int r) {
int randomIndex = random.nextInt(l,r);
swap(arr,l,randomIndex);
int pivot = arr[l];
int i = l;
int j = r;
while (i < j) {
// 先让j从后向前扫描到第一个 < v的元素停止
while (i < j && arr[j] >= pivot) {
j --;
}
arr[i] = arr[j];
// 再让i从前向后扫描到第一个 > v的元素停止
while (i < j && arr[i] <= pivot) {
i ++;
}
arr[j] = arr[i];
}
arr[i] = pivot;
return i;
}
/**
* 插入排序--每次从无序区间选择第一个元素,插入到有序区间到合适位置
* 稳定
* @param arr
*/
public static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
// 已排序区间[0...1)
// 待排序区间[i ..n)
// 选择无序区间的第一个元素,不断向前看
// 注意看内层循环的终止条件 j >= 1而不是 j >= 0 ?
// 因为此时arr[j] 不断向前看一个元素 j - 1 要合法 j - 1 >= 0
for (int j = i; j >= 1 && arr[j] < arr[j - 1]; j--) {
swap(arr,j,j - 1);
}
}
}
/**
* 二分插入排序
* @param arr
*/
public static void insertionSortBS(int[] arr) {
// 有序区间[0..i)
// 无序区间[i..n)
for (int i = 0; i < arr.length; i++) {
int val = arr[i];
// 有序区间[left...right)
int left = 0;
int right = i;
while (left < right) {
int mid = (left + right) / 2;
if (val < arr[mid]) {
right = mid;
}else {
// val >= arr[mid]
left = mid + 1;
}
}
// 搬移[left..i)的元素
for (int j = i; j > left ; j--) {
arr[j] = arr[j - 1];
}
// left就是待插入的位置
arr[left] = val;
}
}
/**
* 希尔排序 - 缩小增量排序,按照gap将原数组分为gap个子数组,子数组内部先排序,不断缩小gap值,直到gap = 1
* 当gap = 1时,整个数组已经近乎有序,只需要最后来一次插入排序即可
* @param arr
*/
public static void shellSort(int[] arr) {
int gap = arr.length >> 1;
// 预处理阶段
while (gap >= 1) {
// 按照gap分组之后,组内进行插入排序
insertionSortByGap(arr,gap);
gap = gap >> 1;
}
}
private static void insertionSortByGap(int[] arr, int gap) {
// [9,1,2,5,7,4,8,6,3,5] gap = 5
// i = 5
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);
}
}
}
/**
* 归并排序--可以看做是一个树结构的后序遍历
* @param arr
*/
public static void mergeSort(int[] arr) {
mergeSortInternal(arr,0,arr.length - 1);
}
public static void mergeSortInternal(int[] arr,int l,int r) {
//递归结束条件,只有一个元素时
// if(r == l) {
// return;
// }
//小数组直接使用插入排序
if(r - l <= 15) {
insertionSort(arr,l,r);
return;
}
int mid = l + (r - l) / 2;
mergeSortInternal(arr, l, mid);
mergeSortInternal(arr,mid + 1,r);
// arr[l..mid] 和 arr[mid + 1...r]有序 只需要合并这两个子数组即可
//当arr[mid]<arr[mid + 1]时,说明数组1的最大值已经小于数组2的最小值,无需合并
if(arr[mid] > arr[mid + 1]) {
//前后两个子数组还存在乱序,才需要合并
merge(arr,l,mid,r);
}
}
/**
* 为归并排序做优化的带参数插入排序
* @param arr
* @param l
* @param 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 + 1 && arr[j] < arr[j - 1]; j--) {
swap(arr,j,j - 1);
}
}
}
/**
* 合并两个有序的子数组
* @param arr
* @param l
* @param mid
* @param r
*/
public static void merge(int[] arr,int l,int mid,int r) {
// 先创建一个新的数组aux,将子数组的值复制给新数组
int[] aux = new int[r - l + 1];
// l = 2,r = 4
// arr[2..4]
// aux[0..2] 索引下标差了个l偏移量
for(int i = 0;i < aux.length;i++) {
aux[i] = arr[i + l];
}
//数组1和数组2的开始下标
int i = l,j = mid + 1;
for (int k = l; k <= r; k++) {
if (i > mid) {
// 第一个数组已经遍历完毕
arr[k] = aux[j - l];
j ++;
}else if (j > r) {
// 第二个子数组遍历完毕
arr[k] = aux[i - l];
i ++;
}else if (aux[i - l] <= aux[j - l]) {
// 将aux[i - l]写回arr[k]
arr[k] = aux[i - l];
i ++;
}else {
// aux[i - l] > aux[j - l] 写回aux[j - l]
arr[k] = aux[j - l];
j ++;
}
}
}
public static void heapSort(int[] arr) {
// 1.先将任意数组进行heapify调整为最大堆
//找到最后一个非叶子节点,进行下沉操作
for (int i = (arr.length - 1) / 2; i >= 0; i--) {
siftDown(arr,i,arr.length);
}
// 2.不断交换堆顶元素到数组末尾,每交换一个元素,就有一个元素落在了最终位置
for (int i = arr.length - 1; i > 0; i--) {
// arr[i] 就是未排序数组的最大值,交换末尾
swap(arr,0,i);
siftDown(arr,0,i);
}
}
private static void siftDown(int[] arr,int i,int length) {
//还有左孩子
while (2 * i + 1 < length) {
int j = 2 * i + 1;//当前节点的左孩子
//判断是否有右孩子且比较左右孩子的值
if (j + 1 < length && arr[j + 1] > arr[j]) {
j = j + 1;//当前节点的右孩子
}
//此时的j一定是左右孩子的最大值索引
//有孩子且值比孩子大,无需下沉
if (arr[i] >arr[j]) {
break;
}else {
//交换当前节点和孩子的值
swap(arr,i,j);
i = j;
}
}
}
private static void swap(int[] arr,int a,int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}