《演算法圖解》希望大家支援正版,下面是我在閱讀完這本書之後做的一些總結。
第一章
演算法簡介
二分查詢
例如,想在電話簿中尋找以名字開頭為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…