第一章:基础算法
排序
快速排序
分治算法
- 确定分界点x(有三种方法,a、直接取左边界q[l],b、取中间值q[$(l +r)/ 2$], c、随机)
- 调整区间,将区间划分为两段,左边所有的数都是小于等于x,右边所有的数大于等于x
- 递归处理左右两段
public void quickSort(int q[], int l, int r){
if (l >= r) return;
//数值x为分界数
//移动策略:左右两个指针都是先分别往右、往左移动,再进行比较,所以这里给i和j的初始赋值是l-1,和 r+1
int i = l - 1, j = r + 1, x = q[l + r >> 1];
//while循环的目的在于使得下标i左边所有的数都是小于x的,j下标右边所有的数都是大于x的
while (i < j){
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) {
//交换去q[i] 与 q[j]
q[i] = q[i] ^ q[j];
q[j] = q[i] ^ q[j];
q[i] = q[i] ^ q[j];
}
}
//递归
// 注意:这里j不能换成i
quickSort(q, l, j);
quickSort(q, j + 1, r);
}
归并排序
- 确定分界点:$mid = (l +r) / 2$
- 递归排序
left
和right
,使得left
和right
有序 - 归并,将
left
和right
两个有序的数列合并成一个有序的数组
/**
* 归并排序代码模板
* @param q 待排序的模板
* @param l 起始点下标
* @param r 终点下标
*/
void mergeSort(int q[], int l, int r){
if (l >= r) return;
//中间结点下标
int mid = l + r >> 1;
mergeSort(q, l, mid);
mergeSort(q, mid + 1, r);
int k = 0, i = l, j = mid + 1;
//比较left 和 right 的值,将较小的值放到tmp中
while (i <= mid && j <= r) {
if (q[i] < q[j]) {
tmp[k ++ ] = q[i ++ ];
} else {
tmp[k ++ ] = q[j ++ ];
}
}
//rigth 部分已经遍历完了,但是 left 还剩余部分,则将left 剩下的数放到tmp中
while (i <= mid) {tmp[k ++ ] = q[i ++ ];}
//left 部分已经遍历完了,但是 right 还剩余部分,则将right 剩下的数放到tmp中
while (j <= r) {tmp[k ++ ] = q[j ++ ];}
//将已经排序好的临时变量tmp,复制到数组q中
for (i = l, j = 0; i <= r; i ++, j ++ ) {q[i] = tmp[j];}
}
堆排序
/**
* 堆分两种:
* 1、小根堆
* 此代码思路:
* 先将数组视为完全二叉树,将该完全二叉树转化为小根堆。在小根堆的基础上进行排序
* 但是根据上课老师的思路:
* 实现堆还存在另外一种思路:
* 那就是边遍历数组边新建小根堆
* 2、大根堆
* 思路同上
*/
import java.util.*;
public class Sort {
public static void main(String[] args) {
int[] arr2 = new int[]{3, 2, 3, 1, 2, 4, 5, 5, 6};
heapSort(arr2);
System.out.println(Arrays.toString(arr2));
}
/**
* 堆排序,(大堆根)从小到大排序
*
* @param array
*/
public static void heapSort(int[] array) {
//1、把无序数组构建成最大堆
/*
* 注意:
* for循环中i的值为什么是从(array.length / 2)开始
* 根据堆排序的思想,我们知道要从最后一个非叶子节点开始进行下沉调整,然后向前遍历
* 所以:i = array.length / 2 -1 ,且 i--
* */
for (int i = array.length / 2 - 1; i >= 0; i--) {
downAdjust(array, i, array.length - 1);
}
//2、循环删除堆顶元素,移到集合尾部,调整堆产生新的堆顶
for (int i = array.length - 1; i > 0; i--) {
//最后一个元素和第一个元素进行交换
int temp = array[i];
array[i] = array[0];
array[0] = temp;
//"下沉"调整最大堆
downAdjust(array, 0, i - 1);
}
}
/**
* "下沉"调整
*
* @param array 待调整堆
* @param parentIndex 要下沉的父节点
* @param endIndex 堆的有效大小
*/
public static void downAdjust(int[] array, int parentIndex, int endIndex) {
//temp保存父节点值,用于最后的赋值,
int temp = array[parentIndex];
//根据父节点的位置计算出子节点的位置
//下面计算的是左孩子的位置
int childIndex = 2 * parentIndex + 1;
/*
当childIndex < length 成立时,存在左孩子
如果也存在右孩子,先定位到左右孩子中较大的那一项
*/
while (childIndex <= endIndex) {
if (childIndex + 1 <= endIndex && array[childIndex + 1] > array[childIndex]) { //取当前节点中较大的那个
childIndex++;
}
/*
1、若父节点大于孩子节点,则退出当前循环
2、若父节点小于孩子节点,我们一开始想到的就是将父节点与孩子的值进行交换,
但是,我们应该考虑到该函数的作用在于:将父节点下沉到最小的位置上
如果孩子节点还存在着孩子节点,我们暂且称之为孙子节点,
若孙子节点的值是也是大于父节点,那么我们应当将父节点下沉到孙子节点处,
以此类推,直到父节点大于子节点,或是不存在子节点,才结束
3、最后将父子节点值赋值到子节点
*/
if (temp > array[childIndex]) { //tmp 为父节点
//父节点大于左右子节点
break;
} else {
//父节点小于左右子节点
array[parentIndex] = array[childIndex];
parentIndex = childIndex;
childIndex = 2 * parentIndex + 1;
}
}
array[parentIndex] = temp;
}
}
二分
如果有单调性那么肯定可以二分,如果没单调性,也可以二分。二分的本质是边界,而不是单调性
整数二分
// 检查x是否满足某种性质
//x 为下标
bool check(int x) {
/* ... */
}
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int binarySearch01(int l, int r){
while (l < r)
{
//区别
int mid = l + r >> 1;
if (check(mid)) r = mid; //check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int binarySearch02(int l, int r){
while (l < r){
//区别
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid; //check()判断mid是否满足性质
else r = mid - 1;
}
return l;
}
练习:789. 数的范围
浮点数二分
//检查x是否满足某种性质
bool check(double x) {
/* ... */
}
double binarySearch03(double l, double r){
const double eps = 1e-8; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
前缀和
一维前缀和
//下标一定从1开始
S[0] = 0;//可以帮助处理边界
S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]
二维前缀和
//S[i, j] = 第i行j列格子左上部分所有元素的和
//注意:数组S下标都是从1开始,以避免边界问题
S[i,j] = S[i - 1, j] + S[i, j - 1] - S[i - 1, j - 1] + a[i][j]
//以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
//因为S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] 多减了一个S[x1 - 1, y1 - 1], 所以最后加上了
//S[x1 - 1, y1 - 1]
差分
一维差分
给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c
二维差分
给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
双指针算法
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
//O(n^2)
}
}
双指针算法的核心在于优化暴力搜索,将其时间复杂度O(n^2),转化成O(n)
//i,j均为下标
for (int i = 0, j = 0; i < n; i ++ ){
//check(i,j) 检查i,j之间存在的某种关系是否成立
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
//常见问题分类:
// (1) 对于一个序列,用两个指针维护一段区间
// (2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
位运算
求n的二进制数的第k位数字: n >> k & 1
返回n的最后一位1(最右边的1)的二进制数:lowbit(n) = n & -n == n & (~n + 1)
举例:
x = 10100 lowbit(x) = 100
x = 10010 lowbit(x) = 10
离散化
ArrayList alls = new ArrayList<>(); // 存储所有待离散化的值
Collections.sort(all); // 将所有值排序,从小到大
alls.subList(0, unique(alls)); //提取非重复元素
// 二分求出x对应的离散化的值
int find(int x){ // 找到第一个大于等于x的位置
int l = 0, r = alls.size() - 1;
while (l < r){
int mid = l + r >> 1;
if (alls.get(mid) >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 映射到1, 2, ...n
}
//去重
//将不重复的数放到List的前部分,返回不重复元素的最右边的下标
//注意:此时的list已经被排序过了
static int unique(List list) {
int j = 0;
for (int i = 0; i < list.size(); i++) {
if (i == 0 || list.get(i) != list.get(i - 1)) {
list.set(j, list.get(i));
j++;
}
}
return j;
}
区间合并
// 将所有存在交集的区间合并
void merge(ArrayList segs){
//存储结果
ArrayList res;
//按照Pairs默认的排序方法进行排序
Collections.sort(seg);
//st为起始, ed为结束
//我们维护的临时区间的左右位置
int st = -2e9, ed = -2e9; // -2e9: 2 * 10 ^ 9;
//遍历
for (Pairs seg : segs)
//维护的区间的最右边位置小于seg的最左边的位置,那么则把该区间加入到答案中
if (ed < seg.first){
if (st != -2e9) {
res.add(new Pairs(st, ed));
}
//更新临时区间
st = seg.first, ed = seg.second;
}else {
//存在交集,更新区间最右边
ed = Math.max(ed, seg.second);
}
//防止输入数组里是没有任何区间的
if (st != -2e9) res.add(new Pairs(st, ed));
segs = res;
}
//存储区间的start和end
class Pairs implements Comparable {
int first;
int second;
public Pairs(int first, int second) {
this.first = first;
this.second = second;
}
//添加从小到大排序的方法,当first相同时,则按照second从小到大排序
//那么Collection的排序会按照该方法排序
@Override
public int compareTo(Object o) {
Pairs pairs = (Pairs) o;
if (pairs.first < first)
return 1;
else if (pairs.first == first) {
if (pairs.second < second)
return 1;
else {
return -1;
}
} else {
return -1;
}
}
}