關(guān)于我的倉庫
- 這篇文章是我為面試準(zhǔn)備的學(xué)習(xí)總結(jié)中的一篇
- 我將準(zhǔn)備面試中找到的所有學(xué)習(xí)資料,寫的Demo,寫的博客都放在了這個(gè)倉庫里iOS-Engineer-Interview
- 歡迎star????
- 其中的博客在簡書,CSDN都有發(fā)布
- 博客中提到的相關(guān)的代碼Demo可以在倉庫里相應(yīng)的文件夾里找到
前言
- 該系列為學(xué)習(xí)《數(shù)據(jù)結(jié)構(gòu)與算法之美》的系列學(xué)習(xí)筆記
- 總結(jié)規(guī)律為一周一更,內(nèi)容包括其中的重要知識(shí)帶你,以及課后題的解答
- 算法的學(xué)習(xí)學(xué)與刷題并進(jìn),希望能真正養(yǎng)成解算法題的思維
- LeetCode刷題倉庫:LeetCode-All-In
- 多說無益,你應(yīng)該開始打代碼了
11講排序(上):為什么插入排序比冒泡排序更受歡迎
- 開始進(jìn)入排序章節(jié)了,專注于會(huì),懂,好吧
- 敲之前我回我回,敲不出來我的我的
- GOGOGO
如何比較排序算法
-
執(zhí)行效率
- 最好,最壞,平均時(shí)間復(fù)雜度
- 這樣可以看出對(duì)于有序度比較高/低的測試數(shù)據(jù)效果如何
-
內(nèi)存損耗
- 以空間復(fù)雜度衡量
- 這里顯然原地排序算法比較屌,就是空間復(fù)雜度為O(1)的算法
-
穩(wěn)定性
- 如果待排序的序列中存在值相等的元素,經(jīng)過排序之后,相等元素之間原有的先后順序不變
- 不變就是穩(wěn)定排序算法,變就是穩(wěn)定排序算法
- 這里對(duì)于數(shù)字看起來沒有什么意義,但是如果我們比較的是一個(gè)對(duì)象,就可以在比較A的基礎(chǔ)上,保證B的順序不變
- 比如我們希望實(shí)現(xiàn)按金額排序訂單,對(duì)于金額相同的訂單又希望下單時(shí)間從早到晚有序
- 我們的做法其實(shí)就是先對(duì)下單時(shí)間排序,再對(duì)金額穩(wěn)定排序:
img
冒泡排序(Bubble Sort)
- 冒泡就是對(duì)于相鄰元素做比較,如果順序不對(duì)就進(jìn)行交換
原理
- 一次冒泡的詳細(xì)過程:
- 完成排序就只要進(jìn)行六次這樣的操作:
- 進(jìn)行優(yōu)化就是,假如有一次沒有任何交換,說明已經(jīng)有序,可以終止排序了
代碼
void bubbleSort(vector<int> &arr) {
int arrLen = arr.size();
if (arrLen == 0) {
return ;
}
for (int i = 0; i < arrLen; i++) {
bool flag = false;
for (int j = 0; j < arrLen - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
flag = true;
}
}
if (!flag) {
break;
}
}
return ;
}
特點(diǎn)分析
- 原地排序算法
- 由于我們?cè)O(shè)定了當(dāng)相鄰兩個(gè)元素大小相等的時(shí)候,不做交換,所以冒泡是穩(wěn)定的
- 時(shí)間復(fù)雜度為O(n2)
插入排序(Insertion Sort)
- 插入排序就是將待排序區(qū)間的插入到已排序區(qū)間即可
原理
- 將數(shù)據(jù)區(qū)域分成已排序區(qū)間和未排序區(qū)間,初始已排序區(qū)間只有一個(gè)元素就是第一個(gè)元素
代碼
void insertionSort(vector<int> &arr) {
int arrLen = arr.size();
if (arrLen == 0) {
return ;
}
for (int i = 1; i < arrLen; i++) {
int value = arr[i];
int j = i - 1;
for (; j >= 0; j--) {
if (arr[j] > value) {
arr[j + 1] = arr[j];
} else {
break;
}
}
arr[j + 1] = value;
}
}
特點(diǎn)分析
- 原地排序算法
- 我們可以選擇將后面出現(xiàn)的元素,插入到前面的出現(xiàn)的元素后面【對(duì)于相同的元素】,所以是穩(wěn)定的
- 時(shí)間復(fù)雜度為O(n2)
選擇排序(Selection Sort)
- 選擇排序本質(zhì)就是從未排序區(qū)間中找到最小的元素,放到已排序區(qū)間的末尾
原理
- 將數(shù)據(jù)區(qū)域分成已排序區(qū)間和未排序區(qū)間,初始已排序區(qū)間只有一個(gè)元素就是第一個(gè)元素
代碼
void selectionSort(vector<int> &arr) {
int arrLen = arr.size();
if (arrLen == 0) {
return ;
}
for (int i = 0; i < arrLen; i++) {
int minNum = arr[i];
int minIndex = i;
for (int j = i; j < arrLen; j++) {
if (minNum > arr[j]) {
minNum = arr[j];
minIndex = j;
}
}
swap(arr[i], arr[minIndex]);
}
}
特點(diǎn)分析
- 原地排序算法
- 比如5,8,5,2,9這樣一組數(shù)據(jù),使用選擇排序算法來排序的話,第一次找到最小元素2,與第一個(gè)5交換位置,那第一個(gè)5和中間的5順序就變了,所以就不穩(wěn)定了。正是因此,相對(duì)于冒泡排序和插入排序,選擇排序就稍微遜色了。【不穩(wěn)定】
- 時(shí)間復(fù)雜度為O(n2)
希爾排序(Shell Sort)
- 將需要排序的序列劃分為若干個(gè)較小的序列,對(duì)這些序列進(jìn)行直接插入排序,通過這樣的操作可使需要排序的數(shù)列基本有序,最后再使用一次直接插入排序
原理
- 在希爾排序中首先要解決的是怎樣劃分序列,對(duì)于子序列的構(gòu)成不是簡單地分段,而是采取將相隔某個(gè)增量的數(shù)據(jù)組成一個(gè)序列。一般選擇增量的規(guī)則是:取上一個(gè)增量的一半作為此次子序列劃分的增量,一般初始值元素的總數(shù)量
代碼
void shellSort(vector<int> &arr) {
int arrLen = arr.size();
if (arrLen == 0) {
return ;
}
int d = arrLen / 2;
int x, j, k = 1;
while (d >= 1) {
for (int i = d; i < arrLen; i++) {
x = arr[i];
j = i - d;
// 直接插入排序,會(huì)向前找所適合的位置
while (j >= 0 && arr[j] > x) {
// 交換位置
arr[j + d] = arr[j];
j = j - d;
}
arr[j + d] = x;
}
d = d / 2;
}
}
特點(diǎn)分析
- 原地排序算法
- 不穩(wěn)定
- 時(shí)間復(fù)雜度為O n的3/2次【比log(n)快】
總結(jié)
- 在真正地使用中,我們傾向于使用插入排序,因?yàn)椴簧婕敖粨Q,操作次數(shù)少,雖然它的時(shí)間復(fù)雜度和冒泡一樣,而選擇排序更是弟中弟
課后題:我們今天講的幾種排序算法,都是基于數(shù)組實(shí)現(xiàn)的。如果數(shù)據(jù)存儲(chǔ)在鏈表中,這三種排序算法還能工作嗎?如果能,那相應(yīng)的時(shí)間、空間復(fù)雜度又是多少呢?
- 對(duì)于老師所提課后題,覺得應(yīng)該有個(gè)前提,是否允許修改鏈表的節(jié)點(diǎn)value值,還是只能改變節(jié)點(diǎn)的位置。一般而言,考慮只能改變節(jié)點(diǎn)位置,冒泡排序相比于數(shù)組實(shí)現(xiàn),比較次數(shù)一致,但交換時(shí)操作更復(fù)雜;插入排序,比較次數(shù)一致,不需要再有后移操作,找到位置后可以直接插入,但排序完畢后可能需要倒置鏈表;選擇排序比較次數(shù)一致,交換操作同樣比較麻煩。綜上,時(shí)間復(fù)雜度和空間復(fù)雜度并無明顯變化,若追求極致性能,冒泡排序的時(shí)間復(fù)雜度系數(shù)會(huì)變大,插入排序系數(shù)會(huì)減小,選擇排序無明顯變化。
12講排序(下):如何用快排思想在O(n)內(nèi)查找第K大元素
歸并排序(Merge Sort)
- 如果要排序一個(gè)數(shù)組,我們先把數(shù)組從中間分成前后兩部分,然后對(duì)前后兩部分分別排序,再將排好序的兩部分合并在一起,這樣整個(gè)數(shù)組就都有序了。
原理
- 先看一次分解圖
這個(gè)的關(guān)鍵將在于merge函數(shù),也就是將兩個(gè)已經(jīng)有序的子數(shù)組合并到一起應(yīng)該怎么做
這里其實(shí)就和我們進(jìn)行鏈表的插入一樣,兩個(gè)子數(shù)組同時(shí)遍歷,比較,將小的跟在大的后面,這是這里我們不再是只要進(jìn)行節(jié)點(diǎn)指向就可以解決問題了,而是需要使用輔助數(shù)組,在輔助數(shù)組里進(jìn)行插入,在最后給原數(shù)組進(jìn)行賦值
代碼
void merge(vector<int> &arr, int l, int mid, int r) {
int help[r - l + 1];
int lIndex = l;
int rIndex = mid + 1;
int i = 0;
while (lIndex <= mid && rIndex <= r) {
help[i++] = arr[lIndex] < arr[rIndex] ? arr[lIndex++] : arr[rIndex++];
}
while (lIndex <= mid) {
help[i++] = arr[lIndex++];
}
while (rIndex <= r) {
help[i++] = arr[rIndex++];
}
for (i = 0; i < r - l + 1; i++) {
arr[l + i] = help[i];
}
}
static void mergeSort(vector<int> &arr, int l, int r) {
if (l == r) {
return;
}
int mid = (l + r) / 2;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
void mergeSort(vector<int> &arr) {
int arrLen = arr.size();
if (arrLen == 0) {
return ;
}
mergeSort(arr, 0, arrLen - 1);
}
特點(diǎn)分析
- 非原地算法
- 在merge時(shí),遇到相同的元素,我們可以保證先把前一個(gè)數(shù)組里的數(shù)據(jù)放入,這樣就保證了不會(huì)錯(cuò)位,所以是穩(wěn)定的
- 時(shí)間:O(nlogn) 空間:O(n)
快速排序(Quick Sort)
原理
- 如果要排序數(shù)組中下標(biāo)從p到r之間的一組數(shù)據(jù),我們選擇p到r之間的任意一個(gè)數(shù)據(jù)作為pivot(分區(qū)點(diǎn))
- 我們遍歷p到r之間的數(shù)據(jù),將小于pivot的放到左邊,將大于pivot的放到右邊,將pivot放到中間。經(jīng)過這一步驟之后,數(shù)組p到r之間的數(shù)據(jù)就被分成了三個(gè)部分,前面p到q-1之間都是小于pivot的,中間是pivot,后面的q+1到r之間是大于pivot的
- 這里有個(gè)partition分區(qū)函數(shù),就和上面的merge一樣需要一波理解,我們需要把所有比游標(biāo)小的數(shù)字放在左邊,把比游標(biāo)大的數(shù)字放在右邊,返回游標(biāo)的下標(biāo)
- 具體操作如圖:
代碼
int partition(vector<int> &arr, int p, int r) {
int pivot = arr[r];
int i = p;
for (int j = p; j < r; j++) {
if (arr[j] < pivot) {
swap(arr[i], arr[j]);
i++;
}
}
swap(arr[i], arr[r]);
return i;
}
static void quickSort(vector<int> &arr, int p, int r) {
if (p >= r) {
return;
}
int q = partition(arr, p, r);
quickSort(arr, p, q - 1);
quickSort(arr, q + 1, r);
}
void quickSort(vector<int> &arr) {
int arrLen = arr.size();
if (arrLen == 0) {
return ;
}
quickSort(arr, 0, arrLen - 1);
}
特點(diǎn)分析
- 原地算法
- 不穩(wěn)定算法
- 時(shí)間:O(nlogn)
快速排序與歸并排序的異同
- 兩種排序很大一個(gè)區(qū)別就是歸并排序是從下到上的,快速排序是從上到下的
如何用快排思想在O(n)內(nèi)查找第K大元素
- 這個(gè)問題其實(shí)就是在快排的partition就可以找到,每次選擇區(qū)分點(diǎn)后,把小的放前面,大的放后面
- 這樣我們可以判斷出我們要找的數(shù)字所在區(qū)間是哪一個(gè),對(duì)其繼續(xù)劃分
實(shí)際題目LeetCode 215 數(shù)組中的第K個(gè)最大元素
在未排序的數(shù)組中找到第 k 個(gè)最大的元素。請(qǐng)注意,你需要找的是數(shù)組排序后的第 k 個(gè)最大的元素,而不是第 k 個(gè)不同的元素。
示例 1:
輸入: [3,2,1,5,6,4] 和 k = 2
輸出: 5
示例 2:
輸入: [3,2,3,1,2,4,5,5,6] 和 k = 4
輸出: 4
說明:
你可以假設(shè) k 總是有效的,且 1 ≤ k ≤ 數(shù)組的長度。
題解
class Solution {
public:
int partition(vector<int> &nums, int p, int r) {
if (p == r) {
return p;
}
int pivot = nums[r];
int i = p;
for (int j = p; j < r; j++) {
if (nums[j] > pivot) {
swap(nums[i], nums[j]);
i++;
}
}
swap(nums[i], nums[r]);
return i;
}
int findKthLargest(vector<int>& nums, int k) {
int len = nums.size();
if (len == 0 || len < k) {
return 0;
}
int res = 0;
int p = 0;
int r = len;
while (1) {
int index = partition(nums, p, r - 1);
// res += index;
if ((index + 1) == k) {
return nums[index];
} else if ((index + 1) > k) {
r = index;
} else {
p = index + 1;
}
}
return -99;
}
};
課后題:現(xiàn)在你有10個(gè)接口訪問日志文件,每個(gè)日志文件大小約300MB,每個(gè)文件里的日志都是按照時(shí)間戳從小到大排序的。你希望將這10個(gè)較小的日志文件,合并為1個(gè)日志文件,合并之后的日志仍然按照時(shí)間戳從小到大排列。如果處理上述排序任務(wù)的機(jī)器內(nèi)存只有1GB,你有什么好的解決思路,能“快速”地將這10個(gè)日志文件合并嗎?
- 每次從各個(gè)文件中取一條數(shù)據(jù),在內(nèi)存中根據(jù)數(shù)據(jù)時(shí)間戳構(gòu)建一個(gè)最小堆,然后每次把最小值給寫入新文件,同時(shí)將最小值來自的那個(gè)文件再出來一個(gè)數(shù)據(jù),加入到最小堆中。這個(gè)空間復(fù)雜度為常數(shù),但沒能很好利用1g內(nèi)存,而且磁盤單個(gè)讀取比較慢,所以考慮每次讀取一批數(shù)據(jù),沒了再從磁盤中取,時(shí)間復(fù)雜度還是一樣O(n)。
- 先構(gòu)建十條io流,分別指向十個(gè)文件,每條io流讀取對(duì)應(yīng)文件的第一條數(shù)據(jù),然后比較時(shí)間戳,選擇出時(shí)間戳最小的那條數(shù)據(jù),將其寫入一個(gè)新的文件,然后指向該時(shí)間戳的io流讀取下一行數(shù)據(jù),然后繼續(xù)剛才的操作,比較選出最小的時(shí)間戳數(shù)據(jù),寫入新文件,io流讀取下一行數(shù)據(jù),以此類推,完成文件的合并, 這種處理方式,日志文件有n個(gè)數(shù)據(jù)就要比較n次,每次比較選出一條數(shù)據(jù)來寫入,時(shí)間復(fù)雜度是O(n),空間復(fù)雜度是O(1),幾乎不占用內(nèi)存,這是我想出的認(rèn)為最好的操作了,希望老師指出最佳的做法??!
13講線性排序:如何根據(jù)年齡給100萬用戶數(shù)據(jù)排序
桶排序(Bucket Sort)
- 桶排序是大一進(jìn)來學(xué)的第一個(gè)排序了,后面做了這么多題用到的場景其實(shí)也挺多的,對(duì)桶排序有特殊的情感,是咱們的老朋友
- 核心思想是將要排序的數(shù)據(jù)分到幾個(gè)有序的桶里,每個(gè)桶里的數(shù)據(jù)再單獨(dú)進(jìn)行排序。桶內(nèi)排完序之后,再把每個(gè)桶里的數(shù)據(jù)按照順序依次取出,組成的序列就是有序的了。
- 如果要排序的數(shù)據(jù)有n個(gè),我們把它們均勻地劃分到m個(gè)桶內(nèi),每個(gè)桶里就有k=n/m個(gè)元素。每個(gè)桶內(nèi)部使用快速排序,時(shí)間復(fù)雜度為O(k * logk)。m個(gè)桶排序的時(shí)間復(fù)雜度就是O(m * k * logk),因?yàn)閗=n/m,所以整個(gè)桶排序的時(shí)間復(fù)雜度就是O(n*log(n/m))。當(dāng)桶的個(gè)數(shù)m接近數(shù)據(jù)個(gè)數(shù)n時(shí),log(n/m)就是一個(gè)非常小的常量,這個(gè)時(shí)候桶排序的時(shí)間復(fù)雜度接近O(n)。
適用場景
- 桶排序比較適合用在外部排序中。所謂的外部排序就是數(shù)據(jù)存儲(chǔ)在外部磁盤中,數(shù)據(jù)量比較大,內(nèi)存有限,無法將數(shù)據(jù)全部加載到內(nèi)存中。
- 將所有訂單根據(jù)金額劃分到100個(gè)桶里,第一個(gè)桶我們存儲(chǔ)金額在1元到1000元之內(nèi)的訂單,第二桶存儲(chǔ)金額在1001元到2000元之內(nèi)的訂單,以此類推。每一個(gè)桶對(duì)應(yīng)一個(gè)文件,并且按照金額范圍的大小順序編號(hào)命名(00,01,02…99)。
計(jì)數(shù)排序(Counting Sort)
- 計(jì)數(shù)排序就是之前我理解的桶排序,一個(gè)桶里只存放一個(gè)數(shù)據(jù)的個(gè)數(shù)
- 這里額外講到了怎么根據(jù)桶中的內(nèi)容推算在有序數(shù)組中的位置
- 首先,進(jìn)行順序求和,結(jié)果就是小于等于k的個(gè)數(shù)【OS:這有啥難的呢】
- 之后就是遍歷原數(shù)組和這個(gè)C數(shù)組來復(fù)原這個(gè)排序后的序列【這有啥用咧】
- 我們從后到前依次掃描數(shù)組A。比如,當(dāng)掃描到3時(shí),我們可以從數(shù)組C中取出下標(biāo)為3的值7,也就是說,到目前為止,包括自己在內(nèi),分?jǐn)?shù)小于等于3的考生有7個(gè),也就是說3是數(shù)組R中的第7個(gè)元素(也就是數(shù)組R中下標(biāo)為6的位置)。當(dāng)3放入到數(shù)組R中后,小于等于3的元素就只剩下了6個(gè)了,所以相應(yīng)的C[3]要減1,變成6。
基數(shù)排序(Radix Sort)
- 基數(shù)排序就很簡單,只要是一位一位排序就行
- 就和第一次講排序的時(shí)候提到的穩(wěn)定排序一樣,從后往前,保證穩(wěn)定排序
- 另外對(duì)于長度不齊的可以通過補(bǔ)零來對(duì)齊
- 基數(shù)排序?qū)σ判虻臄?shù)據(jù)是有要求的,需要可以分割出獨(dú)立的“位”來比較,而且位之間有遞進(jìn)的關(guān)系,如果a數(shù)據(jù)的高位比b數(shù)據(jù)大,那剩下的低位就不用比較了。除此之外,每一位的數(shù)據(jù)范圍不能太大,要可以用線性排序算法來排序,否則,基數(shù)排序的時(shí)間復(fù)雜度就無法做到O(n)了
課后題:假設(shè)我們現(xiàn)在需要對(duì)D,a,F(xiàn),B,c,A,z這個(gè)字符串進(jìn)行排序,要求將其中所有小寫字母都排在大寫字母的前面,但小寫字母內(nèi)部和大寫字母內(nèi)部不要求有序。比如經(jīng)過排序之后為a,c,z,D,F(xiàn),B,A,這個(gè)如何來實(shí)現(xiàn)呢?如果字符串中存儲(chǔ)的不僅有大小寫字母,還有數(shù)字。要將小寫字母的放到前面,大寫字母放在最后,數(shù)字放在中間,不用排序算法,又該怎么解決呢?
- 用兩個(gè)指針a、b:a指針從頭開始往后遍歷,遇到大寫字母就停下,b從后往前遍歷,遇到小寫字母就停下,交換a、b指針對(duì)應(yīng)的元素;重復(fù)如上過程,直到a、b指針相交。
- 利用桶排序思想,弄小寫,大寫,數(shù)字三個(gè)桶,遍歷一遍,都放進(jìn)去,然后再從桶中取出來就行了。相當(dāng)于遍歷了兩遍,復(fù)雜度O(n)
14講排序優(yōu)化:如何實(shí)現(xiàn)一個(gè)通用的、高性能的排序函數(shù)
前面講的八種排序算法總結(jié)
- 這里要注意的是雖然歸并排序看起來很爽,但是由于需要重開一塊空間進(jìn)行分區(qū),所以空間復(fù)雜度太高,我們不使用
- 歸并排序可以做到平均情況、最壞情況下的時(shí)間復(fù)雜度都是O(nlogn)
如何優(yōu)化快速排序
- 當(dāng)我們需要排序的序列本身就已經(jīng)是接近有序的時(shí)候,我們的快速排序效率是最低的,因?yàn)槊看芜x擇的分區(qū)點(diǎn)都是最后一個(gè)數(shù)據(jù),這樣時(shí)間復(fù)雜度就會(huì)退化到O(n2)
- 理想的分區(qū)點(diǎn)應(yīng)該是在左右兩個(gè)分區(qū)中,數(shù)據(jù)的數(shù)量都差不多
- 這里介紹兩種分區(qū)算法
三數(shù)取中法
- 選擇第一個(gè),中間一個(gè),最后一個(gè)三個(gè)數(shù)中間那個(gè)作為分區(qū)點(diǎn)
- 如果要排序的數(shù)組比較大,那“三數(shù)取中”可能就不夠了,可能要“五數(shù)取中”或者“十?dāng)?shù)取中”
隨機(jī)法
- 機(jī)法就是每次從要排序的區(qū)間中,隨機(jī)選擇一個(gè)元素作為分區(qū)點(diǎn)。這種方法并不能保證每次分區(qū)點(diǎn)都選的比較好,但是從概率的角度來看,也不大可能會(huì)出現(xiàn)每次分區(qū)點(diǎn)都選的很差的情況,所以平均情況下,這樣選的分區(qū)點(diǎn)是比較好的。時(shí)間復(fù)雜度退化為最糟糕的O(n2)的情況,出現(xiàn)的可能性不大。
Glibc中的qsort()函數(shù)
- ibc是GNU發(fā)布的libc庫,即c運(yùn)行庫。glibc是linux系統(tǒng)中最底層的api,幾乎其它任何運(yùn)行庫都會(huì)依賴于glibc
- 對(duì)于數(shù)據(jù)特別小的,會(huì)使用插入排序;較小的使用歸并排序;特別大的是用快速排序
- 這很重要的一點(diǎn)是因?yàn)槲覀兊臅r(shí)間復(fù)雜度代表的是一種上升趨勢,對(duì)于數(shù)據(jù)在代入不同的值的時(shí)候要區(qū)別思考
課后題:在今天的內(nèi)容中,我分析了C語言的中的qsort()的底層排序算法,你能否分析一下你所熟悉的語言中的排序函數(shù)都是用什么排序算法實(shí)現(xiàn)的呢?都有哪些優(yōu)化技巧?
-
查看了下Arrays.sort的源碼,主要采用TimSort算法, 大致思路是這樣的:
1 元素個(gè)數(shù) < 32, 采用二分查找插入排序(Binary Sort)
2 元素個(gè)數(shù) >= 32, 采用歸并排序,歸并的核心是分區(qū)(Run)
3 找連續(xù)升或降的序列作為分區(qū),分區(qū)最終被調(diào)整為升序后壓入棧
4 如果分區(qū)長度太小,通過二分插入排序擴(kuò)充分區(qū)長度到分區(qū)最小闕值
5 每次壓入棧,都要檢查棧內(nèi)已存在的分區(qū)是否滿足合并條件,滿足則進(jìn)行合并
6 最終棧內(nèi)的分區(qū)被全部合并,得到一個(gè)排序好的數(shù)組Timsort的合并算法非常巧妙:
1 找出左分區(qū)最后一個(gè)元素(最大)及在右分區(qū)的位置
2 找出右分區(qū)第一個(gè)元素(最小)及在左分區(qū)的位置
3 僅對(duì)這兩個(gè)位置之間的元素進(jìn)行合并,之外的元素本身就是有序的
15講二分查找(上):如何用最省內(nèi)存的方式實(shí)現(xiàn)快速查找功能
- 二分查找雖然簡單,但隨著做的題目多了,發(fā)現(xiàn)后續(xù)的變化是真的多,甚至不需要一定使用有序的序列
導(dǎo)入:智力題,猜數(shù)字
- 二分查找針對(duì)的是一個(gè)有序的數(shù)據(jù)集合,查找思想有點(diǎn)類似分治思想。每次都通過跟區(qū)間的中間元素對(duì)比,將待查找的區(qū)間縮小為之前的一半,直到找到要查找的元素,或者區(qū)間被縮小為0
驚人的查找速度
- 我們的時(shí)間復(fù)雜度是O(logn),這是一個(gè)很牛掰的速度,就算我們需要查找的數(shù)據(jù)集大小為2的32次也就大約是42億,也只需要查找32次就能出答案了
- 這里可以參考下阿基米德與國王下棋的故事參考下指數(shù)的強(qiáng)大
這是一個(gè)很著名的故事:阿基米德與國王下棋,國王輸了,國王問阿基米德要什么獎(jiǎng)賞?阿基米德對(duì)國王說:“我只要在棋盤上第一格放一粒米,第二格放二粒,第三格放四粒,第四格放十六?!催@個(gè)方法放滿整個(gè)棋盤就行.”國王以為要不了多少糧食,就隨口答應(yīng)了,結(jié)果國王輸了
最后需要的大米數(shù)量為2的64次方-1
代碼實(shí)現(xiàn)
非遞歸
- 學(xué)到了,使用mid = low+((high-low)>>1)
- 別問為什么,問就是學(xué)了
int BinarySearch(vector<int> &arr, int value) {
int len = arr.size();
int left = 0;
int right = len - 1;
int mid = left + ((right - left) >> 1);
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (arr[mid] == value) {
return mid;
} else if (arr[mid] < value) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return right;
}
- 這樣子返回right,如果查找不到value,那么返回的就是在其中靠左的下標(biāo)
- e.g. arr為{1, 3, 7, 9, 13},value為0返回-1,value為2返回0
遞歸
- 遞歸做法沒得說,學(xué)學(xué)學(xué)
int bsearchInternally(vector<int> &arr, int left, int right, int value) {
if (left > right) {
return -1;
}
int mid = left + ((right - left) >> 1);
if (arr[mid] == value) {
return mid;
} else if (arr[mid] < value) {
return bsearchInternally(arr, mid + 1, right, value);
} else {
return bsearchInternally(arr, left, mid - 1, value);
}
}
- 這個(gè)遞歸應(yīng)該算是比較好理解的那種了
二分的局限性
- 首先,二分查找依賴的是順序表結(jié)構(gòu),簡單點(diǎn)說就是數(shù)組。
- 其次,二分查找針對(duì)的是有序數(shù)據(jù)。
- 再次,數(shù)據(jù)量太小不適合二分查找。
- 最后,數(shù)據(jù)量太大也不適合二分查找。
開篇的思考題:如何在1000萬個(gè)整數(shù)中快速查找某個(gè)整數(shù)?
- 雖然大部分情況下,用二分查找可以解決的問題,用散列表、二叉樹都可以解決。但是,我們后面會(huì)講,不管是散列表還是二叉樹,都會(huì)需要比較多的額外的內(nèi)存空間。如果用散列表或者二叉樹來存儲(chǔ)這1000萬的數(shù)據(jù),用100MB的內(nèi)存肯定是存不下的。而二分查找底層依賴的是數(shù)組,除了數(shù)據(jù)本身之外,不需要額外存儲(chǔ)其他信息,是最省內(nèi)存空間的存儲(chǔ)方式,所以剛好能在限定的內(nèi)存大小下解決這個(gè)問題。
課后題
如何編程實(shí)現(xiàn)“求一個(gè)數(shù)的平方根”?要求精確到小數(shù)點(diǎn)后6位。
- 根據(jù)x的值,判斷求解值y的取值范圍。假設(shè)求解值范圍min < y < max。若0<x<1,則min=x,max=1;若x=1,則y=1;x>1,則min=1,max=x;在確定了求解范圍之后,利用二分法在求解值的范圍中取一個(gè)中間值middle=(min+max)÷2,判斷middle是否是x的平方根?若(middle+0.000001)(middle+0.000001)>x且(middle-0.000001)(middle-0.000001)<x,根據(jù)介值定理,可知middle既是求解值;若middlemiddle > x,表示middle>實(shí)際求解值,max=middle; 若middlemiddle < x,表示middle<實(shí)際求解值,min =middle;之后遞歸求解!
備注:因?yàn)槭潜A?位小數(shù),所以middle上下浮動(dòng)0.000001用于介值定理的判斷
我剛才說了,如果數(shù)據(jù)使用鏈表存儲(chǔ),二分查找的時(shí)間復(fù)雜就會(huì)變得很高,那查找的時(shí)間復(fù)雜度究竟是多少呢?如果你自己推導(dǎo)一下,你就會(huì)深刻地認(rèn)識(shí)到,為何我們會(huì)選擇用數(shù)組而不是鏈表來實(shí)現(xiàn)二分查找了。
-
說說第二題吧,感覺爭議比較大:
假設(shè)鏈表長度為n,二分查找每次都要找到中間點(diǎn)(計(jì)算中忽略奇偶數(shù)差異):
第一次查找中間點(diǎn),需要移動(dòng)指針n/2次;
第二次,需要移動(dòng)指針n/4次;
第三次需要移動(dòng)指針n/8次;
......
以此類推,一直到1次為值總共指針移動(dòng)次數(shù)(查找次數(shù)) = n/2 + n/4 + n/8 + ...+ 1,這顯然是個(gè)等比數(shù)列,根據(jù)等比數(shù)列求和公式:Sum = n - 1.
最后算法時(shí)間復(fù)雜度是:O(n-1),忽略常數(shù),記為O(n),時(shí)間復(fù)雜度和順序查找時(shí)間復(fù)雜度相同
但是稍微思考下,在二分查找的時(shí)候,由于要進(jìn)行多余的運(yùn)算,嚴(yán)格來說,會(huì)比順序查找時(shí)間慢