《演算法圖解》讀書筆記

旭旭旭旭渣發表於2018-02-26

《演算法圖解》希望大家支援正版,下面是我在閱讀完這本書之後做的一些總結。

第一章

演算法簡介

二分查詢

例如,想在電話簿中尋找以名字開頭為K的人,我們如果從頭開始找,那我們可能要翻閱半本電話簿才能找到,但我們知道K大概會在中間位置,那我們便可以在中間部分直接查詢,在這種情況下,我們所使用的演算法就是二分查詢。

二分查詢,也稱折半查詢、對數查詢,是一種在有序陣列中查詢某一特定元素的查詢演演算法。

# 二分查詢(while版本)
def binary_search(list, item):
    # item為目標元素
    # 用於根據要在其中查詢的列表部分
    low = 0
    high = len(list) - 1
    while low <= high:
        # 只要範圍沒有縮小到只包含一個元素,就繼續執行
        mid = (low + high) / 2
        # 檢查中間元素
        guess = list[mid]
        if guess == item:
            # 找到了目標元素
            return mid
        if guess > item:
            # 猜的元素大了
            high = mid - 1
        else:
            # 猜的元素小了
            low = mid + 1
    # 沒有找到目標元素
    return None

# 本書並沒有將遞迴的相關內容,這是我在看到後面的遞迴內容回頭補上的
# 二分查詢(遞迴版本)
def binary_search_recursion(list, low, high, item):
    if low > high:
        # 沒有找到目標元素
        return -1
    mid = low + (high - low) / 2
    if list[mid] > item:
        # 猜的元素大了
        return binary_search_recursion(list, low, mid - 1, item)
    if list[mid] < item:
        # 猜的元素笑了
        return binary_search_recursion(list, mid + 1, high, item)
    return mid
複製程式碼

執行時間

每次介紹演算法時,我們都將討論其執行時間,一般而言選擇效率最高的演算法,以最大限度地減少執行時間或者佔用時間。

大O表示法

大O表示法是一種特殊的表示法,指出了演算法的速度有多塊,通常情況下大O表示法指的是最糟情況下的執行時間。

下面列舉一些常見的大O執行時間:

  • O(log n),也叫對數時間,常見演算法包括二分查詢
  • O(n),也叫線性時間,常見演算法包括簡單查詢
  • O(n * log n),常見演算法比如速度比較快的快速排序
  • O(n^2),常見演算法包括選擇排序
  • O(n!),常見演算法包括旅行商問題

第二章

選擇排序

陣列

陣列內的所有元素都是需要緊密相連的,所以插入或者刪除新的元素對原有資料的改動會比較大,但可以迅速的根據下標讀取元素。

連結串列

連結串列內的所有元素可能分佈在不同的記憶體空間,他們之間通過指向進行連線,因此在插入或者刪除元素的時候只需要改變指向就可以,但是想要讀取此鏈上指定位置的元素要從頭開始遍歷。

陣列和連結串列的比較

/ 陣列 連結串列
讀取 O(1) O(n)
插入 O(n) O(1)
刪除 O(n) O(1)

選擇排序

選擇排序是一種簡單直觀的排序演算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

# 選擇排序
def find_smallest(list):
    smallest = list[0]
    smallest_index = 0
    for i in range(1, len(list)):
        if list[i] < smallest:
            smallest = list[i]
            smallest_index = i
    return smallest_index


def selection_sort(list):
    new_list = []
    for i in range(len(list)):
        smallest = find_smallest(list)
        new_list.append(list.pop(smallest))
    return new_list
複製程式碼

第三章

遞迴

遞迴,又譯為遞迴,在數學與電腦科學中,是指在函式的定義中使用函式自身的方法。 遞迴只是讓解決方案更清晰,並沒有效能上的優勢。在有些情況下,使用迴圈的效能更好。

基線條件和遞迴條件

由於遞迴函式呼叫自己,因此編寫的函式容易導致無限迴圈。因此指定停止遞迴的條件就是基線條件,繼續執行函式的條件就是遞迴條件。

棧為後進先出的一種資料結構,遞迴中會生成一系列的呼叫棧。

階乘遞迴

函式呼叫自身,稱為遞迴。如果尾呼叫自身,就稱為尾遞迴。 遞迴非常耗費記憶體,因為需要同時儲存成千上百個呼叫記錄,很容易發生"棧溢位"錯誤(stack overflow)。但對於尾遞迴來說,由於只存在一個呼叫記錄,所以永遠不會發生"棧溢位"錯誤。

# 遞迴
def factorial(x):
    if x == 1:
        return 1
    else:
        return x * factorial(x-1)

# 本書並沒有寫相應的尾遞迴內容,這裡也是我自己加的
# 尾遞迴
def factorial_recursion(x, total):
    if x == 1:
        return total
    else:
        return factorial(x-1, x * total)
複製程式碼

第四章

快速排序

快速排序使用分而治之的策略,對資料進行遞迴式排序。

分而治之

分而治之是一種著名的遞迴式問題解決方法,具體步驟如下:

  • 找出基線
  • 不斷將問題分解(或者說縮小規模),直到符合基線條件

歐幾里得演算法

在數學中,輾轉相除法,又稱歐幾里得演算法,是求最大公約數的演算法。 兩個整數的最大公約數是能夠同時整除它們的最大的正整數。輾轉相除法基於如下原理:兩個整數的最大公約數等於其中較小的數和兩數的差的最大公約數。

快速排序

基線條件

基線條件為陣列為空或只包含一個元素。

基準值

快速排序需要對陣列進行分解,因此需要一個基準值,以基準值對陣列元素進行分割槽,一般選取陣列中第一個元素為基準值。再在被分割槽的部分重複以上過程,最後可以得到排序結果。

# 快速排序
def quick_sort(array):
    if len(array) < 2:
        # 基線條件
        return array
    else:
        # 遞迴條件
        # 基準值
        pivot = array[0]
        # 分割槽
        less = [i for i in array[1:] if i <= pivot]
        greater = [i for i in array[1:] if i > pivot]
        return quick_sort(less) + [pivot] + quick_sort(greater)
複製程式碼

歸併排序

簡單來說就是先將陣列不斷細分成最小的單位,然後每個單位分別排序,排序完畢後合併,重複以上過程最後就可以得到排序結果。同樣也是採用分而治之的思想。

# 本書只是稍稍提到了相關內容,併為對此進行詳細展開,這裡是筆者自己加上的內容
# 歸併排序
def merge_list(list_a, list_b):
    # 合併陣列
    new_list = []
    index_a = 0
    index_b = 0
    while index_a < len(list_a) and index_b < len(list_b):
        if list_a[index_a] < list_b[index_b]:
            new_list.append(list_a[index_a])
            index_a += 1
        else:
            new_list.append(list_b[index_b])
            index_b += 1
    while index_a < len(list_a):
        new_list.append(list_a[index_a])
        index_a += 1
    while index_b < len(list_b):
        new_list.append(list_b[index_b])
        index_b += 1
    return new_list


def merge_sort(array, low, high):
    # low 初始下標
    # high 結尾下標
    new = []
    if low < high:
        mid = (low + high) / 2
        # 遞迴排序最小單位陣列
        merge_sort(array, low, mid)
        merge_sort(array, mid + 1, high)
        # 歸併陣列
        list_a = array[low:mid+1]
        list_b = array[mid+1:high+1]
        new = merge_list(list_a, list_b)
        start = low
        for i in new:
            array[start] = i
            start += 1
    return array
複製程式碼

當資料量越來越大時,

歸併排序:比較次數少,速度慢。

快速排序:比較次數多,速度快。

第五章

雜湊表

雜湊函式

需要滿足的要求:

  • 同一輸入的輸出必須一致
  • 不同的輸入對映到不同的索引

雜湊表應用

查詢

實現電話簿:

  • 建立對映
  • 查詢
# 實現電話簿
def creat_phone_book():
    # 建立雜湊表
    phone_book = dict()
    phone_book["aa"] = 123456
    phone_book["bb"] = 654321
    # 查詢
    print phone_book["aa"]
複製程式碼

防止重複

用來記錄是否目標已經存在
# 投票防重複
voted = {}
def check_voted(name):
    if voted.get(name):
        print "已經存在"
    else:
        voted[name] = True
        print "請投票"
複製程式碼

快取

實現快速響應,無需等待耗時處理

# 實現快取
cache = {}
def get_data(url):
    if cache.get(url):
        return cache[url]
    else:
        data = "這裡進行獲取資料的耗時操作"
        # 完成之後進行快取
        cache[url] = data
        return data
複製程式碼

雜湊衝突

給兩個鍵分配的位置相同,這種情況下就需要在這個位置上儲存一個連結串列。

  • 雜湊函式很重要,最理想的情況是雜湊函式均勻地對映到雜湊表的不同位置
  • 如果雜湊表儲存的連結串列過長,會導致雜湊表的速度急劇下降
/ 雜湊表(平均情況) 雜湊表(最糟情況) 陣列 連結串列
讀取 O(1) O(n) O(1) O(n)
插入 O(1) O(n) O(n) O(1)
刪除 O(1) O(n) O(n) O(1)

避免衝突:

  • 使用低的裝填因子
  • 良好的雜湊函式

裝填因子

裝填因子 = 雜湊表包含的元素數/位置總數 一旦裝填因子變大,就需要在雜湊表中新增位置,這被稱為調整長度。 一般裝填因子大於0.7,就調整雜湊表的長度

第六章

廣度優先搜尋

圖模擬一組連線,一個圖是表示物件與物件之間的關係的方法。

廣度優先搜尋

廣度優先搜尋是一種用於圖的查詢演算法。可解決如下問題:

  • 從節點a出發,有前往節點b的路徑嘛?
  • 從節點a出發,前往節點b的哪條路徑最短?

佇列

佇列是一種先進先出的資料結構。

實現廣度優先搜尋

# 建立圖
graph = dict()
graph["you"] = ["alice", "bob", "claire"]
graph["bob"] = ["anuj", "peggy"]
graph["alice"] = ["peggy"]
graph["claire"] = ["tom", "jonny"]
graph["anuj"] = []
graph["peggy"] = []
graph["tom"] = []
graph["jonny"] = []


# 判斷這個人是不是商人
def person_is_seller(name):
    return name[-1] == "m"


# 廣度優先搜尋
def bfs(name):
    # 建立搜尋佇列
    search_queue = deque()
    search_queue += graph[name]
    # 已經搜尋過的節點陣列, 防止無限迴圈
    searched = []
    while search_queue:
        person = search_queue.popleft()
        if person not in searched:
            if person_is_seller(person):
                print "找到商人 %s" % person
                return True
            else:
                search_queue += graph[person]
                searched.append(person)
    return False
複製程式碼

執行時間

廣度優先搜尋過程中的

佇列時間是固定的即 O(1 * 人數)

搜尋過程中的時間為O(邊數)

因此廣度優先搜尋的執行時間為O(人數 + 邊數)

第七章

狄克斯特拉演算法(dijkstra)

計算加權圖最短路徑且不適用於負權圖

狄克斯特拉演算法步驟

  • 找出權重最小的節點,即可在最短時間內到達的節點
  • 更新該節點的鄰居的路徑權重
  • 重複這個步驟,直到對圖中每一個節點都這麼做
  • 計算最終路徑

權重

狄克斯特拉演算法用於每條邊都有關聯數字的圖,這些數字稱為權重

在無向圖中每條邊都是一個環,在有向圖中從節點a開始走一圈又能回到節點a,這便是

負權邊

在圖中有的邊權為負數,這時就不能使用狄克斯特拉演算法,這是因為dijkstra演算法在計算最短路徑時,不會因為負邊的出現而更新已經計算過的頂點的路徑長度,這樣一來,在存在負邊的圖中,就可能有某些頂點最終計算出的路徑長度不是最短的長度。

實現

《演算法圖解》讀書筆記

# 建立圖
graph = {}
graph["start"] = {}
graph["start"]["a"] = 6
graph["start"]["b"] = 2
graph["a"] = {}
graph["a"]["fin"] = 1
graph["b"] = {}
graph["b"]["a"] = 3
graph["b"]["fin"] = 5
graph["fin"] = {}

# 建立開銷
infinity = float("inf")
costs = {}
costs["a"] = 6
costs["b"] = 2
costs["fin"] = infinity

# 建立父節點
parents = {}
parents["a"] = "start"
parents["b"] = "start"
parents["fin"] = None

# 已經確定的節點,防止無限迴圈
processed = []


# 尋找權重最小的節點
def find_lowest_cost_node(costs):
    # 最小的花費
    lowest_cost = float("inf")
    # 最小花費的節點
    lowest_cost_node = None
    for node in costs:
        cost = costs[node]
        if cost < lowest_cost and node not in processed:
            lowest_cost = cost
            lowest_cost_node = node
    return lowest_cost_node


# dijkstra演算法
def dijkstra():
    node = find_lowest_cost_node(costs)
    while node is not None:
        cost = costs[node]
        neighbors = graph[node]
        for n in neighbors.keys():
            new_cost = cost + neighbors[n]
            if costs[n] > new_cost:
                costs[n] = new_cost
                parents[n] = node
        processed.append(node)
        node = find_lowest_cost_node(costs)
    print costs
    print parents
    print processed
複製程式碼

第八章

貪婪演算法

貪婪演算法最大的優點就是簡單易行,每步都採取最優的做法

廣播臺覆蓋問題

# 廣播臺覆蓋問題
def greedy():
    states_needed = set(["mt", "wa", "or", "id", "nv", "ut", "ca", "az"])
    stations = {}
    stations["kone"] = set(["id", "nv", "ut"])
    stations["ktwo"] = set(["wa", "id", "mt"])
    stations["kthree"] = set(["or", "nv", "ca"])
    stations["kfour"] = set(["nv", "ut"])
    stations["kfive"] = set(["ca", "az"])
    final_stations = set()
    while states_needed:
        best_station = None
        states_covered = set()
        for station, states in stations.items():
            covered = states_needed & states
            if len(covered) > len(states_covered):
                best_station = station
                states_covered = covered
        states_needed -= states_covered
        final_stations.add(best_station)
    print final_stations
複製程式碼

NP完全問題

必須計算每個可能的集合

時間複雜度近似O(2^n)

  • 涉及所有組合的問題通常是NP完全問題
  • 元素較少時演算法的執行速度非常快,但隨著元素數量的增加,速度會變得非常慢
  • 不能將問題分為小問題,必須考慮各種可能的情況,這可能是NP完全問題
  • 如果問題涉及序列(如旅行商問題中城市序列)且難以解決,它可能就是NP完全問題
  • 如果問題涉及集合(如廣播臺集合)且難以解決,它可能就是NP完全問題
  • 如果問題可轉化為集合覆蓋問題或旅行商問題,那它肯定是NP完全問題

第九章

動態規劃

揹包問題

# 揹包問題
# 這裡使用了圖解中的吉他,音響,電腦,手機做的測試,資料保持一致
w = [0, 1, 4, 3, 1]   #n個物體的重量(w[0]無用)
p = [0, 1500, 3000, 2000, 2000]   #n個物體的價值(p[0]無用)
n = len(w) - 1   #計算n的個數
m = 4   #揹包的載重量

x = []   #裝入揹包的物體,元素為True時,對應物體被裝入(x[0]無用)
v = 0
#optp[i][j]表示在前i個物體中,能夠裝入載重量為j的揹包中的物體的最大價值
optp = [[0 for col in range(m + 1)] for raw in range(n + 1)]
#optp 相當於做了一個n*m的全零矩陣的趕腳,n行為物件,m列為自揹包載重量

def knapsack_dynamic(w, p, n, m, x):
    #計算optp[i][j]
    for i in range(1, n + 1):       # 物品一件件來
        for j in range(1, m + 1):   # j為子揹包的載重量,尋找能夠承載物品的子揹包
            if (j >= w[i]):         # 當物品的重量小於揹包能夠承受的載重量的時候,才考慮能不能放進去
                optp[i][j] = max(optp[i - 1][j], optp[i - 1][j - w[i]] + p[i])    # optp[i - 1][j]是上一個單元的值, optp[i - 1][j - w[i]]為剩餘空間的價值
            else:
                optp[i][j] = optp[i - 1][j]

    #遞推裝入揹包的物體,尋找跳變的地方,從最後結果開始逆推
    j = m
    for i in range(n, 0, -1):
        if optp[i][j] > optp[i - 1][j]:
            x.append(i)
            j = j - w[i]

    #返回最大價值,即表格中最後一行最後一列的值
    v = optp[n][m]
    return v

print '最大值為:' + str(knapsack_dynamic(w, p, n, m, x))
print '物品的索引:', x
複製程式碼

第十章

K最近鄰演算法

特徵抽取

兩個點之間的特徵相似度可用畢達哥拉斯公式表示

迴歸

K最近鄰演算法做兩項基本工作——分類和迴歸:

  • 分類就是編組
  • 迴歸就是預測結果

挑選合適的特徵

總結

這本書總的來說就是一本演算法入門書,在閱讀完這本書之後,很多理念比之前更加容易理解,但是還有很多東西要理解,這裡我也只是做了簡單的記錄,總之還算一次不錯的閱讀。

寫在最後

這次所有的程式碼都在這裡了github.com/sosoneo/Rea…

相關文章