算法基礎——列表排序


目錄

  LOW B 三人組

      冒泡排序

      選擇排序

      插入排序

  NB 三人組

      快速排序

      堆排序

      歸並排序

  其他

      希爾排序

      計數排序


 

 

列表排序即將無需列表變為有序,Python的內置函數為sort()。應用的場景主要有:各種榜單、各種表格、給二分查找用、 其他算法用等等。

有關列表排序的算法有很多,主要分為:

  • low B三人組: 冒泡排序、 選擇排序、 插入排序
  • NB三人組: 快速排序、 堆排序、 歸並排序

  • 其他排序算法: 計數排序、 希爾排序、 桶排序

算法排序的關鍵點在於有序區和無序區,我們將一個待排序的列表定為無序區,依次取出其中的元素進行排序,用於存放已排好序的元素的區域稱為有序區

為了更形象的表示出每個排序算法的用時,我們先寫一個用於計算時間的裝飾器預備上

#在timewrap.py中:
import time


def cal_time(func):
    def wrapper(*args, **kwargs):
        t1 = time.time()
        result = func(*args, **kwargs)
        t2 = time.time()
        print("%s running time: %s secs." % (func.__name__, t2-t1))
        return result
    return wrapper
timewrap.py

 

Low B 三人組

  Low B三人組分別指冒泡排序、 選擇排序、 插入排序

  冒泡排序(Bubble Sort)的思想(這里用升序舉例,即排序后的結果為從小到大)是將一個待排序的列表理解為垂直結構,索引為0的元素在最下面。然后從索引為0的位置的元素開始,一次向上比較,若大於上面一個元素則兩個元素交換位置(可以理解為下面的泡泡冒了上來),直到遇到比它大的元素或到達最頂端(即該元素為列表中的最大值)后停止。若該數到達最頂端,則繼續由索引為0的元素重復上述冒泡運動;若遇到更大的元素,則由該大元素向上冒。冒泡排序總的平均時間復雜度為  ,空間復雜度:O(1)

冒泡排序算法的運作如下:(從后往前)
  1. 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
  2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對。在這一點,最后的元素應該會是最大的數。
  3. 針對所有的元素重復以上的步驟,除了最后一個。
  4. 持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較。

  冒泡排序是把小的元素往前調或者把大的元素往后調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。所以,如果兩個元素相等,是不會發生交換的;如果兩個相等的元素沒有相鄰,那么即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前后順序並沒有改變,所以冒泡排序是一種穩定排序算法

文字擼不明白的可看原理圖,如下:

                    

知道了原理后我們來寫代碼

def bubble_sort(li):
    for i in range(len(li)-1):#i是索引,表示趟數,第i趟時無序區(0,len(li)-i)
        for j in range(len(li)-i-1):#j是除去i個元素后的列表的索引(循環進行了幾次就說明有幾個元素已經被排好序)
            if li[j] > li[j+1]:
                li[j], li[j+1] = li[j+1], li[j]

如果冒泡排序執行了一趟而沒有交換發生,說明該列表已經是有序狀態,可以直接結束算法。所以我們可以將上述代碼進行優化:

import random
from timewrap import *

@cal_time
def bubble_sort_2(li):
    for i in range(len(li) - 1):
        # i 表示趟數
        # 第 i 趟時: 無序區:(0,len(li) - i)
        change = False
        for j in range(0, len(li) - i - 1):
            if li[j] > li[j+1]:
                li[j], li[j+1] = li[j+1], li[j]
                change = True
        if not change:
            return

li = list(range(10000))

bubble_sort_2(li)#bubble_sort_2 running time: 0.0010001659393310547 secs.
print(li)#0~9999已排好序

 

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

  思想:一趟遍歷記錄最小的數,放到第一個位置; 再一趟遍歷記錄剩余列表中最小的數,繼續放置;

  選擇排序是給每個位置選擇當前元素最小的,比如給第一個位置選擇最小的,在剩余元素里面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,因為只剩下它一個最大的元素了。那么,在一趟選擇,如果一個元素比當前元素小,而該小的元素又出現在一個和當前元素相等的元素后面,那么交換后穩定性就被破壞了。比較拗口,舉個例子,序列5 8 5 2 9,我們知道第一遍選擇第1個元素5會和2交換,那么原序列中兩個5的相對前后順序就被破壞了,所以選擇排序是一個不穩定的排序算法。

選擇排序代碼如下:

import random
from timewrap import *

@cal_time
def select_sort(li):
    for i in range(len(li) - 1):
        # i 表示趟數,也表示無序區開始的位置
        min_loc = i   # 最小數的位置
        for j in range(i + 1, len(li) - 1):#去除已經歸為的最小數
            if li[j] < li[min_loc]:
                min_loc = j
        li[i], li[min_loc] = li[min_loc], li[i]


li = list(range(10000))
select_sort(li)#select_sort running time: 9.220226049423218 secs.
print(li)#0~9999已排好序

 

  思路:列表被分為有序區和無序區兩個部分。最初有序區只有一個元素。 每次從無序區選擇一個元素,插入到有序區的位置,直到無序區變空。插入排序總的平均時間復雜度為  ,空間復雜度:O(1)

可以理解為撲克牌抓牌的過程

              

基本代碼如下:

import random
from timewrap import *

@cal_time
def insert_sort(li):
    for i in range(1, len(li)):
        # i 表示無序區第一個數
        tmp = li[i] # 摸到的牌
        j = i - 1 # j 指向有序區最后位置
        while li[j] > tmp and j >= 0:
            #循環終止條件: 1. li[j] <= tmp; 2. j == -1
            li[j+1] = li[j]
            j -= 1
        li[j+1] = tmp


li = list(range(10000))
insert_sort(li)#insert_sort running time: 0.003001689910888672 secs.
print(li)#0~9999已排好序

 

 

NB 三人組

  NB三人組分別是: 快速排序、 堆排序、 歸並排序

  快速排序(Quicksort)是對冒泡排序的一種改進。它的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然后再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。

原理圖如下:

          

示例代碼如下:

import random
from timewrap import *
import copy
import sys


sys.setrecursionlimit(100000)#修改遞歸最大深度,默認為997

def partition(li, left, right):
    # ri = random.randint(left, right)
    # li[left], li[ri] = li[ri], li[left]
    tmp = li[left]
    while left < right:
        while left < right and li[right] >= tmp:
            right -= 1#找下一個
        li[left] = li[right]#while條件不成立,說明右邊比temp小,右邊數與temp的位置交換
        while left < right and li[left] <= tmp:
            left += 1
        li[right] = li[left]#while條件不成立,說明左邊比temp大,左邊數與temp的位置交換
    li[left] = tmp
    return left
#修改上面的 <= 和 >= 即可由將列表由升序排變為降序排

def _quick_sort(li, left, right):
    if left < right:    # 至少有兩個元素
        mid = partition(li, left, right)
        _quick_sort(li, left, mid-1)#左邊進行快排遞歸
        _quick_sort(li, mid+1, right)#右邊進行快排遞歸

@cal_time
def quick_sort(li):
    return _quick_sort(li, 0, len(li)-1)


li = list(range(10000))
# random.shuffle(li)#為防止最壞情況發生,最好先用這局代碼完全打亂列表順序
quick_sort(li)
print(li)

 

快速排序的最壞情況

  快排的運行時間依賴於划分是否平衡,而平衡與否又依賴於用戶划分的主元素。

  • 如果划分是平衡的,那么快速排序算法性能與歸並排序一樣。
  • 如果划分時不平衡的,那么快速排序的性能就接近於插入排序了

因此,快排的最壞情況的發生與快速排序中主元素的選擇是有重大的關系;當主元素是最小元素或最大元素時會使快排性能最差

 

  堆排序(Heapsort)是指利用堆積樹(堆)這種數據結構所設計的一種排序算法,它是選擇排序的一種。可以利用數組的特點快速定位指定索引的元素。

堆的時間復雜度是O(N*logN)空間復雜度是O(1),且是一種不穩定的排序方式。

  在了解堆排序之前我們首先要掌握有關完全二叉樹的知識點,二叉樹博客地址:http://www.cnblogs.com/zhuminghui/p/8409508.html

  堆、是一個完全二叉樹的數據類型,堆根據數據結構的不同可以分為大根堆和小根堆

大根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點大

小根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點小

            

              大根堆                     小根堆

 

堆排序的核心就是要構造堆,將數據構造成堆經過以下步驟就可以得到有序的數據:

  1.   建立堆
  2.   得到堆頂元素,為最大元素
  3.   去掉堆頂,將堆最后一個元素放到堆頂,
  4.   此時可通過一次調整重新使堆有序。
  5.   堆頂元素為第二大元素。 重復步驟3,直到堆變空。

假設我們有這樣一個數據結構:

              

首先我們要構造堆:

            

然后挨個出數(注意每次都要構造堆):

            

用代碼實現:

from timewrap import *
import random

def sift(li, low, high):
    """
    構造堆的過程
    :param li:
    :param low: 堆根節點的位置
    :param high: 堆最后一個節點的位置
    :return:
    """
    i = low         # 父親的位置
    j = 2 * i + 1   # 孩子的位置
    tmp = li[low]   # 最原來的根的值
    while j <= high:
        if j + 1 <= high and li[j+1] > li[j]: # 如果右孩子存在並且右孩子更大
            j += 1
        if tmp < li[j]: # 如果最原來的根的值比孩子小
            li[i] = li[j]  # 把孩子向上移動一層
            i = j
            j = 2 * i + 1
        else:
            break
    li[i] = tmp# 最原來的根的值放到對應的位置上(葉子節點)

@cal_time
def heap_sort(li):
    n = len(li)
    # 1. 建堆
    for i in range(n//2-1, -1, -1):
        sift(li, i, n-1)
    # 2. 挨個出數
    for j in range(n-1, -1, -1):    # j表示堆最后一個元素的位置
        li[0], li[j] = li[j], li[0]
        # 堆的大小少了一個元素 (j-1)
        sift(li, 0, j-1)


li = list(range(10000))
random.shuffle(li)
heap_sort(li)#heap_sort running time: 0.07304835319519043 secs.
print(li)#0~9999已排好序

 

Python中內置的堆排序模塊

  在Python中堆排序有一個內置模塊——heapq模塊,利用它我們可以快速實現一個堆排序

import heapq, random

li = [5,8,7,6,1,4,9,3,2]
heapq.heapify(li)#將列表轉化為堆
print(li)#[1, 2, 4, 3, 8, 7, 9, 5, 6]
print(heapq.heappop(li))#彈出堆的最小值  1
print(heapq.heappop(li))#彈出堆的最小值  2
heapq.heappush(li,10)#插入一個值 
print(li)#[3, 5, 4, 6, 8, 7, 9, 10]
import heapq, random

def heap_sort(li):
    heapq.heapify(li)
    n = len(li)
    new_li = []
    for i in range(n):
        new_li.append(heapq.heappop(li))
    return new_li

li = list(range(10000))
random.shuffle(li)
li = heap_sort(li)
print(li)#從小到大排序

#內置方法直接一行代碼解決問題
print(heapq.nsmallest(100, li))#從小到大排序
print(heapq.nlargest(100, li))#從大到小排序
heapq模塊使用方法

 

堆排序例題

  現在有n個數,設計算法找出前k大的數(k<n)。

思路:取列表前k個元素(假設k=5)建立一個小根堆。堆頂就是目前這k個數中最小的數。 依次向后遍歷原列表,對於列表中的元素,如果小於堆頂,則忽略該元素;如果大於堆頂,則將堆頂更換為該元素,並且對堆進行一次調整,使得堆頂永遠為目前k個數中的最小數。直到遍歷完列表所有元素后,倒序彈出堆頂。

li=[6,8,1,9,3,0,7,2,4,5]
def topk(li,k):
    heap=li[0:k]
    for i in range(k//2-1,-1,-1):
        sift(heap,i,k-1)
    for i in range(k,len(li)):
        if li[i] > heap[0]:
            heap[0]=li[i]
            sift(heap,0,k-1)
    for i in range(k-1,-1,-1):
        heap[0],heap[i]=heap[i],heap[0]
        sift(heap,0,i-1)
View Code

 

  歸並排序(MERGE-SORT)是建立在歸並操作上的一種有效的排序算法,該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合並,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合並成一個有序表,稱為二路歸並

  歸並過程為:比較a[i]和b[j]的大小,若a[i]≤b[j],則將第一個有序表中的元素a[i]復制到r[k]中,並令i和k分別加上1;否則將第二個有序表中的元素b[j]復制到r[k]中,並令j和k分別加上1,如此循環下去,直到其中一個有序表取完,然后再將另一個有序表中剩余的元素復制到r中從下標k到下標t的單元。歸並排序的算法我們通常用遞歸實現,先把待排序區間[s,t]以中點二分,接着把左邊子區間排序,再把右邊子區間排序,最后把左區間和右區間用一次歸並操作合並成有序的區間[s,t]。。

   時間復雜度為O(nlog₂n) 這是該算法中最好、最壞和平均的時間性能。 空間復雜度為 O(n)。歸並排序比較占用內存,但卻是一種 效率高且穩定的算法。

 一次歸並的代碼如下:

def merge(li, low, mid, high):
    i = low
    j = mid + 1
    ltmp = []
    while i <= mid and j <= high:#列表被分為了[low:mid+1],[mid+1:high]兩部分
        #分別取兩段的小的部分
        if li[i] < li[j]:
            ltmp.append(li[i])
            i += 1
        else:
            ltmp.append(li[j])
            j += 1
    while i <= mid:#右取完了段
        ltmp.append(li[i])
        i += 1
    while j <= high:#左段取完了
        ltmp.append(li[j])
        j += 1
    li[low:high+1] = ltmp

 

有時列表的復雜度會比較大,這時我們就需要做好幾次歸並操作才能使得列表有序,這時我們可以用到遞歸。

基本思路:

    分解:將列表越分越小,直至分成一個元素。

    終止條件:一個元素是有序的。

    合並:將兩個有序列表歸並,列表越來越大。

使用遞歸使得列表有序:

import random
from timewrap import *
import copy
import sys


def merge(li, low, mid, high):
    i = low
    j = mid + 1
    ltmp = []
    while i <= mid and j <= high:#列表被分為了[low:mid+1],[mid+1:high]兩部分
        #分別取兩段的小的部分
        if li[i] < li[j]:
            ltmp.append(li[i])
            i += 1
        else:
            ltmp.append(li[j])
            j += 1
    while i <= mid:#右取完了段
        ltmp.append(li[i])
        i += 1
    while j <= high:#左段取完了
        ltmp.append(li[j])
        j += 1
    li[low:high+1] = ltmp


def _merge_sort(li, low, high):
    if low < high:  # 至少兩個元素
        mid = (low + high) // 2
        _merge_sort(li, low, mid)
        _merge_sort(li, mid+1, high)
        merge(li, low, mid, high)
        print(li[low:high+1])

@cal_time
def merge_sort(li):
    # 因為函數要進行遞歸,無法直接安裝飾器,所以在外面加個殼。
    # 不使用裝飾器的話不用寫這個函數,直接用上面的函數就可以
    return _merge_sort(li, 0, len(li)-1)


li = list(range(16))
random.shuffle(li)
merge_sort(li)

print(li)

 

NB 三人組小結 

  • 三種排序算法的時間復雜度都是O(nlogn)
  • 一般情況下,就運行時間而言: 快速排序 < 歸並排序 < 堆排序
  • 三種排序算法的缺點:
      • 快速排序:極端情況下排序效率低
      • 歸並排序:需要額外的內存開銷
      • 堆排序:在快的排序算法中相對較慢

 

前面六種算法的復雜度總結

 

 

 

其他排序算法

  這里補充兩個排序算法——希爾排序和計數算法

  希爾排序(Shell's Sort)是插入排序的一種又稱“縮小增量排序”(Diminshing Increment Sort),是直接插入排序算法的一種更高效的改進版本。希爾排序是 非穩定排序算法。該方法因D.L.Shell於1959年提出而得名。
  希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。

基本思想:

  首先取一個整數d1=n/2,將元素分為d1個組,每組相鄰量元素之間距離為d1,在各組內進行直接插入排序; 取第二個整數d2=d1/2,重復上述分組排序過程,直到di=1,即所有元素在同一組內進行直接插入排序。

擼不懂文字的看圖:

基本代碼實現:

def shell_sort(li):
    d = len(li) // 2#d1
    while d > 0:
        for i in range(d, len(li)):
            tmp = li[i]
            j = i - d#j=1 2 3...
            while li[j] > tmp and j >= 0:
                li[j+d] = li[j]#交換
                j -= d
            li[j+d] = tmp
        d = d >> 1# y>>x 符號表示將y轉化成二進制數后砍掉最后x位,效果與 y/= x 一樣

 

  計數排序是一個非基於比較的排序算法,該算法於1954年由 Harold H. Seward 提出。它的優勢在於在對一定范圍內的整數排序時,它的復雜度為Ο(n+k)(其中k是整數的范圍),快於任何比較排序算法。 

  當然計數排序是一種犧牲空間換取時間的算法,而且當O(k)>O(n*log(n))的時候其效率反而不如基於比較的排序(基於比較的排序的時間復雜度在理論上的下限是O(n*log(n)), 如歸並排序,堆排序)

計數排序對輸入的數據有附加的限制條件:
    1、輸入的線性表的元素屬於有限偏序集S;
    2、設輸入的線性表的長度為n,|S|=k(表示集合S中元素的總數目為k),則k=O(n)。
在這兩個條件下,計數排序的復雜性為O(n)。
  計數排序的基本思想是對於給定的輸入序列中的每一個元素x,確定該序列中值小於x的元素的個數(此處並非比較各元素的大小,而是通過對元素值的計數和計數值的累加來確定)。一旦有了這個信息,就可以將x直接存放到最終的輸出序列的正確位置上。例如,如果輸入序列中只有17個元素的值小於x的值,則x可以直接存放在輸出序列的第18個位置上。當然,如果有多個元素具有相同的值時,我們不能將這些元素放在輸出序列的同一個位置上,因此,上述方案還要作適當的修改。

 

 
算法過程:
  假設輸入的線性表L的長度為n,L=L1,L2,..,Ln;線性表的元素屬於有限偏序集S,|S|=k且k=O(n),S={S1,S2,..Sk};則計數排序可以描述如下:
    1、掃描整個集合S,對每一個Si∈S,找到在線性表L中小於等於Si的元素的個數T(Si);
    2、掃描整個線性表L,對L中的每一個元素Li,將Li放在輸出線性表的第T(Li)個位置上,並將T(Li)減1。

基本代碼實現:

import random
from timewrap import *

@cal_time
def count_sort(li, max_num = 100):
    count = [0 for i in range(max_num+1)]#[0,0,0,0,0,0,...]
    for num in li:
        count[num]+=1#li中每有一個元素,就在count中下標為該元素的位置加一,最后得到的就是下標位置(表示li的元素值)是幾(表示li中該元素的個數)
    li.clear()#清空li
    for i, val in enumerate(count):
        for _ in range(val):
            li.append(i)#將count中不為0的元素的索引值一個一個加到li中,得到的li就是排好序的li

li = [random.randint(0,100) for i in range(100000)]
count_sort(li)

 

 

 

 

                                                      

 

 

 

 

 

 

 

 

                                     

 


免責聲明!

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



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