算法——列表排序和常用排序算法


一、列表排序

  排序就是將一組“無序”的記錄序列調整為“有序”的記錄序列。

  列表排序:將無序列表變為有序列表。

    輸入:列表

    輸出:有序列表

  兩種基本的排序方式:升序降序

  python內置的排序函數:sort()。

二、常見排序算法  

名稱

復雜度

說明

備注

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

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

 

1、冒泡排序(Bubble Sort)

  列表每兩個相鄰的數,如果前面比后面大,則交換這兩個數。

  一趟排序完成后,則無序區減少一個數,有序區增加一個數。

  代碼關鍵點:趟、無序區范圍

(1)圖示說明

          

  這樣排序一趟后,最大的數9,就到了列表最頂成為了有序區,下面的部分則還是無序區。然后在無序區不斷重復這個過程,每完成一趟排序,無序區減少一個數,有序區增加一個數。圖示最后一張圖要開始第六趟排序,排序從第0趟開始計數。剩一個數的時候不需要排序了,因此整個排序排了n-1趟。

(2)代碼示例 

import random

def bubble_sort(li):
    for i in range(len(li)-1):    # 總共是n-1趟
        for j in range(len(li)-i-1):   # 每一趟都有箭頭,從0開始到n-i-1
            if li[j] > li[j+1]:  # 比對箭頭指向和箭頭后面的那個數的值
                # 當箭頭所指數大於后面的數時交換位置, 升序排列;條件相反則為降序排列
                li[j], li[j+1] = li[j+1], li[j]


li = [random.randint(0, 10000) for i in range(30)]
print(li)
bubble_sort(li)
print(li)

"""
[5931, 5978, 6379, 4217, 9597, 4757, 4160, 3310, 6916, 2463, 9330, 8043, 8275, 5614, 8908, 7799, 9256, 3097, 9447, 9327, 7604, 9464, 417, 927, 1720, 145, 6451, 7050, 6762, 6608]
[145, 417, 927, 1720, 2463, 3097, 3310, 4160, 4217, 4757, 5614, 5931, 5978, 6379, 6451, 6608, 6762, 6916, 7050, 7604, 7799, 8043, 8275, 8908, 9256, 9327, 9330, 9447, 9464, 9597]
"""

  如果要打印出每次排序結果:

import random

def bubble_sort(li):
    for i in range(len(li)-1):    # 總共是n-1趟
        for j in range(len(li)-i-1):   # 每一趟都有箭頭,從0開始到n-i-1
            if li[j] > li[j+1]:  # 比對箭頭指向和箭頭后面的那個數的值
                # 當箭頭所指數大於后面的數時交換位置, 升序排列;條件相反則為降序排列
                li[j], li[j+1] = li[j+1], li[j]
        print(li)


li = [random.randint(0, 10000) for i in range(5)]
print(li)
bubble_sort(li)
print(li)

"""
[1806, 212, 4314, 1611, 8355]
[212, 1806, 1611, 4314, 8355]
[212, 1611, 1806, 4314, 8355]
[212, 1611, 1806, 4314, 8355]
[212, 1611, 1806, 4314, 8355]
[212, 1611, 1806, 4314, 8355]
"""

(3)算法時間復雜度

  n是列表的長度,算法中也沒有發生循環折半的過程,具備兩層關於n的循環,因此它的時間復雜度是O(n2)

(4)冒泡排序優化

  如果在一趟排序過程中沒有發生交換就可以認定已經排好序了。因此可做如下優化:

import random

def bubble_sort(li):
    for i in range(len(li)-1):    # 總共是n-1趟
        exchange = False
        for j in range(len(li)-i-1):   # 每一趟都有箭頭,從0開始到n-i-1
            if li[j] > li[j+1]:  # 比對箭頭指向和箭頭后面的那個數的值
                # 當箭頭所指數大於后面的數時交換位置, 升序排列;條件相反則為降序排列
                li[j], li[j+1] = li[j+1], li[j]
                exchange = True   # 如果發生了交換就置為true
        print(li)
        if not exchange:
            # 如果exchange還是False,說明沒有發生交換,結束代碼
            return


# li = [random.randint(0, 10000) for i in range(5)]
li = [1806, 212, 4314, 1611, 8355]
bubble_sort(li)

"""
[212, 1806, 1611, 4314, 8355]
[212, 1611, 1806, 4314, 8355]
[212, 1611, 1806, 4314, 8355]
"""

  對比前面排序的次數少了很多,算法得到了優化~

2、選擇排序(Selection Sort)

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

  算法關鍵點:有序區和無序區、無序區最小數的位置

(1)簡單的選擇排序

def select_sort_simple(li):
    li_new = []
    for i in range(len(li)):
        min_val = min(li)   # 找到最小的數,也需要遍歷一邊O(n)
        li_new.append(min_val)
        li.remove(min_val)   # 按值刪除,如果有重復的先刪除最左邊的,刪除之后,后面元素需要向前移動補位,因此也是O(n)
    return li_new


li = [3, 2, 4, 1, 5, 6, 8, 7, 9]
print(select_sort_simple(li))
"""
[1, 2, 3, 4, 5, 6, 7, 8, 9]
"""

  注意這里的remove操作和min操作都不是O(1)的操作,都需要進行遍歷,因此它的時間復雜度是O(n2)。

  而且前面冒泡排序是原地排序不需要開啟一個新的列表,二這個版本的選擇排序不是原地排序,多占了一份內存。

(2)優化后的選擇排序

def select_sort(li):
    # 和冒泡排序類似,在n-1趟完成后,無序區只剩一個數,這個數一定是最大的
    for i in range(len(li)-1):   # i是第幾趟
        min_loc = i     # 最小值的位置
        for j in range(i+1, len(li)):   # 遍歷無序區,從i開始是自己跟自己比,因此從i+1開始
            if li[j] < li[min_loc]:   # 如果遍歷的這個數小於現在min_loc位置上的數
                min_loc = j     # 修改min_loc的index,循環完后,min_loc一定是無序區最小數的下標
        li[i], li[min_loc] = li[min_loc], li[i]  # 將i和min_loc對應的值進行位置交換
        print(li)   # 打印每趟執行完的排序,分析過程


li = [3, 2, 4, 1, 5, 6, 8, 7, 9]
select_sort(li)
# print(li)   # [1, 2, 3, 4, 5, 6, 7, 8, 9]

  這里只有兩層循環,時間復雜度是O(n2)。

3、插入排序(Insertion Sort)

  元素被分為有序區和無序區兩部分。初始時手里(有序區)只有一張牌,每次(從無序區)摸一張牌,插入到手里已有牌的正確位置,直到無序區變空。

(1)圖示說明

  一開始手里的牌只有5

  

  第一張摸到的牌是7,比5大插到5的右邊:

  

  第二張摸到的牌是4,需要將5和7的位置向右挪,將4插到最前面:

  

  后面的情況依次類推。

(2)代碼示例

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:  # 循環條件
            """
            循環終止條件:如果手里最后一張牌 <= 摸到的牌  or j == -1
                比如手里有牌457,新摸到一張6(index=3),當比對5與6時,5<6,滿足了循環終止條件,插到列表j+1處,即index=2處.
                比如手里的牌是4567,新摸到一張3(index=4),一個個比對均比3大,到4與3比較時,由於比4小,再次循環j=-1,滿足終止條件插到列表j+1處,即最前面
            """
            li[j + 1] = li[j]  # 通過循環條件,將手里的牌左移
            j -= 1  # 手里的牌對比箭頭左移
        li[j + 1] = tmp  # 將摸到的牌插入有序區
        print(li)  # 打印每一趟排序過程


li = [3, 2, 4, 1, 5, 6, 9, 6, 8]
print('原列表', li)
insert_sort(li)

print('排序結果', li)

  這個循環主要是在找插入的位置。

  時間復雜度:O(n2)。

(3)查看排序算法執行時間和效率

  准備好cal_time.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

  檢查10000個隨機數字排序:

import random
from cal_time 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:  # 循環條件
            li[j + 1] = li[j]  # 通過循環條件,將手里的牌左移
            j -= 1  # 手里的牌對比箭頭左移
        li[j + 1] = tmp  # 將摸到的牌插入有序區
        # print(li)  # 打印每一趟排序過程

li = list(range(10000))
random.shuffle(li)
insert_sort(li)
"""
insert_sort running time: 4.496495723724365 secs.
"""

4、快速排序(Quick Sort)

  快速排序思路:取一個元素p(第一個元素),使元素p歸位;列表被p分為兩部分,左邊都比p小,右邊都比p大;遞歸完成排序。

  算法關鍵點:歸位、遞歸。

 (1)圖示說明

  

(2)元素歸位過程分析

  5要歸位,先用一個變量將5存起來,兩個箭頭表示當前列表的left和right:

  

  列表左邊有了一個空位,從右邊開始找一個比5小的數填入:

  

  此時右邊有了一個空位,右邊是給比5大的數准備的,從左邊開始找比5大的數填入:

  

  同理,此時左邊又有了空位繼續從右邊開始找比5小的數填過去,以此類推

  

 

  最后要找比5大的數放到右邊去,但是3<5,這時left和right重合了,此時說明位置已經在中間了,將5放回。

   

(3)歸位代碼實現

def partition(li, left, right):
    """
    歸位函數
    :param li: 列表
    :param left: 左箭頭
    :param right: 右箭頭
    :return:
    """
    tmp = li[left]
    while left < right:
        while left < right and li[right] >= tmp:    # 從右邊找一個比tmp小的數放過來
            # 注意由於循環條件是li[right] >= tep,在兩個箭頭相遇時不會退出循環,因此添加left<right條件
            right -= 1   # 如果比tmp大則right往左走一步
        li[left] = li[right]    # 將右邊找的數插入到左邊空位處
        print(li)  # 打印排序過程

        while left<right and li[left] <= tmp:      # 從左邊找一個比tmp大的數放入右邊的空位
            left += 1    # 如果比tmp小則left往右走一步
        li[right] = li[left]    # 將左邊的值寫入到右邊空位處
        print(li)  # 打印排序過程


    # 循環終止條件:left>=right
    li[left] = tmp    # 將tmp歸位


li = [5,7,4,6,3,1,2,9,8]
print("原列表", li)
partition(li, 0, len(li)-1)
print("排序結果", li)
"""
原列表 [5, 7, 4, 6, 3, 1, 2, 9, 8]
[2, 7, 4, 6, 3, 1, 2, 9, 8]
[2, 7, 4, 6, 3, 1, 7, 9, 8]
[2, 1, 4, 6, 3, 1, 7, 9, 8]
[2, 1, 4, 6, 3, 6, 7, 9, 8]
[2, 1, 4, 3, 3, 6, 7, 9, 8]
[2, 1, 4, 3, 3, 6, 7, 9, 8]
排序結果 [2, 1, 4, 3, 5, 6, 7, 9, 8]
"""

  注意無論從左邊找還是從右邊找,都需要添加left<right條件,在箭頭相遇時跳出循環。還可以注意到每次寫入空位,並不是真正的空位,仍由原元素占位在空位出,直到tmp歸位,整個列表才沒有了重復的元素。

(4)快速排序代碼實現

def partition(li, left, right):
    """
    歸位函數
    :param li: 列表
    :param left: 左箭頭
    :param right: 右箭頭
    :return:
    """
    tmp = li[left]
    while left < right:
        while left < right and li[right] >= tmp:    # 從右邊找一個比tmp小的數放過來
            # 注意由於循環條件是li[right] >= tep,在兩個箭頭相遇時不會退出循環,因此添加left<right條件
            right -= 1   # 如果比tmp大則right往左走一步
        li[left] = li[right]    # 將右邊找的數插入到左邊空位處
        print(li)  # 打印排序過程

        while left<right and li[left] <= tmp:      # 從左邊找一個比tmp大的數放入右邊的空位
            left += 1    # 如果比tmp小則left往右走一步
        li[right] = li[left]    # 將左邊的值寫入到右邊空位處
        print(li)  # 打印排序過程


    # 循環終止條件:left>=right
    li[left] = tmp    # 將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)


li = [5,7,4,6,3,1,2,9,8]
quick_sort(li, 0, len(li)-1)
print(li)

  注意這里使用了partition歸位函數和快速排序遞歸框架完成了快速排序設計。

(5)快速排序的效率

  快速排序的時間復雜度:O(nlogn),每一層排序的復雜度是O(n),總共有logn層。

(6)快速排序改寫

  想給quick_sort添加裝飾器查看排序運行效率,但是遞歸函數不能添加裝飾器,因此需要做如下改寫:

from cal_time import *

def partition(li, left, right):......

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):
    _quick_sort(li, 0, len(li)-1)

(7)測試驗證快排和冒泡排序執行效率

# -*- coding:utf-8 -*-
__author__ = 'Qiushi Huang'

import random
from cal_time import *
import copy   # 復制模塊


def partition(li, left, right):
    """
    歸位函數
    :param li: 列表
    :param left: 左箭頭
    :param right: 右箭頭
    :return:
    """
    tmp = li[left]
    while left < right:
        while left < right and li[right] >= tmp:    # 從右邊找一個比tmp小的數放過來
            # 注意由於循環條件是li[right] >= tep,在兩個箭頭相遇時不會退出循環,因此添加left<right條件
            right -= 1   # 如果比tmp大則right往左走一步
        li[left] = li[right]    # 將右邊找的數插入到左邊空位處
        # print(li)  # 打印排序過程

        while left<right and li[left] <= tmp:      # 從左邊找一個比tmp大的數放入右邊的空位
            left += 1    # 如果比tmp小則left往右走一步
        li[right] = li[left]    # 將左邊的值寫入到右邊空位處
        # print(li)  # 打印排序過程


    # 循環終止條件:left>=right
    li[left] = tmp    # 將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):
    _quick_sort(li, 0, len(li)-1)


@cal_time
def bubble_sort(li):
    for i in range(len(li)-1):    # 總共是n-1趟
        exchange = False
        for j in range(len(li)-i-1):   # 每一趟都有箭頭,從0開始到n-i-1
            if li[j] > li[j+1]:  # 比對箭頭指向和箭頭后面的那個數的值
                # 當箭頭所指數大於后面的數時交換位置, 升序排列;條件相反則為降序排列
                li[j], li[j+1] = li[j+1], li[j]
                exchange = True   # 如果發生了交換就置為true
        # print(li)
        if not exchange:
            # 如果exchange還是False,說明沒有發生交換,結束代碼
            return

li = list(range(10000))
random.shuffle(li)

li1 = copy.deepcopy(li)   # 深拷貝
li2 = copy.deepcopy(li)

quick_sort(li1)
bubble_sort(li2)
"""
quick_sort running time: 0.03162503242492676 secs.
bubble_sort running time: 10.773478269577026 secs.
"""
print(li1)  # [0, 1, 2, 3, 4,..., 9997, 9998, 9999]
print(li2)
冒泡排序和快速排序效率對比

  對比運行時間,可以發現針對10000個元素的數組排序,快速排序的效率比冒泡排序高了幾百倍。

  時間復雜度O(nlogn)和O(n2)在數量越大的情況下,效率相差將越來越大。

  快速排序的最好情況時間復雜度是O(n),一般情況時間復雜度是O(nlogn),最壞情況時間復雜度是O(n2)。

(8)快速排序存在的問題

  首先python有一個遞歸最大深度的問題,默認是999,修改遞歸最大深度方法:

import sys

sys.setrecursionlimit(100000)   # 修改遞歸最大深度

雖然可以修改;而且遞歸會相當消耗一部分的系統資源。

  其次快速排序有一個最壞情況出現:倒序排列的數組,在這種情況下,快速排序無法兩邊同時排序,每次只能排序一個數字。因此在這種情況下快速排序的時間復雜度是:O(n2)

  加入隨機化解決該問題:即不再找第一個元素歸位,而是隨機找一個值與第一個元素交換,然后繼續執行快速排序,就可以解決倒序例子時間復雜度特別高的情況。但是這個方法不能完全避免最壞情況,比如每次隨機都恰好選中了最大的一個數,但是這種修改可以讓最壞情況無法被設計出來,發生最壞情況的概率也會非常非常小。

5、堆排序(Heap-Sort)

算法——堆和堆排序介紹

6、歸並排序(Merge-Sort)

算法——歸並和歸並排序 

三、排序總結 

1、冒泡排序、選擇排序、插入排序

  冒泡排序、選擇排序、插入排序的時間復雜度都是O(n2),且都是原地排序。

2、快速排序、堆排序、歸並排序

  快速排序、堆排序、歸並排序這三種排序算法的時間復雜度都是O(nlogn)。 但有常數差異。

(1)一般情況下,就運行時間來比較:

    快速排序(速度最快)< 歸並排序 < 堆排序

(2)三種排序算法的缺點:

  快速排序:極端情況下排序效率低。

  歸並排序:需要額外的內存開銷。

  堆排序:在快的排序算法中相對較慢。

3、六種排序算法對比總結

  

(1)遞歸占用空間

  遞歸需要用系統占的空間,快速排序在平均情況下需要遞歸logn層,所以平均情況下需要消耗O(logn)的空間復雜度;最壞情況下需要遞歸n層,因此需要消耗O(n)的時間復雜度。

  歸並雖然也有遞歸,但他已經開了一個列表了占用O(n),歸並遞歸需要的空間復雜度是O(logn)小於O(n),因此統計空間復雜度是O(n)。

(2)排序算法穩定性

  假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱為不穩定的。

  判斷是否算法是否穩定:挨着換的穩定,不挨着換的不穩定

(3)代碼復雜度

  算法是否好寫,是否容易理解。

 


免責聲明!

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



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