看書還得寫筆記,文字的寫不來,還是寫電子的,自己的字跟狗爬一樣,打出來的字好多了。
后續把自己看的基本關於網絡的書也寫點博客,一便於查尋,二便於加強記憶,要不然跟小說一樣,看了就忘了。
第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數據結構與算法分析。
為最后的搬磚腳本添加算法基礎。