算法是一組完成任務的指令。任何代碼片段都可視爲算法。
算法是一種通過有限過程解決問題的解決方案。
大O 表示法:
大O表示法是一種特殊的表示法,指出了算法的速度有多快;
大O表示法讓你能夠比較操作數,它指出了算法運行時間的增速;
大O 表示法指出了最糟情況下的運行時間。
舉例,假設列表包含n個元素:
簡單查找需要檢查每個元素,因此需要執行n次操作。使用大O表示法,這個運行時間爲 O(n)。
二分查找需要執行log n次操作。使用大O表示法,這個運行時間爲 O(log n)。
1,數組
數組中的元素在內存中都是相連的(緊靠在一起的)。當增加元素時,要請求計算機重新分配一塊可容納所有元素的內存,再將所有元素都移到那裏。
需要隨機地讀取元素時,數組的效率很高,因爲可迅速找到數組的任何元素。數組隨機讀取某個元素的時間複雜度爲O(1)。
在同一個數組中,所有元素的類型都必須相同(都爲int、double等)。
#將python中的list轉換爲數組(list中的所有元素的類型必須相同) import array arr = array.array('i',[0,1,1,2,3])
2,鏈表
鏈表中的元素可存儲在內存的任何地方;鏈表的每個元素都存儲了下一個元素的地址,從而使一系列隨機的內存地址串在一起。
鏈表的問題:無法直接讀取鏈表的最後一個元素,因爲你不知道它所處的地址,必須先訪問元素#1,依次下去;跳躍取值時,效率很低。
鏈表的優勢在插入元素和刪除元素方面。
使用鏈表時,插入元素很簡單,只需修改它前面的那個元素指向的地址。
使用數組時,則必須將後面的元素都向後移;如果沒有足夠的空間,可能還得將整個數組複製到其他地方!
因此,當需要在中間插入元素時,鏈表是更好的選擇。
要刪除一個元素,鏈表也是更好的選擇,因爲只需修改前一個元素指向的地址即可。而使用數組時,刪除元素後,必須將後面的元素都向前移。
python實現單鏈表:
class Node: def __init__(self,data=None): self.data = data self.next = None def __repr__(self): return str(self.data) class LinkedList: def __init__(self): self.head = None self.length = 0 def is_empty(self): return self.length == 0 def append(self,item): if isinstance(item,Node): this_node = item else: this_node = Node(data=item) if self.head == None: self.head = this_node else: node = self.head while node.next: node = node.next node.next = this_node self.length += 1 def insert(self,index,item): if type(index) is int: if index < 0 or index >= self.length: print("Index value is out of range.") return this_node = Node(data=item) current_node = self.head if index == 0: self.head = this_node this_node.next = current_node return while index - 1: current_node = current_node.next index -= 1 this_node.next = current_node.next current_node.next = this_node self.length += 1 return else: print("Index value is not int.") return def delete(self,index): if type(index) is int: if index < 0 or index >= self.length: print("Index value is out of range.") return if index == 0: self.head = self.head.next else: current_node = self.head while index - 1: current_node = current_node.next index -= 1 this_node = current_node.next current_node.next = this_node.next self.length -= 1 return else: print("Index value is not int.") return def __repr__(self): if self.is_empty(): print("Linked list is empty") else: nlist = "" node = self.head nlist += "Head-->" + node.data + " " while node.next: node = node.next nlist += "-->" + node.data + " " return nlist
3,棧,隊列,雙端隊列
棧 stack :一個有序的項的集合。添加項和移除項發生在同一端,即後進先出,形式如「彈夾」「疊盤子」。
隊列 queue:一系列有序的元素的集合。先進先出,形式如「排隊」。
雙端隊列 deque:一系列有序的元素的集合。允許從兩端插入和從兩端刪除。
python實現棧:
class Stack: def __init__(self): self.items = [] def is_empty(self): return self.items == [] def peek(self): return self.items[-1] def push(self,item): self.items.append(item) def pop(self): return self.items.pop() def size(self): return len(self.items)
二分查找只能查找有序的元素列表!這個前提條件得記住。
二分法思路:先取列表中間值與查找元素比較,若查找元素小於中間值,則在小於中間值的那一半列表中再取中間值與查找元素比較,以此類推,直到找到該元素。
一般而言,對於包含n個元素的列表,用二分查找最多需要log n步,所以二分法的時間複雜度爲O(log n)。
def binary_search(list,item): low = 0 high = len(list)-1 while low <= high: mid = (low + high)//2 guess = list[mid] if guess == item: return mid elif guess < item: low = mid + 1 else: high = mid -1 return None
散列表(Hash table),也叫哈希表,是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
散列表(hash table)是實現字典操作的一種有效的數據結構。
儘管最壞的情況下,散列表中查找一個元素的時間與鏈表中查找的時間相同,達到了O(n)。
然而實際應用中,散列的查找的性能是極好的。在一些合理的假設下,在散列表中查找一個元素的平均時間是O(1)。
散列函數是這樣的函數,即無論你給它什麼數據,它都還你一個數字(即將輸入映射到數字):
1,它必須是一致的,相同的輸入始終映射到同一個數字;
2,它應將不同的輸入映射到不同的數字
建立散列表操作步驟:
1) step1 取數據元素的關鍵字key,計算其哈希函數值(地址)。若該地址對應的存儲空間還沒有被佔用,則將該元素存入;否則執行step2解決衝突。
2) step2 根據選擇的衝突處理方法,計算關鍵字key的下一個存儲地址。若下一個存儲地址仍被佔用,則繼續執行step2,直到找到能用的存儲地址爲止。
衝突處理方法:
影響哈希查找效率的一個重要因素是哈希函數本身。當兩個不同的數據元素的哈希值相同時,就會發生衝突。爲減少發生衝突的可能性,哈希函數應該將數據儘可能分散地映射到哈希表的每一個表項中。
解決衝突的方法有以下兩種:
(1) 開放地址法
(2) 鏈地址法
注:Python字典dict的實現是使用開放地址法中的二次探查來解決衝突的。
1,冒泡排序--時間複雜度:O(n²)
冒泡排序:對一個列表多次重複遍歷,比較相鄰的兩項,對排錯的項進行交換順序。每遍歷一次,就有一個最大項排在了正確的位置。
def bubble_sort(alist): for passnum in range(len(alist)-1,0,-1): for i in range(passnum): if alist[i] > alist[i+1]: alist[i],alist[i+1] = alist[i+1],alist[i]
短路冒泡排序:冒泡排序改良版;如果在某次遍歷過程中沒有交換,我們就可斷定列表已經排好。這樣就可以使冒泡排序在已知列表排好的情況下提前結束。
def short_bubble_sort(alist): exchanges = True passnum = len(alist)-1 while passnum > 0 and exchanges: exchanges = False for i in range(passnum): if alist[i] > alist[i+1]: exchanges = True alist[i],alist[i+1] = alist[i+1],alist[i] passnum -= 1
2,選擇排序--時間複雜度:O(n²)
選擇排序提高了冒泡排序的性能,它每遍歷一次列表只交換一次數據,即進行一次遍歷時找到最大的項,完成遍歷後,再把它換到正確的位置。
#選擇排序代碼一 def find_smallest(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 selection_sort(arr): new_arr = [] for i in range(len(arr)): smallest_index = find_smallest(arr) new_arr.append(arr.pop(smallest_index)) return new_arr
#選擇排序代碼二 def selection_sort(alist): for passnum in range(len(alist)-1,0,-1): maxnum = 0 for i in range(1,passnum+1): if alist[i] > alist[maxnum]: maxnum = i alist[i],alist[maxnum] = alist[maxnum],alist[i]
3,插入排序--時間複雜度:O(n²)
插入排序:從第二個元素開始,每個元素依次與前面的有序子列表比較,直到找到自己的位置,插入,跳出本次遍歷。
def insertion_sort(alist): for i in range(1,len(alist)): for j in range(i): if alist[i] < alist[j]: alist.insert(j,alist.pop(i)) break
1,遞歸
遞歸指的是調用自己的函數。
Leigh Caldwell在Stack Overflow上說:「如果使用循環,程序的性能可能更高;如果使用遞歸,程序可能更容易理解。如何選擇要看什麼對你來說更重要」。
編寫遞歸函數時,必須告訴它何時停止遞歸。正因爲如此,每個遞歸函數都有兩部分:基線條件(base case)和遞歸條件(recursive case)。遞歸條件指的是函數調用自己,而基線條件則指的是函數不再調用自己,從而避免形成無限循環。
計算機在內部使用被稱爲調用棧的棧。通過調用棧來理解遞歸是一種好方法。
def greet(name): print "hello, " + name + "!" greet2(name) print "getting ready to say bye..." bye() def greet2(name): print "how are you, " + name + "?" def bye(): print "ok bye!"
#遞歸函數 def fact(x): if x == 1: return 1 else: return x * fact(x-1)
2,分而治之
分而治之(divide and conquer,D&C)—— 一種著名的遞歸式問題解決方法。
D&C算法是遞歸的。使用D&C解決問題的過程包括兩個步驟。
(1) 找出基線條件,這種條件必須儘可能簡單。
(2) 不斷將問題分解(或者說縮小規模),直到符合基線條件。
舉例:要將下面這塊地均勻地分成方塊,且分出的方塊要儘可能大,試試用分而治之解決。
歐幾里得算法:適用於這小塊地的最大方塊,也是適用於整塊地的最大方塊。
注:均勻分上面這塊地的適用的最大方塊爲80 m× 80 m。
1,快速排序
快速排序是一種常用的排序算法,比選擇排序快得多。快速排序使用了D&C(分而治之)。
快速排序最佳情況也是平均情況的時間複雜度:O(nlogn);最糟情況下爲 O(n²)。
快速排序思路:
(1) 選擇基準值。
(2) 將數組分成兩個子數組:小於基準值的元素和大於基準值的元素。
(3) 對這兩個子數組進行快速排序。(遞歸)
def quicksort(arr): if len(arr)<2: return arr else: pivot = arr[0] less = [i for i in arr[1:] if i <= pivot] greater = [i for i in arr[1:] if i > pivot] return quicksort(less) + [pivot] + quicksort(greater)
2,歸併排序
歸併排序(MERGE-SORT)是建立在歸併操作上的一種有效的排序算法,該算法是採用分治法(D&C)的一個非常典型的應用。
歸併排序的時間複雜度始終爲O(nlogn)!
歸併排序原理:
一、將一個序列從中間位置分成兩個子序列;
二、再將這兩個子序列按照第一步繼續二分下去;
三、直到所有子序列的長度都爲1,此時可以認爲這些子序列是有序序列;
四、依次兩兩合併有序序列,最終得到一個完整的有序序列。
def merge_list(alist,blist): clist = [] i = j = 0 while i < len(alist) and j < len(blist): if alist[i] < blist[j]: clist.append(alist[i]) i += 1 else: clist.append(blist[j]) j += 1 while i < len(alist): clist.append(alist[i]) i += 1 while j < len(blist): clist.append(blist[j]) j += 1 return clist def merge_sort(tlist): if len(tlist) <= 1: return tlist middle = len(tlist)//2 left = merge_sort(tlist[:middle]) right = merge_sort(tlist[middle:]) return merge_list(left,right)
1,樹
節點(Node)是樹的基本構成部分。它可以有其他專屬的名稱,我們稱之爲「鍵(key)」。 一個節點可能有更多的信息,我們稱之爲「負載(payload)」。
邊(Edge)也是另一個樹的基本構成部分。邊連接兩個節點,並表示它們之間存在聯繫。每個節點(除了根節點)都有且只有一條與其他節點相連的入邊(指向該節點的邊),每個節點可能有許多條出邊(從該節點指向其他節點的邊)。
根節點是樹中唯一一個沒有入邊的節點。
路徑是由邊連接起來的節點的有序排列。例如:動物界 → 脊索動物門 → 哺乳動物綱 → 食肉動物目 → 貓科 → 貓屬 → 家貓 就是一條路徑。
同一個節點的所有子節點互爲兄弟節點。
沒有子節點的節點成爲稱爲葉節點。
一個節點的層數是指從根節點到該節點的路徑中的邊的數目。定義根節點的層數爲0。
樹的高度等於所有節點的層數的最大值。
樹的定義一:樹是節點和連接節點的邊的集合。
樹的定義二:一棵樹有根和其他子樹組成,這些子樹也是樹。
二叉樹:每個節點最多含有兩個子樹的樹稱爲二叉樹
堆(heap)是計算機科學中一類特殊的數據結構的統稱。堆通常是一個可以被看做一棵樹的數組對象。堆總是一棵完全二叉樹。
2,圖
圖模擬一組連接;圖由節點(node)和邊(edge)組成。一個節點可能與衆多節點直接相連,這些節點被稱爲鄰居。
圖用於模擬不同的東西是如何相連的。
有向圖:其中的關係是單向的
無向圖:沒有箭頭,直接相連的節點互爲鄰居
最短路徑問題(shorterst-path problem)
(1) 使用圖來建立問題模型。
(2) 使用廣度優先搜索解決問題。
廣度優先搜索是一種用於圖的查找算法,可幫助回答兩類問題:
第一類問題:從節點A出發,有前往節點B的路徑嗎?
第二類問題:從節點A出發,前往節點B的哪條路徑最短?
廣度優先搜索實現算法:
graph = {"rock":["mike1","mike2","mike3"],"mike1":["jack1","jack2"],"jack1":[],"jack2":[], "mike2":["rock","mike1","mike3"],"mike3":["mike2","rock","manmm"],"manmm":[]} def person_is_seller(name): return name[-1] == "m" from collections import deque def search(name): search_queue = deque() search_queue += graph[name] searched = [] while search_queue: person = search_queue.popleft() if not person 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爲頂點(vertice)數,E爲邊數。