什麼是算法
算法是獨立存在的一種解決問題的方法和思想。node
算法的五大特性python
- 輸入: 算法具備0個或多個輸入
- 輸出: 算法至少有1個或多個輸出
- 有窮性: 算法在有限的步驟以後會自動結束而不會無限循環,而且每個步驟能夠在可接受的時間內完成
- 肯定性:算法中的每一步都有肯定的含義,不會出現二義性
- 可行性:算法的每一步都是可行的,也就是說每一步都可以執行有限的次數完成
第一次嘗試:mysql
算法效率衡量
1.執行時間反應算法效率面試
對於同一問題,咱們給出了兩種解決算法,在兩種算法的實現中,咱們對程序執行的時間進行了測算,發現兩段程序執行的時間相差懸殊(214.583347秒相比於0.182897秒),由此咱們能夠得出結論:實現算法程序的執行時間能夠反應出算法的效率,即算法的優劣。算法
2.單靠時間值絕對可信嗎?sql
假設咱們將第二次嘗試的算法程序運行在一臺配置古老性能低下的計算機中,狀況會如何?極可能運行的時間並不會比在咱們的電腦中運行算法一的214.583347秒快多少。shell
單純依靠運行的時間來比較算法的優劣並不必定是客觀準確的!數據庫
程序的運行離不開計算機環境(包括硬件和操做系統),這些客觀緣由會影響程序運行的速度並反應在程序的執行時間上。那麼如何才能客觀的評判一個算法的優劣呢?數組
3.時間複雜度與「大O記法」
咱們假定計算機執行算法每個基本操做的時間是固定的一個時間單位,那麼有多少個基本操做就表明會花費多少時間單位。顯然對於不一樣的機器環境而言,確切的單位時間是不一樣的,可是對於算法進行多少個基本操做(即花費多少時間單位)在規模數量級上倒是相同的,由此能夠忽略機器環境的影響而客觀的反應算法的時間效率。
對於算法的時間效率,咱們能夠用「大O記法」來表示。
「大O記法」:對於單調的整數函數f,若是存在一個整數函數g和實常數c>0,使得對於充分大的n總有f(n)<=c*g(n),就說函數g是f的一個漸近函數(忽略常數),記爲f(n)=O(g(n))。也就是說,在趨向無窮的極限意義下,函數f的增加速度受到函數g的約束,亦即函數f與函數g的特徵類似。
時間複雜度:假設存在函數g,使得算法A處理規模爲n的問題示例所用時間爲T(n)=O(g(n)),則稱O(g(n))爲算法A的漸近時間複雜度,簡稱時間複雜度,記爲T(n)
4.如何理解「大O記法」
對於算法進行特別具體的細緻分析雖然很好,但在實踐中的實際價值有限。對於算法的時間性質和空間性質,最重要的是其數量級和趨勢,這些是分析算法效率的主要部分。而計量算法基本操做數量的規模函數中那些常量因子能夠忽略不計。例如,能夠認爲3n2和100n2屬於同一個量級,若是兩個算法處理一樣規模實例的代價分別爲這兩個函數,就認爲它們的效率「差很少」,都爲n2級。
5.最壞時間複雜度
分析算法時,存在幾種可能的考慮:
- 算法完成工做最少須要多少基本操做,即最優時間複雜度
- 算法完成工做最多須要多少基本操做,即最壞時間複雜度
- 算法完成工做平均須要多少基本操做,即平均時間複雜度
對於最優時間複雜度,其價值不大,由於它沒有提供什麼有用信息,其反映的只是最樂觀最理想的狀況,沒有參考價值。
對於最壞時間複雜度,提供了一種保證,代表算法在此種程度的基本操做中必定能完成工做。
對於平均時間複雜度,是對算法的一個全面評價,所以它完整全面的反映了這個算法的性質。但另外一方面,這種衡量並無保證,不是每一個計算都能在這個基本操做內完成。並且,對於平均狀況的計算,也會由於應用算法的實例分佈可能並不均勻而難以計算。
所以,咱們主要關注算法的最壞狀況,亦即最壞時間複雜度。
6.時間複雜度的幾條基本計算規則
- 基本操做,即只有常數項,認爲其時間複雜度爲O(1)
- 順序結構,時間複雜度按加法進行計算
- 循環結構,時間複雜度按乘法進行計算
- 分支結構,時間複雜度取最大值
- 判斷一個算法的效率時,每每只須要關注操做數量的最高次項,其它次要項和常數項能夠忽略
- 在沒有特殊說明時,咱們所分析的算法的時間複雜度都是指最壞時間複雜度
- 可是,我一次面試的時候,被問到某算法的時間複雜度,我脫口而出該算法的最壞時間複雜度,面試官說我錯了,人家問的是最優時間複雜度......
算法分析
1第一次嘗試的算法核心部分
1 for a in range(0, 1001): 2 for b in range(0, 1001): 3 for c in range(0, 1001): 4 if a**2 + b**2 == c**2 and a+b+c == 1000: 5 print("a, b, c: %d, %d, %d" % (a, b, c))
時間複雜度:
T(n) = O(n*n*n) = O(n3)
2.第二次嘗試的算法核心部分
1 for a in range(0, 1001): 2 for b in range(0, 1001-a): 3 c = 1000 - a - b 4 if a**2 + b**2 == c**2: 5 print("a, b, c: %d, %d, %d" % (a, b, c))
時間複雜度:
T(n) = O(n*n*(1+1)) = O(n*n) = O(n2)
因而可知,咱們嘗試的第二種算法要比第一種算法的時間複雜度好多的。
常見時間複雜度
執行次數函數舉例 | 階 | 非正式術語 |
---|---|---|
12 | O(1) | 常數階 |
2n+3 | O(n) | 線性階 |
3n2+2n+1 | O(n2) | 平方階 |
5log2n+20 | O(logn) | 對數階 |
2n+3nlog2n+19 | O(nlogn) | nlogn階 |
6n3+2n2+3n+4 | O(n3) | 立方階 |
2n | O(2n) | 指數階 |
注意,常常將log2n(以2爲底的對數)簡寫成logn
常見時間複雜度之間的關係
所消耗的時間從小到大
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
Python內置類型性能分析
timeit模塊
timeit模塊能夠用來測試一小段Python代碼的執行速度。
1 class timeit.Timer(stmt='pass', setup='pass', timer=<timer function>) 2 Timer是測量小段代碼執行速度的類。 3 4 stmt參數是要測試的代碼語句(statment); 5 6 setup參數是運行代碼時須要的設置; 7 8 timer參數是一個定時器函數,與平臺有關。 9 10 timeit.Timer.timeit(number=1000000) 11 Timer類中測試語句執行速度的對象方法。number參數是測試代碼時的測試次數,默認爲1000000次。方法返回執行代碼的平均耗時,一個float類型的秒數。
list的操做測試
1 def test1(): 2 l = [] 3 for i in range(1000): 4 l = l + [i] 5 6 7 def test2(): 8 l = [] 9 for i in range(1000): 10 l.append(i) 11 12 13 def test3(): 14 l = [i for i in range(1000)] 15 16 17 def test4(): 18 l = list(range(1000)) 19 20 21 from timeit import Timer 22 23 t1 = Timer("test1()", "from __main__ import test1") 24 print("concat ", t1.timeit(number=1000), "seconds") 25 t2 = Timer("test2()", "from __main__ import test2") 26 print("append ", t2.timeit(number=1000), "seconds") 27 t3 = Timer("test3()", "from __main__ import test3") 28 print("comprehension ", t3.timeit(number=1000), "seconds") 29 t4 = Timer("test4()", "from __main__ import test4") 30 print("list range ", t4.timeit(number=1000), "seconds") 31 32 # concat 1.8858006307834618 seconds 33 # append 0.0998275414804981 seconds 34 # comprehension 0.040617778672828786 seconds 35 # list range 0.01583742648539621 seconds
list內置操做的時間複雜度
dict內置操做的時間複雜度
數據結構
咱們如何用Python中的類型來保存一個班的學生信息? 若是想要快速的經過學生姓名獲取其信息呢?
實際上當咱們在思考這個問題的時候,咱們已經用到了數據結構。列表和字典均可以存儲一個班的學生信息,可是想要在列表中獲取一名同窗的信息時,就要遍歷這個列表,其時間複雜度爲O(n),而使用字典存儲時,可將學生姓名做爲字典的鍵,學生信息做爲值,進而查詢時不須要遍歷即可快速獲取到學生信息,其時間複雜度爲O(1)。
咱們爲了解決問題,須要將數據保存下來,而後根據數據的存儲方式來設計算法實現進行處理,那麼數據的存儲方式不一樣就會致使須要不一樣的算法進行處理。咱們但願算法解決問題的效率越快越好,因而咱們就須要考慮數據究竟如何保存的問題,這就是數據結構。
在上面的問題中咱們能夠選擇Python中的列表或字典來存儲學生信息。列表和字典就是Python內建幫咱們封裝好的兩種數據結構。
概念
數據是一個抽象的概念,將其進行分類後獲得程序設計語言中的基本類型。如:int,float,char等。數據元素之間不是獨立的,存在特定的關係,這些關係即是結構。數據結構指數據對象中數據元素之間的關係。
Python給咱們提供了不少現成的數據結構類型,這些系統本身定義好的,不須要咱們本身去定義的數據結構叫作Python的內置數據結構,好比列表、元組、字典。而有些數據組織方式,Python系統裏面沒有直接定義,須要咱們本身去定義實現這些數據的組織方式,這些數據組織方式稱之爲Python的擴展數據結構,好比棧,隊列等。
算法與數據結構的區別
數據結構只是靜態的描述了數據元素之間的關係。
高效的程序須要在數據結構的基礎上設計和選擇算法。
程序 = 數據結構 + 算法
總結:算法是爲了解決實際問題而設計的,數據結構是算法須要處理的問題載體
抽象數據類型(Abstract Data Type)
抽象數據類型(ADT)的含義是指一個數學模型以及定義在此數學模型上的一組操做。即把數據類型和數據類型上的運算捆在一塊兒,進行封裝。引入抽象數據類型的目的是把數據類型的表示和數據類型上運算的實現與這些數據類型和運算在程序中的引用隔開,使它們相互獨立。
最經常使用的數據運算有五種:
- 插入
- 刪除
- 修改
- 查找
- 排序
順序表
1.順序表的基本形式
圖a表示的是順序表的基本形式,數據元素自己連續存儲,每一個元素所佔的存儲單元大小固定相同,元素的下標是其邏輯地址,而元素存儲的物理地址(實際內存地址)能夠經過存儲區的起始地址Loc (e0)加上邏輯地址(第i個元素)與存儲單元大小(c)的乘積計算而得,即:
Loc(ei) = Loc(e0) + c*i
故,訪問指定元素時無需從頭遍歷,經過計算即可得到對應地址,其時間複雜度爲O(1)。
若是元素的大小不統一,則須採用圖b的元素外置的形式,將實際數據元素另行存儲,而順序表中各單元位置保存對應元素的地址信息(即連接)。因爲每一個連接所需的存儲量相同,經過上述公式,能夠計算出元素連接的存儲位置,然後順着連接找到實際存儲的數據元素。注意,圖b中的c再也不是數據元素的大小,而是存儲一個連接地址所需的存儲量,這個量一般很小。
圖b這樣的順序表也被稱爲對實際數據的索引,這是最簡單的索引結構。
2.順序表的結構與實現
2.1順序表的結構
一個順序表的完整信息包括兩部分,一部分是表中的元素集合,另外一部分是爲實現正確操做而需記錄的信息,即有關表的總體狀況的信息,這部分信息主要包括元素存儲區的容量和當前表中已有的元素個數兩項。
2.2順序表的兩種基本實現方式
圖a爲一體式結構,存儲表信息的單元與元素存儲區以連續的方式安排在一塊存儲區裏,兩部分數據的總體造成一個完整的順序表對象。
一體式結構總體性強,易於管理。可是因爲數據元素存儲區域是表對象的一部分,順序表建立後,元素存儲區就固定了。
圖b爲分離式結構,表對象裏只保存與整個表有關的信息(即容量和元素個數),實際數據元素存放在另外一個獨立的元素存儲區裏,經過連接與基本表對象關聯。
元素存儲區替換
一體式結構因爲順序表信息區與數據區連續存儲在一塊兒,因此若想更換數據區,則只能總體搬遷,即整個順序表對象(指存儲順序表的結構信息的區域)改變了。
分離式結構若想更換數據區,只需將表信息區中的數據區連接地址更新便可,而該順序表對象不變。
元素存儲區擴充
用分離式結構的順序表,若將數據區更換爲存儲空間更大的區域,則能夠在不改變表對象的前提下對其數據存儲區進行了擴充,全部使用這個表的地方都沒必要修改。只要程序的運行環境(計算機系統)還有空閒存儲,這種表結構就不會由於滿了而致使操做沒法進行。人們把採用這種技術實現的順序表稱爲動態順序表,由於其容量能夠在使用中動態變化。
擴充的兩種策略
-
每次擴充增長固定數目的存儲位置,如每次擴充增長10個元素位置,這種策略可稱爲線性增加。
特色:節省空間,可是擴充操做頻繁,操做次數多。
-
每次擴充容量加倍,如每次擴充增長一倍存儲空間。
特色:減小了擴充操做的執行次數,但可能會浪費空間資源。以空間換時間,推薦的方式。
3.順序表的操做
3.1增長元素
如圖所示,爲順序表增長新元素111的三種方式
a. 尾端加入元素,時間複雜度爲O(1)
b. 非保序的加入元素(不常見),時間複雜度爲O(1)
c. 保序的元素加入,時間複雜度爲O(n)
3.2刪除元素
a. 刪除表尾元素,時間複雜度爲O(1)
b. 非保序的元素刪除(不常見),時間複雜度爲O(1)
c. 保序的元素刪除,時間複雜度爲O(n)
4.Python中的順序表
Python中的list和tuple兩種類型採用了順序表的實現技術,具備前面討論的順序表的全部性質。
tuple是不可變類型,即不變的順序表,所以不支持改變其內部狀態的任何操做,而其餘方面,則與list的性質相似。
list的基本實現技術:
Python標準類型list就是一種元素個數可變的線性表,能夠加入和刪除元素,並在各類操做中維持已有元素的順序(即保序),並且還具備如下行爲特徵:
-
基於下標(位置)的高效元素訪問和更新,時間複雜度應該是O(1);
爲知足該特徵,應該採用順序表技術,表中元素保存在一塊連續的存儲區中。
-
容許任意加入元素,並且在不斷加入元素的過程當中,表對象的標識(函數id獲得的值)不變。
爲知足該特徵,就必須能更換元素存儲區,而且爲保證更換存儲區時list對象的標識id不變,只能採用分離式實現技術。
在Python的官方實現中,list就是一種採用分離式技術實現的動態順序表。這就是爲何用list.append(x) (或 list.insert(len(list), x),即尾部插入)比在指定位置插入元素效率高的緣由。
在Python的官方實現中,list實現採用了以下的策略:在創建空表(或者很小的表)時,系統分配一塊能容納8個元素的存儲區;在執行插入操做(insert或append)時,若是元素存儲區滿就換一塊4倍大的存儲區。但若是此時的表已經很大(目前的閥值爲50000),則改變策略,採用加一倍的方法。引入這種改變策略的方式,是爲了不出現過多空閒的存儲位置。
鏈表
1.爲何須要鏈表
順序表的構建須要預先知道數據大小來申請連續的存儲空間,而在進行擴充時又須要進行數據的搬遷,因此使用起來並非很靈活。
鏈表結構能夠充分利用計算機內存空間,實現靈活的內存動態管理。
2.鏈表的定義
鏈表(Linked list)是一種常見的基礎數據結構,是一種線性表,可是不像順序表同樣連續存儲數據,而是在每個節點(數據存儲單元)裏存放下一個節點的位置信息(即地址)。
3.單向鏈表
單向鏈表也叫單鏈表,是鏈表中最簡單的一種形式,它的每一個節點包含兩個域,一個信息域(元素域)和一個連接域。這個連接指向鏈表中的下一個節點,而最後一個節點的連接域則指向一個空值。
- 表元素域elem用來存放具體的數據。
- 連接域next用來存放下一個節點的位置(python中的標識)
- 變量p指向鏈表的頭節點(首節點)的位置,從p出發能找到表中的任意節點。
節點實現
1 class SingleNode(object): 2 """單鏈表的結點""" 3 def __init__(self,item): 4 # _item存放數據元素 5 self.item = item 6 # _next是下一個節點的標識 7 self.next = None
單鏈表的操做
- is_empty() 鏈表是否爲空
- length() 鏈表長度
- travel() 遍歷整個鏈表
- add(item) 鏈表頭部添加元素
- append(item) 鏈表尾部添加元素
- insert(pos, item) 指定位置添加元素
- remove(item) 刪除節點
- search(item) 查找節點是否存在
單鏈表的實現:
1 lass SingleLinkList(object): 2 """單鏈表""" 3 4 def __init__(self): 5 self._head = None 6 7 def is_empty(self): 8 """判斷鏈表是否爲空""" 9 return self._head == None 10 11 def length(self): 12 """鏈表長度""" 13 # cur初始時指向頭節點 14 cur = self._head 15 count = 0 16 # 尾節點指向None,當未到達尾部時 17 while cur != None: 18 count += 1 19 # 將cur後移一個節點 20 cur = cur.next 21 return count 22 23 def travel(self): 24 """遍歷鏈表""" 25 cur = self._head 26 while cur != None: 27 print(cur.item) 28 cur = cur.next 29 print('')
頭部添加元素
1 def add(self, item): 2 """頭部添加元素""" 3 # 先建立一個保存item值的節點 4 node = SingleNode(item) 5 # 將新節點的連接域next指向頭節點,即_head指向的位置 6 node.next = self._head 7 # 將鏈表的頭_head指向新節點 8 self._head = node
尾部添加元素
1 def append(self, item): 2 """尾部添加元素""" 3 node = SingleNode(item) 4 # 先判斷鏈表是否爲空,如果空鏈表,則將_head指向新節點 5 if self.is_empty(): 6 self._head = node 7 # 若不爲空,則找到尾部,將尾節點的next指向新節點 8 else: 9 cur = self._head 10 while cur.next != None: 11 cur = cur.next 12 cur.next = node
指定位置添加元素
1 def insert(self, pos, item): 2 """指定位置添加元素""" 3 # 若指定位置pos爲第一個元素以前,則執行頭部插入 4 if pos <= 0: 5 self.add(item) 6 # 若指定位置超過鏈表尾部,則執行尾部插入 7 elif pos > (self.length()-1): 8 self.append(item) 9 # 找到指定位置 10 else: 11 node = SingleNode(item) 12 count = 0 13 # pre用來指向指定位置pos的前一個位置pos-1,初始從頭節點開始移動到指定位置 14 pre = self._head 15 while count < (pos-1): 16 count += 1 17 pre = pre.next 18 # 先將新節點node的next指向插入位置的節點 19 node.next = pre.next 20 # 將插入位置的前一個節點的next指向新節點 21 pre.next = node
刪除節點
1 def remove(self,item): 2 """刪除節點""" 3 cur = self._head 4 pre = None 5 while cur != None: 6 # 找到了指定元素 7 if cur.item == item: 8 # 若是第一個就是刪除的節點 9 if not pre: 10 # 將頭指針指向頭節點的後一個節點 11 self._head = cur.next 12 else: 13 # 將刪除位置前一個節點的next指向刪除位置的後一個節點 14 pre.next = cur.next 15 break 16 else: 17 # 繼續按鏈表後移節點 18 pre = cur 19 cur = cur.next
查找節點是否存在
1 def search(self,item): 2 """鏈表查找節點是否存在,並返回True或者False""" 3 cur = self._head 4 while cur != None: 5 if cur.item == item: 6 return True 7 cur = cur.next 8 return False
測試
1 if __name__ == "__main__": 2 ll = SingleLinkList() 3 ll.add(1) 4 ll.add(2) 5 ll.append(3) 6 ll.insert(2, 4) 7 print "length:",ll.length() 8 ll.travel() 9 print ll.search(3) 10 print ll.search(5) 11 ll.remove(1) 12 print "length:",ll.length() 13 ll.travel()
鏈表與順序表的對比
鏈表失去了順序表隨機讀取的優勢,同時鏈表因爲增長告終點的指針域,空間開銷比較大,但對存儲空間的使用要相對靈活。
鏈表與順序表的各類操做複雜度以下所示:
操做 | 鏈表 | 順序表 |
---|---|---|
訪問元素 | O(n) | O(1) |
在頭部插入/刪除 | O(1) | O(n) |
在尾部插入/刪除 | O(n) | O(1) |
在中間插入/刪除 | O(n) | O(n) |
注意雖然表面看起來複雜度都是 O(n),可是鏈表和順序表在插入和刪除時進行的是徹底不一樣的操做。鏈表的主要耗時操做是遍歷查找,刪除和插入操做自己的複雜度是O(1)。順序表查找很快,主要耗時的操做是拷貝覆蓋。由於除了目標元素在尾部的特殊狀況,順序表進行插入和刪除時須要對操做點以後的全部元素進行先後移位操做,只能經過拷貝和覆蓋的方法進行。
4.單向循環鏈表
單鏈表的一個變形是單向循環鏈表,鏈表中最後一個節點的next域再也不爲None,而是指向鏈表的頭節點
操做
- is_empty() 判斷鏈表是否爲空
- length() 返回鏈表的長度
- travel() 遍歷
- add(item) 在頭部添加一個節點
- append(item) 在尾部添加一個節點
- insert(pos, item) 在指定位置pos添加節點
- remove(item) 刪除一個節點
- search(item) 查找節點是否存在
- 實現
1 class Node(object): 2 """節點""" 3 def __init__(self, item): 4 self.item = item 5 self.next = None 6 7 8 class SinCycLinkedlist(object): 9 """單向循環鏈表""" 10 def __init__(self): 11 self._head = None 12 13 def is_empty(self): 14 """判斷鏈表是否爲空""" 15 return self._head == None 16 17 def length(self): 18 """返回鏈表的長度""" 19 # 若是鏈表爲空,返回長度0 20 if self.is_empty(): 21 return 0 22 count = 1 23 cur = self._head 24 while cur.next != self._head: 25 count += 1 26 cur = cur.next 27 return count 28 29 def travel(self): 30 """遍歷鏈表""" 31 if self.is_empty(): 32 return 33 cur = self._head 34 print(cur.item) 35 while cur.next != self._head: 36 cur = cur.next 37 print(cur.item) 38 print("") 39 40 41 def add(self, item): 42 """頭部添加節點""" 43 node = Node(item) 44 if self.is_empty(): 45 self._head = node 46 node.next = self._head 47 else: 48 #添加的節點指向_head 49 node.next = self._head 50 # 移到鏈表尾部,將尾部節點的next指向node 51 cur = self._head 52 while cur.next != self._head: 53 cur = cur.next 54 cur.next = node 55 #_head指向添加node的 56 self._head = node 57 58 def append(self, item): 59 """尾部添加節點""" 60 node = Node(item) 61 if self.is_empty(): 62 self._head = node 63 node.next = self._head 64 else: 65 # 移到鏈表尾部 66 cur = self._head 67 while cur.next != self._head: 68 cur = cur.next 69 # 將尾節點指向node 70 cur.next = node 71 # 將node指向頭節點_head 72 node.next = self._head 73 74 def insert(self, pos, item): 75 """在指定位置添加節點""" 76 if pos <= 0: 77 self.add(item) 78 elif pos > (self.length()-1): 79 self.append(item) 80 else: 81 node = Node(item) 82 cur = self._head 83 count = 0 84 # 移動到指定位置的前一個位置 85 while count < (pos-1): 86 count += 1 87 cur = cur.next 88 node.next = cur.next 89 cur.next = node 90 91 def remove(self, item): 92 """刪除一個節點""" 93 # 若鏈表爲空,則直接返回 94 if self.is_empty(): 95 return 96 # 將cur指向頭節點 97 cur = self._head 98 pre = None 99 # 若頭節點的元素就是要查找的元素item 100 if cur.item == item: 101 # 若是鏈表不止一個節點 102 if cur.next != self._head: 103 # 先找到尾節點,將尾節點的next指向第二個節點 104 while cur.next != self._head: 105 cur = cur.next 106 # cur指向了尾節點 107 cur.next = self._head.next 108 self._head = self._head.next 109 else: 110 # 鏈表只有一個節點 111 self._head = None 112 else: 113 pre = self._head 114 # 第一個節點不是要刪除的 115 while cur.next != self._head: 116 # 找到了要刪除的元素 117 if cur.item == item: 118 # 刪除 119 pre.next = cur.next 120 return 121 else: 122 pre = cur 123 cur = cur.next 124 # cur 指向尾節點 125 if cur.item == item: 126 # 尾部刪除 127 pre.next = cur.next 128 129 def search(self, item): 130 """查找節點是否存在""" 131 if self.is_empty(): 132 return False 133 cur = self._head 134 if cur.item == item: 135 return True 136 while cur.next != self._head: 137 cur = cur.next 138 if cur.item == item: 139 return True 140 return False 141 142 if __name__ == "__main__": 143 ll = SinCycLinkedlist() 144 ll.add(1) 145 ll.add(2) 146 ll.append(3) 147 ll.insert(2, 4) 148 ll.insert(4, 5) 149 ll.insert(0, 6) 150 print("length:",ll.length()) 151 ll.travel() 152 print(ll.search(3)) 153 print(ll.search(7)) 154 ll.remove(1) 155 print("length:",ll.length()) 156 ll.travel()
5.雙向鏈表
一種更復雜的鏈表是「雙向鏈表」或「雙面鏈表」。每一個節點有兩個連接:一個指向前一個節點,當此節點爲第一個節點時,指向空值;而另外一個指向下一個節點,當此節點爲最後一個節點時,指向空值。
操做
- is_empty() 鏈表是否爲空
- length() 鏈表長度
- travel() 遍歷鏈表
- add(item) 鏈表頭部添加
- append(item) 鏈表尾部添加
- insert(pos, item) 指定位置添加
- remove(item) 刪除節點
- search(item) 查找節點是否存在
實現
1 class Node(object): 2 """雙向鏈表節點""" 3 def __init__(self, item): 4 self.item = item 5 self.next = None 6 self.prev = None 7 8 9 class DLinkList(object): 10 """雙向鏈表""" 11 def __init__(self): 12 self._head = None 13 14 def is_empty(self): 15 """判斷鏈表是否爲空""" 16 return self._head == None 17 18 def length(self): 19 """返回鏈表的長度""" 20 cur = self._head 21 count = 0 22 while cur != None: 23 count += 1 24 cur = cur.next 25 return count 26 27 def travel(self): 28 """遍歷鏈表""" 29 cur = self._head 30 while cur != None: 31 print(cur.item) 32 cur = cur.next 33 print("") 34 35 def add(self, item): 36 """頭部插入元素""" 37 node = Node(item) 38 if self.is_empty(): 39 # 若是是空鏈表,將_head指向node 40 self._head = node 41 else: 42 # 將node的next指向_head的頭節點 43 node.next = self._head 44 # 將_head的頭節點的prev指向node 45 self._head.prev = node 46 # 將_head 指向node 47 self._head = node 48 49 def append(self, item): 50 """尾部插入元素""" 51 node = Node(item) 52 if self.is_empty(): 53 # 若是是空鏈表,將_head指向node 54 self._head = node 55 else: 56 # 移動到鏈表尾部 57 cur = self._head 58 while cur.next != None: 59 cur = cur.next 60 # 將尾節點cur的next指向node 61 cur.next = node 62 # 將node的prev指向cur 63 node.prev = cur 64 65 66 67 def search(self, item): 68 """查找元素是否存在""" 69 cur = self._head 70 while cur != None: 71 if cur.item == item: 72 return True 73 cur = cur.next 74 return False
指定位置插入節點
1 def insert(self, pos, item): 2 """在指定位置添加節點""" 3 if pos <= 0: 4 self.add(item) 5 elif pos > (self.length()-1): 6 self.append(item) 7 else: 8 node = Node(item) 9 cur = self._head 10 count = 0 11 # 移動到指定位置的前一個位置 12 while count < (pos-1): 13 count += 1 14 cur = cur.next 15 # 將node的prev指向cur 16 node.prev = cur 17 # 將node的next指向cur的下一個節點 18 node.next = cur.next 19 # 將cur的下一個節點的prev指向node 20 cur.next.prev = node 21 # 將cur的next指向node 22 cur.next = node
刪除元素
1 def remove(self, item): 2 """刪除元素""" 3 if self.is_empty(): 4 return 5 else: 6 cur = self._head 7 if cur.item == item: 8 # 若是首節點的元素便是要刪除的元素 9 if cur.next == None: 10 # 若是鏈表只有這一個節點 11 self._head = None 12 else: 13 # 將第二個節點的prev設置爲None 14 cur.next.prev = None 15 # 將_head指向第二個節點 16 self._head = cur.next 17 return 18 while cur != None: 19 if cur.item == item: 20 # 將cur的前一個節點的next指向cur的後一個節點 21 cur.prev.next = cur.next 22 # 將cur的後一個節點的prev指向cur的前一個節點 23 cur.next.prev = cur.prev 24 break 25 cur = cur.next
測試
1 if __name__ == "__main__": 2 ll = DLinkList() 3 ll.add(1) 4 ll.add(2) 5 ll.append(3) 6 ll.insert(2, 4) 7 ll.insert(4, 5) 8 ll.insert(0, 6) 9 print("length:",ll.length()) 10 ll.travel() 11 print(ll.search(3)) 12 print(ll.search(4)) 13 ll.remove(1) 14 print("length:",ll.length()) 15 ll.travel()
棧
棧(stack),有些地方稱爲堆棧,是一種容器,可存入數據元素、訪問元素、刪除元素,它的特色在於只能容許在容器的一端(稱爲棧頂端指標,英語:top)進行加入數據(英語:push)和輸出數據(英語:pop)的運算。沒有了位置概念,保證任什麼時候候能夠訪問、刪除的元素都是此前最後存入的那個元素,肯定了一種默認的訪問順序。
因爲棧數據結構只容許在一端進行操做,於是按照後進先出(LIFO, Last In First Out)的原理運做。
棧結構實現:
棧能夠用順序表實現,也能夠用鏈表實現。
棧的操做
- Stack() 建立一個新的空棧
- push(item) 添加一個新的元素item到棧頂
- pop() 彈出棧頂元素
- peek() 返回棧頂元素
- is_empty() 判斷棧是否爲空
- size() 返回棧的元素個數
1 class Stack(object): 2 """棧""" 3 4 def __init__(self): 5 self.__li = [] 6 7 def is_empty(self): 8 """判斷是否爲空""" 9 return self.__li == [] 10 11 def push(self, item): 12 """加入元素""" 13 self.__li.append(item) 14 15 def pop(self): 16 """彈出元素""" 17 return self.__li.pop() 18 19 def peek(self): 20 """返回棧頂元素""" 21 return self.__li[len(self.__li) - 1] 22 23 def size(self): 24 """返回棧的大小""" 25 return len(self.__li) 26 27 28 if __name__ == "__main__": 29 stack = Stack() 30 stack.push("hello") 31 stack.push("world") 32 stack.push("lcg") 33 print(stack.size()) 34 print(stack.peek()) 35 print(stack.pop()) 36 print(stack.pop()) 37 print(stack.pop())
隊列
隊列(queue)是隻容許在一端進行插入操做,而在另外一端進行刪除操做的線性表。
隊列是一種先進先出的(First In First Out)的線性表,簡稱FIFO。容許插入的一端爲隊尾,容許刪除的一端爲隊頭。隊列不容許在中間部位進行操做!假設隊列是q=(a1,a2,……,an),那麼a1就是隊頭元素,而an是隊尾元素。這樣咱們就能夠刪除時,老是從a1開始,而插入時,老是在隊列最後。這也比較符合咱們一般生活中的習慣,排在第一個的優先出列,最後來的固然排在隊伍最後。
隊列的實現:
同棧同樣,隊列也能夠用順序表或者鏈表實現。
操做
- Queue() 建立一個空的隊列
- enqueue(item) 往隊列中添加一個item元素
- dequeue() 從隊列頭部刪除一個元素
- is_empty() 判斷一個隊列是否爲空
- size() 返回隊列的大小
1 class Queue(object): 2 """隊列""" 3 def __init__(self): 4 self.__li = [] 5 6 def is_empty(self): 7 return self.__li == [] 8 9 def enqueue(self, item): 10 """進隊列""" 11 self.__li.insert(0,item) 12 13 def dequeue(self): 14 """出隊列""" 15 return self.__li.pop() 16 17 def size(self): 18 """返回大小""" 19 return len(self.__li) 20 21 if __name__ == "__main__": 22 q = Queue() 23 q.enqueue("hello") 24 q.enqueue("world") 25 q.enqueue("lcg") 26 print(q.size()) 27 print(q.dequeue()) 28 print(q.dequeue()) 29 print(q.dequeue())
雙端隊列
雙端隊列(deque,全名double-ended queue),是一種具備隊列和棧的性質的數據結構。
雙端隊列中的元素能夠從兩端彈出,其限定插入和刪除操做在表的兩端進行。雙端隊列能夠在隊列任意一端入隊和出隊。
操做
- Deque() 建立一個空的雙端隊列
- add_front(item) 從隊頭加入一個item元素
- add_rear(item) 從隊尾加入一個item元素
- remove_front() 從隊頭刪除一個item元素
- remove_rear() 從隊尾刪除一個item元素
- is_empty() 判斷雙端隊列是否爲空
- size() 返回隊列的大小
1 class Deque(object): 2 """雙端隊列""" 3 def __init__(self): 4 self.__li = [] 5 6 def is_empty(self): 7 """判斷隊列是否爲空""" 8 return self.__li == [] 9 10 def add_front(self, item): 11 """在隊頭添加元素""" 12 self.__li.insert(0,item) 13 14 def add_rear(self, item): 15 """在隊尾添加元素""" 16 self.__li.append(item) 17 18 def remove_front(self): 19 """從隊頭刪除元素""" 20 return self.__li.pop(0) 21 22 def remove_rear(self): 23 """從隊尾刪除元素""" 24 return self.__li.pop() 25 26 def size(self): 27 """返回隊列大小""" 28 return len(self.__li) 29 30 31 if __name__ == "__main__": 32 deque = Deque() 33 deque.add_front(1) 34 deque.add_front(2) 35 deque.add_rear(3) 36 deque.add_rear(4) 37 print(deque.size()) 38 print(deque.remove_front()) 39 print(deque.remove_front()) 40 print(deque.remove_rear()) 41 print(deque.remove_rear())
排序與搜索
排序算法(英語:Sorting algorithm)是一種能將一串數據依照特定順序進行排列的一種算法。
排序算法的穩定性
穩定性:穩定排序算法會讓本來有相等鍵值的紀錄維持相對次序。也就是若是一個排序算法是穩定的,當有兩個相等鍵值的紀錄R和S,且在本來的列表中R出如今S以前,在排序過的列表中R也將會是在S以前。
當相等的元素是沒法分辨的,好比像是整數,穩定性並非一個問題。然而,假設如下的數對將要以他們的第一個數字來排序。
1
|
(
4
,
1
) (
3
,
1
) (
3
,
7
)(
5
,
6
)
|
在這個情況下,有可能產生兩種不一樣的結果,一個是讓相等鍵值的紀錄維持相對的次序,而另一個則沒有:
1
2
|
(
3
,
1
) (
3
,
7
) (
4
,
1
) (
5
,
6
) (維持次序)
(
3
,
7
) (
3
,
1
) (
4
,
1
) (
5
,
6
) (次序被改變)
|
不穩定排序算法可能會在相等的鍵值中改變紀錄的相對次序,可是穩定排序算法歷來不會如此。不穩定排序算法能夠被特別地實現爲穩定。做這件事情的一個方式是人工擴充鍵值的比較,如此在其餘方面相同鍵值的兩個對象間之比較,(好比上面的比較中加入第二個標準:第二個鍵值的大小)就會被決定使用在原先數據次序中的條目,看成一個同分決賽。然而,要記住這種次序一般牽涉到額外的空間負擔。
冒泡排序
冒泡排序(英語:Bubble Sort)是一種簡單的排序算法。它重複地遍歷要排序的數列,一次比較兩個元素,若是他們的順序錯誤就把他們交換過來。遍歷數列的工做是重複地進行直到沒有再須要交換,也就是說該數列已經排序完成。這個算法的名字由來是由於越小的元素會經由交換慢慢「浮」到數列的頂端。
冒泡排序算法的運做以下:
- 比較相鄰的元素。若是第一個比第二個大(升序),就交換他們兩個。
- 對每一對相鄰元素做一樣的工做,從開始第一對到結尾的最後一對。這步作完後,最後的元素會是最大的數。
- 針對全部的元素重複以上的步驟,除了最後一個。
- 持續每次對愈來愈少的元素重複上面的步驟,直到沒有任何一對數字須要比較。
冒泡排序的分析
交換過程圖示(第一次):
代碼實現:O(n2)
1 def bubble_sort(alist): 2 n = len(alist) 3 for j in range(n - 1): 4 for i in range(0, n - 1 - j): 5 if alist[i] > alist[i + 1]: 6 alist[i], alist[i + 1] = alist[i + 1], alist[i] 7 8 9 if __name__ == '__main__': 10 li = [1, 2, 3, 4, 9, 8, 7, 6] 11 bubble_sort(li) 12 print(li)
優化:O(n)
1 def bubble_sort(alist): 2 n = len(alist) 3 for j in range(n - 1): 4 exchange = False 5 for i in range(n - 1 - j): 6 if alist[i] > alist[i + 1]: 7 alist[i], alist[i + 1] = alist[i + 1], alist[i] 8 exchange = True 9 if not exchange: 10 break
時間複雜度
- 最優時間複雜度:O(n) (表示遍歷一次發現沒有任何能夠交換的元素,排序結束。)
- 最壞時間複雜度:O(n2)
- 穩定性:穩定
冒泡排序的演示
選擇排序
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工做原理以下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,而後,再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到已排序序列的末尾。以此類推,直到全部元素均排序完畢。
選擇排序的主要優勢與數據移動有關。若是某個元素位於正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,所以對n個元素的表進行排序總共進行至多n-1次交換。在全部的徹底依靠交換去移動元素的排序方法中,選擇排序屬於很是好的一種。
選擇排序分析
排序過程:
紅色表示當前最小值,黃色表示已排序序列,藍色表示當前位置。
代碼實現:
1 def select_sort(alist): 2 n = len(alist) 3 for j in range(n - 1): 4 min_index = j 5 for i in range(j + 1, n): 6 if alist[min_index] > alist[i]: 7 min_index = i 8 alist[j], alist[min_index] = alist[min_index], alist[j] 9 10 11 if __name__ == '__main__': 12 li = [1, 2, 3, 4, 9, 8, 7, 6] 13 select_sort(li) 14 print(li)
時間複雜度
- 最優時間複雜度:O(n2)
- 最壞時間複雜度:O(n2)
- 穩定性:不穩定(考慮升序每次選擇最大的狀況)
選擇排序演示:
插入排序
插入排序(英語:Insertion Sort)是一種簡單直觀的排序算法。它的工做原理是經過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,在從後向前掃描過程當中,須要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。
插入排序分析
代碼實現:
1 def insert_sort(alist): 2 n = len(alist) 3 for j in range(1, n): 4 i = j 5 while i > 0: 6 if alist[i] < alist[i - 1]: 7 alist[i], alist[i - 1] = alist[i - 1], alist[i] 8 i -= 1 9 else: 10 break 11 12 13 if __name__ == '__main__': 14 li = [1, 2, 3, 4, 9, 8, 7, 6] 15 insert_sort(li) 16 print(li)
時間複雜度
- 最優時間複雜度:O(n) (升序排列,序列已經處於升序狀態)
- 最壞時間複雜度:O(n2)
- 穩定性:穩定
希爾排序
希爾排序(Shell Sort)是插入排序的一種。也稱縮小增量排序,是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法。該方法因DL.Shell於1959年提出而得名。 希爾排序是把記錄按下標的必定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減小,每組包含的關鍵詞愈來愈多,當增量減至1時,整個文件恰被分紅一組,算法便終止。
希爾排序過程
希爾排序的基本思想是:將數組列在一個表中並對列分別進行插入排序,重複這過程,不過每次用更長的列(步長更長了,列數更少了)來進行。最後整個表就只有一列了。將數組轉換至表是爲了更好地理解這算法,算法自己仍是使用數組進行排序。
例如,假設有這樣一組數[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],若是咱們以步長爲5開始進行排序,咱們能夠經過將這列表放在有5列的表中來更好地描述算法,這樣他們就應該看起來是這樣(豎着的元素是步長組成):
1
2
3
4
|
13
14
94
33
82
25
59
94
65
23
45
27
73
25
39
10
|
而後咱們對每列進行排序:
1
2
3
4
|
10
14
73
25
23
13
27
94
33
39
25
59
94
65
82
45
|
將上述四行數字,依序接在一塊兒時咱們獲得:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ]。這時10已經移至正確位置了,而後再以3爲步長進行排序:
1
2
3
4
5
6
|
10
14
73
25
23
13
27
94
33
39
25
59
94
65
82
45
|
排序以後變爲:
1
2
3
4
5
6
|
10
14
13
25
23
33
27
25
59
39
65
73
45
94
82
94
|
最後以1步長進行排序(此時就是簡單的插入排序了)
希爾排序的分析
代碼實現
1 def shell_sort(alist): 2 n = len(alist) 3 # 初始步長 4 gap = n / 2 5 while gap > 0: 6 # 按步長進行插入排序 7 for i in range(gap, n): 8 j = i 9 # 插入排序 10 while j>=gap and alist[j-gap] > alist[j]: 11 alist[j-gap], alist[j] = alist[j], alist[j-gap] 12 j -= gap 13 # 獲得新的步長 14 gap = gap / 2 15 16 alist = [54,26,93,17,77,31,44,55,20] 17 shell_sort(alist) 18 print(alist)
時間複雜度
- 最優時間複雜度:根據步長序列的不一樣而不一樣
- 最壞時間複雜度:O(n2)
- 穩定想:不穩定
快速排序
快速排序(英語:Quicksort),又稱劃分交換排序(partition-exchange sort),經過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的全部數據都比另一部分的全部數據都要小,而後再按此方法對這兩部分數據分別進行快速排序,整個排序過程能夠遞歸進行,以此達到整個數據變成有序序列。
步驟爲:
- 從數列中挑出一個元素,稱爲"基準"(pivot),
- 從新排序數列,全部元素比基準值小的擺放在基準前面,全部元素比基準值大的擺在基準的後面(相同的數能夠到任一邊)。在這個分區結束以後,該基準就處於數列的中間位置。這個稱爲分區(partition)操做。
- 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
遞歸的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞歸下去,可是這個算法總會結束,由於在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。
快速排序的分析
代碼實現
1 def quick_sort(alist, start, end): 2 """快速排序""" 3 4 # 遞歸的退出條件 5 if start >= end: 6 return 7 8 # 設定起始元素爲要尋找位置的基準元素 9 mid = alist[start] 10 11 # low爲序列左邊的由左向右移動的遊標 12 low = start 13 14 # high爲序列右邊的由右向左移動的遊標 15 high = end 16 17 while low < high: 18 # 若是low與high未重合,high指向的元素不比基準元素小,則high向左移動 19 while low < high and alist[high] >= mid: 20 high -= 1 21 # 將high指向的元素放到low的位置上 22 alist[low] = alist[high] 23 24 # 若是low與high未重合,low指向的元素比基準元素小,則low向右移動 25 while low < high and alist[low] < mid: 26 low += 1 27 # 將low指向的元素放到high的位置上 28 alist[high] = alist[low] 29 30 # 退出循環後,low與high重合,此時所指位置爲基準元素的正確位置 31 # 將基準元素放到該位置 32 alist[low] = mid 33 34 # 對基準元素左邊的子序列進行快速排序 35 quick_sort(alist, start, low-1) 36 37 # 對基準元素右邊的子序列進行快速排序 38 quick_sort(alist, low+1, end) 39 40 41 alist = [54,26,93,17,77,31,44,55,20] 42 quick_sort(alist,0,len(alist)-1) 43 print(alist)
時間複雜度
- 最優時間複雜度:O(nlogn)
- 最壞時間複雜度:O(n2)
- 穩定性:不穩定
從一開始快速排序平均須要花費O(n log n)時間的描述並不明顯。可是不難觀察到的是分區運算,數組的元素都會在每次循環中走訪過一次,使用O(n)的時間。在使用結合(concatenation)的版本中,這項運算也是O(n)。
在最好的狀況,每次咱們運行一次分區,咱們會把一個數列分爲兩個幾近相等的片斷。這個意思就是每次遞歸調用處理一半大小的數列。所以,在到達大小爲一的數列前,咱們只要做log n次嵌套的調用。這個意思就是調用樹的深度是O(log n)。可是在同一層次結構的兩個程序調用中,不會處理到原來數列的相同部分;所以,程序調用的每一層次結構總共所有僅須要O(n)的時間(每一個調用有某些共同的額外耗費,可是由於在每一層次結構僅僅只有O(n)個調用,這些被概括在O(n)係數中)。結果是這個算法僅需使用O(n log n)時間。
歸併排序
歸併排序是採用分治法的一個很是典型的應用。歸併排序的思想就是先遞歸分解數組,再合併數組。
將數組分解最小以後,而後合併兩個有序數組,基本思路是比較兩個數組的最前面的數,誰小就先取誰,取了後相應的指針就日後移一位。而後再比較,直至一個數組爲空,最後把另外一個數組的剩餘部分複製過來便可。
歸併排序的分析
代碼實現
1 def merge_sort(alist): 2 if len(alist) <= 1: 3 return alist 4 # 二分分解 5 num = len(alist)/2 6 left = merge_sort(alist[:num]) 7 right = merge_sort(alist[num:]) 8 # 合併 9 return merge(left,right) 10 11 def merge(left, right): 12 '''合併操做,將兩個有序數組left[]和right[]合併成一個大的有序數組''' 13 #left與right的下標指針 14 l, r = 0, 0 15 result = [] 16 while l<len(left) and r<len(right): 17 if left[l] < right[r]: 18 result.append(left[l]) 19 l += 1 20 else: 21 result.append(right[r]) 22 r += 1 23 result += left[l:] 24 result += right[r:] 25 return result 26 27 alist = [54,26,93,17,77,31,44,55,20] 28 sorted_alist = mergeSort(alist) 29 print(sorted_alist)
時間複雜度
- 最優時間複雜度:O(nlogn)
- 最壞時間複雜度:O(nlogn)
- 穩定性:穩定
常見排序算法效率比較
搜索
搜索是在一個項目集合中找到一個特定項目的算法過程。搜索一般的答案是真的或假的,由於該項目是否存在。 搜索的幾種常見方法:順序查找、二分法查找、二叉樹查找、哈希查找
二分法查找
二分查找又稱折半查找,優勢是比較次數少,查找速度快,平均性能好;其缺點是要求待查表爲有序表,且插入刪除困難。所以,折半查找方法適用於不常常變更而查找頻繁的有序列表。首先,假設表中元素是按升序排列,將表中間位置記錄的關鍵字與查找關鍵字比較,若是二者相等,則查找成功;不然利用中間位置記錄將表分紅前、後兩個子表,若是中間位置記錄的關鍵字大於查找關鍵字,則進一步查找前一子表,不然進一步查找後一子表。重複以上過程,直到找到知足條件的記錄,使查找成功,或直到子表不存在爲止,此時查找不成功。
二分法查找實現
(非遞歸實現)
1 def binary_search(alist, item): 2 first = 0 3 last = len(alist)-1 4 while first<=last: 5 midpoint = (first + last)/2 6 if alist[midpoint] == item: 7 return True 8 elif item < alist[midpoint]: 9 last = midpoint-1 10 else: 11 first = midpoint+1 12 return False 13 testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,] 14 print(binary_search(testlist, 3)) 15 print(binary_search(testlist, 13))
(遞歸實現)
1 def binary_search(alist, item): 2 if len(alist) == 0: 3 return False 4 else: 5 midpoint = len(alist)//2 6 if alist[midpoint]==item: 7 return True 8 else: 9 if item<alist[midpoint]: 10 return binary_search(alist[:midpoint],item) 11 else: 12 return binary_search(alist[midpoint+1:],item) 13 14 testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,] 15 print(binary_search(testlist, 3)) 16 print(binary_search(testlist, 13))
時間複雜度
- 最優時間複雜度:O(1)
- 最壞時間複雜度:O(logn)
樹與樹算法
樹的概念
樹(英語:tree)是一種抽象數據類型(ADT)或是實做這種抽象數據類型的數據結構,用來模擬具備樹狀結構性質的數據集合。它是由n(n>=1)個有限節點組成一個具備層次關係的集合。把它叫作「樹」是由於它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具備如下的特色:
- 每一個節點有零個或多個子節點;
- 沒有父節點的節點稱爲根節點;
- 每個非根節點有且只有一個父節點;
- 除了根節點外,每一個子節點能夠分爲多個不相交的子樹;
好比說:
樹的術語
- 節點的度:一個節點含有的子樹的個數稱爲該節點的度;
- 樹的度:一棵樹中,最大的節點的度稱爲樹的度;
- 葉節點或終端節點:度爲零的節點;
- 父親節點或父節點:若一個節點含有子節點,則這個節點稱爲其子節點的父節點;
- 孩子節點或子節點:一個節點含有的子樹的根節點稱爲該節點的子節點;
- 兄弟節點:具備相同父節點的節點互稱爲兄弟節點;
- 節點的層次:從根開始定義起,根爲第1層,根的子節點爲第2層,以此類推;
- 樹的高度或深度:樹中節點的最大層次;
- 堂兄弟節點:父節點在同一層的節點互爲堂兄弟;
- 節點的祖先:從根到該節點所經分支上的全部節點;
- 子孫:以某節點爲根的子樹中任一節點都稱爲該節點的子孫。
- 森林:由m(m>=0)棵互不相交的樹的集合稱爲森林;
樹的種類
- 無序樹:樹中任意節點的子節點之間沒有順序關係,這種樹稱爲無序樹,也稱爲自由樹;
- 有序樹:樹中任意節點的子節點之間有順序關係,這種樹稱爲有序樹;
- 二叉樹:每一個節點最多含有兩個子樹的樹稱爲二叉樹;
- 徹底二叉樹:對於一顆二叉樹,假設其深度爲d(d>1)。除了第d層外,其它各層的節點數目均已達最大值,且第d層全部節點從左向右連續地緊密排列,這樣的二叉樹被稱爲徹底二叉樹,其中滿二叉樹的定義是全部葉節點都在最底層的徹底二叉樹;
- 平衡二叉樹(AVL樹):當且僅當任何節點的兩棵子樹的高度差不大於1的二叉樹;
- 排序二叉樹(二叉查找樹(英語:Binary Search Tree),也稱二叉搜索樹、有序二叉樹);
- 霍夫曼樹(用於信息編碼):帶權路徑最短的二叉樹稱爲哈夫曼樹或最優二叉樹;
- B樹:一種對讀寫操做進行優化的自平衡的二叉查找樹,可以保持數據有序,擁有多餘兩個子樹。
- 二叉樹:每一個節點最多含有兩個子樹的樹稱爲二叉樹;
樹的存儲與表示
順序存儲:將數據結構存儲在固定的數組中,然在遍歷速度上有必定的優點,但因所佔空間比較大,是非主流二叉樹。二叉樹一般以鏈式存儲。
鏈式存儲:
因爲對節點的個數沒法掌握,常見樹的存儲表示都轉換成二叉樹進行處理,子節點個數最多爲2
常見的一些樹的應用場景
1.xml,html等,那麼編寫這些東西的解析器的時候,不可避免用到樹
2.路由協議就是使用了樹的算法
3.mysql數據庫索引
4.文件系統的目錄結構
5.因此不少經典的AI算法其實都是樹搜索,此外機器學習中的decision tree也是樹結構
二叉樹
二叉樹的基本概念
二叉樹是每一個節點最多有兩個子樹的樹結構。一般子樹被稱做「左子樹」(left subtree)和「右子樹」(right subtree)
二叉樹的性質(特性)
性質1: 在二叉樹的第i層上至多有2^(i-1)個結點(i>0)
性質2: 深度爲k的二叉樹至多有2^k - 1個結點(k>0)
性質3: 對於任意一棵二叉樹,若是其葉結點數爲N0,而度數爲2的結點總數爲N2,則N0=N2+1;
性質4:具備n個結點的徹底二叉樹的深度必爲 log2(n+1)
性質5:對徹底二叉樹,若從上至下、從左至右編號,則編號爲i 的結點,其左孩子編號必爲2i,其右孩子編號必爲2i+1;其雙親的編號必爲i/2(i=1 時爲根,除外)
(1)徹底二叉樹——若設二叉樹的高度爲h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第h層有葉子結點,而且葉子結點都是從左到右依次排布,這就是徹底二叉樹。
(2)滿二叉樹——除了葉結點外每個結點都有左右子葉且葉子結點都處在最底層的二叉樹。
二叉樹的節點表示以及樹的建立
經過使用Node類中定義三個屬性,分別爲elem自己的值,還有lchild左孩子和rchild右孩子
1 class Node(object): 2 """節點類""" 3 def __init__(self, elem=-1, lchild=None, rchild=None): 4 self.elem = elem 5 self.lchild = lchild 6 self.rchild = rchild
樹的建立,建立一個樹的類,並給一個root根節點,一開始爲空,隨後添加節點
1 class Tree(object): 2 """樹類""" 3 def __init__(self, root=None): 4 self.root = root 5 6 def add(self, elem): 7 """爲樹添加節點""" 8 node = Node(elem) 9 #若是樹是空的,則對根節點賦值 10 if self.root == None: 11 self.root = node 12 else: 13 queue = [] 14 queue.append(self.root) 15 #對已有的節點進行層次遍歷 16 while queue: 17 #彈出隊列的第一個元素 18 cur = queue.pop(0) 19 if cur.lchild == None: 20 cur.lchild = node 21 return 22 elif cur.rchild == None: 23 cur.rchild = node 24 return 25 else: 26 #若是左右子樹都不爲空,加入隊列繼續判斷 27 queue.append(cur.lchild) 28 queue.append(cur.rchild)
二叉樹的遍歷
樹的遍歷是樹的一種重要的運算。所謂遍歷是指對樹中全部結點的信息的訪問,即依次對樹中每一個結點訪問一次且僅訪問一次,咱們把這種對全部節點的訪問稱爲遍歷(traversal)。那麼樹的兩種重要的遍歷模式是深度優先遍歷和廣度優先遍歷,深度優先通常用遞歸,廣度優先通常用隊列。通常狀況下能用遞歸實現的算法大部分也能用堆棧來實現。
深度優先遍歷
對於一顆二叉樹,深度優先搜索(Depth First Search)是沿着樹的深度遍歷樹的節點,儘量深的搜索樹的分支。
那麼深度遍歷有重要的三種方法。這三種方式常被用於訪問樹的節點,它們之間的不一樣在於訪問每一個節點的次序不一樣。這三種遍歷分別叫作先序遍歷(preorder),中序遍歷(inorder)和後序遍歷(postorder)。咱們來給出它們的詳細定義,而後舉例看看它們的應用。
先序遍歷 在先序遍歷中,咱們先訪問根節點,而後遞歸使用先序遍歷訪問左子樹,再遞歸使用先序遍歷訪問右子樹
根節點->左子樹->右子樹
1 def preorder(self, root): 2 """遞歸實現先序遍歷""" 3 if root == None: 4 return 5 print root.elem 6 self.preorder(root.lchild) 7 self.preorder(root.rchild)
中序遍歷 在中序遍歷中,咱們遞歸使用中序遍歷訪問左子樹,而後訪問根節點,最後再遞歸使用中序遍歷訪問右子樹
左子樹->根節點->右子樹
1 def inorder(self, root): 2 """遞歸實現中序遍歷""" 3 if root == None: 4 return 5 self.inorder(root.lchild) 6 print root.elem 7 self.inorder(root.rchild)
後序遍歷 在後序遍歷中,咱們先遞歸使用後序遍歷訪問左子樹和右子樹,最後訪問根節點
左子樹->右子樹->根節點
1 def postorder(self, root): 2 """遞歸實現後續遍歷""" 3 if root == None: 4 return 5 self.postorder(root.lchild) 6 self.postorder(root.rchild) 7 print root.elem
廣度優先遍歷(層次遍歷)
從樹的root開始,從上到下從從左到右遍歷整個樹的節點
1 def breadth_travel(self, root): 2 """利用隊列實現樹的層次遍歷""" 3 if root == None: 4 return 5 queue = [] 6 queue.append(root) 7 while queue: 8 node = queue.pop(0) 9 print node.elem, 10 if node.lchild != None: 11 queue.append(node.lchild) 12 if node.rchild != None: 13 queue.append(node.rchild)
補充:
排序算法:https://www.toptal.com/developers/sorting-algorithms/
紙上談兵系列:http://www.cnblogs.com/vamei/archive/2013/03/22/2974052.html