快排、歸併、二分、高精度、字首和差分、雙指標思路及程式碼
基礎演算法
目錄快速排序
思路
第一步:確定分界點,對於給定的無序陣列,先界定一箇中點值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題