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

1、列表排序

  排序就是將一組「無序」的記錄序列調整爲「有序」的記錄序列。html

  列表排序:將無序列表變爲有序列表。python

    輸入:列表算法

    輸出:有序列表數組

  兩種基本的排序方式:升序降序數據結構

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

2、常見排序算法  

名稱框架

複雜度dom

說明ide

備註函數

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

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

 

一、冒泡排序(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]
"""

  對比前面排序的次數少了不少,算法獲得了優化~

二、選擇排序(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)。

三、插入排序(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.
"""

四、快速排序(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)

  加入隨機化解決該問題:即再也不找第一個元素歸位,而是隨機找一個值與第一個元素交換,而後繼續執行快速排序,就能夠解決倒序例子時間複雜度特別高的狀況。可是這個方法不能徹底避免最壞狀況,好比每次隨機都剛好選中了最大的一個數,可是這種修改可讓最壞狀況沒法被設計出來,發生最壞狀況的機率也會很是很是小。

五、堆排序(Heap-Sort)

算法——堆和堆排序介紹

六、歸併排序(Merge-Sort)

算法——歸併和歸併排序 

3、排序總結 

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

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

二、快速排序、堆排序、歸併排序

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

(1)通常狀況下,就運行時間來比較:

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

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

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

  歸併排序:須要額外的內存開銷。

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

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

  

(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)代碼複雜度

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

相關文章
相關標籤/搜索