常用算法與設計模式


同一問題可用不同算法解決,而一個算法的質量優劣將影響到算法乃至程序的效率。算法分析的目的在於選擇合適算法和改進算法。

計算機科學中,算法的時間復雜度是一個函數,它定量描述了該算法的運行時間。這是一個關於代表算法輸入值的字符串的長度的函數。時間復雜度常用大O符號(Order)表述,不包括這個函數的低階項和首項系數。使用這種方式時,時間復雜度可被稱為是漸近的,它考察當輸入值大小趨近無窮時的情況。

定義

在計算機科學中,算法的時間復雜度是一個函數,它定量描述了該算法的運行時間。這是一個關於代表算法輸入值的字符串的長度的函數。時間復雜度常用大O符號表述,不包括這個函數的低階項和首項系數。

算法復雜度

算法復雜度分為時間復雜度和空間復雜度。其作用: 時間復雜度是指執行算法所需要的計算工作量;而空間復雜度是指執行這個算法所需要的內存空間。(算法的復雜性體現在運行該算法時的計算機所需資源的多少上,計算機資源最重要的是時間和空間(即寄存器)資源,因此復雜度分為時間和空間復雜度)。

時間復雜度

1. 一般情況下,算法的基本操作重復執行的次數是模塊n的某一個函數f(n),因此,算法的時間復雜度記做:T(n)=O(f(n))

分析:隨着模塊n的增大,算法執行的時間的增長率和 f(n) 的增長率成正比,所以 f(n) 越小,算法的時間復雜度越低,算法的效率越高。

2. 在計算時間復雜度的時候,先找出算法的基本操作,然后根據相應的各語句確定它的執行次數,再找出 T(n) 的同數量級(它的同數量級有以下:1,log(2)n,n,n log(2)n ,n的平方,n的三次方,2的n次方,n!),找出后,f(n) = 該數量級,若 T(n)/f(n) 求極限可得到一常數c,則時間復雜度T(n) = O(f(n))

例:算法:

則有 T(n) = n 的平方+n的三次方,根據上面括號里的同數量級,我們可以確定 n的三次方 為T(n)的同數量級

則有 f(n) = n的三次方,然后根據 T(n)/f(n) 求極限可得到常數c

則該算法的時間復雜度:T(n) = O(n^3) 注:n^3即是n的3次方。

3.在pascal中比較容易理解,容易計算的方法是:看看有幾重for循環,只有一重則時間復雜度為O(n),二重則為O(n^2),依此類推,如果有二分則為O(logn),二分例如快速冪、二分查找,如果一個for循環套一個二分,那么時間復雜度則為O(nlogn)。

 常用排序

 

名稱

復雜度

說明

備注

冒泡排序
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 個箱子里 ( 分配 ) ,然后按序號依次將各非空的箱子首尾連接起來 ( 收集 ) 。

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

冒泡排序

冒泡排序(BubbleSort)的基本概念是:依次比較相鄰的兩個數,將小數放在前面,大數放在后面。即在第一趟:首先比較第1個和第2個數,將小數放前,大數放后。然后比較第2個數和第3個數,將小數放前,大數放后,如此繼續,直至比較最后兩個數,將小數放前,大數放后。

冒泡排序流程至此第一趟結束,將最大的數放到了最后。在第二趟:仍從第一對數開始比較(因為可能由於第2個數和第3個數的交換,使得第1個數不再小於第2個數),將小數放前,大數放后,一直比較到倒數第二個數(倒數第一的位置上已經是最大的),第二趟結束,在倒數第二的位置上得到一個新的最大數(其實在整個數列中是第二大的數)。如此下去,重復以上過程,直至最終完成排序。

由於在排序過程中總是小數往前放,大數往后放,相當於氣泡往上升,所以稱作冒泡排序。

用二重循環實現,外循環變量設為i,內循環變量設為j。假如有10個數需要進行排序,則外循環重復9次,內循環依次重復9,8,...,1次。每次進行比較的兩個元素都是與內循環j有關的,它們可以分別用a[j]和a[j+1]標識,i的值依次為1,2,...,9,對於每一個i,j的值依次為1,2,...10-i。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Version:Python3.5.0

import random, time

def bubble_sort(array):
    ''' 從小到大 冒泡法,每次土地循環把最大的結果放到最后'''

    for i in range(len(array)):     # 大循環,循環列表的個數的元素
        for j in range(len(array)-i-1): # 小循環,每次從第一個元素比較兩個數大小
            if array[j] > array[j+1]:
                array[j], array[j+1] = array[j+1], array[j]    # 把最大的元素放到最后
            
if __name__ == '__main__':
    array = [random.randint(1, 1000000) for i in range(50000)]
    start_time = time.time()
    bubble_small_to_big(array)
    end_time = time.time()
    total_time = end_time - start_time
    print('開始時間:%s, 結束時間:%s, 總共花費時間:%s秒' %
          (start_time, end_time, total_time))
  '''  
  開始時間:1464748778.018224,
  結束時間:1464749177.4116025,
  總共花費時間:399.39337849617004秒
  ''' 

插入排序

有一個已經有序的數據序列,要求在這個已經排好的數據序列中插入一個數,但要求插入后此數據序列仍然有序,這個時候就要用到一種新的排序方法--插入排序法,插入排序的基本操作就是將一個數據插入到已經排好序的有序數據中,從而得到一個新的、個數加一的有序數據,算法適用於少量數據的排序,時間復雜度為O(n^2)。是穩定的排序方法。插入算法把要排序的數組分成兩部分:第一部分包含了這個數組的所有元素,但將最后一個元素除外(讓數組多一個空間才有插入的位置),而第二部分就只包含這一個元素(即待插入元素)。在第一部分排序完成后,再將這個最后元素插入到已排好序的第一部分中。

插入排序的基本思想是:每步將一個待排序的紀錄,按其關鍵碼值的大小插入前面已經排序的文件中適當位置上,直到全部插入完為止。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Version:Python3.5.0
import time, random

def insertion_sort(array):
    for i in range(1, len(array)):  # 默認第一個數排序好了
        position = i    # 記錄當前索引位置
        p_value = array[i]  # 記錄當前位置的值
        while position > 0 and p_value < array[position-1]:  # 比較與前面一個數的大小
            array[position] = array[position-1]     # 把前面的一個值往后移動一個位置
            position -= 1   # 往前移動一個位置

        array[position] = p_value   # 把p_value放到正確的位置上

if __name__ == '__main__':
    array = [random.randint(1, 1000000) for i in range(50000)]
    # array = [12, 33, 3, 41, 8, 42, 74, 1, 6, 15]
    start_time = time.time()
    insertion_sort(array)
    end_time = time.time()
    total_time = end_time - start_time
    print(array[:100])
    print('開始時間:%s, 結束時間:%s, 總共花費時間:%s秒' %
          (start_time, end_time, total_time))
    '''
    開始時間:1464753187.1083782,
    結束時間:1464753348.6856112,
    總共花費時間:161.57723307609558秒
    '''

選擇排序

選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理是每一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,直到全部待排序的數據元素排完。 選擇排序是不穩定的排序方法(比如序列[5, 5, 3]第一次就將第一個[5]與[3]交換,導致第一個5挪動到第二個5后面)。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Version:Python3.5.0

import random, time

def seletion_sort(array):
    ''' 選擇排序法 從小到大,每次循環把最小的索引值放到最前面'''

    for i in range(len(array)):     # 大循環,循環列表的個數的元素
        smallet_index = i   # 默認最小的索引為i
        for j in range(i+1, len(array)): # 小循環,每次比較array[i]與array[j]兩個數大小
            if array[i] > array[j]:
                smallet_index = j     # 記錄最小索引號
        array[i], array[smallet_index] = array[smallet_index], array[i]  # 交換數據  


if __name__ == '__main__':
    # 生成50000個隨機數的數據列表
    array = [random.randrange(1000000) for i in range(50000)]    
    start_time = time.time()
    seletion_sort(array)
    end_time = time.time()
    total_time = end_time - start_time
    print('開始時間:%s, 結束時間:%s, 總共花費時間:%s秒' %
          (start_time, end_time, total_time))
    '''
    開始時間:1464512429.3297048,
    結束時間:1464512563.4953785,
    總共花費時間:134.16567373275757秒
    '''

快速排序

設要排序的數組是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兩邊的數組分別執行此步驟,然后再分解數組,直到數組不能再分解為止(只有一個數據),才能得到正確結果。
 
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Version:Python3.5.0
import time, random

def quick_sort(array, left, right):

    if left >= right:  # 當i=j時,說明數據已經比較完成,即返回排序好的數組
        return True
    base = array[left]  # 選擇數組第一個數據作為基數
    i = left
    j = right
    while i < j:
        # 從右邊找一個比基數base小的數,大於base的話,j減一,往前找
        while i < j and array[j] >= base:   # i<j 這個條件必須加上,防止j減到負數,比如base是最小情況
            j -= 1
        # 上面的while不成立,說明找到一個比base小的數據
        if i < j:
            array[i] = array[j]     # 把找到的數賦值給array[i]

        # print('\033[31m往左找排序后的數組:%s\033[0m' % array, i, j)
        # 從左邊找一個比基數base大的數,小於base的話,i加一,往后找
        while i < j and array[i] < base:    # i<j 這個條件必須加上,防止i的值大於j,比如base是最大情況
            i += 1
        # 上面的while不成立,說明找到一個比base大的數據
        if i < j:
            array[j] = array[i]  # 把找到的數賦值給array[j]

        # print('往右找排序后的數組:%s' % array, i, j)

    # 當 i=j 時,說明這一次匹配完成
    array[i] = base   # 把基數base放到i索引的位置
    # 把索引為i的數據分成兩半,左邊的數都比array[i]小,右邊的比array[i]大,然后分別遞歸調用
    quick_sort(array, left, i-1)
    quick_sort(array, j+1, right)

if __name__ == '__main__':
    array = [random.randrange(1000000) for i in range(50000)]   # 隨機生成五萬個數據
    # array = [2, 44, 55, 31, 12, 4, 31, 9]
    start_time = time.time()
    quick_sort(array, 0, len(array)-1)
    end_time = time.time()
    print('排序好前30個數據:%s' % array[:30])
    # print('排序后數據:%s' % array)
    print('總共花費時間:%s秒' % (end_time - start_time))

二叉樹遍歷

樹的特征和定義


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

樹(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、1、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

 

 

如何判斷平衡二叉樹?

2008111712242127

(b)左邊的圖 左子數的高度為3,右子樹的高度為1,相差超過1

(b)右邊的圖 -2的左子樹高度為0  右子樹的高度為2,相差超過1

 

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Version:Python3.5.0

class TreeNode(object):
    def __init__(self, data=None, left=None, right=None):
        self.data = data
        self.left = left
        self.right = right

class BTree(object):
    def __init__(self, root=None):
        self.root = root

    def preOrder(self, treenode):
        ''' 前序遍歷:根節點->左子樹->右子樹 '''
        if treenode == None:
            return
        print(treenode.data, end=' ')
        self.preOrder(treenode.left)
        self.preOrder(treenode.right)

    def inOrder(self, treenode):
        ''' 中序遍歷:左子樹->根節點->右子樹 '''
        if treenode == None:
            return

        self.inOrder(treenode.left)
        print(treenode.data, end=' ')
        self.inOrder(treenode.right)

    def postOrder(self, treenode):
        ''' 后序遍歷:左子樹->右子樹->根節點 '''
        if treenode == None:
            return

        self.postOrder(treenode.left)
        self.postOrder(treenode.right)
        print(treenode.data, end=' ')

if __name__ == '__main__':
    n1 = TreeNode(data='A')
    n2 = TreeNode('B',n1,None)
    n3 = TreeNode('C')
    n4 = TreeNode('D')
    n5 = TreeNode('E',n3,n4)
    n6 = TreeNode('F',n2,n5)
    n7 = TreeNode('G',n6)
    n8 = TreeNode('H')
    root = TreeNode('I',n7,n8)

    bt = BTree(root)
    print('前序遍歷結果:')
    bt.preOrder(bt.root)

    print('\n中序遍歷結果:')
    bt.inOrder(bt.root)

    print('\n后序遍歷結果:')
    bt.postOrder(bt.root)
    '''
    前序遍歷結果:
    I G F B A E C D H
    中序遍歷結果:
    A B F C E D G I H
    后序遍歷結果:
    A B C D E F G H I
    '''

堆排序

希爾排序

箱排序


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM