演算法圖解-筆記
ch1_演算法簡介
對數的定義:冪運算的逆運算
大O表示法:指出演算法有多快,也就是演算法執行時間的增速
- 談論演算法的速度時,我們說的是隨著輸入的增加,其執行時間將以什麼樣的速度增加
- O(logn)比O(n)快
- 演算法執行時間是從增速的角度衡量的
ch2_選擇排序
陣列:意味著所有資料在記憶體中是緊密相連的
- 資料過大需要轉移位置
- 預備可能增加的資料需要留出位置,這將會浪費記憶體
- 陣列支援隨機訪問
- 陣列讀取速度很快
- 同一個陣列中,所有元素型別必須相同
連結串列:可以存放在記憶體的任何地方,每一個元素都存放了下個元素的地址
- 插入刪除元素很方便
- 無法迅速找到某個元素的地址
陣列 | 連結串列 | |
---|---|---|
讀取 | O(1) | O(N) |
插入 | O(n) | O(1) |
刪除 | O(n) | O(1) |
選擇排序:檢查列表中每個元素,找到最大最小的,讓後從剩下的資料中繼續找,直到排序結束,O(N*N)
ch3_遞迴
遞迴函式有兩部分
- base case:函式不用在呼叫自己的條件
- recursive case:函式呼叫自己的條件
def countdown(i):
print i
if i<= 1:
return
else:
countdown(i-1)
- 遞迴指的是呼叫自己的函式
- 每個遞迴函式都有兩個條件:基線條件和遞迴條件
- 棧有兩種操作:壓入和彈出
- 所有函式呼叫都進入呼叫棧(LIFO)
- 呼叫棧可能很長,將佔用大量記憶體
ch4_快速排序
利用了分而治之的思想(divide and conquer(D&C))
- 找出簡單的基線條件
- 確定如何縮小問題的規模,使其複合base case(函式不呼叫自己)
注意:再回到basecase前,函式呼叫都不會完成,遞迴會爆粗你這些函式呼叫的狀態,放入棧區。
涉及陣列的遞迴函式,基線條件通常是陣列為空或者只包含了一個元素。
def sum(arr): total = 0 if len(arr) == 0: return 0 elif len(arr) == 1: return arr[0] else: return arr[0]+sum(arr[1:])
快速排序程式碼:
- 基線條件是當陣列為空或者只有一個元素
- 目標是不斷逼近基線
- 首先選取一個元素
- 找到比它小的部分放左邊,比他大的部分放右邊
- 左右兩邊遞迴呼叫
def quicksort(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 quicksort(less)+[pivot]+quicksort(greater)
總結:
- D&C將問題逐步分解,使用D&C時,基線條件很可能是空陣列或只包含一個元素的陣列
- 實現快速排序時,請隨機選擇作為基準的元素,平均時間為O(nlogn)
- 大O表示法隱去了常量,這就是快速排序比合並排序更快的原因
CH5_散列表
散列表的查詢時間為O(1)
雜湊函式
- 必須是一致的(同樣的輸入返回同樣的輸出)
- 將不同的輸入對映到不同的數字
- 雜湊函式直到陣列多大,只返回有效的索引
散列表的應用
-
DNS解析:將域名變為ip
-
檢查重複
-
散列表應用於快取
小結:
- 散列表適用於模擬對映關係
- 防止衝突
- 快取/記住資料,避免伺服器在通過處理生成他們
散列表的衝突
衝突:給兩個鍵分配的位置相同
處理方案:如果兩個鍵對映到了同一個位置,就在這個位置存放一個連結串列
如果雜湊函式選擇的不好,所有的鍵都會對映到一個位置,理想狀態是函式將鍵均勻的對映到散列表的不同位置
散列表儲存的連結串列很長,散列表的速度將會急速下降
為了避免解決衝突
-
較低的裝填因子
-
良好的雜湊函式
裝填因子 = 散列表包含的元素數/位置總數
一旦裝填因子超過0.7,就應該調整散列表長度
ch6_廣度優先搜尋
用於解決圖問題中的最短路徑問題
什麼是圖:用於模擬不同的東西是如何相連的
廣度優先搜尋解決兩類問題:
- 從A節點 出發,有前往B節點的路徑嗎?
- 從節點A出發,前往節點B的那條路徑最短?
佇列
佇列不能隨機訪問隊中的元素,只能入隊和出隊
是一種先進先出(FIFO)的資料結構
棧則是一種(LIFO)的資料結構
#利用散列表實現圖
graph = {}
graph["you"] = ["alice","bob","claire"]
graph["bob"] = ["anuj","peggy"]
graph["alice"] = ["peggy"]
graph["claire"] = ["thom","jonny"]
graph["anuj"] = []
graph["peggy"] = []
graph["jonny"] = []
graph["thom"] = []
#芒果經銷商案例
#廣度優先
# 1、建立一個佇列,用於儲存要檢查的人
from collections import deque
def person_is_seller(name):
return name[-1] == 'm'
def search(name):
search_queue = deque()
search_queue += graph["you"]#將你的鄰居放進佇列
searched = []
# 2、當佇列不為空,就去出佇列中的第一個人
while search_queue:
person = search_queue.popleft()
#3 當這個個人沒有被檢查
if person not in searched:
#4 檢查這個人是否是芒果經銷商
if person_is_seller(person):
print(person+"is a mongo seller!")
return True
else:
#5 不是芒果經銷商、將這個人的朋友都加入搜尋隊列
search_queue += graph[person]
searched.append(person)
return False
執行時間:O(V+E):V是頂點數、E是邊數
總結:
- 對於檢查過的人、不要再檢查,會導致無限迴圈
- 佇列是先進先出的
- 棧是後進先出的
- 搜尋列表必須是佇列
ch7_狄克斯特拉演算法
簡介:適用於有向無環圖
- 找出”最便宜“的節點,可在短時間內到達的節點
- 更新該節點鄰居的開銷
- 重複這個過程
- 計算最終路徑
"""狄克斯特拉演算法"""
# ------------整個圖的散列表(字典)--------
graph = {}
# 起點
graph["start"] = {} # 起點是一個散列表(字典)
graph["start"]["A"] = 6 # 儲存權重,start-A
graph["start"]["B"] = 2 # 儲存權重,start-B
print(graph["start"].keys())
# 其他節點
graph["A"] = {} # A節點也是一個字典
graph["A"]["destination"] = 1 # A-終點
graph["B"] = {}
graph["B"]["A"] = 3 # B-A
graph["B"]["destination"] = 5 # B - 終點
# 終點
graph["destination"] = {} # 終點沒有任何鄰居
print(graph["B"])
#建立開銷表
#對於不知道的開銷(終點),我們設定為無窮大
infinity = float("inf")
costs = {}
costs["B"] = 6
costs["A"] = 2
costs["destination"] = infinity
#建立一個儲存父節點的散列表
parents = {}
parents["A"] = "start"
parents["B"] = "start"
parents["destination"] = 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
"""狄克斯特拉演算法"""
node = find_lowest_cost_node(costs) # 在未處理的節點中找到權重開銷最小的節點
while node:
cost = costs[node]
neighbors = graph[node]
for i in neighbors.keys(): # 遍歷當前節點的所有鄰居xiaojie
new_cost = cost + neighbors[i] # neighbors[i]相當於neighbors.value鍵值,即對應的權重值
if costs[i] > new_cost: # 原先直接前往i節點的開銷和現在路勁進行比較
costs[i] = new_cost
parents[i] = node
processed.append(node)
node = find_lowest_cost_node(costs)
print(costs["destination"])
- 廣度優先搜素用於在非加權圖中查詢最短路徑
- 狄克斯特拉演算法用於在加權圖中查詢最短路徑
- 僅當權重為正時,狄克斯特拉演算法才管用
- 如果有負權邊,請用貝爾曼-福德演算法
ch8_貪婪演算法
每一步都尋找全域性最優解、最終得到的就是全域性最優解
NP完全問題
- 集合覆蓋問題、旅行商問題
- 涉及集合和序列、難以解決
- 不能將問題分成小問題,必須考慮各種可能的情況
- 涉及所有組合的問題
貪婪演算法:
- 尋找區域性最優解,企圖以這種方式獲得全域性最優解
- 對於NP完全問題,沒有快速解決的方案
- NP完全問題,最好的方法是近似演算法
- 貪婪演算法是個好主意
有了上述分析,再用一句大白話來總結一下,P就是能在多項式時間內解決的問題,NP就是能在多項式時間驗證答案正確與否的問題。說一個問題是NP的,並不是說這個問題不能在多項式時間內解決,而是說目前為止,可能暫時尚未找到解法。所以,再強調一遍,NP不是P的否定。NP與P不是對立的,因為所有的P問題都是NP問題。
tips
在學習演算法設計與分析時,經常會提到NP完全性問題。目前已知的NP完全性問題就有2000多個,在圖上定義的許多組合優化問題是NP完全性問題,如貨郎問題、排程問題、最大團問題、最大獨立集合問題、Steiner樹問題、揹包問題、裝箱問題等,遇到這類問題時,通常從以下幾個方面來考慮,並尋求解決辦法:
(1) 動態規劃法:較高的解題效率。
(2) 分枝限界法: 較高的解題效率。
(3) 概率分析法: 平均效能很好。
(4) 近似演算法: 近似解代替最優解。
(5)啟發式演算法:根據具體問題的啟發式搜尋策略在求解,在實際使用可能很有效,但有時很難說清它的道理。
# You pass an array in, and it gets converted to a set.
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():
# stations 是一個鍵值對
# seation = station的 key
# states = station 的value
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)
ch9_動態規劃
動態規劃將大問題拆解為小問題,逐步計算最大價值。
由於縱軸粒度加大,需要擴充表格
但僅當每個子問題都是離散的,即不依賴於其他子問題時,動態規劃才管用。
動態規劃c語言
https://blog.csdn.net/qq_34207422/article/details/69067708
其他要學習的部分
-
二叉樹:資料查詢時,插入新陣列需要重新排序很麻煩,設計二叉查詢樹,插入和刪除操作更快
- 二叉查詢樹不能隨機訪問
- B數(資料庫)、紅黑樹、堆、伸展樹
-
反向索引:搜尋引擎常用
-
傅立葉變換:數字訊號分析
-
並行演算法
-
mapreduce:分散式演算法、一種特殊的並行演算法
-
布隆過濾器
-
SHA
-
線性規劃