《數(shù)據(jù)結(jié)構(gòu)與算法之美》11~15筆記

關(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
fb8394a588b12ff6695cfd664afb17cd

如何比較排序算法

  • 執(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ì)過程:
img
  • 完成排序就只要進(jìn)行六次這樣的操作:
img
  • 進(jìn)行優(yōu)化就是,假如有一次沒有任何交換,說明已經(jīng)有序,可以終止排序了
a9783a3b13c11a5e064c5306c261e8e6

代碼

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)
fe107c06da8b290fb78fcce4f6774c0f

插入排序(Insertion Sort)

  • 插入排序就是將待排序區(qū)間的插入到已排序區(qū)間即可

原理

  • 將數(shù)據(jù)區(qū)域分成已排序區(qū)間和未排序區(qū)間,初始已排序區(qū)間只有一個(gè)元素就是第一個(gè)元素
b60f61ec487358ac037bf2b6974d2de1

代碼

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è)元素
32371475a0b08f0db9861d102474181d

代碼

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ù)量
152AFA4194EF2E84767FDCC14520FDCB

代碼

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é)

348604caaf0a1b1d7fee0512822f0e50
  • 在真正地使用中,我們傾向于使用插入排序,因?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ù)組就都有序了。

原理

  • 先看一次分解圖
db7f892d3355ef74da9cd64aa926dc2b
  • 這個(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)行賦值

95897ade4f7ad5d10af057b1d144a22f

代碼

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的
4d892c3a2e08a17f16097d07ea088a81
  • 這里有個(gè)partition分區(qū)函數(shù),就和上面的merge一樣需要一波理解,我們需要把所有比游標(biāo)小的數(shù)字放在左邊,把比游標(biāo)大的數(shù)字放在右邊,返回游標(biāo)的下標(biāo)
  • 具體操作如圖:
086002d67995e4769473b3f50dd96de7

代碼

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)

快速排序與歸并排序的異同

aa03ae570dace416127c9ccf9db8ac05
  • 兩種排序很大一個(gè)區(qū)別就是歸并排序是從下到上的,快速排序是從上到下的

如何用快排思想在O(n)內(nèi)查找第K大元素

  • 這個(gè)問題其實(shí)就是在快排的partition就可以找到,每次選擇區(qū)分點(diǎn)后,把小的放前面,大的放后面
  • 這樣我們可以判斷出我們要找的數(shù)字所在區(qū)間是哪一個(gè),對(duì)其繼續(xù)劃分
898d94fc32e0a795fd65897293b98791

實(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ù)按照順序依次取出,組成的序列就是有序的了。
987564607b864255f81686829503abae
  • 如果要排序的數(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ù)
adc75672ef33fa54b023a040834fcbc9
  • 這里額外講到了怎么根據(jù)桶中的內(nèi)容推算在有序數(shù)組中的位置
  • 首先,進(jìn)行順序求和,結(jié)果就是小于等于k的個(gè)數(shù)【OS:這有啥難的呢】
dd6c62b12b0dc1b3a294af0fa1ce371f
  • 之后就是遍歷原數(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。
1d730cb17249f8e92ef5cab53ae65784

基數(shù)排序(Radix Sort)

  • 基數(shù)排序就很簡單,只要是一位一位排序就行
  • 就和第一次講排序的時(shí)候提到的穩(wěn)定排序一樣,從后往前,保證穩(wěn)定排序
df0cdbb73bd19a2d69a52c54d8b9fc0c
  • 另外對(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é)

1f6ef7e0a5365d6e9d68f0ccc71755fd
  • 這里要注意的是雖然歸并排序看起來很爽,但是由于需要重開一塊空間進(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ù)字

045608AB-D447-49CA-A46B-D186FA6EC54A
8bce81259abf0e9a06f115e22586b829
  • 二分查找針對(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í)間慢

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,951評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,566評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評(píng)論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,683評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,899評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,135評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評(píng)論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,528評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,844評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容