《數據結構與算法之美》 學習筆記

02 如何抓住重點,系統高效地學習數據結構與算法

什麼是數據結構?什麼是算法?python

  • 從廣義上講,數據結構就是指一組數據的存儲結構算法就是操做數據的一組方法;
  • 從俠義上講,是指某些著名的數據結構和算法,好比隊列、棧、堆、二分查找、動態規劃等;

數據結構和算法是相輔相成的,數據結構是爲了算法服務的,算法要做用在特定的數據結構之上。所以,咱們沒法孤立數據結構來說算法,也沒法孤立算法來說數據結構。算法

複雜度分析數據庫

  • 用於考量一效率和資源消耗的方法;

經常使用的數據結構和算法編程

  • 數組、鏈表、棧、隊列、散列表、二叉樹、堆、調錶、圖、Trie 樹;
  • 遞歸、排序、二分查找、搜索、哈希算法、貪心算法、分治算法、回溯算法、動態規劃、字符串匹配算法;

事半功倍的學習技巧數組

  • 邊學邊練。適度刷題;
  • 多問、多思考、多互動;
  • 大概升級學習法
  • 知識須要沉澱,不要試圖一會兒掌握全部;

03 & 04 複雜度分析

如何分析、統計算法的執行效率和資源消耗?

爲何須要複雜度分析?緩存

經過實際的代碼運行來統計運行效率的方法叫作是過後統計法,這種方法存在以下以下問題:安全

  • 測試結構很是依賴測試環境;
  • 測試結構受數據規模的影響很大;

因此,咱們須要一個不用具體的測試數據來測試,能夠粗略地估計算法的執行效率的方法,這就是 時間、空間複雜度分析方法bash

大 O 複雜度表示法

公式:T(n) = O(f(n))數據結構

  • n:表示數據規模的大小;
  • T(n):表示代碼執行的時間;
  • f(n):表示每行代碼執行的次數總和;
  • O:表示代碼的執行時間 T(n) 與 f(n) 表達式成正比;

這種複雜度表示方法只是表示一種變化趨勢,當 n 很大時,公式中的低階、常量、係數三部分並不左右增加趨勢,因此能夠忽略。多線程

示例代碼 01

int cal(int n){
    int sum = 0
    int i = 1;
    for(;i<=n;i++){
        sum = sum + i;
    }
}

假設每行代碼執行的時間都同樣,爲 unit_time,那麼上述代碼總的執行時間爲:(2n+2)*unit_time,大 O 表示法爲:T(n) = O(2n+2),當 n 很大時,可記爲 T(n) = O(n)

示例代碼 02

int cal(int n){
    int sum = 0;
    int i = 1;
    int j = 1;
    for(;i<=n;++i){
        j = 1;
        for(;<=n;++j){
            sum = sum + i*j
        }
    }
}

假設每行代碼執行的時間都同樣,爲 unit_time,那麼上述代碼總的執行時間爲:(2n2+2n+3)*unit_time, 大 O 表示法爲:T(n) = O(2n2+2n+3), 當 n 很大時,可記爲 T(n) = O(n2)

時間複雜度分析

漸進時間複雜度

  • 只關注循環執行次數最多的一段代碼;
  • 加法法則:總複雜度等於量級最大的那段代碼的複雜度;(若是 T1(n) = O(f(n)),T2(n) = O(g(n)); 那麼 T(n) = T1(n) + T2(n) = max(O(f(n)),O(g(n))) = O(max(f(n),g(n))))
  • 乘法法則:嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積;(若是 T1(n) = O(f(n)),T2(n) =O(g(n));那麼 T(n) = T1(n) * T2(n) = O(f(n)) * O(g(n)) = O(f(n) * g(n)))

幾種常見時間複雜度實例分析

  • 複雜度量級(按數量級遞增)
  • 常量階 O(1)
  • 指數階 O(2n)
  • 對數階 O(logn)
  • 階乘階 O(n!)
  • 線性階 O(n)
  • 線性對數階 O(nlogn)
  • 平方階 O(n2)
  • 立方階 O(n3)
  • k次方階 O(nk)
  • ......

對於上述羅列的複雜度量級,能夠粗略地分爲兩類:多項式量級和非多項式量級。其中,非多項式量級只有兩個:O(2n) 和 O(n!)。當數據規模 n 愈來愈大時,非多項式量級算法的執行時間會急劇增長,求解問題的執行時間會無線增加。蘇歐陽,非多項式時間複雜度的算法實際上是效率很是低的算法。

空間複雜度分析

漸進空間複雜度

表示算法的存儲空間與數據規模之間的增加關係,常見的空間複雜度以下:

  • O(1)
  • O(n)
  • O(n2)

淺析最好、最壞、平均、均攤時間複雜度

  • 最壞、最好狀況時間複雜度
  • 平均狀況時間複雜度
  • 均攤時間複雜度

05 數組

是一種線性表數據結構,用一組連續的內存空間來存儲一組具備相同類型的數據。

  • 支持隨機訪問;
  • 低效的 插入刪除,平均複雜度爲 O(n);
  • 警戒數組的訪問越界問題;

使用建議:

  • 若是特別關注性能,或者但願使用基本類型,能夠選用數組;
  • 若是數據大小事先已知,而且對數據的操做很是簡單,能夠直接使用數組;
  • 當要表示多維數組時,用數組每每會更加直觀;
  • 對於業務開發,直接使用集合類型就足夠了,省時省力;若是時做一些很是底層的開發,這個時候數組就會優於集合;

爲何在大多數的編程語言中,數組要從 0 開發編號,而不是 1 ?

從數組存儲的內存模型上來看,下標 最確切的定義應該是 偏移(offset),這樣就能確保正確計算出每次隨機訪問的元素對於的內存地址,這樣就好理解了。

06 & 07 鏈表

是一種線性數據結構,用一組非連續的內存空間來存儲一組具備相同類型的數據。

  • 不存儲越界問題;
  • 相比數組,插入和刪除較爲高效;

數組 VS 鏈表 時間複雜度比較:

數組 鏈表
插入、刪除 O(n) O(1)
隨機訪問 O(1) O(n)

常見的鏈表類型:

  • 單鏈表
  • 循環鏈表
  • 雙向鏈表
  • 雙向循環鏈表(以空間換時間)

緩存問題

緩存策略常有以下三種方式:

  • 先進先出策略 FIFO(First In,First Out)
  • 最少使用策略 LFU(Least Frequently Used)
  • 最近最少使用策略 LRU(Least Recently Used)

如何基於鏈表實現 LRU 緩存淘汰算法?

思路:維護一個有序單鏈表,越靠近鏈表尾部的結點是越早以前訪問,當有一個新的數據被訪問時,從鏈表頭開始順序遍歷單鏈表。

  1. 若是此數據以前已經被緩存在鏈表中了,咱們遍歷獲得這個數據對應的結點,並將其從原來的位置刪除,而後再插入到鏈表的頭部。
  2. 若是此數據沒有在緩存鏈表中,又能夠分爲兩種狀況:

    • 若是此時緩存未滿,則將此結點直接擦汗如到鏈表的頭部;
    • 若是此時緩存已滿,則鏈表尾結點刪除,將心的數據結點插入到鏈表頭部。

時間複雜度爲:O(n)

如何輕鬆寫出正確的鏈表代碼?

  • 理解指針或引用的含義
  • 警戒指針丟失和內存泄漏
  • 利用哨兵簡化實現難度
  • 重點留意邊界條件處理
  • 舉例畫圖,輔助思考
  • 多寫多練,沒有捷徑

5 種常見的鏈表操做

  • 單鏈表反轉
  • 鏈表中環的檢測
  • 兩個有序鏈表合併
  • 刪除鏈表倒數第 n 個結點
  • 求鏈表的中間結點

08 棧

當某個數據集合只涉及在一端插入和刪除數據,而且知足後進先出、先進後出的特性,咱們就應該首選 這種數據結構

無論是順序棧仍是鏈式棧,入棧、出棧只涉及棧頂個別數據的操做,全部時間複雜度都是 O(1)。棧是一種操做受限的數據結構,只支持入棧和出棧操做。後進先出是它最大的特色。棧既能夠經過數組實現,也能夠經過鏈表實現。

內存中的堆棧和數據結構中的堆棧不是一個概念,內存中的堆棧是真實存在的物理區,數據結構中的堆棧是抽象出來的數據存儲結構:

內存空間在邏輯上分爲三部分:

  • 代碼區:存儲方法體的二級制代碼。高級調度(做業調度)、中級調度(內存調度)、低級調度(進程調度)控制代碼區執行代碼的卻換;
  • 靜態數據區:存儲全局變量、靜態變量、常量,由系統自動分配和回收;
  • 棧區:存儲運行方法的形參、局部變量、返回值,由系統自動分配和回收;
  • 堆區:new 一個對象的引用或地址存儲在棧區,執行該對象存儲在堆區中的真實數據。

09 隊列

先進者先出

無論是順序隊列仍是鏈式隊列,主要的兩個操做是入隊和出隊,最大特色是先進先出。

幾種高級的隊列結構:

  • 阻塞隊列(生產者-消費者問題);
  • 併發隊列(多線程與原子鎖操做);

## 10 遞歸

遞歸須要知足的三個條件:

  • 一個問題的解能夠分解爲幾個子問題的解;
  • 這個問題與分解以後的子問題,出來數據規模不一樣,求解思路徹底同樣;
  • 存在遞歸終止條件;

如何編寫遞歸代碼?

  • 遞推公式
  • 終止條件

缺點:

  • 堆棧溢出
  • 重複計算
  • 函數調用耗時多
  • 空間複雜度高
  • ......

11&12 排序

常見排序算法:

排序算法 時間複雜度 是否基於比較
冒泡、插入、選擇 O(n2)
快排、歸併 O(nlogn)
桶、計數、基數 O(n)

如何分析一個 「排序算法」?

  • 執行效率
    • 最好、最壞、平均狀況的時間複雜度;
    • 時間複雜度的係數、常數、低階;
    • 比較次數和交換(移動)次數;
  • 內存消耗
  • 穩定性

冒泡排序

冒泡排序只會操做相鄰的兩個數據。每次冒泡操做都會對相鄰的兩個元素進行比較,看是否知足大小關係要求。若是不知足就讓它倆互換。一次冒泡會讓至少一 個元素移動到它應該在的位置,重複n次,就完成了n個數據的排序工做。

示例代碼:

class Solution():
    def bubbleSort(self, lis: list, n: int):
        if n <= 1:
            return
        for i in range(len(lis)):
            flag = False
            for j in range(len(lis)-i-1):
                if lis[j] > lis[j+1]:
                    lis[j], lis[j+1] = lis[j+1], lis[j]
                    flag = True
            if not flag:
                break

arr = [4, 5, 6, 3, 2, 1]
print(arr)
Solution().bubbleSort(arr, len(arr))
print(arr)
  • 冒泡的過程只涉及相鄰數據的交換操做,只須要常量級的臨時空間,因此它的空間複雜度爲O(1),是一個原地排序算法。
  • 在冒泡排序中,只有交換才能夠改變兩個元素的先後順序。爲了保證冒泡排序算法的穩定性,當有相鄰的兩個元素大小相等的時候,咱們不作交換,相同大小的 數據在排序先後不會改變順序,因此冒泡排序是穩定的排序算法。
  • 最好狀況下,要排序的數據已是有序的了,咱們只須要進行一次冒泡操做,就能夠結束了,因此最好狀況時間複雜度是O(n)。而最壞的狀況是,要排序的數據 恰好是倒序排列的,咱們須要進行n次冒泡操做,因此最壞狀況時間複雜度爲O(n2)。

插入排序

插入算法的核心思想是取未排序區間中的元素,在已排序區間中找到合適的插入位置將其插入,並保證已排序區間數據一直有序。重複這個過程,直到未排序區間中元素爲空,算法結束。

示例代碼:

class Solution():
    def insertionSort(self, lis: list, n: int):
        if n <= 1:
            return
        for i in range(1, len(lis)):
            val = lis[i]
            j = i-1
            while j >= 0:
                if lis[j] > val:
                    lis[j+1] = lis[j]
                j -= 1
            lis[j+1] = val


attr = [4, 5, 6, 3, 2, 1]
print(attr)
Solution().insertionSort(attr, len(attr))
print(attr)
  • 從實現過程能夠很明顯地看出,插入排序算法的運行並不須要額外的存儲空間,因此空間複雜度是O(1),也就是說,這是一個原地排序算法。
  • 在插入排序中,對於值相同的元素,咱們能夠選擇將後面出現的元素,插入到前面出現元素的後面,這樣就能夠保持原有的先後順序不變,因此插入排序是穩定 的排序算法。
  • 若是要排序的數據已是有序的,咱們並不須要搬移任何數據。若是咱們從尾到頭在有序數據組裏面查找插入位置,每次只須要比較一個數據就能肯定插入的位 置。因此這種狀況下,最好是時間複雜度爲O(n)。注意,這裏是從尾到頭遍歷已經有序的數據。 若是數組是倒序的,每次插入都至關於在數組的第一個位置插入新的數據,因此須要移動大量的數據,因此最壞狀況時間複雜度爲O(n2)。對於插入排序來講,每次插入操做都至關於在數組中插入一個數據,循環執行 n 次插入操做,因此平均時間複雜度爲O(n2)。

選擇排序

選擇排序算法的實現思路有點相似插入排序,也分已排序區間和未排序區間。可是選擇排序每次會從未排序區間中找到最小的元素,將其放到已排序區間的末 尾。

示例代碼:

class Solution():
    def selectSort(self, lis: list, n: int):
        if n <= 1:
            return
        for i in range(0, len(lis) - 1):
            index = i
            for j in range(i+1, len(lis)):
                if lis[index] > lis[j]:
                    index = j
            lis[i], lis[index] = lis[index], lis[i]


attr = [4, 5, 6, 3, 2, 1]
print(attr)
Solution().selectSort(attr, len(attr))
print(attr)
  • 選擇排序空間複雜度爲O(1),是一種原地排序算法。
  • 選擇排序的最好狀況時間複雜度、最壞狀況和平均狀況時間複雜度都爲O(n2)。
  • 選擇排序每次都要找剩餘未排序元素中的最小值,並和前面的元素 交換位置,這樣破壞了穩定性。是一種不穩定的排序算法。
是否原地排序 是否穩定 最好 最壞 平均
冒泡 O(n) O(n2) O(n2)
插入 O(n) O(n2) O(n2)
選擇 O(n2) O(n2) O(n2)

歸併排序

核心思想:利用分而治之的思想,遞歸解決問題。若是要排序一個數組,咱們先把數組從中間分紅先後兩部分,而後對先後兩部分分別排序,再將排好序的兩部分合並在一 起,這樣整個數組就都有序了。

示例代碼:

class Solution():
    def mergeSort(self, arr):
        print("Splitting ", arr)
        if len(arr) > 1:
            mid = len(arr)//2
            lefthalf = arr[:mid]
            righthalf = arr[mid:]

            self.mergeSort(lefthalf)
            self.mergeSort(righthalf)

            i = 0
            j = 0
            k = 0
            while i < len(lefthalf) and j < len(righthalf):
                if lefthalf[i] < righthalf[j]:
                    arr[k] = lefthalf[i]
                    i = i+1
                else:
                    arr[k] = righthalf[j]
                    j = j+1
                k = k+1

            while i < len(lefthalf):
                arr[k] = lefthalf[i]
                i = i+1
                k = k+1

            while j < len(righthalf):
                arr[k] = righthalf[j]
                j = j+1
                k = k+1
            print("Merging ", arr)


arr = [4, 5, 6, 3, 2, 1]
print(arr)
Solution().mergeSort(arr)
print(arr)

性能分析:

  • 是一個穩定的排序算法。
  • 時間複雜度是O(nlogn)。
  • 空間複雜度是O(n)。

快速排序

快排核心思想就是分治和分區。若是要排序數組中下標從p到r之間的一組數據,咱們選擇p到r之間的任意一個數據做爲pivot(分區點)。 咱們遍歷p到r之間的數據,將小於pivot的放到左邊,將大於pivot的放到右邊,將pivot放到中間。通過這一步驟以後,數組p到r之間的數據就被分紅了三個部分,前 面p到q-1之間都是小於pivot的,中間是pivot,後面的q+1到r之間是大於pivot的。

示例代碼:

class Solution():
    def quickSort(self, arr: list):
        self.quickHelper(arr, 0, len(arr)-1)

    def quickHelper(self, arr: list, first: int, last: int):
        if first < last:
            splitpoint = self.partition(arr, first, last)
            self.quickHelper(arr, first, splitpoint-1)
            self.quickHelper(arr, splitpoint+1, last)

    def partition(self, arr: list, first: int, last: int):
        pivot = arr[first]
        left = first + 1
        right = last

        done = False
        while not done:
            while left <= right and arr[left] <= pivot:
                left = left + 1
            while arr[right] >= pivot and right >= left:
                right = right - 1
            if right < left:
                done = True
            else:
                temp = arr[left]
                arr[left] = arr[right]
                arr[right] = temp
        temp = arr[first]
        arr[first] = arr[right]
        arr[right] = temp

        return right


arr = [4, 5, 6, 3, 2, 1]
print(arr)
Solution().quickSort(arr)
print(arr)

性能分析:

  • 時間複雜度也是O(nlogn)。

可是,公式成立的前提是每次分區操做,咱們選擇的pivot都很合適,正好能將大區間對等地一分爲二。但實際上這種狀況是很難實現的

13 線性排序

桶排序

核心思想是將要排序的數據分到幾個有序的桶裏,每一個桶裏的數據再單獨進行排序。桶內排完序之 後,再把每一個桶裏的數據按照順序依次取出,組成的序列就是有序的了。

桶排序比較適合用在外部排序中。所謂的外部排序就是數據存儲在外部磁盤中,數據量比較大,內存有限,沒法將數據所有加載到內存中。

計數排序

計數排序實際上是桶排序的一種特殊狀況。當要排序的n個數據,所處的範圍並不大的時候,好比最大值是k,咱們就能夠把數據劃分紅k個桶。每一個桶 內的數據值都是相同的,省掉了桶內排序的時間。

示例代碼:

class Solution:
    def countingSort(self, arr: list, n: int):
        if n <= 1:
            return

        mv = arr[0]
        for v in arr:
            if mv < v:
                mv = v

        c = [0 for x in range(mv+1)]

        for i in range(n):
            c[arr[i]] += 1

        for i in range(1, mv+1):
            c[i] = c[i-1] + c[i]

        r = [0 for x in range(n)]
        i = n-1
        while i >= 0:
            index = c[arr[i]] - 1
            r[index] = arr[i]
            c[arr[i]] -= 1
            i -= 1

        for i in range(n):
            arr[i] = r[i]


arr = [4, 5, 6, 3, 2, 1]
print(arr)
Solution().countingSort(arr, len(arr))
print(arr)

計數排序只能用在數據範圍不大的場景中,若是數據範圍 k 比要排序的數據 n 大不少,就不適合用計數排序了。並且,計數排序只能給非負整數排序,若是要排序的數據是其餘類型的,要將其在不改變相對大小的狀況下,轉化爲非負整數。

基數排序

基數排序對要排序的數據是有要求的,須要能夠分割出獨立的「位」來比較,並且位之間有遞進的關係,若是a數據的高位比b數據大,那剩下的低 位就不用比較了。除此以外,每一位的數據範圍不能太大,要能夠用線性排序算法來排序,不然,基數排序的時間複雜度就沒法作到O(n)了。

14 排序優化

時間複雜度 是否穩定排序 是否原地排序
冒泡排序 O(n2)
插入排序 O(n2)
選擇排序 O(n2)
快速排序 O(nlog2)
歸併排序 O(nlog2)
計數排序 O(n+k) k是數據範圍
桶排序 O(n)
基數排序 O(dn) d 是維度

如何優化快速排序?

  • 三數取中法
  • 隨機法

15&16 二分查找

二分查找(Binary Search)算法,也叫折半查找算法。時間複雜度爲 O(longn)

示例代碼:

  • 遞歸實現
class Solution:
    def bsearch(self, arr: list, n: int, val: int):
        return self.bsearchInternally(arr, 0, n-1, val)

    def bsearchInternally(self, arr: list, low: int, high: int, val: int):
        if low > high:
            return -1
        mid = low + ((high-low) >> 1)
        if arr[mid] == val:
            return mid
        elif arr[mid] < val:
            return self.bsearchInternally(arr, mid+1, high, val)
        else:
            return self.bsearchInternally(arr, low, mid-1, val)


arr = [1, 2, 3, 4, 2, 2, 3, 5]
v = Solution().bsearch(arr, len(arr), 4)
print(v)
  • 非遞歸實現
class Solution:
    def bsearch(self, arr: list, n: int, val: int):
        low = 0
        high = n - 1
        while low <= high:
            mid = (low+high) // 2
            if arr[mid] == val:
                return mid
            elif arr[mid] < val:
                low = mid + 1
            else:
                high = mid - 1
        return -1


arr = [1, 2, 3, 4, 2, 2, 3, 5]
v = Solution().bsearch(arr, len(arr), 4)
print(v)

應用場景的侷限性:

  • 二分查找只能用在數據是經過順序表來存儲的數據結構上;
  • 二分查找針對的是有序數據;
  • 數據量過小或太大不適合二分查找;

二分查找的變形問題:

  • 查找第一個值等於給定值的元素

示例代碼:

class Solution:
    def bsearch(self, arr: list, n: int, val: int):
        low = 0
        high = n-1
        while low <= high:
            mid = low + ((high-low) >> 1)
            if arr[mid] > val:
                high = mid - 1
            elif arr[mid] < val:
                low = mid + 1
            else:
                if mid == 0 or arr[mid-1] != val:
                    return mid
                else:
                    high = mid - 1
        return -1


arr = [1, 2, 3, 4, 2, 2, 3, 5]
v = Solution().bsearch(arr, len(arr), 4)
print(v)
  • 查找最後一個值等於給定值的元素

示例代碼:

# 待修改
class Solution:
    def bsearch(self, arr: list, n: int, val: int):
        low, high = 0, n-1
        while low <= high:
            mid = low + ((high-low) >> 1)
            if arr[mid] > val:
                high = mid - 1
            elif arr[mid] < val:
                low = mid + 1
            else:
                if mid == n-1 or arr[mid+1] != val:
                    return mid
                else:
                    low = mid + 1
        return -1


arr = [1, 2, 3, 4, 2, 2, 3, 5]
v = Solution().bsearch(arr, len(arr), 3)
print(v)
  • 查找第一個大於等於給定值的元素

示例代碼:

# 待修改
class Solution:
    def bsearch(self, arr: list, n: int, val: int):
        low, high = 0, n-1
        while low <= high:
            mid = low + ((high-low) >> 1)
            if arr[mid] >= val:
                if mid == 0 or arr[mid - 1] < val:
                    return mid
                else:
                    high = mid-1
            else:
                low = mid + 1
        return -1


arr = [1, 2, 3, 4, 2, 2, 3, 5]
v = Solution().bsearch(arr, len(arr), 3)
print(v)
  • 查找最後一個小於等於給定值的元素

示例代碼:

# 待修改
class Solution:
    def bsearch(self, arr: list, n: int, val: int):
        low, high = 0, n-1
        while low <= high:
            mid = low + ((high-low) >> 1)
            if arr[mid] > val:
                high = mid - 1
            else:
                if mid == n - 1 or arr[mid + 1] > val:
                    return mid
                else:
                    low = mid + 1
        return -1


arr = [1, 2, 3, 4, 2, 2, 3, 5]
v = Solution().bsearch(arr, len(arr), 3)
print(v)

17 跳錶

Redis 的有序集合就是使用跳錶來實現的。

跳錶使用空間換時間的設計思路,經過後見多級索引來提升查詢訂單效率,實現了基於鏈表的 「二分查找」。調錶是一種動態結構,支持快速的插入、刪除、查找操做,時間複雜度都是 O(longn)

跳錶的空間複雜度是 O(n),不過,跳錶的實現很是靈活,能夠經過改變索引構建策略,有效平衡執行效率和內存消耗。雖然跳錶的代碼實現起來並不簡單,可是做爲一種動態結構,比起紅黑樹來講,實現要簡單不少。因此不少時候,咱們爲了代碼的簡單、易讀,比起紅黑樹,咱們更傾向用跳錶。

18&19&20 散列表

Word 文檔中的單詞拼寫檢查功能

散列表是由數組演化而來的,藉助散列函數堆數組進行擴展,利用的是數組支持按照下標隨機訪問元素的特性。

散列衝突的解決方法:

  • 開放尋址法
  • 鏈表法

散列表的查詢效率不能籠統地說成是 O(1),它跟散列函數、裝載因子、散列衝突等都有關係。若是散列函數涉及得很差,或者裝載因子太高,均可能致使散列衝突發生的機率升高,查詢效率降低。

如何設計散列函數?

直接尋址法、平方取中法、摺疊法、隨機數法等

裝載因子過大怎麼辦?

裝載因子閾值的設置要權衡時間、空間複雜度。若是內存空間沒關係,對執行效率要求很高,能夠下降負載因子的閥值;相反,若是內存空間緊張,對執行效率要求又不高,能夠增長負載因子的值,甚至能夠大於 1。

如何避免低效地擴容?

經過均攤的方法,將一次性擴容的代價,均攤到屢次插入操做中,就避免了一次性擴容耗時過多的狀況。這種實現方式,任何狀況下,插入一個數據的時間 複雜度都是O(1)。

工業級散列表分析要素:

  • 初始大小
  • 裝載因子和動態擴容
  • 散列衝突解決方法
  • 散列函數

工業級散列表特徵:

  • 支持快速的查詢、插入、刪除操做;
  • 內存佔用合理,不能浪費過多的內存空間;
  • 性能穩定,極端狀況下,散列表的性能也不會退化到沒法接受的狀況;

工業級散列表設計思路:

  • 設計一個合適的散列函數;
  • 定義裝載因子閾值,而且設計動態擴容策略;
  • 選擇合適的散列衝突解決方法;

21&22 哈希算法

將任意長度的二進制值串映射爲固定長度的二進制值串,這個映射的規則就是哈希算法,而 經過原始數據映射以後獲得的二進制值串就是哈希值。

知足以下幾點要求:

  • 從哈希值不能反向推導出原始數據(因此哈希算法也叫單向哈希算法);
  • 對輸入數據很是敏感,哪怕原始數據只修改了一個Bit,最後獲得的哈希值也大不相同;
  • 散列衝突的機率要很小,對於不一樣的原始數據,哈希值相同的機率很是小;
  • 哈希算法的執行效率要儘可能高效,針對較長的文本,也能快速地計算出哈希值。

應用場景:

  • 安全加密
  • 惟一標識
  • 數據校驗
  • 散列函數
  • 負載均衡
  • 數據切片
  • 分佈式存儲

23&24 二叉樹

想要存儲一棵二叉樹,咱們有兩種方法,一種是基於指針或者引用的二叉鏈式存儲法,一種是基於數組的順序存儲法。

二叉樹的遍歷:

  • 前序遍歷:對於樹中的任意節點來講,先打印這個節點,而後再打印它的左子樹,最後打印它的右子樹。
  • 中序遍歷:對於樹中的任意節點來講,先打印它的左子樹,而後再打印它自己,最後打印它的右子樹。
  • 後序遍歷:對於樹中的任意節點來講,先打印它的左子樹,而後再打印它的右子樹,最後打印這個節點自己。

實際上,二叉樹的前、中、後序遍歷就是一個遞歸的過程。

二叉查找樹

二叉查找樹是二叉樹中最經常使用的一種類型,也叫二叉搜索樹。顧名思義,二叉查找樹是爲了實現快速查找而生的。不過,它不只僅支持快速查找一個數據,還支 持快速插入、刪除一個數據。

二叉查找樹要求,在樹中的任意一個節點,其左子樹中的每一個節點的值,都要小於這個節點的值,而右子樹節點的值都大 於這個節點的值。

25&26 紅黑樹

知足要求:

  • 根節點是黑色的;
  • 每一個葉子結點都是黑色的空節點(NIL),也就是說,葉子節點不存儲數據;
  • 任何相鄰的節點都不能同時爲紅色,也就是說,紅色節點是被黑色節點隔開的;
  • 每一個節點,從該節點到達其可達葉子節點的因此路徑,都包含相同數目的黑色節點;

紅黑樹是一種平衡二叉查找樹,它是爲了解決普通二叉查找樹在數據更新的過程當中,複雜度退化的問題而產生的,紅黑樹的高度近似 log2n,因此它是近似平衡,插入、刪除、查找操做的時間複雜度都是 O(logn)。

由於紅黑樹是一種性能很是穩定的二叉查找樹,因此,在工程中,但凡是用到動態插入、刪除、查找數據的場景,均可以用到它。不過,它實現起來比較複雜,若是本身寫代碼實現,難度會有些高,這個時候,咱們其實更傾向用跳錶來代替它。

27 遞歸樹

  • 實戰一:分析快速排序的時間複雜度
  • 實戰二:分析斐波那契數列的時間複雜度
  • 實戰三:分析全排列的時間複雜度

28&29 堆和堆排序

堆的特色:

  • 是一個徹底二叉樹;
  • 隊中每個節點的值都必須大於等於(或小於等於)其子樹中每一個節點的值;

對於每一個節點值都大於等於子樹中每一個節點值的堆,咱們叫作 「大頂堆」;對於每一個節點的值都小於等於子樹中每一個節點值的堆,咱們叫作 「小頂堆」。

爲何快速排序要比堆排序性能好?

  • 堆排序數據訪問方式沒有快速排序友好;
  • 對於一樣的數據,在排序過程當中,堆排序算法的數據交換次數要多於快速排序;

堆的應用:

  • 優先級隊列
    • 合併有序小文件
    • 高性能定時器
  • 利用堆求 Top K
  • 利用堆求中位數

30&31 圖

非線性數據結構

相關概念:

  • 頂點
  • 度(出度、入度)
  • 有向圖
  • 無向圖
  • 帶權無向圖(權重)

存儲方法:

  • 鄰接矩陣
  • 鄰接表
  • 外部存儲(數據庫等)

鄰接矩陣存儲方法的缺點是比較浪費空間,可是優勢是查詢效率高,並且方便矩陣運算。鄰接表存儲方法中每一個頂點都對應一個鏈表,存儲與其相鏈接的其餘頂 點。儘管鄰接表的存儲方式比較節省存儲空間,但鏈表不方便查找,因此查詢效率沒有鄰接矩陣存儲方式高。針對這個問題,鄰接表還有改進升級版,即將鏈表換成更加高效的動態數據結構,好比平衡二叉查找樹、跳錶、散列表等。

搜索方法:

  • 深度優先搜索(DFS)
  • 廣度優先搜索(BFS)

廣度優先搜索和深度優先搜索是圖上的兩種最經常使用、最基本的搜索算法,比起其餘高級的搜索算法,好比A、IDA等,要簡單粗暴,沒有什麼優化,因此,也被 叫做暴力搜索算法。因此,這兩種搜索算法僅適用於狀態空間不大,也就是說圖不大的搜索。 廣度優先搜索,通俗的理解就是,地毯式層層推動,從起始頂點開始,依次往外遍歷。廣度優先搜索須要藉助隊列來實現,遍歷獲得的路徑就是,起始頂點到終 止頂點的最短路徑。深度優先搜索用的是回溯思想,很是適合用遞歸實現。換種說法,深度優先搜索是藉助棧來實現的。在執行效率方面,深度優先和廣度優先搜索的時間複雜度都是O(E),空間複雜度是O(V)。

32&33&34 字符串

匹配算法

BF 算法

全稱叫 Brute Force 算法,中文叫做暴力匹配算法,也叫樸素匹配算法。

RK 算法

全稱叫 Rabin-Karp 算法,是 BF 算法的改進版。

BM 算法

全稱叫 Boyer-Moore 算法。是一種很是搞笑的字符串匹配算法。

BM 算法核心思想是,利用模式串自己的特色,在模式串中某個字符與主串不能匹配的時候,將模式串日後多滑動幾位,以此來減小沒必要要的字符比較,提升匹配的效率。BM算法構建的規則有兩類,壞字符規則和好後綴規則。好後綴規則能夠獨立於壞字符規則使用。由於壞字符規則的實現比較耗內存,爲了節省內存,咱們能夠只用好後綴規則來實現 BM 算法。

MKP 算法

KMP算法的核心思想是:咱們假設主串是a,模式串是b。在模式串與主串匹配的過程當中,當遇到不可匹配的字符的時候,咱們但願找到一些規律,能夠將模式串日後多滑動幾位,跳過那些確定不會匹配的狀況。

BM算法有兩個規則,壞字符和好後綴。KMP算法借鑑BM算法的思想,能夠總結成好前綴規則。這裏面最難懂的就是next數組的計算。若是用最笨的方法來計 算,確實不難,可是效率會比較低。因此,我講了一種相似動態規劃的方法,按照下標i從小到大,依次計算next[i],而且next[i]的計算經過前面已經計算出來 的next[0],next[1],……,next[i-1]來推導。 KMP算法的時間複雜度是O(n+m)。

35 Trie 樹

Trie樹,也叫「字典樹」。顧名思義,它是一個樹形結構。它是一種專門處理字符串匹配的數據結構,用來解決在一組字符串集合中快速查找某個字符串的問題。

若是用來構建Trie樹的這一組字符串中,前綴重複的狀況不是不少,那Trie樹這種數 據結構整體上來說是比較費內存的,是一種空間換時間的解決問題思路。

儘管比較耗費內存,可是對內存不敏感或者內存消耗在接受範圍內的狀況下,在Trie樹中作字符串匹配仍是很是高效的,時間複雜度是O(k),k表示要匹配的字符串的長度。 可是,Trie樹的優點並不在於,用它來作動態集合數據的查找,由於,這個工做徹底能夠用更加合適的散列表或者紅黑樹來替代。Trie樹最有優點的是查找前綴匹配的字符 串,好比搜索引擎中的關鍵詞提示功能這個場景,就比較適合用它來解決,也是Trie樹比較經典的應用場景。

36 AC 自動機

AC自動機是基於Trie樹的一種改進算法,它跟Trie樹的關係,就像單模式串中,KMP算法與BF算法的關係同樣。KMP算法中有一個很是關鍵的next數組,類比 到AC自動機中就是失敗指針。並且,AC自動機失敗指針的構建過程,跟KMP算法中計算next數組極其類似。因此,要理解AC自動機,最好先掌握KMP算法,由於AC自動機其實就是KMP算法在多模式串上的改造。

整個AC自動機算法包含兩個部分,第一部分是將多個模式串構建成AC自動機,第二部分是在AC自動機中匹配主串。第一部分又分爲兩個小的步驟,一個是將模 式串構建成Trie樹,另外一個是在Trie樹上構建失敗指針。

37 貪心算法

貪心算法有不少經典的應用,好比霍夫曼編碼(Huffman Coding)、Prim和Kruskal最小生成樹算法、還 有Dijkstra單源最短路徑算法。

實際上,貪心算法適用的場景比較有限。這種算法思想更多的是指導設計基礎算法。好比最小生成樹算法、單源最短路徑算法,這些算法都用到了貪心算法。

38 分治算法

分治算法(divide and conquer)的核心思想其實就是四個字,分而治之 ,也就是將原問題劃分紅n個規模較小,而且結構與原問題類似的子問題,遞歸地解決這些 子問題,而後再合併其結果,就獲得原問題的解。

分治算法是一種處理問題的思想,遞歸是一種編程技巧。實際上,分治算法通常都比較適合用遞歸來實現。分治算法的遞歸實現中,每一層遞歸都會涉及這樣三個操做:

  • 分解:將原問題分解成一系列子問題;
  • 解決:遞歸地求解各個子問題,若子問題足夠小,則直接求解;
  • 合併:將子問題的結果合併成原問題。

分治算法能解決的問題,通常須要知足下面這幾個條件:

  • 原問題與分解成的小問題具備相同的模式;
  • 原問題分解成的子問題能夠獨立求解,子問題之間沒有相關性,這一點是分治算法跟動態規劃的明顯區別,等咱們講到動態規劃的時候,會詳細對比這兩種算法;
  • 具備分解終止條件,也就是說,當問題足夠小時,能夠直接求解;
  • 能夠將子問題合併成原問題,而這個合併操做的複雜度不能過高,不然就起不到減少算法整體複雜度的效果了。

39 回溯算法

回溯算法的思想很是簡單,大部分狀況下,都是用來解決廣義的搜索問題,也就是,從一組可能的解中,選擇出一個知足要求的解。回溯算法很是適合用遞歸來 實現,在實現的過程當中,剪枝操做是提升回溯效率的一種技巧。利用剪枝,咱們並不須要窮舉搜索全部的狀況,從而提升搜索效率。

40 動態規劃

相關文章
相關標籤/搜索