快排、归并、二分、高精度、前缀和差分、双指针思路及代码
基础算法
快速排序
思路
第一步:确定分界点,对于给定的无序数组,先界定一个中点值pivot
。(注意:该值不一定是下标为中点的值,可以是任何数,一般来说取第一个、最后一个或者中间值)然后利用双指针i
和j
,左边一个右边一个同时往里走。
第二步:划分区间,对于左指针i
,每走一步判断该下标的值是不是大于pivot
,如果i
的值大于pivot
,则停下。如果不是,则i ++
。对于右指针j
,每走一步判断该下标的值是不是大于pivot
,如果j
的值小于pivot
,则停下。如果不是,则j --
。当两个指针都停下时,说明找到了逆序的值,此时判断i
是不是小于j
,如果是,则交换这两个值。
第三步:递归排序,该循环一直走到两指针相遇。此时对于pivot
来说,左边是小于等于它,右边是大于等于它的值,但区间内不是排好序的。接下来递归处理左区间和右区间。
代码
void quick_sort(int q[], int l, int r) // l是排序的区间左边界,r是右边界
{
if (l >= r) return; // 递归结束条件,如果区间只有一个数,则不用排序,直接结束
int x = l + r >> 1, i = l - 1, j = r + 1;
// 取pivot中点和双指针
while (i < j)
{
do i ++; while (q[i] < q[x]);
do j --; while (q[j] > q[x]);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}
传入 i 和 j 互换的问题
结束循环后,i
和j
相等,此时递归排序左边和右边,对于传入的参数j
的问题。
如果x的边界是q【i】以及左边递归(l, i - 1)右边递归 (i , r)时,x的边界是q【l】以及左边递归(l, i - 1)右边递归 (i , r)时,当传入【0,1】时 左边空集,右边死循环。同理,边界是q【r】时,也会出错 左边(l,j)右边(j + 1,r)
归并排序
思路
第一步:确定分界点 ( l + r )/ 2
第二步:递归排序左边和右边
第三步:归并,两个有序的数组合并成一个有序的数组
代码:
int tmp[N];
void merge_sort(int q[], int l, int r)
{
if (l >= r) return;
int mid = l + r >> 1;
merge_sort(q, l, mid), merge_sort(q, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
if (q[i] <= q[j]) tmp[k ++] = q[i ++];
else tmp[k ++] = q[j ++];
while (i <= mid) tmp[k ++] = q[i ++];
while (j <= r) tmp[k ++] = q[j ++];
for (i = l, j = 0; i <= r; i ++, j ++) q[i] = tmp[j];
}
整数二分
思路
二分的本质并不是单调性,而是边界。
假设目标值在闭区间[l, r]中, 每次将区间长度缩小一半,当l = r时,我们就找到了目标值。想象 mid是刀,将数组砍成两半
代码1
二分红色部分时,如果满足红色的性质(大于等于mid),则一定是区间 mid ~ r之间,可能恰好等于mid,所以 l = mid区间缩小,如果不满足红色兴之(小于mid),则一定是在 l ~ mid-1 之间,于是 r = mid-1 缩小区间。 【有减就加】
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
代码2
二分绿色部分时,如果满足绿色的性质(小于mid),则在区间 l ~ mid之间, r = mid 更新,否则在 mid+1 ~ r之间,l = mid+1 更新区间
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
ret urn l;
}
浮点数二分
不用考虑边界问题。求 x 的平方根
int main()
{
double x;
cin >> x;
double l = 0, r = x;
while (r - l > 1e-8)
{
double mid = (l + r) / 2;
if (mid * mid >= x) r = mid;
else l = mid; // 没有边界!!
}
return l;
}
高精度加法
大整数存储可以用数组,个位的idx为0
代码
vector<int> add(vector<int> &A, vecotr<int> &B)
{
vector<int> C; // 结果ans
int carry = 0; // 进位
for (int i = 0; i < A.size() || i < B.size(); i ++)
{
if (i < A.size()) t += A[i];
if (i < B.size()) t += B[i]; // 如果A B没有超过,则加上
C.push_back(carry % 10); // 存进数组
carry /= 10;
}
if (carry) C.push_back(i);
return C;
}
vector<int> strToInt(string a) // 字符串的数字转为数组存储
{
vector<int> ans;
for (int i = 0; i < a.size(); i ++) ans.push_back(a[i] - '0');
return ans;
}
高精度减法
存储方式和加法一样,因为有可能计算会包含加减乘除,所以格式保持一致减少其他情况。
计算减法时,如果A位大于等于B,则直接减,如果A < B,则向前借一位再减。记得每次减去进位 carry(下图中的 t )
代码始终保证A大于B,如果A小于B,则计算B - A,然后加上负号、
代码
vector<int> sub(vector<int> &A, vecotr<int> &B)
{
if (cmp(A, B))
{
vector<int> C;
for (int i = 0, carry = 0; i < A.size(); i ++)
{
carry = A[i] - caryy;
if (i < B.size()) carry -= B[i]; // 判断是否越界,比如B已经空了
// 这里把两种情况合在一起考虑,如果A-B大于等于0,则结果就是t,如果小于0,结果需要加上10
// 结果是正数时,t加10再模10,结果不变,如果是正数的话,则恰好是需要的答案,存入数组即可
C.push_back((caryy + 10) % 10); // 此时存入数组中的数并没有改变carry的值
if (caryy < 0) carry = 1;
else carry = 0;
}
while (C.size() > 1 && C.back() == 0) C.pop_back(); // 去掉前导0
return C;
}
else return -sub(B, A);
}
// 判断A是否大于B
bool cmp(vector<int> &A, vecotr<int> &B)
{
if (A.size() != B.size()) return A.size() > B.size(); // 先判断位数是否相等
for (int i = A.size() - 1; i >= 0; i --)
if (A[i] != B[i]) // 从高位往低位判断,不相等则直接返回最高位的大小
return A[i] > B[i];
return true;
}
高精度乘法
一般高精度乘法是一个比较大的数(位数超过106)乘上一个比较小的数(数值不超过106)
计算时,大数A的每位乘上B再加上进位carry模上10则是答案上的每位
代码
vector<int> mul(vector<int> &A, int b)
{
vector<int> C;
int carry = 0;
for (int i = 0; i < A.size(); i ++)
{
if (i < A.size()) carry += A[i] * b;
C.push_back(carry % 10);
carry /= 10;
}
}
高精度除法
除法有前导0的可能,需要特殊处理
代码
// A除B,商是C,余数是r
vector<int> div(vector<int> &A, int b)
{
vector<int> C;
int r = 0;
for (int i = A.size() - 1; i >= 0; i --)
{
r = r * 10 + A[i]; // 数字是逆序存储
C.push_back(r / b); // 如果不够除,会存入0,所以可能有前导0存在
r %= b;
}
reverse(C.begin(), C.end()); // 存入数组都是正序,所以翻转一下,配合加减乘的存储方式
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
前缀和
什么是前缀和?给定一个数组 a1 a2 a3 ... an ,前缀和数组表示 Si = a1 + a2 + a3 + ... + ai
一维前缀和思路
注意:下标从1开始,避免S[i - 1] 的数组越界
前缀和作用:快速求出原数组中一段数的和, 求 a[l] ~ a[r]的和,等于 S[r] - S[l -1] 。如果使用循环,时间复杂度是$O(n)$的,而前缀和数组是$O(1)$的
具体公式:使用for循环,S[i] = S[i - 1] + a[i]
,S[0] = 0
二维前缀和思路
S[i] [j]表示以a[i] [j]为点所有左上角的数之和。
如何求S[i][j]
? 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[x2][y1 - 1] - S[x1 - 1][y2] + S[x1 - 1][y1 - 1]
差分
什么是差分?差分就是前缀和的逆运算
给定一个数组 a1 a2 a3 ... an ,构造另外一个数组b1 b2 b3 ... bn,使得 ai = b1 + b2 + b3 +...+ bi,a数组是b数组的前缀和,b数组是a数组的差分数组
构造步骤:b1 = a1 ; b2 = a2 - a1;b3 = a3 - a2; bn = an - an-1
用处:如果有b数组,可以O(n)
的时间得到a数组,b数组求前缀和就是a数组
场景:给定一个数组A,在区间[l, r]
之间的数都加上c
,则可以通过其差分数组B来完成。具体如下:
- al = b1 + b2 + b3 +...+ bl, 如果给bl + c,则变成al = b1 + b2 + b3 +...+ bl + c,相当于al加上了c
- al+1 = b1 + b2 + b3 +...+ bl + c + bl+1,al+1也加上了c
- 说明给如果给bl加上c,则al之后的所有数都会加上c。ar 以及 ar+1 之后的数也会加上c,解决办法是br+1 减去c
原理是a数组是b数组的前缀和,把b[l]加上c,a求每一个数都会自动加上c
二维差分
和一维类似
b[x1][y1] += c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y1] -= c;
b[x2 + 1][y2 + 1] += c;
双指针
双指针就是通过两个变量来遍历,代替两层循环。比如快排属于双指针。
核心思想:将O(n^2)
的算法优化到O(n)
具体模板:
for (int i = 0, j = 0; i < n; i ++)
{
// 当j小于i,并且i和j满足某种性质,则j++
while (j < i && check(i, j)) j ++;
// 每道题目具体逻辑
}
离散化
背景条件:数值比较大的数,值域在 0 ~ 109 之间, 但是数的个数比较少,操作的时候有可能需要用下标来操作。把这些数映射成0至n-1的自然数就叫做离散化。
代码
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 去重
// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 映射到 1, 2, 3, ... n
}
unique函数实现
vector<int>::interator unique(vector<int> &a)
{
int j = 0;
for (int i = 0; i < a.size(); i ++)
if (!i || a[i] != a[i - 1])
a[j ++] = a[i];
return a.begin() + j;
}
区间合并
LeetCode Hot 100的第26题