文章目录
前言
今天是更新第 5 天,今天的主要内容还是 Java 基础 - 「排序」和「二分」
正文
1. 排序
冒泡排序
依次比较两个相邻的元素,如果顺序错误就把他们交换过来,两两都比较完一遍称为一轮冒泡,重复以上步骤直到数组有序。
n 个元素在第一轮冒泡时需要比较 n - 1次,第一轮比较完成后数组最大/最小的元素会在最后的位置,后续每轮冒泡完成后,下一次比较次数总会减一。
如果在某一轮的冒泡完成后,没有发生元素交换,说明数组排序完成,结束循环即可。
// 版本一
int[] nums = {5, 2, 7, 4, 1, 3, 8, 9};
int n = nums.length;
for (int i = 0; i < n - 1; i++) {
boolean flag = true;
for (int j = 0; j < n - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
int t = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = t;
flag = false;
}
}
if (flag) {
break;
}
}
System.out.println(Arrays.toString(nums));
// 进一步优化: 记录最后一次发生交换的下标作为下一轮冒泡的比较次数,如果次数为0,说明数组有序
int[] nums = {5, 2, 7, 4, 1, 3, 8, 9};
int n = nums.length;
int cnt = n - 1;
while (true) {
int last = 0;
for (int j = 0; j < cnt; j++) {
if (nums[j] > nums[j + 1]) {
int t = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = t;
last = j;
}
}
cnt = last;
if (cnt == 0) {
break;
}
}
System.out.println(Arrays.toString(nums));
选择排序
将数组划分为两个部分,已经完成排序的部分和没有完成排序的部分,每一轮从未排序的部分中找出最小/最大的元素,放入已经排序的部分,重复以上步骤直到数组有序
int[] nums = {5, 2, 7, 4, 1, 3, 8, 9};
int n = nums.length;
for (int i = 0; i < n - 1; i++) {
int idx = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[idx]) {
idx = j;
}
}
if (idx != i) {
int t = nums[i];
nums[i] = nums[idx];
nums[idx] = t;
}
}
System.out.println(Arrays.toString(nums));
选择排序与冒泡排序的比较
- 二者平均时间复杂度都是O(n^2)
- 选择排序一般要快于冒泡,因为其交换次数少
- 如果集合有序度高,冒泡优于选择
- 冒泡排序属于稳定排序算法,而选择排序属于不稳定排序算法
插入排序
int[] nums = {5, 2, 7, 4, 1, 3, 8, 9};
int n = nums.length;
// i 为待插入元素的索引
for (int i = 1; i < n; i++) {
// val 为待插入的元素值
int val = nums[i];
// j 表示已排序区域的索引
int j;
for (j = i - 1; j >= 0; j--) {
if (val < nums[j]) {
nums[j + 1] = nums[j];
} else {
break;
}
}
nums[j + 1] = val;
}
System.out.println(Arrays.toString(nums));
插入排序和选择排序比较
- 二者平均时间复杂度都是O(n^2)
- 大部分情况下,插入略优于选择
- 有序集合插入的时间复杂度为O(n)
- 插入排序属于稳定排序算法,而选择排序属于不稳定排序
希尔排序
是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的数字越来越多,当增量减至 1 时,整个数组恰被分成一组,算法便终止。
快速排序
基于分治的一种快排方法,平均时间复杂度是O(nlog2n),最坏时间复杂度O(n^2),数据量较大时,优势非常明显,属于不稳定排序
单边循环快排(lomuto洛穆托分区方案)
- 选择最右元素作为基准点元素
- j指针负责找到比基准点小的元素,一旦找到则与i进行交换
- i指针维护小于基准点元素的边界,也是每次交换的目标索引
- 最后基准点与i交换,i即为分区位置
双边循环快排(并不完全等价于hoare霍尔分区方案)
-
选择最左元素作为基准点元素
-
j指针负责从右向左找比基准点小的元素
-
i指针负责从左向右找比基准点大的元素
-
一旦找到二者交换,直至i,j相交
-
最后基准点与i(此时i与j相等)交换,i即为分区位置
public static void quickSort(int[] arr, int l, int r) { // 分区中只有一个元素时,退出排序 if (l >= r) return; // 基准点为数组的中间元素,比基准点小的元素放在基准点左边,反之放在右边 int x = arr[l + r >> 1], i = l - 1, j = r + 1; // 类似于双边循环快排的实现 while (i < j) { while (arr[++i] < x); while (arr[--j] > x); if (i < j) { int t = arr[i]; arr[i] = arr[j]; arr[j] = t; } } // 循环结束后,递归处理左右两个分区 quickSort(arr, l, j); quickSort(arr, j + 1, r); }
归并排序
基于分治,将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序是一种稳定的排序方法。
public static void mergeSort(int[] arr, int l, int r) {
if (l >= r) return;
// 以中间位置为分界点
int mid = l + r >> 1;
// 先递归分解,再合并
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while (i <= mid) temp[k++] = arr[i++];
while (j <= r) temp[k++] = arr[j++];
for (i = l, j = 0; i <= r; i++, j++) {
arr[i] = temp[j];
}
}
JDK Arrays 类的 sort 方法
Arrays.sort(a);
使用ctrl+左键进入sort()方法
Arrays.sort()
关于sort()
的方法一共有14
个,就目前调用的来看是以下这种最基础的。
// 默认排序的区间为整个数组
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
// 指定了排序的开始位置和结束位置
public static void sort(int[] a, int fromIndex, int toIndex) {
// 进行边界检查
rangeCheck(a.length, fromIndex, toIndex);
DualPivotQuicksort.sort(a, fromIndex, toIndex - 1, null, 0, 0);
}
DualPivotQuicksort
DualPivotQuicksort
即双轴快排,定义了七种
原始类型的排序方法。实现了sort方法并且定义了以下调整参数:
// 构造器私有防止实例化
private DualPivotQuicksort() {}
// 归并排序的最大运行次数
private static final int MAX_RUN_COUNT = 67;
// 归并排序的最大运行长度
private static final int MAX_RUN_LENGTH = 33;
// 如果要排序的数组的长度小于该常数,则优先使用快速排序而不是归并排序
private static final int QUICKSORT_THRESHOLD = 286;
// 如果要排序的数组的长度小于此常数,则优先使用插入排序而不是快速排序
private static final int INSERTION_SORT_THRESHOLD = 47;
// 如果要排序的字节数组的长度大于该常数,则优先使用计数排序而不是插入排序
private static final int COUNTING_SORT_THRESHOLD_FOR_BYTE = 29;
// 如果要排序的 short 或 char 数组的长度大于此常数,则优先使用计数排序而不是快速排序
private static final int COUNTING_SORT_THRESHOLD_FOR_SHORT_OR_CHAR = 3200;
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
该方法定义:
static void sort(int[] a, int left, int right,
int[] work, int workBase, int workLen)
进入DualPivotQuicksort的sort
方法:
static void sort(int[] a, int left, int right,
int[] work, int workBase, int workLen) {
// Use Quicksort on small arrays
if (right - left < QUICKSORT_THRESHOLD) {
sort(a, left, right, true);
return;
}
首先进行了判断,如果要排序的数组小于
了之前定义的QUICKSORT_THRESHOLD=286
,则优先使用快速排序
而不是归并排序
,即进入if中的排序sort(a, left, right, true)
;
DualPivotQuicksort.sort(a, left, right, true)
该方法定义:
private static void sort(int[] a, int left, int right, boolean leftmost)
进入if中的sort(a, left, right, true)
方法,我们只截取他的逻辑部分而非排序实现部分。
private static void sort(int[] a, int left, int right, boolean leftmost) {
int length = right - left + 1;
// Use insertion sort on tiny arrays
if (length < INSERTION_SORT_THRESHOLD) {
if (leftmost) {
/*
* Traditional (without sentinel) insertion sort,
* optimized for server VM, is used in case of
* the leftmost part.
*/
for (int i = left, j = i; i < right; j = ++i) {
int ai = a[i + 1];
while (ai < a[j]) {
a[j + 1] = a[j];
if (j-- == left) {
break;
}
}
a[j + 1] = ai;
}
} else {
// 其他处理逻辑.....
该方法中,首先判断了数组长度是否小于INSERTION_SORT_THRESHOLD=47
,如果小于就使用插入排序,而不是快速排序。leftmost
是来选择使用传统的(无标记)插入排序还是成对插入排序,leftmost是表示此部分是否在范围内的最左侧,因为我们最先开始调用的就是基础的sort,没有其他参数,所以就是从头开始排序,leftmost便默认为true
,使用传统(无标记)插入排序,如果为false,使用成对插入排序。
总结
JDK Arrays 类的 sort 方法会根据待排序数组的长度决定使用哪种排序方法。
2. 二分查找
这里二分写的比较简单,不了解二分的可以去看一下这个视频 二分查找 或者去找一些文章看一下。
Arrays.binarySearch(int[] a, key)
Arrays.binarySearch(int[] a, int fromIndex, int toIndex, int key)
// Arrays类的二分查找方法源码
// 如果不指定开始位置和结束位置,默认查找范围为整个数组
public static int binarySearch(int[] a, int key) {
return binarySearch0(a, 0, a.length, key);
}
// Like public version, but without range checks.
private static int binarySearch0(int[] a, int fromIndex, int toIndex, int key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
int midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
// 找不到 key 则返回 -(应该出现的位置 + 1)
// mid 下标的不同写法,两种方式是等价的,第二种可以有效避免整数溢出
mid = (left + right) / 2;
// left/2 + right/2 -> left - left/2 + right/2 -> left - (left - right)/2
mid = left + (right - left) / 2;
// 也可以使用位运算,效率更高,这里使用无符号右移要求左边必须是一个正数,普通右移没有限制
mid = (left + right) >>> 1;
// 二分查找的其他实现方式
while (l < r) {
int mid = l + ((r - l) >> 1);
if (check(mid)) {
r = mid;
} else {
l = mid + 1;
}
}
while (l < r) {
int mid = l + ((r - l + 1) >> 1);
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
// 红蓝二分法
while (l + 1 != r) {
int mid = l + ((r - l) >> 1);
if (check(mid)) {
l = mid;
} else {
r = mid;
}
}
// 浮点数二分
// eps 表示精度,取决于题目对精度的要求,一般这里的精度比所求精度高 2 位
double eps = 1e-6;
while (r - l > eps) {
int mid = l + ((r - l) >> 1);
if (check(mid)) {
r = mid;
} else {
l = mid;
}
}