排序与查找#
堆排序、归并、插入、快速排序、大根堆重建、希尔排序;二分查找与 KMP 模板。
排序算法#
堆排序#
PriorityQueue 就是一个小根堆结构,可以直接使用。
void heapSort(int[] arr) {
if (arr == null || arr.length < 2)
return;
// 构建大根堆(方法一)
// for (int i = 0; i < arr.length; i++)
// heapInsert(arr, i);
// 构建大根堆(方法二,更快)
for (int i = arr.length - 1; i >= 0; i--)
heapify(arr, i, arr.length);
// 每次选择并移除堆顶元素,放到末尾
int heapSize = arr.length;
swap(arr, 0, --heapSize);
while (heapSize > 0) {
heapify(arr, 0, heapSize);
swap(arr, 0, --heapSize);
}
}
归并排序#
void mergeSort(int[] arr, int[] tmp, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, tmp, left, mid);
mergeSort(arr, tmp, mid + 1, right);
merge(arr, tmp, left, mid, right);
}
}
void merge(int[] arr, int[] tmp, int left, int mid, int right) {
int pLeft = left;
int pRight = mid + 1;
int pTmp = left;
// 将左右子数组较小的元素依次插入到 tmp 中
while (pLeft <= mid && pRight <= right) {
if (arr[pLeft] <= arr[pRight])
tmp[pTmp++] = arr[pLeft++];
else
tmp[pTmp++] = arr[pRight++];
}
// 复制剩余元素到 tmp 中
while (pLeft <= mid)
tmp[pTmp++] = arr[pLeft++];
while (pRight <= right)
tmp[pTmp++] = arr[pRight++];
// 必须保存局部的排序结果,否则下次还是乱序
for (int i = left; i <= right; i++)
arr[i] = tmp[i];
}
// 调用:int[] arr = new int[]{7, 3, 2, 6};
int[] tmp = new int[arr.length]; // 辅助空间
mergeSort(arr, tmp, 0, arr.length - 1);
插入排序#
void insertSort(int[] arr) {
int j; // 用于扫描 i 之前的元素
for (int i = 1; i < arr.length; i++) {
int tmp = arr[i];
for (j = i; j > 0 && arr[j - 1] > tmp; j--)
arr[j] = arr[j - 1]; // 向后移动元素
arr[j] = tmp;
}
}
快速排序#
void quickSort(int[] arr, int left, int right) {
if (left < right) {
int pivot = arr[left]; // 随机选基准点
int i = left, j = right; // 不修改原变量
while (i < j) {
while (i < j && arr[j] > pivot)
j--; // 从右往左:首个比 pivot 小的值
if (i < j) {
arr[i] = arr[j]; // 丢失 arr[i]
i++;
}
while (i < j && arr[i] < pivot)
i++; // 从左往右:首个比 pivot 大的值
if (i < j) {
arr[j] = arr[i];
j--;
}
}
arr[i] = pivot; // 找回 arr[i]
// -- partition 和递归代码的分割线 -- //
quickSort(arr, left, i - 1);
quickSort(arr, i + 1, right);
}
}
// 调用:quickSort(arr, 0, arr.length - 1);
重建大根堆#
// 向已有堆的末尾插入元素,重建大根堆
void heapInsert(int[] arr, int i) {
while (arr[i] > arr[(i - 1) / 2]) {
swap(arr, i, (i - 1) / 2);
i = (i - 1) / 2;
}
}
// 移除堆顶元素(放在末尾),重建大根堆
void heapify(int[] arr, int i, int heapSize) {
while (i < heapSize) {
int l = 2 * i + 1; // 左孩子指针
int r = 2 * i + 2; // 右孩子指针
int max = i;
if (l < heapSize && arr[l] > arr[max])
max = l;
if (r < heapSize && arr[r] > arr[max])
max = r;
if (max == i)
break;
swap(arr, i, max);
i = max;
}
}
希尔排序#
void shellSort(int[] arr) {
for (int step = arr.length / 2; step >= 1; step /= 2) {
for (int r = step; r < arr.length; r++) {
int tmp = arr[r]; // 把 r 放到最终位置
int l = r - step;
while (l >= 0 && arr[l] > tmp) {
arr[l + step] = arr[l]; // 将 l 右移
l -= step;
}
arr[l + step] = tmp; // 放置
}
}
}
查找算法#
二分查找#
// 注意这里有多处使用 return
int binarySearch(int[] arr, int target, int left, int right) {
if (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target)
return mid;
if (arr[mid] > target) // 向左查找
return binarySearch(arr, target, left, mid - 1);
if (arr[mid] < target) // 向右查找
return binarySearch(arr, target, mid + 1, right);
}
return -1; // 没找到
}
二分答案#
当问题具有单调性(可行/不可行以某个答案为分界)时,可以对”答案”本身二分,用 check(mid) 判断当前答案是否可行。这是”二分算法”题单的核心(二分答案/最小化最大值/最大化最小值/第 K 小)。
模板:最大化最小值(如分割数组的最大值最小、分配糖果):
// check(mid) 判断"答案至少为 mid"是否可行,具有单调性
bool check(int mid) { /* 贪心/模拟验证 */ }
int maxMin(vector<int>& nums, int lo, int hi) {
int ans = lo;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (check(mid)) { // mid 可行,尝试更大的答案
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
}
boolean check(int mid) { /* 贪心/模拟验证 */ }
int maxMin(int lo, int hi) {
int ans = lo;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (check(mid)) { // mid 可行,尝试更大的答案
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
}
模板:最小化最大值(如机器人能否在时限内完成、划分 k 段的最大段和):
// check(mid) 判断"最大值不超过 mid"是否可行
bool check(int mid) { /* 贪心分段验证 */ }
int minMax(vector<int>& nums, int lo, int hi) {
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (check(mid))
hi = mid; // 可行则收窄上界
else
lo = mid + 1;
}
return lo;
}
boolean check(int mid) { /* 贪心分段验证 */ }
int minMax(int lo, int hi) {
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (check(mid)) hi = mid; // 可行则收窄上界
else lo = mid + 1;
}
return lo;
}
Note
边界选取:
lo/hi取答案的上下界(如数组最大值、总和);无法确定时用0与”足够大”(如1e9);判断单调性:
check(mid)必须随 mid 单调变化(mid 越大越容易/越难满足),否则不能二分;典型应用:
LC410 分割数组的最大值、LC875 爱吃香蕉的珂珂、LC1482 制作 m 束花所需的最少天数;“第 K 小”类问题(如
LC668 乘法表中第 k 小的数)同样二分答案,统计 <= mid 的个数与 k 比较。
KMP 算法#
// next[] = 构建最长公共前后缀长度数组
void getNext(vector<int>& next, string pat) {
// 初始化 next 数组的第一个元素为 0
int j = 0;
next[0] = 0;
for (int i = 1; i < pat.size(); i++) {
// 当前字符不匹配时,回退 j 到 next[j-1] 的位置
while (j > 0 && pat[i] != pat[j]) {
j = next[j - 1];
}
// 当前字符匹配时,则 j 自增
if (pat[i] == pat[j]) {
j++;
}
// next[i] 表示当前位置与模式串匹配的起始位置
next[i] = j;
}
}
// 返回 pat 匹配 txt 的起始位置,若匹配失败则返回 -1
int kmp(string pat, string txt) {
int n = txt.size();
int m = pat.size();
vector<int> next(m);
getNext(next, pat);
int j = 0;
for (int i = 0; i < n; i++) {
// 当当前字符不匹配且 j 大于 0 时,调整 j 的位置
while (j > 0 && txt[i] != pat[j]) {
j = next[j - 1];
}
// 当前字符匹配时,j 自增
if (txt[i] == pat[j]) {
j++;
}
// 匹配成功,返回匹配的起始位置
if (j == m) {
return i - m + 1;
}
}
// 匹配失败,返回 -1
return -1;
}
// next 数组记录最长相等的前后缀长度
void getNext(int[] next, String pat) {
next[0] = 0;
int j = 0; // 失配后的回退点
// 循环从 1 开始,不是 0
for (int i = 1; i < pat.length(); i++) {
char chi = pat.charAt(i);
char chj = pat.charAt(j);
while (j > 0 && chi != chj)
j = next[j - 1]; // 回退
if (chi == chj)
j++;
next[i] = j;
}
}
int strStr(String txt, String pat) {
if (pat.length() == 0)
return 0;
int[] next = new int[pat.length()];
getNext(next, pat);
int j = 0;
for (int i = 0; i < txt.length(); i++) {
chi = txt.charAt(i);
chj = pat.charAt(j);
while (j > 0 && chi != chj)
j = next[j - 1];
if (chi == chj)
j++;
if (j == pat.length())
return i - pat.length() + 1;
}
return -1;
}