各種排序和搜索算法的實現 ------ Python數據結構與算法第7章

1. 排序算法的穩定性

       排序算法(Sorting algorithm)是一種能將 一串數據依照特定順序進行排列 的一種算法。穩定性:穩定排序算法會讓原本有相等鍵值的紀錄維持相對次序。也就是如果一個排序算法是穩定的,當有兩個相等鍵值的紀錄R和S,且 在原本的列表中R出現在S之前,在排序過的列表中R也將會是在S之前。 當相等的元素是無法分辨的,比如像是整數,穩定性並不是一個問題。然而,假設以下的數對將要以他們的第一個數字來排序:

(4,1) (3,1) (3,7) (5,6

在這個狀況下,有可能產生兩種不同的結果,一個是 讓相等鍵值的紀錄維持相對的次序,而另外一個則沒有:

# 結果1:維持次序
(3,1) (3,7) (4,1) (5,6)
# 結果2:次序被改變
(3,7) (3,1) (4,1) (5,6)

      不穩定排序算法可能會在相等的鍵值中改變紀錄的相對次序,但是穩定排序算法從來不會如此。不穩定排序算法可以被 特別地實現爲穩定。作這件事情的一個方式是人工擴充鍵值的比較,如此在其他方面相同鍵值的兩個對象間之比較(比如上面的比較中加入第二個標準:第二個鍵值的大小)就會被決定使用在原先數據次序中的條目,當作一個同分決賽。然而,要記住這種次序通常牽涉到額外的空間負擔。

2. 常見排序算法效率比較

在這裏插入圖片描述

3. 冒泡排序

      冒泡排序(Bubble Sort)是一種簡單的排序算法。它重複地遍歷要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。遍歷數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端。冒泡排序最優時間複雜度:O(n),表示遍歷一次發現沒有任何可以交換的元素,排序結束。最壞時間複雜度:O(n2)。是一種比較 穩定 的排序算法。

第一種實現:

def bubble_sort(alist):
    n = len(alist)
    for i in range(n - 1):
        for j in range(n - 1 - i):
            if alist[j] > alist[j + 1]:
                alist[j], alist[j + 1] = alist[j + 1], alist[j]
    return alist


test_list = [9, 2, 3, 3, 1, 0]
bubble_sort(test_list)
print(test_list)
"""
[0, 1, 2, 3, 3, 9]
"""

第二種實現:

def bubble_sort(alist):
    n = len(alist)
    for i in range(n - 1,0,-1):
        for j in range(i):
            if alist[j] > alist[j + 1]:
                alist[j], alist[j + 1] = alist[j + 1], alist[j]
    return alist


test_list = [9, 2, 3, 3, 1, 0]
bubble_sort(test_list)
print(test_list)
"""
[0, 1, 2, 3, 3, 9]
"""

冒泡排序優化:

def bubble_sort(alist):
    n = len(alist)
    for i in range(n - 1, 0, -1):
        count = 0
        for j in range(i):
            if alist[j] > alist[j + 1]:
                alist[j], alist[j + 1] = alist[j + 1], alist[j]
                count += 1
        if count == 0:
            return
    return alist

test_list = [9, 2, 3, 3, 1, 0]
bubble_sort(test_list)
print(test_list)
"""
[0, 1, 2, 3, 3, 9]
"""
4. 選擇排序

      選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

      選擇排序的主要優點與數據移動有關。如果某個元素位於正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對n個元素的表進行排序總共進行至多n-1次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於非常好的一種

      如果是排好序的,也需要n2次遍歷才能確定,所以最優時間複雜度是O(n2),最壞時間複雜度也是O(n2)。選擇排序是不穩定相同大小的元素,排序後前面的元素總會先被放到最後,後面的元素總會後被放到最後

def selection_sort(alist):
    n = len(alist)
    for i in range(n - 1):
        min_index = i
        for j in range(i + 1, n):
            if alist[j] < alist[min_index]:
                min_index = j
        # 判斷當前最小值的索引是否還是i,如果是則i不用交換位置,否則需要交換位置
        if min_index != i:
            alist[i], alist[min_index] = alist[min_index], alist[i]


alist = [54, 226, 93, 17, 77, 31, 44, 55, 20]
selection_sort(alist)
print(alist)
"""
[17, 20, 31, 44, 54, 55, 77, 93, 226]
"""
5. 插入排序

      插入排序(Insertion Sort)是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中 從後向前掃描,找到相應位置並插入。插入排序在實現上,從後向前掃描過程中,需要 反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。

      最優時間複雜度O(n),也就是已經排好序的情況下。最壞時間複雜度:O(n2),插入排序也是一種穩定的排序算法。
在這裏插入圖片描述

def insert_sort(alist):
    for i in range(1, len(alist)):
        for j in range(i, 0, -1):
            if alist[j] < alist[j - 1]:
                alist[j], alist[j - 1] = alist[j - 1], alist[j]
            """
            # 最優時間複雜度的情況
            else:
                break
            """

alist = [6, 5, 3, 1, 8, 7, 2, 4, ]
insert_sort(alist)
print(alist)
"""
[1, 2, 3, 4, 5, 6, 7, 8]
"""

使用while循環來實現,也是同樣的道理:

def insert_sort(alist):
    for i in range(1, len(alist)):
        while i > 0:
            if alist[i] < alist[i - 1]:
                alist[i], alist[i - 1] = alist[i - 1], alist[i]
            # 最優時間複雜度的情況
            # else:
            #     break
            i -= 1

alist = [6, 5, 3, 1, 8, 7, 2, 4, ]
insert_sort(alist)
print(alist)
"""
[1, 2, 3, 4, 5, 6, 7, 8]
"""
6. 希爾排序

      希爾排序(Shell Sort)是插入排序的一種。也稱縮小增量排序,是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法。希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止

      希爾排序的基本思想是:將數組列在一個表中並對列分別進行插入排序,重複這過程,不過每次用更長的列(步長更長了,列數更少了)來進行,最後整個表就只有一列了。
在這裏插入圖片描述

def shell_sort(alist):
    n = len(alist)
    # 如果是9個元素,則gap從4開始(不能確定取半才能是最優的)
    gap = n // 2
    # gap最小縮小爲1,也就是不能大於0
    while gap > 0:
        for i in range(gap, n):
            for i in range(i, 0, -gap):
                if alist[i] < alist[i - gap]:
                    alist[i], alist[i - gap] = alist[i - gap], alist[i]
            """
            # 與內層的for循環是等價的
            while i > 0:
                if alist[i] < alist[i - gap]:
                    alist[i], alist[i - gap] = alist[i - gap], alist[i]
                i -= gap
            """

        # 縮短gap
        gap //= 2

alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
shell_sort(alist)
print(alist)
"""
[17, 20, 26, 31, 44, 54, 55, 77, 93]
"""

希爾排序的最優時間複雜度是 根據步長序列的不同而不同。最壞時間複雜度:O(n2),也就是 當gap取1的時候就是普通的插入排序。 相同的值在不同的分組中排序,最後合併分組,不能保證這兩個相同值順序還是和之前一致,所以這是不穩定的排序算法!

7. 快速排序

       快速排序(Quicksort),又稱 劃分交換排序(partition-exchange sort)。通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。步驟:

① 從數列中挑出一個元素,稱爲"基準"(pivot)

② 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區結束之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作。

③ 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
在這裏插入圖片描述
       遞歸的最底部情形是數列的大小是0或1,也就是 永遠都已經被排序好了。雖然一直遞歸下去,但是這個算法總會結束,因爲在每次的迭代中,它至少會把一個元素擺到它最後的位置去。

def quick_sort(alist, first, last):
    # 遞歸結束條件
    if first >= last:
        return
    # 基準值
    pivot_value = alist[first]
    # 每次傳入的索引位置:first和last都是不同的
    low_pointer = first
    high_pointer = last
    while low_pointer < high_pointer:
    	# 當列表中高索引位置的元素大於或者等於基準值時,只需要索引-1,也就是索引移動到上一個元素
        while low_pointer < high_pointer and alist[high_pointer] >= pivot_value:
            high_pointer -= 1
        # 當存在小於基準值的元素,while循環停止,索引移動停止,把這個索引放到低索引位置
        alist[low_pointer] = alist[high_pointer]
        # 當列表中低索引位置的元素小於基準值時,只需要低索引+1,也就是移動到下一個元素
        while low_pointer < high_pointer and alist[low_pointer] <= pivot_value:
            low_pointer += 1
        # 當存在大於基準值的元素,while循環停止,索引移動停止,把這個索引放到高索引位置
        alist[high_pointer] = alist[low_pointer]
    # low_pointer也可以換成high_pointer,因爲這個時候low_pointer與high_pointer是等價的
    alist[low_pointer] = pivot_value
    # low_pointer左邊的列表進行快速排序
    quick_sort(alist, first, low_pointer - 1)
    # low_pointer右邊的列表進行快速排序
    quick_sort(alist, low_pointer + 1, last)

alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
quick_sort(alist, 0, len(alist) - 1)
print(alist)
"""
[17, 20, 17, 17, 31, 54, 17, 17, 93]
"""

      最優時間複雜度: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)時間。

8. 歸併排序

       歸併排序是採用 分治法 的一個非常典型的應用。歸併排序的思想就是 先遞歸分解數組,再合併數組。將數組分解最小之後,然後合併兩個有序數組,基本思路是比較兩個數組的最前面的數,誰小就先取誰,取了後相應的指針就往後移一位。然後再比較,直至一個數組爲空,最後把另一個數組的剩餘部分複製過來即可。
在這裏插入圖片描述

def merger_sort(alist):
    # 每次都要獲取分組後列表的長度
    n = len(alist)
    # 根據列表的長度分組
    mid_elem = n // 2
    # 當列表只有一個元素時無法分組就直接返回這個列表
    if n <= 1:
        return alist
    # 分組後左邊的元素形成的新列表
    left_elems = merger_sort(alist[:mid_elem])
    # 分組之後右邊的元素形成的新列表
    right_elems = merger_sort(alist[mid_elem:])
    # 左右列表分別設置一個指針
    left_pointer, right_pointer = 0, 0
    # 新的列表用於存放需要合併的且排序好的左右子列表
    ret = []
    # 只要指針的值小於左右子列表的長度,需要繼續排序
    while left_pointer < len(left_elems) and right_pointer < len(right_elems):
        # 如果左子列表的元素小於或者等於右子列表則將左子列表的該元素添加到新列表中,指針向後移動1次
        if left_elems[left_pointer] <= right_elems[right_pointer]:
            ret.append(left_elems[left_pointer])
            left_pointer += 1
        # 如果右子列表的元素小於或者等於左子列表則將右子列表的該元素添加到新列表中,指針向後移動一次
        else:
            ret.append(right_elems[right_pointer])
            right_pointer += 1
    # 剩下的元素直接追加到列表後面,比如1,2,3,4排好1,2剩下的3,4直接追加到列表中
    # 這裏由於執行效率的原因不建議使用列表相加
    ret.extend(left_elems[left_pointer:])
    ret.extend(right_elems[right_pointer:])
    return ret


alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
sorted_alist = merger_sort(alist)
print(sorted_alist)
"""
[17, 20, 26, 31, 44, 54, 55, 77, 93]
"""

      最優時間複雜度:O(nlogn),最壞時間複雜度:O(nlogn),歸併排序是一種穩定的排序算法。歸併排序雖然在時間複雜度上要比其它排序算法減小了,但是 空間複雜度卻增加了,因爲新增了新的列表,佔用了更多的內存空間。

9. 搜索算法

      搜索是在一個項目集合中找到一個特定項目的算法過程,搜索通常的答案是真的或假的。 搜索的幾種常見方法:順序查找、二分法查找、二叉樹查找、哈希查找。

10. 二分法查找

      二分查找又稱折半查找,優點:比較次數少,查找速度快,平均性能好;其缺點:要求待查表爲有序表,且插入刪除困難。因此,折半查找方法適用於不經常變動而查找頻繁的有序列表。首先,假設表中元素是按升序排列,將表中間位置記錄的關鍵字與查找關鍵字比較,如果兩者相等,則查找成功;否則利用中間位置記錄將表分成前、後兩個子表,如果中間位置記錄的關鍵字大於查找關鍵字,則進一步查找前一子表,否則進一步查找後一子表。重複以上過程,直到找到滿足條件的記錄,使查找成功,或直到子表不存在爲止,此時查找不成功。二分查找只能作用於有序的順序表。
在這裏插入圖片描述

11. 二分法查找的兩種實現

不使用遞歸實現:

def binary_search(alist, item):
    first_index = 0
    end_index = len(alist) - 1
    while end_index >= first_index:
        middle_index = (end_index + first_index) // 2
        if item == alist[middle_index]:
            return True
        elif item < alist[middle_index]:
            end_index = middle_index - 1
        else:
            first_index = middle_index + 1
    return False


test_list = [0, 1, 2, 8, 13, 17, 19, 32, 42, ]
print(binary_search(test_list, 3))
print(binary_search(test_list, 17))
"""
False
True
"""

使用遞歸實現:

def binary_search(alist, item):
    len_alist = len(alist)
    if len_alist == 0:
        return False
    else:
        middle_index = len_alist // 2
        if item == alist[middle_index]:
            return True
        elif item < alist[middle_index]:
            return binary_search(alist[:middle_index], item)
        else:
            return binary_search(alist[middle_index + 1:], item)


test_list = [0, 1, 2, 8, 13, 17, 19, 32, 42, ]
print(binary_search(test_list, 3))
print(binary_search(test_list, 17))
"""
False
True
"""

最優時間複雜度:O(1),最壞時間複雜度:O(logn)

Github:https://github.com/ThanlonSmith/Data-Structure-Python3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章