Python數據結構應用5——排序(Sorting)

在具體算法以前,首先來看一下排序算法衡量的標準:html

  1. 比較:比較兩個數的大小的次數所花費的時間。
  2. 交換:當發現某個數不在適當的位置時,將其交換到合適位置花費的時間。

冒泡排序(Bubble Sort)

這是一個面試常常考的排序,雖然簡單,可是要保證一點都不出錯也不簡單。python

冒泡,顧名思義,每一次冒出一個泡泡出來,這個泡泡是剩餘數中最大的那個數。因此,若是有n個數待排序,那麼須要冒(n-1)次泡泡。即最外層循環須要len(list)-1次。面試

每一次冒泡的過程即內層循環。每次取一對數(pair),按順序從最開始的一對數開始,比較兩個數哪一個數比較大就交換到上部(右邊/冒泡),而後依次執行下一對數(每次進1),這個內層循環的次數隨着外層循環的次數增長而減小,由於一旦進行了一次外層循環,已經排好序的數就多了一個。算法

一次冒泡的過程以下圖:
shell

def bubble_sort(a_list):
    for pass_num in range(len(a_list)-1,0,-1):
        for i in range(pass_num):
            if a_list[i] > a_list[i+1]:
                a_list[i],a_list[i+1] = a_list[i+1],a_list[i]
a_list = [54, 26, 93, 17, 77, 31, 44, 55, 20]
bubble_sort(a_list)
print(a_list)
[17, 20, 26, 31, 44, 54, 55, 77, 93]

這種冒泡排序是很耗時間的,每一次外層循環,須要比較的次數以下圖所示:ide

因此,一共須要\(\frac{1}{2}n^{2}-\frac{1}{2}n\) 次比較,時間複雜度是\(O(n^{2})\),若是看最壞的狀況,即每次比較後都須要交換兩個數,那麼總時間✖️2函數

事實上,在不少狀況下,冒泡排序並不須要完成全部的外循環就已經將全部數排好序啦,可是因爲程序的笨蛋性,他仍是在一直的執行下去,浪費時間。那麼,咱們能夠將冒泡排序進行改進,讓其知道一旦全部數據已是按順序排好時就中止工做,以進行時間優化:優化

def short_bubble_sort(a_list):
    exchanges = True   # 此標誌用來記錄一輪循環中是否進行了交換
    pass_num = len(a_list)-1
    while pass_num > 0 and exchanges:
        exchanges = False
        for i in range(pass_num):
            if a_list[i]>a_list[i+1]:
                exchanges = True
                a_list[i],a_list[i+1] = a_list[i+1],a_list[i]
            pass_num -= 1
a_list=[20, 30, 40, 90, 50, 60, 70, 80, 100, 110]
short_bubble_sort(a_list)
print(a_list)
[20, 30, 40, 50, 60, 70, 80, 90, 100, 110]

選擇排序(Selection Sort)

選擇排序其實每次外層循環的結果和冒泡排序很像,即每次在待排序元素中找到最大的元素,將其與應該放的位置交換。即每進行一次外層循環就多排好了一個元素。ui

選擇排序的時間複雜度仍爲\(O(n^{2})\),可是因爲其元素交換的次數比冒泡排序要少,因此消耗的時間比冒泡排序要短。spa

def selection_sort(a_list):
    for fill_slot in range(len(a_list)-1,0,-1):
        # fill_slot 這一輪最大元素將要放入的位置
        pos_of_max=0
        # 這一輪最大元素的位置
        for location in range(1, fill_slot+1):
            if a_list[location]>a_list[pos_of_max]:
                pos_of_max = location
        a_list[fill_slot],a_list[pos_of_max]=a_list[pos_of_max],a_list[fill_slot]
a_list = [54, 26, 93, 17, 77, 31, 44, 55, 20]
selection_sort(a_list)
print(a_list)
[17, 20, 26, 31, 44, 54, 55, 77, 93]

插入排序(Insertion Sort)

插入排序就像插撲克牌,每一次插一張牌到合適大小的位置,直到n張牌插入完畢。因此插入排序須要n-1次插入操做,即外層循環。每一次插入操做須要在已經排好序的數中依次進行比較,直到找到合適的位置。因此插入排序的時間複雜度在最好的狀況下爲\(O(n)\),最壞狀況下爲\(O(n^{2})\)

插入排序過程以下圖所示:

def insertion_sort(a_list):
    for index in range(1, len(a_list)):
        # index 爲該輪要插入元素的位置
        current_value = a_list[index]
        position = index   
        while position>0 and a_list[position-1]>current_value:
                a_list[position] = a_list[position-1]
                position = position - 1
        a_list[position] = current_value
a_list = [54, 26, 93, 17, 77, 31, 44, 55, 20]
insertion_sort(a_list)
print(a_list)
[17, 20, 26, 31, 44, 54, 55, 77, 93]

希爾排序(Shell Sort)

希爾排序,也稱遞減增量排序算法,是插入排序的一種更高效的改進版本。希爾排序是非穩定排序算法。

希爾排序是基於插入排序的如下兩點性質而提出改進方法的:

插入排序在對幾乎已經排好序的數據操做時,效率高,便可以達到線性排序的效率,但插入排序通常來講是低效的,由於插入排序每次只能將數據移動一位

以下圖所示,這個list中一共有9個數,咱們將這9個數分紅三個sublists,位置增量爲3(如圖每一列的深色部分爲一個sublist)。對於每一個sublist進行一次插入排序,且保持原來的位置放置在一個新的list中。

對於這個新的list,咱們進行一次標準的插入排序。注意到,因爲咱們對於以前的sublist已經進行過排序,因此咱們減小了此次標準插入排序的移動操做數。

def shell_sort(a_list):
    increment = len(a_list) // 2  # (步進數)
    while increment > 0:
        for start_position in range(increment):
            gap_insertion_sort(a_list, start_position, increment)
        print("After increments of size", increment, "The list is",a_list)
        increment = increment // 2
        
def gap_insertion_sort(a_list, start, gap):
    for i in range(start+gap, len(a_list), gap):
        # 如下爲插入排序
        current_value = a_list[i]
        position = i
        while position >= gap and a_list[position-gap]>current_value:
            a_list[position] = a_list[position-gap]
            position = position - gap
            a_list[position] = current_value
        
a_list = [54, 26, 93, 17, 77, 31, 44, 55, 20]
shell_sort(a_list)
print(a_list)
After increments of size 4 The list is [20, 26, 44, 17, 54, 31, 93, 55, 77]
After increments of size 2 The list is [20, 17, 44, 26, 54, 31, 77, 55, 93]
After increments of size 1 The list is [17, 20, 26, 31, 44, 54, 55, 77, 93]
[17, 20, 26, 31, 44, 54, 55, 77, 93]

步進increment在希爾排序中是一個重要的參數。上列函數shell_sort()使用了不一樣的步進。首先,創造了n/2個sublist,接下來,創造了n/4個sublist,步進也逐次減少。下圖是第一次循環中的sublist選擇:

歸併排序(Merge Sort)

從這裏,開始介紹分治策略(divide and conquer)

歸併排序其實跟二分搜索很相像,採用的是遞歸的方法。每次將待排序的list'平均'分紅左右兩個sublists,而後分別進行排序,依次遞歸,直到sublist的長度<=1。

第一個圖是list的divide的過程:

第二個圖是sublists的conquer(merge)的過程:

def merge_sort(a_list):
    print('splitting', a_list)
    if len(a_list)>1:
        mid = len(a_list) // 2
        # 這兩個half須要額外的空間
        left_half = a_list[:mid]
        right_half = a_list[mid:]
        merge_sort(left_half)
        merge_sort(right_half)
        # 當左右兩個sublist都排好序時,每次選擇兩個sublist的最小的數
        # 而後在這兩數中選擇更小的數依次放入待返回的list中
        i,j,k=0,0,0
        while i<len(left_half) and j<len(right_half):
            if left_half[i] < right_half[j]:
                a_list[k] = left_half[i]
                i = i + 1
            else:
                a_list[k] = right_half[j]
                j = j + 1
            k = k + 1
        while i < len(left_half): 
            a_list[k] = left_half[i] 
            i = i + 1
            k = k + 1
        while j < len(right_half): 
            a_list[k] = right_half[j] 
            j = j + 1 
            k = k + 1
        print("Merging ", a_list)
        
a_list = [54, 26, 93, 17, 77, 31, 44, 55, 20]
merge_sort(a_list)
print(a_list)
splitting [54, 26, 93, 17, 77, 31, 44, 55, 20]
splitting [54, 26, 93, 17]
splitting [54, 26]
splitting [54]
splitting [26]
Merging  [26, 54]
splitting [93, 17]
splitting [93]
splitting [17]
Merging  [17, 93]
Merging  [17, 26, 54, 93]
splitting [77, 31, 44, 55, 20]
splitting [77, 31]
splitting [77]
splitting [31]
Merging  [31, 77]
splitting [44, 55, 20]
splitting [44]
splitting [55, 20]
splitting [55]
splitting [20]
Merging  [20, 55]
Merging  [20, 44, 55]
Merging  [20, 31, 44, 55, 77]
Merging  [17, 20, 26, 31, 44, 54, 55, 77, 93]
[17, 20, 26, 31, 44, 54, 55, 77, 93]

歸併排序的時間複雜度,又是時候祭出這張圖了(來自《算法導論》)。來看這個圖,對於每個conquer(merge)過程,merge後進行排序的時間消耗爲len(sublist),在圖中體現爲n, n/2, n/4, ...。因此,圖中每一行消耗的總時間爲 n/len(sublist) * len(sublist),其中n爲list的總長度。而一共有log(n)個這樣的行,因此歸併排序的時間複雜度爲\(O(nlog(n))\)

快速排序

快速排序也是一種分治策略,相對於歸併排序來講,快速排序沒有使用額外的空間。

快速排序會在list中選擇一個主元(pivot value),也能夠叫它分割點(split point),一般是list的首尾元素,以下圖,主元是54。接下來,在除去主元的剩餘數中最左邊爲左標記(leftmark),最右邊爲右標記(rightmark),左標記向右移,直到移到的數小於主元數爲止;右標記向左移,直到移到的數大於主元數爲止。而後,交換此時兩個標記的數。這個過程一直進行下去,直到兩個標記移動交叉(cross)則中止移動。

此時,將主元數插入到左標記和右標記之間,則主元左邊的數全都小於主元,主元右邊的數全都大於主元。

對左右兩邊的數構成的sublist進行遞歸quicksort。整個過程以下圖:

def quick_sort(a_list):
    quick_sort_helper(a_list, 0, len(a_list) - 1)
def quick_sort_helper(a_list, first, last):
    # first和last分別是a_list的首尾位置,因爲快速排序沒有額外空間,
    # 因此須要記錄sublist的首尾位置
    if first < last: # 若len(sublist)>0
        split_point = partition(a_list, first, last)
        quick_sort_helper(a_list, first, split_point - 1)
        quick_sort_helper(a_list, split_point + 1, last)
def partition(a_list, first, last):
    pivot_value = a_list[first]
    left_mark = first+1
    right_mark = last
    done = False
    while not done:
        # 
        while left_mark <= right_mark and a_list[left_mark] <= pivot_value:
            left_mark = left_mark + 1
        while left_mark <= right_mark and a_list[right_mark] >= pivot_value:
            right_mark = right_mark - 1
        if right_mark < left_mark:
            done = True
        else: # (right_mark - left_mark) == 1
            a_list[left_mark],a_list[right_mark]=a_list[right_mark],a_list[left_mark]
    a_list[first],a_list[right_mark] = a_list[right_mark],a_list[first]
    return right_mark

a_list = [54, 26, 93, 17, 77, 31, 44, 55, 20]
quick_sort(a_list)
print(a_list)
[17, 20, 26, 31, 44, 54, 55, 77, 93]

快速排序的時間複雜度取決於主元的選擇,若是主元的大小每次都在整個list的中間,那麼divide的過程則就相似於歸併排序的過程,時間複雜度的結果就是\(O(nlog(n))\)

然而,並非全部狀況都是這麼好的。想象一下最壞狀況,若是每次主元都正好選到了剩餘list中最小或者最大的那個數,則每次divide只能分割掉一個元素,這就和選擇排序基本無異了,時間複雜度上升爲\(O(n^{2})\)

因此,爲了不這種狀況的發生,咱們能夠嘗試隨機選擇主元,這能夠減小原始數據的原本結構對於複雜度的影響。

  • Reference:
  1. Problem Solving with Algorithms and Data Structures, Release 3.0
  2. 希爾排序-Wikipedia
相關文章
相關標籤/搜索