快排、归并、二分、高精度、前缀和差分、双指针思路及代码

基础算法

快速排序

思路

第一步:确定分界点,对于给定的无序数组,先界定一个中点值pivot。(注意:该值不一定是下标为中点的值,可以是任何数,一般来说取第一个、最后一个或者中间值)然后利用双指针ij,左边一个右边一个同时往里走。

第二步:划分区间,对于左指针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 互换的问题

结束循环后,ij相等,此时递归排序左边和右边,对于传入的参数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是刀,将数组砍成两半

image-20211102144324654

代码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,然后加上负号、

image-20211103151050637

代码

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则是答案上的每位

image-20211103154924065

代码

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]

image-20211105105023541

差分

什么是差分?差分就是前缀和的逆运算

给定一个数组 a1 a2 a3 ... an ,构造另外一个数组b1 b2 b3 ... bn,使得 ai = b1 + b2 + b3 +...+ bia数组是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来完成。具体如下:

  1. al = b1 + b2 + b3 +...+ bl, 如果给bl + c,则变成al = b1 + b2 + b3 +...+ bl + c,相当于al加上了c
  2. al+1 = b1 + b2 + b3 +...+ bl + c + bl+1,al+1也加上了c
  3. 说明给如果给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的自然数就叫做离散化。

image-20211109111650882

代码

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题

posted @ 2022-05-08 13:36  FailBetter  阅读(153)  评论(0编辑  收藏  举报