《算法圖解》筆記(完結)


看書還得寫筆記,文字的寫不來,還是寫電子的,自己的字跟狗爬一樣,打出來的字好多了。

后續把自己看的基本關於網絡的書也寫點博客,一便於查尋,二便於加強記憶,要不然跟小說一樣,看了就忘了。

 

第1章:算法介紹

理解大O表示法,並非以秒為單位。大O表示法讓你能夠比較操作數,它指出了算法運行時間的增速。

 

大O表示法說的是在查找情況中最糟的情形。

從快到慢暈倒的5種大O運行時間。

O(log n),也叫對數時間,這樣的算法包括二分查找。

O(n), 也叫線性時間,這樣的算法包括簡單查找。

O(n * log n),這樣的算法包括快速排序---一種比較快的排序算法

O(n2)【表示n的平方】, 這樣的算法包括選擇排序---一種速度較慢的排序算法

O(n!), 這樣的算法包括旅行商的解決方案---一種非常慢的算法

 

第一章主要理解:

算法的速度指的並非時間,而是操作數的增速。

討論算法的速度時,我們說的是隨着輸入的增加,其運行時間將以什么樣的速度增加。

算法的運行時間用大O表示法表示。

O(log n)比O(n)快,當需要搜索的元素越多時,前者比后者快很多。

 

小結:

二分查找的速度比簡單查找快很多。

O(logn)比O(n)快。需要搜索的元素越多,前者比后者就快得更多

算法運行時間並不以秒為單位。

算法運行時間是從其增速的角度度量的。

算法運行時間用大O表示法表示

 

最后上書中的二分查找代碼

def binary_search(list, item):
    # 初始化序列的開始序列號,為末尾的序列號
    low = 0
    high = len(list) - 1
    # 只有在開始的序列號小於等於結束的序列號,才執行2分,否則就是找不到元素
    while low <= high:
        # 地板除取出中間值
        middle = (low + high) // 2
        # 取出中間值的值
        guess = list[middle]
        # 如果是的話,就返回這個索引
        if guess == item:
            return middle
        # 當取出來的中間值比要帥選的值大,按取出來中間值的前一位索引就是下一次尋找的結尾。
        elif guess > item:
            high = middle - 1
        # 反之,下一次查找的開始索引中間值的后一位索引,這里我還是比較容易搞混的
        else:
            low = middle + 1
    return None


if __name__ == '__main__':
    print(binary_search('123456', '2'))

 

第2章 選擇排序

主要學習數組與鏈表

數組與鏈表的運行時間

        數組    鏈表

讀取   O(1)   O(n)

插入    O(n)   O(1)

刪除     O(n)   O(1)

 

這里指出一下,僅當能夠立即訪問要刪除的元素時,刪除操作的運行時間才為O(1)。通常我們都記錄了鏈表的第一個元素和最后一個元素,因此刪除這些元素時運行時間為O(1)

 

選擇排序的時間:O(n2)【表示n的平方】

小結:

計算機的內存猶如一大堆抽屜。

需要存儲多個元素時,可使用數組或者鏈表

數組的元素都在一起

鏈表的元素是分開的,其中每個元素都存儲了下一個元素的地址

數組的讀取速度很快

鏈表的插入和刪除速度很快

在同一個數組中,所有元素的類型都必須相同(都為int,double等)

 

代碼:

這是我自己寫的:

def my_sort(arr):
    # 首相取選址范圍,從大到小取,最大為最大索引,最小為0
    for i in range(len(arr) - 1, -1, -1):
        # 開始循環對數組的數據進行比較
        for i in range(i):
            # 如果前面的數字大於后面的數字,兩個數字互相,確保后面的數字大
            if arr[i] > arr[i + 1]:
                arr[i], arr[i + 1] = arr[i + 1], arr[i]
    return arr


if __name__ == '__main__':
    print(my_sort([9, 3, 33, 3, 2, 1, 5, 6]))

 

def findSmallest(arr):
    # 定義初始的最小值
    smallest = arr[0]
    smallest_index = 0
    # 循環讀取列表,返回列表最小的索引
    for i in range(1, len(arr)):
        if arr[i] < smallest:
            smallest = arr[i]
            smallest_index = i
    return smallest_index


def selectionSort(arr):
    # 在一個新的列表中,每次裝入最小的索引
    newArr = []
    for i in range(len(arr)):
        smallest = findSmallest(arr)
        newArr.append(arr.pop(smallest))
    return newArr

if __name__ == '__main__':
    print(selectionSort(list('6754345678987654')))

 選擇排序是一種簡單直觀的排序算法,無論什么數據進去都是 O(n²) 的時間復雜度。所以用到它的時候,數據規模越小越好。唯一的好處可能就是不占用額外的內存空間了吧。

我寫的不用額外占用內存空間,書中的代碼還是需要額外新建一個新列表,但書中的代碼更加容易理解,而且邏輯也很漂亮

 

第三章 遞歸

遞歸時我最討厭的主題,希望書中學完,能夠讓我愛上它一點

 

實際使用中,使用循環的性能更好。高手在Stark Overflow上說過:如果使用循環,程序的性能可能更高;如果使用遞歸,程序可能更容易理解。如何選擇要看什么對你來說更重要

 

每個遞歸函數都有兩部分組成:基線條件(base case)和遞歸條件(recursive case)。遞歸條件指的是函數調用自己,而基線條件則指的是函數不再調用自己,從而避免形成無限循環。

書中舉例了一個好簡單的例子,真的很基礎,但講的不錯。

def fact(x):
    if x == 1:
        return 1
    else:
        return x * fact(x-1)

 注意每個fact調用都有自己的x變量。在一個函數中不能訪問另一個x變量

書p39頁,結合盒子的例子。這個棧包含未完成的函數調用,每個函數調用都包含未檢查完的盒子。使用棧很方便,因為你無需自己跟蹤盒子堆-棧替你這樣做了。

原來Python確實有遞歸次數限制,默認最大次數為998

 

小結:

遞歸指的是調用自己的函數

每個遞歸函數都有兩個條件:基線條件和遞歸條件

棧有兩種操作:壓入和彈出

所有函數調用都進入調用棧

調用棧可能很長,這將占用大量的內存

 

第4章 快速排序

學習分而治之和快速排序。分而治之是本書學習的第一種通用的解決方法。

學習快速排序---一種常用的優雅的排序算法。快速排序使用分而治之的策略。

 

分而治之D&G(divide and cpnquer)

工作原理:

找出基線條件,這個條件必須盡可能的簡單

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

 

涉及數組的遞歸函數時,基線條件通常是數組為空或只包含一個元素。陷入困境時,請檢查基線條件是不是這樣的。

 

書中的3道編程題,沒能寫出來,只能抄答案了。

請編寫書中要求sum函數的代碼

def sum(arr):
    if len(arr) == 0:
        return 0
    # 把第一個值取出來,后面的進行遞歸,當只有一個元素的arr會滿足基線條件
    return arr[0] + sum(arr[1:])

if __name__ == '__main__':
    print(sum(list(range(997))))

 編寫一個遞歸函數來計算列表包含的元素數:

def count(arr):
    if arr == []:
        return 0
    return 1 + count(arr[1:])

if __name__ == '__main__':
    print(count(list(range(100))))

 跟第一個原理差不多

找出列表中最大的數字

def find_max_num(arr):
    # 當兩個元素的時候,進行比較,返回最大值,基線條件
    if len(arr) == 2:
        return arr[0] if arr[0] > arr[1] else arr[1]
    # 遞歸條件,拆分后面索引1的元素到最后的元素進行遞歸條件。
    sun_max = find_max_num(arr[1:])
    return arr[0] if arr[0] > sun_max else sun_max

# 這個我自己真心寫不出來,看的我都有點繞了
if __name__ == '__main__':
    print(find_max_num([1, 2, 3, 4, 99, 5]))

 

正式進入快速排序:

def quick_sort(array):
    '''快速排序'''
    # 基線條件,當只有一個或0個元素的時候飯返回本身
    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)


if __name__ == '__main__':
    print(quick_sort([1, 5, 3, 11, 6, 6, 3, 2, ]))

 

小結:

D&C將問題逐步分解。使用D&C處理列表時,基線條件很可能是空數組或只包含一個元素的數組

實現快速排序時,請隨機的選擇用作基准值的元素。快速排序的平均運行時間為O(nlongn)。

大O表示法中的常量有時候事關重大,這就時快速排序比合並排序快的原因所在

比較簡單查找和二分查找,常量幾乎無關緊要,因為列表很長時,O(logn)的速度比O(n)快很多。

 

第五章 散列表

散列表---最有用的基本數據結構之一。

 

散列函數是這樣的函數,既無論你給它什么數據,它都還你一個數字。

專業術語表達的話,散列函數"將輸入映射到數字"

 

小的小結

散列表適合用於(書中介紹的其實就時Python中的字典數據格式)

模擬映射關系

防止重復

緩存/記住數據,以免服務器再通過處理來生成它們

 

散列沖突,既兩個鍵映射到了同一個位置,最簡單的解決方法,在這個位置存儲一個鏈表。

所以散列函數很重要,如果散列表存儲的鏈表很長,散列表的速度將急劇下降。然而,如果使用的散列函數很好,這些鏈表就不會很長。

 

散列表在平均情況下,操作速度與數組一樣快,而插入和刪除的速度與鏈表一樣快。但在槽糕的情況下,散列表的各種操作就慢了。

所以為了避免沖突,需要較低的填裝因子,良好的散列函數。

 

裝填因子越低,發生沖突的可能性越小,一般裝填因子大於0.7,就調整散列表的長度。

 

本章小結

散列表時一種功能強大的數據結構,其操作速度快,還能讓你以不同的方式建立數據模型。

你可以結合散列函數和數組來創建散列表。

沖突很糟糕,你應該使用可以最大限度減少沖突的散列函數。

散列表的查找,插入和刪除速度都非常快。

散列表適合用於模擬映射關系。

一旦裝填因子超過0.7,就該調整散列表的長度.

散列表可用於緩存數據。

散列表非常適用於防止重復。

 

第六章 廣度優先搜索

廣度優先主要用於非加權圖尋找最短路徑。

 

圖由節點和邊組成,一個節點可能與眾多節點直連,這些節點被稱為鄰居。

 

單線箭頭的叫有向圖,雙向箭頭或者直線為無向圖

 

隊列跟棧不同,一個先進先出,一個先進后出

 

書中的廣度優先代碼,用collections.deque的雙端隊列。

from collections import deque

searched = []
def search(name):
    search_queue = deque()
    # 將要查尋的數據放入隊列
    search_queue += graph(name)
    # 只要有數據就一直執行
    while search_queue:
        # 取出一個數據
        person = search_queue.popleft()
        # 判斷是否符合條件
        if person not in searched:
            if person_is_seller(person):
                print(person + 'is a mango seller!')
                return True
            else:
                # 隊列尾部加上該對象的下一個層級
                search_queue += graph[person]
                searched.append(person)
    return False

 

運行時間大O表示法為O(V+E)V為端點的數量,E為邊數

 

如果任務A依賴與任務B,在列表中任務A就必須在任務B后面,這被稱為拓撲排序,使用它可以根據圖創建一個有序列表。

 

樹是一種特殊的圖,其中沒有往后指的邊。

 

小結

廣度優先搜索指出是否有從A到B的路徑,如果有,官渡優先可搜索出最短路徑

面臨類似於尋找最短路徑的問題時,可嘗試使用圖來建立模型,再使用廣度優先搜索來解決問題。

有向圖中的邊為箭頭,箭頭的方向指定了關系的方向。

無向圖中的邊不帶箭頭,其中的關系是雙向的。

隊列是先進先出FIFO的,棧是先進后出的FILO的

你需要按加入順序檢查搜索列表中的人,否則找到的就不是最短路徑,因此搜索列表必須的是隊列。

對於檢查過的人,務必不要再去檢查,否則可能導致無限循環。

 

第七章 迪克斯特拉算法

計算加權圖的最短路徑。

迪克斯特拉的關鍵4個步驟:

找出最便宜的節點,即可在最短時間內前往的節點

對於該節點的鄰居,檢查是否有前往它們的更短路徑,如果有,就更新其開銷

重復這個過程,直到對圖中的每個節點都這樣做了。

計算最終路徑

 

專業術語介紹:

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

帶權重的圖成為加權圖,不帶權重的圖稱為非加權圖。

要計算非加權圖中的最短路徑,用廣度優先,要計算加權圖中的最短路徑,用迪克斯特拉算法。

 

迪克斯特拉算法只適用與有向無環圖

 

書中的案例,我覺的最關鍵的是在重復操作每個節點的時候,是尋找最便宜的節點,對於起點的節點默認開銷為無窮大float(inf)

 

迪克斯特拉算法不能用於包含負權邊的圖,在包含負權邊的圖中,使用貝爾曼-富德算法。

 

代碼實現:

參考這個吧:https://www.jianshu.com/p/629e6c99dfca

代碼我后續自己在抄寫一下。

 

processed = []

# costs={} costs['a'] = xx, coats['b'] = yy...
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



'''
graph = {}
graph["Start"] = {}
graph["Start"]["A"] = 6
graph["Start"]["B"] = 2
graph["A"] = {}
graph["A"]["End"] = 4
graph["B"] = {}
graph["B"]["C"] = 1
'''


'''
parents = {}
parents["A"] = "Start"
parents["B"] = "Start"
parents["C"] = None
parents["End"] = None
'''
node = find_lowest_cost_node(costs)
# 只要返回節點
while node is not None:
    cost = costs[node]
    # 每一個節點都市字典形式,保存的鄰居的信息與到鄰居的開銷
    neigbors = graph[node]
    # 遍歷所有的鄰居
    for n in neigbors.keys():
        # 鄰居的從起點到鄰居節點的新開銷值
        new_cost = cost + neigbors[n]
        # 如果新開銷值小於原來的開銷值
        if costs[n] > new_cost:
            costs[n] = new_cost
            # 更新父節點字典
            parents[n] = node
    # 放入已經處理節點
    processed.append(node)
    # 繼續執行
    node = find_lowest_cost_node(costs)
    

 每一行都注釋了,很巧妙的算法,讀取每一個節點的信息,算法是理解了,不知道能記住多久

迪傑斯特拉算法(Dijkstra)是由荷蘭計算機科學家狄克斯特拉於1959 年提出的,因此又叫狄克斯特拉算法。牛逼

 

小結:

廣度優先搜索用於在非加權圖中查找最短路徑

迪傑斯特拉算法用於在加權圖中尋找最短路徑

迪克斯特拉算法不能用於包含負權邊的圖,在包含負權邊的圖中,使用貝爾曼-富德算法。

 

第8章 貪婪算法

 

貪婪算法就是你每步都選擇局部最優解,最終得到的就是全局最優解。

 

書中一個集合覆蓋問題,用貪婪算法實現。

states_needed = set(['mt', 'wa', 'or', 'id', 'nv', 'ut', 'ca', 'za'])

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', 'za'])

final_stations = set()

# 只要還有空確的元素
while states_needed:
    best_stations = None
    states_covered = set()
    # 循環讀取每個站點
    for station, states in stations.items():
        covered = states_needed & states
        # 將元素最多的站點先加入
        if len(covered) > len(states_covered):
            best_stations = station
            states_covered = covered
    # 需要的元素減去已經有的元素的站點,剩下需要的元素
    states_needed -= states_covered
    # 將該站點加入
    final_stations.add(best_stations)

# 答案不唯一,set為無序,dict也為無序的
print(final_stations)

 

廣度優先搜索與迪克斯特拉算法都算貪婪算法

 

NP完整問題主要就是判斷什么是NP完整問題,如果是NP完整問題,就可以使用近似算法既可。

 

判斷NP方法的一些條件:

元素較少時算法的運行速度非常快,但隨着元素數量的增加,速度會變得非常慢。

涉及"所有組合"的問題通常是NP完全問題。

不能將問題分成小問題,必須考慮各種可能的情況。這可能是NP完全問題。

如果問題涉及序列(如旅行商問題中的城市)且難以解決,它可能就是NP完全問題。

如果問題涉及集合(如廣播台集合)且難以解決,它可能就是NP完全問題。

如果問題可轉換為集合覆蓋問題或旅行商問題,那它肯定是NP完全問題。

 

小結:

貪婪算法選擇局部最優解,企圖以這種方式獲得全局最優解。

對於NP完全問題,還沒有找到快速解決方案。

面臨NP完全問題時,最佳的做法是使用近似算法。

貪婪算法易於實現、運行速度快,是不錯的近似算法。

 

第9章 動態規划

 動態規划,這是一種解決棘手問題的方法,它將問題分成小問題,並先着手解決這些小問題。

 

每個動態規划的算法都是從一個網格開始。嘗試用書中的背包問題解決方法,手動寫的話,確實比較累。

使用動態規划時,要么考慮拿走整件商品,要么考慮不拿,而沒法判斷該不該拿走商品的一部分。

 

動態規划功能強大,它能夠解決子問題並使用這些答案來解決大問題。但僅當每個子問題都市離散的,既不依賴其他子問題時,動態規划才管用。

 

最長公共子串與最長公共子序列都是使用表格法解決的實例。

代碼如下,相對來說,最長公共子序列的匹配度更好。

先上最佳公共子串

 

if word_a[i] == word_b[j]:
    cell[i][j] = cell[i-1][j-1] + 1
else:
    cell[i][j] = 0

 

 對於最長子串問題,答案為網格中最大的數字---它可能並不位於最后的單元格中

if word_a[i] == word_b[j]:
    cell[i][j] = cell[i-1][j-1] + 1
else:
    cell[i][j] = max(cell[i-1][j], cell[i][j-1])

 在最長子序列,如果兩個字母不同,就選擇上方或者左邊鄰居中較大的那個。

 

小結:

需要在給定約束條件下優化某種指標時,動態規划很有用。

問題可分解為離散子問題時,可使用動態規划來解決。

每種動態規划解決方案都涉及網格

單元格中的值通常就時你要優化的值。

每個單元格都時一個子問題,因此你需要考慮如何將問題分解為子問題。

沒有放之四海皆准的計算動態規划解決方案的公式。

 

第10章 K最近鄰算法

KNN(k-nearest neighbours)算法

完成兩項基本的工作:

分類就時編組

回歸就時預測結果

使用KNN經常使用的時余弦相似度。

 

選擇鄰居個數一般為sqrt(n)個數,n為總量

 

小結:

KNN用於分類和回歸,需要考慮最近的鄰居

分類就時編組

回歸就時預測結果

特征抽取意味着將物品裝換為一系列可比較的數字

能否挑選何時的特征事關KNN算法的成敗。

 

第11章 接下來如何做

 

二叉樹,對於其中的每一個節點,左子節點的值都比它小,而右子節點的值都比它大

二叉查找書,平均運算時間為O(logn),但在最糟糕的情況下需要的時間為O(n)

數組的查找,插入、刪除,大O表示法為:O(logn),O(n),O(n)

二叉查找樹都為O(logn)

數據庫或者高級數據庫使用B樹,紅黑樹,堆,伸展樹。(不懂)

 

后面簡單介紹了一些概念,並行執行,等等,講的非常淺,就不寫了。

 

整體4天斷斷續續把這本書看完,對我的基礎認識有不少提高,但后續講的太少了,很多一筆帶過。前面的基礎講的很仔細,好書推薦。

后面准備看Python數據結構與算法分析。

為最后的搬磚腳本添加算法基礎。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM