1. 程式人生 > 其它 >快排、歸併、二分、高精度、字首和差分、雙指標思路及程式碼

快排、歸併、二分、高精度、字首和差分、雙指標思路及程式碼

基礎演算法

目錄

快速排序

思路

第一步:確定分界點,對於給定的無序陣列,先界定一箇中點值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是刀,將陣列砍成兩半

程式碼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 +...+ 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的自然數就叫做離散化。

程式碼

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題