常用算法模板 Java版——基础算法:排序、二分、高精度、前缀和与差分、位运算、双指针、离散化、区间合并
文章目录
1 排序
Arrays.sort(arr [, fromIndex, toIndex])
Arrays.sort(arr, comparator)
Collections.sort(list)
Collections.sort(list, comparator)
1.1 直接插入排序
public class InsertSort {
// 插入排序
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
for (int j = i; j >= 1 && arr[j] > arr[j - 1]; j--) {
swap(arr, j, j - 1);
}
}
}
// 交换数组中两个元素
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
1.2 快速排序
- 确定枢轴:通常从
arr[l]
、arr[l + r >> 1]
、arr[r]
之中任选一个 - 划分子区间:双指针
i
、j
初始位于待排区间两侧外,先i
后j
相向而行,最终使得左右子区间arr[l ... j]
、arr[j+1 ... r]
左小右大 - 递归排序左右子区间(该写法左子区间右端点必须为
j
)
public class QuickSort {
// 快速排序 arr[l ... r]
public static void quickSort(int[] arr, int l, int r) {
if (l >= r) return;
int x = arr[l + (r - l) / 2]; // 枢轴(选择中间元素)
int i = l - 1, j = r + 1; // 双指针初始位于两侧外(追加1偏移量)
while (i < j) {
do {
i++;
} while (arr[i] < x);
do {
j--;
} while (arr[j] > x);
if (i < j) {
swap(arr, i, j); // 交换元素
}
}
quickSort(arr, l, j); // 递归排序左子区间
quickSort(arr, j + 1, r); // 递归排序右子区间
}
// 交换数组中两个元素的方法
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
1.3 归并排序
- 确定分界点:
mid = l + r >> 1
- 递归排序左右子区间
- 归并左右子区间为有序子区间:挑出两者较小值,相等则优先归并
arr[i]
,使得排序稳定
public class MergeSort {
// 归并排序 arr[l ... r]
public 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[] temp = new int[r - l + 1]; // 辅助数组
int i = l, j = mid + 1, k = 0; // 初始化指针
// 归并左右子区间为有序子区间
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];
}
}
}
2 二分
2.1 整数二分
- 中点将区间划分出左右两子区间
- 判断中间点是否满足某侧区间的性质
check(mid)
,查找x边界,目标在x区间,检测x区间性质。易知该种写法条件检测始终为"≥"或"≤",对应下文check_ge()
(greater_equal)、check_le()
(less_equal),对比目标和中点的位置关系即可得出条件检测函数。 - 返回所检测的x区间的端点x
当查找右边界时中点应为
l + r + 1 >> 1
,简记:有(“右”) 加必有(“右”) 减
// 查找左边界,即第一个满足条件的元素下标 (lower_bound)
public int binarySearchL(int l, int r) {
while (l < r) {
int mid = l + r >> 1; // 计算中间值
if (check_ge(mid, target)) {
r = mid; // 如果中间的值符合条件,则继续在左边查找
} else {
l = mid + 1; // 否则在右边查找
}
}
return l; // 返回左边界
}
// 查找右边界,即最后一个满足条件的元素下标 (upper_bound的前驱)
public int binarySearchR(int l, int r) {
while (l < r) {
int mid = l + r + 1 >> 1; // 计算中间值,向右偏移
if (check_le(mid, target)) {
l = mid; // 如果中间的值符合条件,则继续在右边查找
} else {
r = mid - 1; // 否则在左边查找
}
}
return r; // 返回右边界
}
2.2 浮点数二分
类似整数二分的查找左边界,常写作f(mid) >= target
的形式。解唯一,无需处理边界。要注意浮点精度问题。
public double binarySearchF(double l, double r) {
final double eps = 1e-8; // 精度,视题目而定
while (r - l > eps) {
double mid = (l + r) / 2; // 计算中间值
if (check_ge(mid, target)) {
r = mid; // 目标在左边,更新右边界
} else {
l = mid; // 否则更新左边界
}
}
return l; // 返回左边界,即为目标值的估计
}
3 高精度运算
Java内置大数类:BigInteger、BigDecimal
4 前缀和、差分
以下前缀和与差分数组必须从下标1开始存储
4.1 一维前缀和
对于数列 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an ,规定 a i a_{i} ai 的前缀和为前 i i i 个数的和: S i = a 1 + a 2 + . . . + a i ( i ≥ 1 ) S_i=a_1+a_2+...+a_i\ (i≥1) Si=a1+a2+...+ai (i≥1)
求法: S 0 = 0 , S i = S i − 1 + a i ( i ≥ 1 ) S_0=0,\ S_i=S_{i-1}+a_i\ (i≥1) S0=0, Si=Si−1+ai (i≥1)
应用:求下标区间 [ l , r ] [l,\ r] [l, r]上的片段和 S r − S l − 1 S_r-S_{l-1} Sr−Sl−1
/* int a[1 ... n], s[1 ... n] */
/* 初始化前缀和数组 */
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1] + a[i];
}
/* 求下标区间[l, r]上的片段和 */
int sum = s[r] - s[l - 1]; // sum = a[l] + ... + a[r]
4.2 二维前缀和
对于矩阵 ( a i j ) n × m (a_{ij})_{n\times m} (aij)n×m ,规定 a i j a_{ij} aij 的二维前缀和 S i j S_{ij} Sij 为元素 a i j a_{ij} aij 左上角所有元素的和。
求法: S 0 j = S i 0 = S 00 = 0 , S i j = S i − 1 , j + S i , j − 1 − S i − 1 , j − 1 + a i j ( i , j ≥ 1 ) S_{0j}=S_{i0}=S_{00}=0,\ S_{ij}=S_{i-1,j}+S_{i,j-1}-S_{i-1,j-1} + a_{ij}\ (i,j≥1) S0j=Si0=S00=0, Sij=Si−1,j+Si,j−1−Si−1,j−1+aij (i,j≥1)
应用:求下图以 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) 为左上角、 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 为右下角的子矩阵(含边界)上的片段和,只需将整块左上矩形面积减去红、绿区域(不含待求区域边界)面积再补上多减去的重叠区域面积,即 S = S x 2 y 2 − S x 2 , y 1 − 1 − S x 1 − 1 , y 2 + S x 1 − 1 , y 1 − 1 S=S_{x_2y_2}-S_{x_2,y_1-1}-S_{x_1-1,y_2}+S_{x_1-1,y_1-1} S=Sx2y2−Sx2,y1−1−Sx1−1,y2+Sx1−1,y1−1
/* int a[1 ... n][1 ... m], s[1 ... n][1 ... m] */
/* 初始化前缀和数组 */
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
}
}
/* 求以(x1, y1)为左上角、(x2, y2)为右下角的子矩阵(含边界)上的片段和 */
int sum = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];
4.3 一维差分
由数组 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an 构造差分数组 b 1 , b 2 , . . . , b n b_1, b_2, ..., b_n b1,b2,...,bn ,使得 a i = b 1 + b 2 + . . . + b i a_i=b_1+b_2+...+b_i ai=b1+b2+...+bi , b i = a i − a i − 1 b_i=a_i-a_{i-1} bi=ai−ai−1
操作:给区间 [ l , r ] [l,\ r] [l, r]上所有数加上 C C C ,时间复杂度 O ( 1 ) O(1) O(1)
- 给 b l b_l bl 加上 C C C,使得 a l , a l + 1 . . . , a n a_l,a_{l+1}...,a_n al,al+1...,an 均加上 C C C
- 给 b r + 1 b_{r+1} br+1 减去 C C C,使得 a r + 1 , a r + 2 , . . . , a n a_{r+1},a_{r+2},...,a_n ar+1,ar+2,...,an 均减去本不应加的 C C C
对于原差分数组的初始化亦可采用上述操作,赋值 a i a_i ai 即相当于给区间 [ i , i ] [i,\ i] [i, i] 加上 a i a_i ai
/* int a[1 ... n], s[1 ... n] */
/* 给区间[l, r]上所有数加上c */
public void insert(int l, int r, int c) {
b[l] += c;
b[r + 1] -= c;
}
/* 初始化差分数组 */
for (int i = 1; i <= n; i++) {
insert(i, i, a[i]);
}
/* 将操作过的差分数组变为原数组(前缀和与差分互为逆运算) */
for (int i = 1; i <= n; i++) {
b[i] += b[i - 1];
}
4.4 二维差分
参考一维差分与二维前缀和,差分矩阵中每个数都蕴含于其右下矩阵中的每个数。
操作:给下图以 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) 为左上角、 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 为右下角的子矩形(含边界)加上 C C C,只需给整个右下角加 C C C,给红、绿区域各减 C C C,最后再给重叠区域加上多减的 C C C 即可
对于原差分矩阵初始化操作亦可采用上述操作,参考一维差分
/* int a[1 ... n][1 ... m], s[1 ... n][1 ... m] */
/* 给以(x1, y1)为左上角、(x2, y2)为右下角的子矩阵(含边界)加上c */
public void insert(int x1, int y1, int x2, int y2, int c) {
b[x1][y1] += c;
b[x2 + 1][y1] -= c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y2 + 1] += c;
}
/* 初始化差分矩阵 */
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
insert(i, j, i, j, a[i][j]);
}
}
/* 将操作过的差分矩阵变为原矩阵:求差分矩阵的前缀和 */
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
}
}
5 位运算
- 求
n
n
n 的二进制表示中第
k
k
k 位数字:
n >> k & 1
(先把第 k k k 位数字移到最后一位,再看个位是几,即和 1 1 1 做按位与运算) lowbit(n)
:返回 n n n 的最后一位 1 1 1
public int lowbit(int x) {
return x & -x; // -n = ~n + 1
}
/* 应用 */
// 输出整数x的二进制表示(31位)
for (int i = 0; i < 31; i++) {
System.out.print(x >> i & 1);
}
// 统计x的二进制表示中有几位1
int cnt = 0;
while (x) {
x -= lowbit(x);
cnt++;
}
6 双指针算法
常见的双指针问题:
- 对于一个序列,用两个指针维护一段区间
- 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
/* 朴素算法 O(n^2) */
// for (int i = 0; i < n; i++) {
// for (int j = i; j < n; j++) {
// ...
// }
// }
/* 双指针优化后的算法 O(n) */
/* 例1 */
for (int i = 0, j = 0; i < n; i++) { // i为子序列右端点,j为左端动点
while (j < i && check(i, j)) {
...
j++;
}
...
}
/* 例2 */
for (int i = 0; i < n;) { // i为子序列左端点,j为动态右端动点
int j = i;
while (j < n && check(i, j)) {
...
j++;
}
...
i = j + 1; // 将i直接移至j附近
}
7 离散化
高度分散的整数 → 0 , 1 , 2 , . . . , n − 1 \rightarrow 0, 1, 2, ..., n-1 →0,1,2,...,n−1 或 1 , 2 , . . . , n 1, 2, ..., n 1,2,...,n
List<Integer> alls = new ArrayList<>(); // 存储所有待离散化的值
/* 离散化(保序) */
Collections.sort(alls); // 将所有值排序
alls = new ArrayList<>(new HashSet<>(alls)); // 去重
/* 根据离散化的值k获取原来的值 x */
int x = alls.get(k);
/* 二分求出x对应的离散化的值 */
int find(int x) {
int l = 0, r = alls.size() - 1;
while (l < r) { // 找到第一个大于等于x的位置(唯一)
int mid = l + r >> 1;
if (alls.get(mid) >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 这里+1是为了映射到1, 2, ..., alls.size()
}
8 区间合并
- 先将所有区间按左端点大小排序
- 当前维护区间与下一区间之间分三种情况:包含、有交集(含端点)、无交集
- 包含:无需操作(实为有交集的特殊情况)
- 有交集:更新当前区间右端点为较大的即可,继续维护
- 无交集:结束维护当前区间并保存,更新为下一区间
- 迭代结束后保存当前维护区间
// 使用 List<int[]> 存储区间,int[0]表示左端点,int[1]表示右端点
/* 合并区间 */
List<int[]> merge(List<int[]> segs) {
List<int[]> res = new ArrayList<>();
segs.sort(Comparator.comparingInt(a -> a[0])); // 按左端点大小排序
int st = Integer.MIN_VALUE, ed = Integer.MIN_VALUE; // 当前维护区间(初始化为负无穷)
for (int[] seg : segs) {
if (ed < seg[0]) { // 若与当前维护区间无交集
if (st != Integer.MIN_VALUE) {
res.add(new int[]{st, ed}); // 当前区间结束维护并保存
}
st = seg[0]; // 转移至此区间
ed = seg[1];
} else {
ed = Math.max(ed, seg[1]); // 有交集则比较右端点
}
}
if (st != Integer.MIN_VALUE) {
res.add(new int[]{st, ed}); // 保存最后一个区间
}
return res;
}