python 經常使用算法及解析

1.算法定義 

算法(Algorithm)是指解題方案的準確而完整的描述,是一系列解決問題的清晰指令,算法表明着用系統的方法描述解決問題的策略機制。也就是說,可以對必定規範的輸入,在有限時間內得到所要求的輸出。若是一個算法有缺陷,或不適合於某個問題,執行這個算法將不會解決這個問題。不一樣的算法可能用不一樣的時間、空間或效率來完成一樣的任務。一個算法的優劣能夠用空間複雜度與時間複雜度來衡量。html

一個算法應該具備如下七個重要的特徵:node

①有窮性(Finiteness):算法的有窮性是指算法必須能在執行有限個步驟以後終止;git

②確切性(Definiteness):算法的每一步驟必須有確切的定義;算法

③輸入項(Input):一個算法有0個或多個輸入,以刻畫運算對象的初始狀況,所謂0個輸     入是指算法自己定出了初始條件;shell

④輸出項(Output):一個算法有一個或多個輸出,以反映對輸入數據加工後的結果。沒       有輸出的算法是毫無心義的;數據庫

⑤可行性(Effectiveness):算法中執行的任何計算步驟都是能夠被分解爲基本的可執行       的操做步,即每一個計算步均可以在有限時間內完成(也稱之爲有效性);編程

⑥高效性(High efficiency):執行速度快,佔用資源少;數組

⑦健壯性(Robustness):對數據響應正確。數據結構

 

 

2. 時間複雜度

計算機科學中,算法的時間複雜度是一個函數,它定量描述了該算法的運行時間,時間複雜度經常使用大O符號(大O符號(Big O notation)是用於描述函數漸進行爲的數學符號。更確切地說,它是用另外一個(一般更簡單的)函數來描述一個函數數量級的漸近上界。在數學中,它通常用來刻畫被截斷的無窮級數尤爲是漸近級數的剩餘項;在計算機科學中,它在分析算法複雜性的方面很是有用。)表述,使用這種方式時,時間複雜度可被稱爲是漸近的,它考察當輸入值大小趨近無窮時的狀況。app

大O,簡而言之能夠認爲它的含義是「order of」(大約是)

無窮大漸近
大O符號在分析算法效率的時候很是有用。舉個例子,解決一個規模爲 n 的問題所花費的時間(或者所需步驟的數目)能夠被求得:T(n) = 4n^2 - 2n + 2。
當 n 增大時,n^2; 項將開始占主導地位,而其餘各項能夠被忽略——舉例說明:當 n = 500,4n^2; 項是 2n 項的1000倍大,所以在大多數場合下,省略後者對錶達式的值的影響將是能夠忽略不計的。

 

數學表示掃盲貼 http://www.cnblogs.com/alex3714/articles/5910253.html  

 

1、計算方法
1.一個算法執行所耗費的時間,從理論上是不能算出來的,必須上機運行測試才能知道。但咱們不可能也沒有必要對每一個算法都上機測試,只需知道哪一個算法花費的時間多,哪一個算法花費的時間少就能夠了。而且一個算法花費的時間與算法中語句的執行次數成正比例,哪一個算法中語句執行次數多,它花費時間就多。
一個算法中的語句執行次數稱爲語句頻度或時間頻度。記爲T(n)。
2.通常狀況下,算法的基本操做重複執行的次數是模塊n的某一個函數f(n),所以,算法的時間複雜度記作:T(n)=O(f(n))。隨着模塊n的增大,算法執行的時間的增加率和f(n)的增加率成正比,因此f(n)越小,算法的時間複雜度越低,算法的效率越高。
在計算時間複雜度的時候,先找出算法的基本操做,而後根據相應的各語句肯定它的執行次數,再找出T(n)的同數量級(它的同數量級有如下:1,Log2n ,n ,nLog2n ,n的平方,n的三次方,2的n次方,n!),找出後,f(n)=該數量級,若T(n)/f(n)求極限可獲得一常數c,則時間複雜度T(n)=O(f(n))。
3.常見的時間複雜度
按數量級遞增排列,常見的時間複雜度有:
常數階O(1),  對數階O(log2n),  線性階O(n),  線性對數階O(nlog2n),  平方階O(n^2), 立方階O(n^3),..., k次方階O(n^k), 指數階O(2^n) 。
其中,
1.O(n),O(n^2), 立方階O(n^3),..., k次方階O(n^k) 爲多項式階時間複雜度,分別稱爲一階時間複雜度,二階時間複雜度。。。。
2.O(2^n),指數階時間複雜度,該種不實用
3.對數階O(log2n),   線性對數階O(nlog2n),除了常數階之外,該種效率最高
例:算法:
for(i=1;i<=n;++i)
  {
     for(j=1;j<=n;++j)
     {
         c[ i ][ j ]=0; //該步驟屬於基本操做 執行次數:n^2
          for(k=1;k<=n;++k)
               c[ i ][ j ]+=a[ i ][ k ]*b[ k ][ j ]; //該步驟屬於基本操做 執行次數:n^3
     }
  }

 

  則有 T(n)= n^2+n^3,根據上面括號裏的同數量級,咱們能夠肯定 n^3爲T(n)的同數量級
  則有f(n)= n^3,而後根據T(n)/f(n)求極限可獲得常數c
  則該算法的 時間複雜度:T(n)=O(n^3)
4、

 

定義:若是一個問題的規模是n,解這一問題的某一算法所須要的時間爲T(n),它是n的某一函數 T(n)稱爲這一算法的「時間複雜性」。

當輸入量n逐漸加大時,時間複雜性的極限情形稱爲算法的「漸近時間複雜性」。

咱們經常使用大O表示法表示時間複雜性,注意它是某一個算法的時間複雜性。大O表示只是說有上界,
由定義若是f(n)=O(n),那顯然成立f(n)=O(n^2),它給你一個上界,但並非上確界,但人們在表示的時候通常都習慣表示前者。

此外,一個問題自己也有它的複雜性,若是某個算法的複雜性到達了這個問題複雜性的下界,那就稱這樣的算法是最佳算法。

「大O記法」:在這種描述中使用的基本參數是 n,即問題實例的規模,把複雜性或運行時間表達爲n的函數。
這裏的「O」表示量級 (order),好比說「二分檢索是 O(logn)的」,也就是說它須要「經過logn量級的步驟去檢索一個規模爲n的數組」記法 O ( f(n) )表示當 n增大時,
運行時間至多將以正比於 f(n)的速度增加。

這種漸進估計對算法的理論分析和大體比較是很是有價值的,但在實踐中細節也可能形成差別。
例如,一個低附加代價的O(n2)算法在n較小的狀況下可能比一個高附加代價的 O(nlogn)算法運行得更快。
固然,隨着n足夠大之後,具備較慢上升函數的算法必然工做得更快。

O(1)

Temp=i;i=j;j=temp;                    

以上三條單個語句的頻度均爲1,該程序段的執行時間是一個與問題規模n無關的常數。算法的時間複雜度爲常數階,
記做T(n)=O(1)。若是算法的執行時間不隨着問題規模n的增長而增加,即便算法中有上千條語句,
其執行時間也不過是一個較大的常數。此類算法的時間複雜度是O(1)。

O(n^2)

2.1. 交換i和j的內容
     
sum=0;                 (一次)
     for(i=1;i<=n;i++)       (n次 )
        for(j=1;j<=n;j++) (n^2次 )
         sum++;       (n^2次 )


'''
解:T(n)=2n^2+n+1 =O(n^2)
'''

 



2.2.   
    
for (i=1;i<n;i++)
    {
        y=y+1;         ①   
        for (j=0;j<=(2*n);j++)    
           x++;        ②      
    }         
'''
解: 語句1的頻度是n-1
          語句2的頻度是(n-1)*(2n+1)=2n^2-n-1
          f(n)=2n^2-n-1+(n-1)=2n^2-2
          該程序的時間複雜度T(n)=O(n^2).         

O(n)     

''' 
                 

 

                                     
2.3.
    
a=0;
    b=1;                      ①
    for (i=1;i<=n;i++) ②
    {  
       s=a+b;    ③
       b=a;     ④  
       a=s;     ⑤
    }
'''
解:語句1的頻度:2,        
           語句2的頻度: n,        
          語句3的頻度: n-1,        
          語句4的頻度:n-1,    
          語句5的頻度:n-1,                                  
          T(n)=2+n+3(n-1)=4n-1=O(n).
                                                                                                 
O(log2n )
'''

 



2.4.
    
 i=1;       ①
    while (i<=n)
         i=i*2; ②
'''
解: 語句1的頻度是1,  
          設語句2的頻度是f(n),   則:2^f(n)<=n;f(n)<=log2n    
          取最大值f(n)= log2n,
          T(n)=O(log2n )

O(n^3)
'''

 



2.5.
    
for(i=0;i<n;i++)
    {  
       for(j=0;j<i;j++)  
       {
          for(k=0;k<j;k++)
             x=x+2;  
       }
    }
'''
解:當i=m, j=k的時候,內層循環的次數爲k當i=m時, j 能夠取 0,1,...,m-1 ,
因此這裏最內循環共進行了0+1+...+m-1=(m-1)m/2次因此,i從0取到n,
則循環共進行了: 0+(1-1)*1/2+...+(n-1)n/2=n(n+1)(n-1)/6因此時間複雜度爲O(n^3).
'''

 

                                  

咱們還應該區分算法的最壞狀況的行爲和指望行爲。如快速排序的最 壞狀況運行時間是 O(n^2),但指望時間是 O(nlogn)。經過每次都仔細 地選擇基準值,咱們有可能把平方狀況 (即O(n^2)狀況)的機率減少到幾乎等於 0。在實際中,精心實現的快速排序通常都能以 (O(nlogn)時間運行。
下面是一些經常使用的記法:


訪問數組中的元素是常數時間操做,或說O(1)操做。一個算法如 果能在每一個步驟去掉一半數據元素,如二分檢索,一般它就取 O(logn)時間。用strcmp比較兩個具備n個字符的串須要O(n)時間。常規的矩陣乘算法是O(n^3),由於算出每一個元素都須要將n對 元素相乘並加到一塊兒,全部元素的個數是n^2。
指數時間算法一般來源於須要求出全部可能結果。例如,n個元 素的集合共有2n個子集,因此要求出全部子集的算法將是O(2n)的。指數算法通常說來是太複雜了,除非n的值很是小,由於,在 這個問題中增長一個元素就致使運行時間加倍。不幸的是,確實有許多問題 (如著名的「巡迴售貨員問題」 ),到目前爲止找到的算法都是指數的。若是咱們真的遇到這種狀況,一般應該用尋找近似最佳結果的算法替代之。

 

經常使用排序

 

名稱

複雜度

說明

備註

冒泡排序
Bubble Sort

O(N*N)

將待排序的元素看做是豎着排列的「氣泡」,較小的元素比較輕,從而要往上浮

 

插入排序

Insertion sort

O(N*N)

逐一取出元素,在已經排序的元素序列中從後向前掃描,放到適當的位置

起初,已經排序的元素序列爲空

選擇排序

O(N*N)

首先在未排序序列中找到最小元素,存放到排序序列的起始位置,而後,再從剩餘未排序元素中繼續尋找最小元素,而後放到排序序列末尾。以此遞歸。

 

快速排序

Quick Sort

O(n *log2(n))

先選擇中間值,而後把比它小的放在左邊,大的放在右邊(具體的實現是從兩邊找,找到一對後交換)。而後對兩邊分別使用這個過程(遞歸)。

 

堆排序HeapSort

O(n *log2(n))

利用堆(heaps)這種數據結構來構造的一種排序算法。堆是一個近似徹底二叉樹結構,並同時知足堆屬性:即子節點的鍵值或索引老是小於(或者大於)它的父節點。

近似徹底二叉樹

希爾排序

SHELL

O(n1+)

0<£<1

選擇一個步長(Step) ,而後按間隔爲步長的單元進行排序.遞歸,步長逐漸變小,直至爲1.

 

箱排序
Bin Sort

O(n)

設置若干個箱子,把關鍵字等於 k 的記錄全都裝入到第k 個箱子裏 ( 分配 ) ,而後按序號依次將各非空的箱子首尾鏈接起來 ( 收集 ) 。

分配排序的一種:經過" 分配 " 和 " 收集 " 過程來實現排序。

 

 

冒泡排序(Bubble Sort)

冒泡排序(Bubble Sort),是一種 計算機科學領域的較簡單的 排序算法
它重複地走訪過要排序的數列,一次比較兩個元素,若是他們的順序錯誤就把他們交換過來。走訪數列的工做是重複地進行直到沒有再須要交換,也就是說該數列已經排序完成。
這個算法的名字由來是由於越大的元素會經由交換慢慢「浮」到數列的頂端,故名。
data_set = [ 9,1,22,31,45,3,6,2,11 ]
 
loop_count = 0
for j in range(len(data_set)):
    for i in range(len(data_set) - j- 1): # -1 是由於每次比對的都 是i 與i +1,不減1的話,最後一次對比會超出list 
#獲取範圍,-j是由於,每一次大loop就表明排序好了一個最大值,放在了列表最後面,下次loop就不用再運算已經排序好了的值 了
if data_set[i] > data_set[i+1]: #switch tmp = data_set[i] data_set[i] = data_set[i+1] data_set[i+1] = tmp loop_count +=1 print(data_set) print(data_set) print("loop times", loop_count)

 

 

選擇排序

The algorithm works by selecting the smallest unsorted item and then swapping it with the item in the next position to be filled.

The selection sort works as follows: you look through the entire array for the smallest element, once you find it you swap it (the smallest element) with the first element of the array. Then you look for the smallest element in the remaining array (an array without the first element) and swap it with the second element. Then you look for the smallest element in the remaining array (an array without first and second elements) and swap it with the third element, and so on. Here is an example,

data_set = [ 9,1,22,31,45,3,6,2,11 ]
 
smallest_num_index = 0 #初始列表最小值,默認爲第一個
 
loop_count = 0
for j in range(len(data_set)):
    for i in range(j,len(data_set)):
        if data_set[i] < data_set[smallest_num_index]: #當前值 比以前選出來的最小值 還要小,那就把它換成最小值
            smallest_num_index = i
        loop_count +=1
    else:
        print("smallest num is ",data_set[smallest_num_index])
        tmp = data_set[smallest_num_index]
        data_set[smallest_num_index] =  data_set[j]
        data_set[j] = tmp
 
    print(data_set)
    print("loop times", loop_count)

 

The worst-case runtime complexity is O(n2).  

 

插入排序(Insertion Sort)

插入排序(Insertion Sort)的基本思想是:將列表分爲2部分,左邊爲排序好的部分,右邊爲未排序的部分,循環整個列表,每次將一個待排序的記錄,按其關鍵字大小插入到前面已經排好序的子序列中的適當位置,直到所有記錄插入完成爲止。

插入排序很是相似於整撲克牌。

在開始摸牌時,左手是空的,牌面朝下放在桌上。接着,一次從桌上摸起一張牌,並將它插入到左手一把牌中的正確位置上。爲了找到這張牌的正確位置,要將它與手中已有的牌從右到左地進行比較。不管何時,左手中的牌都是排好序的。

也許你沒有意識到,但其實你的思考過程是這樣的:如今抓到一張7,把它和手裏的牌從右到左依次比較,7比10小,應該再往左插,7比5大,好,就插這裏。爲何比較了10和5就能夠肯定7的位置?爲何不用再比較左邊的4和2呢?由於這裏有一個重要的前提:手裏的牌已是排好序的。如今我插了7以後,手裏的牌仍然是排好序的,下次再抓到的牌還能夠用這個方法插入。編程對一個數組進行插入排序也是一樣道理,但和插入撲克牌有一點不一樣,不可能在兩個相鄰的存儲單元之間再插入一個單元,所以要將插入點以後的數據依次日後移動一個單元。

 

source = [92, 77, 67, 8, 6, 84, 55, 85, 43, 67]
  
  
for index in range(1,len(source)):
    current_val = source[index] #先記下來每次大循環走到的第幾個元素的值
    position = index
  
    while position > 0 and source[position-1] > current_val: #當前元素的左邊的緊靠的元素比它大,
    #要把左邊的元素一個一個的往右移一位,給當前這個值插入到左邊挪一個位置出來
source[position] = source[position-1] #把左邊的一個元素往右移一位 position -= 1 #只一次左移只能把當前元素一個位置 ,還得繼續左移只到此元素放到排序好的列表的適當位置 爲止 source[position] = current_val #已經找到了左邊排序好的列表裏不小於current_val的元素的位置,把current_val放在這裏 print(source) #結果: ''' [77, 92, 67, 8, 6, 84, 55, 85, 43, 67] [67, 77, 92, 8, 6, 84, 55, 85, 43, 67] [8, 67, 77, 92, 6, 84, 55, 85, 43, 67] [6, 8, 67, 77, 92, 84, 55, 85, 43, 67] [6, 8, 67, 77, 84, 92, 55, 85, 43, 67] [6, 8, 55, 67, 77, 84, 92, 85, 43, 67] [6, 8, 55, 67, 77, 84, 85, 92, 43, 67] [6, 8, 43, 55, 67, 77, 84, 85, 92, 67] [6, 8, 43, 55, 67, 67, 77, 84, 85, 92] '''

 

快速排序(quick sort)

設要排序的數組是A[0]……A[N-1],首先任意選取一個數據(一般選用數組的第一個數)做爲關鍵數據,而後將全部比它小的數都放到它前面,全部比它大的數都放到它後面,這個過程稱爲一趟快速排序。值得注意的是,快速排序不是一種穩定的排序算法,也就是說,多個相同的值的相對位置也許會在算法結束時產生變更  

注:在待排序的文件中,若存在多個關鍵字相同的記錄,通過排序後這些具備相同關鍵字的記錄之間的相對次序保持不變,該排序方法是穩定的;若具備相同關鍵字的記錄之間的相對次序發生改變,則稱這種排序方法是不穩定的。
要注意的是,排序算法的穩定性是針對全部輸入實例而言的。即在全部可能的輸入實例中,只要有一個實例使得算法不知足穩定性要求,則該排序算法就是不穩定的。 

排序演示

示例

假設用戶輸入了以下數組:
下標
0
1
2
3
4
5
數據
6
2
7
3
8
9
建立變量i=0(指向第一個數據), j=5(指向最後一個數據), k=6( 賦值爲第一個數據的值)。
咱們要把全部比k小的數移動到k的左面,因此咱們能夠開始尋找比6小的數,從j開始,從右往左找,不斷遞減變量j的值,咱們找到第一個下標3的數據比6小,因而把數據3移到下標0的位置,把下標0的數據6移到下標3,完成第一次比較:
下標
0
1
2
3
4
5
數據
3
2
7
6
8
9
i=0 j=3 k=6
接着,開始第二次比較,此次要變成找比k大的了,並且要從前日後找了。遞加變量i,發現下標2的數據是第一個比k大的,因而用下標2的數據7和j指向的下標3的數據的6作交換,數據狀態變成下表:
下標
0
1
2
3
4
5
數據
3
2
6
7
8
9
i=2 j=3 k=6
稱上面兩次比較爲一個循環。
接着,再遞減變量j,不斷重複進行上面的循環比較。
在本例中,咱們進行一次循環,就發現i和j「碰頭」了:他們都指向了下標2。因而,第一遍比較結束。獲得結果以下,凡是k(=6)左邊的數都比它小,凡是k右邊的數都比它大:
下標
0
1
2
3
4
5
數據
3
2
6
7
8
9
若是i和j沒有碰頭的話,就遞加i找大的,尚未,就再遞減j找小的,如此反覆,不斷循環。注意判斷和尋找是同時進行的。
而後,對k兩邊的數據,再分組分別進行上述的過程,直到不能再分組爲止。
注意:第一遍快速排序不會直接獲得最終結果,只會把比k大和比k小的數分到k的兩邊。爲了獲得最後結果,須要再次對下標2兩邊的數組分別執行此步驟,而後再分解數組,直到數組不能再分解爲止(只有一個數據),才能獲得正確結果。
#_*_coding:utf-8_*_
__author__ = 'Alex Li'
 
 
def quick_sort(array,left,right):
    '''
 
    :param array:
    :param left: 列表的第一個索引
    :param right: 列表最後一個元素的索引
    :return:
    '''
    if left >=right:
        return
    low = left
    high = right
    key = array[low] #第一個值
 
    while low < high:#只要左右未碰見
        while low < high and array[high] > key: #找到列表右邊比key大的值 爲止
            high -= 1
        #此時直接 把key(array[low]) 跟 比它大的array[high]進行交換
        array[low] = array[high]
        array[high] = key
 
 
        while low < high and array[low] <= key : #找到key左邊比key大的值,這裏爲什麼是<=而不是<呢?你要思考。。。
            low += 1
            #array[low] =
        #找到了左邊比k大的值 ,把array[high](此時應該剛存成了key) 跟這個比key大的array[low]進行調換
        array[high] = array[low]
        array[low] = key
 
    quick_sort(array,left,low-1) #最後用一樣的方式對分出來的左邊的小組進行同上的作法
    quick_sort(array,low+1, right)#用一樣的方式對分出來的右邊的小組進行同上的作法
 
 
 
if __name__ == '__main__':
 
    array = [96,14,10,9,6,99,16,5,1,3,2,4,1,13,26,18,2,45,34,23,1,7,3,22,19,2]
    #array = [8,4,1, 14, 6, 2, 3, 9,5, 13, 7,1, 8,10, 12]
    print("before sort:", array)
    quick_sort(array,0,len(array)-1)
 
    print("-------final -------")
    print(array)

 

 

 

二叉樹

樹的特徵和定義


  樹是一種重要的非線性 數據結構,直觀地看,它是 數據元素(在樹中稱爲結點)按分支關係組織起來的結構,很象天然界中的樹那樣。 樹結構在客觀世界中普遍存在,如人類社會的族譜和各類社會組織機構均可用樹形象表示。樹在計算機領域中也獲得普遍應用,如在編譯源程序時,可用樹表示源程序的語法結構。又如在 數據庫系統中,樹型結構也是信息的重要組織形式之一。一切具備層次關係的問題均可用樹來描述。
 

樹(Tree)是元素的集合。咱們先以比較直觀的方式介紹樹。下面的數據結構是一個樹:

樹有多個節點(node),用以儲存元素。某些節點之間存在必定的關係,用連線表示,連線稱爲邊(edge)。邊的上端節點稱爲父節點,下端稱爲子節點。樹像是一個不斷分叉的樹根。

每一個節點能夠有多個子節點(children),而該節點是相應子節點的父節點(parent)。好比說,3,5是6的子節點,6是3,5的父節點;1,8,7是3的子節點, 3是1,8,7的父節點。樹有一個沒有父節點的節點,稱爲根節點(root),如圖中的6。沒有子節點的節點稱爲葉節點(leaf),好比圖中的1,8,9,5節點。從圖中還能夠看到,上面的樹總共有4個層次,6位於第一層,9位於第四層。樹中節點的最大層次被稱爲深度。也就是說,該樹的深度(depth)爲4。

 

若是咱們從節點3開始向下看,而忽略其它部分。那麼咱們看到的是一個以節點3爲根節點的樹:

三角形表明一棵樹

再進一步,若是咱們定義孤立的一個節點也是一棵樹的話,原來的樹就能夠表示爲根節點和子樹(subtree)的關係:

 

上述觀察實際上給了咱們一種嚴格的定義樹的方法:

1. 樹是元素的集合。

2. 該集合能夠爲空。這時樹中沒有元素,咱們稱樹爲空樹 (empty tree)。

3. 若是該集合不爲空,那麼該集合有一個根節點,以及0個或者多個子樹。根節點與它的子樹的根節點用一個邊(edge)相連。

上面的第三點是以遞歸的方式來定義樹,也就是在定義樹的過程當中使用了樹自身(子樹)。因爲樹的遞歸特徵,許多樹相關的操做也能夠方便的使用遞歸實現。咱們將在後面看到。

 

樹的實現

樹的示意圖已經給出了樹的一種內存實現方式: 每一個節點儲存元素和多個指向子節點的指針。然而,子節點數目是不肯定的。一個父節點可能有大量的子節點,而另外一個父節點可能只有一個子節點,而樹的增刪節點操做會讓子節點的數目發生進一步的變化。這種不肯定性就可能帶來大量的內存相關操做,而且容易形成內存的浪費。

一種經典的實現方式以下:

樹的內存實現

擁有同一父節點的兩個節點互爲兄弟節點(sibling)。上圖的實現方式中,每一個節點包含有一個指針指向第一個子節點,並有另外一個指針指向它的下一個兄弟節點。這樣,咱們就能夠用統一的、肯定的結構來表示每一個節點。

 

計算機的文件系統是樹的結構,好比Linux文件管理背景知識中所介紹的。在UNIX的文件系統中,每一個文件(文件夾一樣是一種文件),均可以看作是一個節點。非文件夾的文件被儲存在葉節點。文件夾中有指向父節點和子節點的指針(在UNIX中,文件夾還包含一個指向自身的指針,這與咱們上面見到的樹有所區別)。在git中,也有相似的樹狀結構,用以表達整個文件系統的版本變化 (參考版本管理三國志)。

 
 
 
 
 

 二叉樹: 

二叉樹是由n(n≥0)個結點組成的有限集合、每一個結點最多有兩個子樹的有序樹。它或者是空集,或者是由一個根和稱爲左、右子樹的兩個不相交的二叉樹組成。

特色:

(1)二叉樹是有序樹,即便只有一個子樹,也必須區分左、右子樹;

(2)二叉樹的每一個結點的度不能大於2,只能取0、一、2三者之一;

(3)二叉樹中全部結點的形態有5種:空結點、無左右子樹的結點、只有左子樹的結點、只有右子樹的結點和具備左右子樹的結點。

 

二叉樹(binary)是一種特殊的樹。二叉樹的每一個節點最多隻能有2個子節點:

二叉樹

因爲二叉樹的子節點數目肯定,因此能夠直接採用上圖方式在內存中實現。每一個節點有一個左子節點(left children)和右子節點(right children)。左子節點是左子樹的根節點,右子節點是右子樹的根節點。

 

若是咱們給二叉樹加一個額外的條件,就能夠獲得一種被稱做二叉搜索樹(binary search tree)的特殊二叉樹。二叉搜索樹要求:每一個節點都不比它左子樹的任意元素小,並且不比它的右子樹的任意元素大。

(若是咱們假設樹中沒有重複的元素,那麼上述要求能夠寫成:每一個節點比它左子樹的任意節點大,並且比它右子樹的任意節點小)

二叉搜索樹,注意樹中元素的大小

二叉搜索樹能夠方便的實現搜索算法。在搜索元素x的時候,咱們能夠將x和根節點比較:

1. 若是x等於根節點,那麼找到x,中止搜索 (終止條件)

2. 若是x小於根節點,那麼搜索左子樹

3. 若是x大於根節點,那麼搜索右子樹

二叉搜索樹所須要進行的操做次數最多與樹的深度相等。n個節點的二叉搜索樹的深度最多爲n,最少爲log(n)。

 

二叉樹的遍歷

遍歷即將樹的全部結點訪問且僅訪問一次。按照根節點位置的不一樣分爲前序遍歷,中序遍歷,後序遍歷。

前序遍歷:根節點->左子樹->右子樹

中序遍歷:左子樹->根節點->右子樹

後序遍歷:左子樹->右子樹->根節點

例如:求下面樹的三種遍歷

 

前序遍歷:abdefgc

中序遍歷:debgfac

後序遍歷:edgfbca

 

 二叉樹的類型

(1) 徹底二叉樹——若設二叉樹的高度爲h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第h層有 葉子結點,而且葉子結點都是從左到右依次排布,這就是 徹底二叉樹
(2) 滿二叉樹——除了葉結點外每個結點都有左右子葉且葉子結點都處在最底層的二叉樹。
(3)平衡二叉樹——平衡二叉樹又被稱爲AVL樹(區別於AVL算法),它是一棵二叉排序樹,且具備如下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,而且左右兩個子樹都是一棵平衡二叉樹

如何判斷一棵樹是徹底二叉樹?按照定義,

教材上的說法:一個深度爲k,節點個數爲 2^k - 1 的二叉樹爲滿二叉樹。這個概念很好理解,

就是一棵樹,深度爲k,而且沒有空位。

首先對滿二叉樹按照廣度優先遍歷(從左到右)的順序進行編號。

一顆深度爲k二叉樹,有n個節點,而後,也對這棵樹進行編號,若是全部的編號都和滿二叉樹對應,那麼這棵樹是徹底二叉樹。

 

image

 

 

 

二叉樹遍歷實現 

class TreeNode(object):
    def __init__(self,data=0,left=0,right=0):
        self.data = data
        self.left = left
        self.right = right
 
class BTree(object):
    def __init__(self,root=0):
        self.root = root
 
 
    def preOrder(self,treenode):
        if treenode is 0:
            return
        print(treenode.data)
        self.preOrder(treenode.left)
        self.preOrder(treenode.right)
    def inOrder(self,treenode):
        if treenode is 0:
            return
        self.inOrder(treenode.left)
        print(treenode.data)
        self.inOrder(treenode.right)
 
    def postOrder(self,treenode):
        if treenode is 0:
            return
        self.postOrder(treenode.left)
        self.postOrder(treenode.right)
        print(treenode.data)
if __name__ == '__main__':
    n1  = TreeNode(data=1)
    n2 = TreeNode(2,n1,0)
    n3 = TreeNode(3)
    n4 = TreeNode(4)
    n5 = TreeNode(5,n3,n4)
    n6 = TreeNode(6,n2,n5)
    n7 = TreeNode(7,n6,0)
    n8 = TreeNode(8)
    root = TreeNode('root',n7,n8)
 
    bt = BTree(root)
    print("preOrder".center(50,'-'))
    print(bt.preOrder(bt.root))
 
    print("inOrder".center(50,'-'))
    print (bt.inOrder(bt.root))
 
    print("postOrder".center(50,'-'))
    print (bt.postOrder(bt.root))

 

  

堆排序

堆排序,顧名思義,就是基於堆。所以先來介紹一下堆的概念。
堆分爲最大堆和最小堆,其實就是徹底二叉樹。最大堆要求節點的元素都要大於其孩子,最小堆要求節點元素都小於其左右孩子,二者對左右孩子的大小關係不作任何要求,其實很好理解。有了上面的定義,咱們能夠得知,處於最大堆的根節點的元素必定是這個堆中的最大值。其實咱們的堆排序算法就是抓住了堆的這一特色,每次都取堆頂的元素,將其放在序列最後面,而後將剩餘的元素從新調整爲最大堆,依次類推,最終獲得排序的序列。

 

堆排序就是把堆頂的最大數取出,

將剩餘的堆繼續調整爲最大堆,具體過程在第二塊有介紹,以遞歸實現

剩餘部分調整爲最大堆後,再次將堆頂的最大數取出,再將剩餘部分調整爲最大堆,這個過程持續到剩餘數只有一個時結束

 

#_*_coding:utf-8_*_
__author__ = 'Alex Li'
import time,random
def sift_down(arr, node, end):
    root = node
    #print(root,2*root+1,end)
    while True:
        # 從root開始對最大堆調整
 
        child = 2 * root +1  #left child
        if child  > end:
            #print('break',)
            break
        print("v:",root,arr[root],child,arr[child])
        print(arr)
        # 找出兩個child中交大的一個
        if child + 1 <= end and arr[child] < arr[child + 1]: #若是左邊小於右邊
            child += 1 #設置右邊爲大
 
        if arr[root] < arr[child]:
            # 最大堆小於較大的child, 交換順序
            tmp = arr[root]
            arr[root] = arr[child]
            arr[child]= tmp
 
            # 正在調整的節點設置爲root
            #print("less1:", arr[root],arr[child],root,child)
 
            root = child #
            #[3, 4, 7, 8, 9, 11, 13, 15, 16, 21, 22, 29]
            #print("less2:", arr[root],arr[child],root,child)
        else:
            # 無需調整的時候, 退出
            break
    #print(arr)
    print('-------------')
 
def heap_sort(arr):
    # 從最後一個有子節點的孩子仍是調整最大堆
    first = len(arr) // 2 -1
    for i in range(first, -1, -1):
        sift_down(arr, i, len(arr) - 1)
    #[29, 22, 16, 9, 15, 21, 3, 13, 8, 7, 4, 11]
    print('--------end---',arr)
    # 將最大的放到堆的最後一個, 堆-1, 繼續調整排序
    for end in range(len(arr) -1, 0, -1):
        arr[0], arr[end] = arr[end], arr[0]
        sift_down(arr, 0, end - 1)
        #print(arr)
def main():
    # [7, 95, 73, 65, 60, 77, 28, 62, 43]
    # [3, 1, 4, 9, 6, 7, 5, 8, 2, 10]
    #l = [3, 1, 4, 9, 6, 7, 5, 8, 2, 10]
    #l = [16,9,21,13,4,11,3,22,8,7,15,27,0]
    array = [16,9,21,13,4,11,3,22,8,7,15,29]
    #array = []
    #for i in range(2,5000):
    #    #print(i)
    #    array.append(random.randrange(1,i))
 
    print(array)
    start_t = time.time()
    heap_sort(array)
    end_t = time.time()
    print("cost:",end_t -start_t)
    print(array)
    #print(l)
    #heap_sort(l)
    #print(l)
 
 
if __name__ == "__main__":
    main()

 

  

希爾排序(shell sort)

希爾排序(Shell Sort)是插入排序的一種。也稱縮小增量排序,是直接插入排序算法的一種更高效的改進版本,該方法的基本思想是:先將整個待排元素序列分割成若干個子序列(由相隔某個「增量」的元素組成的)分別進行直接插入排序,而後依次縮減增量再進行排序,待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。由於直接插入排序在元素基本有序的狀況下(接近最好狀況),效率是很高的,所以希爾排序在時間效率比直接插入排序有較大提升

首先要明確一下增量的取法:

      第一次增量的取法爲: d=count/2;

      第二次增量的取法爲:  d=(count/2)/2;

      最後一直到: d=1;

看上圖觀測的現象爲:

        d=3時:將40跟50比,因50大,不交換。

                   將20跟30比,因30大,不交換。

                   將80跟60比,因60小,交換。

        d=2時:將40跟60比,不交換,拿60跟30比交換,此時交換後的30又比前面的40小,又要將40和30交換,如上圖。

                   將20跟50比,不交換,繼續將50跟80比,不交換。

        d=1時:這時就是前面講的插入排序了,不過此時的序列已經差很少有序了,因此給插入排序帶來了很大的性能提升。

相關文章
相關標籤/搜索