利用Python实现几种常见排序算法

  • Post author:
  • Post category:python



一、排序算法概览

插入排序:直接插入排序,二分法插入排序

选择排序:直接选择排序,堆排序

交换排序:冒泡排序,快速排序

归并排序


二、代码实现


1.直接插入排序

最简单直接的一种方式,序列在排序中可分为左边已排序部分和右边未排序部分。每次从未排序部分取出一个数,通过与已排序部分逐次比较,移动,最终放到正确的位置。这个算法不使用辅助空序列,空间复杂度为O(1),平均时间复杂度为O(n^2)。当序列为基本有序时,插入排序能做到O(n)。

def inset_sort(lst):
    for i in range(1, len(lst)):
        x = lst[i]
        j = i
        while j > 0 and lst[j-1] > x:
            lst[j] = lst[j-1]
            j -= 1
        lst[j] = x


2.二分法插入排序

在直接插入的基础上,通过二分法可以快速找到应该插入的位置,优化了比较次数。但仍需要逐步移动数据。因此,和直接插入排序比较,复杂度没有质的改变。

def bisect_inset_sort(lst):
    for i in range(1, len(lst)):
        x = lst[i]
        left = 0
        right = i-1
        mid = int(right / 2)
        # 找到最终位置
        while left < right:
            if lst[mid] > x:
                right = mid-1
            elif lst[mid] <= x:
                left = mid+1
            mid = int((left + right) / 2)
        if lst[mid] <= x:
            index = mid+1
        else:
            index = mid
        # 通过交换移动到最终位置
        for j in range(i, index, -1):
            lst[j-1], lst[j] = lst[j], lst[j-1]


3.直接选择排序

每次从未排序的部分选择最大/小值插入已排序末尾。时间复杂度O(n^2),空间复杂度O(1)

def select_sort(lst):
    for i in range(len(lst)-1):
        k = i
        for j in range(i, len(lst)):
            if lst[j] < lst[k]:
                k = j
        if i != k:
            lst[i], lst[k] = lst[k], lst[i]


4.冒泡排序

每一遍检查,都是两两比较并交换位置。最终导致该次最大元素放到正确位置,同时,较大元素可能移动一定位置。平均时间复杂度O(n^2),空间复杂度O(1)。这个算法对已排序的序列不友好,比如逆序序列要顺序排列,每个元素都需要移动最大距离。

def bubble_sort(lst):
    n = len(lst)
    for i in range(n):
        found = False
        # 可以理解为从0到n-i-1中找到最大值放在n-i-1这个位置
        for j in range(0, n - i - 1):
            if lst[j] > lst[j + 1]:
                lst[j], lst[j + 1] = lst[j + 1], lst[j]
                found = True
        # 如果一轮排序下来没有遇到逆序排列,说明已经全部排好
        if not found:
            break


5.快速排序

(1)方法1,以一个元素为基准,将序列划分为大于和小于该基准两部分,分列左右两边,基准在中间。然后对左右两部分再次进行相同操作。易知,如果每次都能选择中间值作为基准,保证划分的两部分大小相同,此时效率最高。平均时间复杂度O(nlogn),对于空间复杂度,虽然快排也没有使用额外辅助(只有几个临时变量),但使用了递归,有了额外花销。与具体实现有关。

def quick_sort(lst):
    base_sort(lst, 0, len(lst)-1)


def base_sort(lst, left, right):
    if left >= right:
        return
    i, j = left, right
    k = lst[i]
    while i < j:
        while i < j and lst[j] >= k:
            j -= 1
        if i < j:
            lst[i] = lst[j]
            i += 1
        while i < j and lst[i] <= k:
            i += 1
        if i < j:
            lst[j] = lst[i]
            j -= 1
    lst[i] = k
    base_sort(lst, left, i-1)
    base_sort(lst, i+1, right)

(2)方法2,将序列根据基准划分为小记录,大记录,未排序。每次从未排序中选第一个元素,如果大于等于基准,指针往后移;若小于基准,交换大记录第一个和本元素

def quick_sort2(lst):
    def qsort(lst, begin, end):
        if begin >= end:
            return
        k = lst[begin]
        i = begin
        for j in range(begin+1, end+1):
            if lst[j] < k:
                i += 1
                lst[i], lst[j] = lst[j], lst[i]
        lst[begin], lst[i] = lst[i], lst[begin]
        # 递归,再对小于k和大于k的两段序列进行排序
        qsort(lst, begin, i-1)
        qsort(lst, i+1, end)

    qsort(lst, 0, len(lst)-1)

(3) 三路快排,即将数据划分为小于基准、等于基准、大于基准三部分,等于基准的部分不参与下一轮排序。这个方法在数组中存在大量相同元素时会非常高效。

def quick_sort3(lst):
    def sort(l, r):
        if l >= r:
            return
        lt = l
        gt = r+1
        i = l+1
        # 基准的选择也可以随机
        v = lst[l]
        while i < gt:
            if lst[i] < v:
                lt += 1
                i += 1
            elif lst[i] > v:
                lst[i], lst[gt-1] = lst[gt-1], lst[i]
                gt -= 1
            else:
                i += 1
        lst[l], lst[lt] = lst[lt], lst[l]
        sort(l, lt-1)
        sort(gt, r)
    sort(0, len(lst)-1)


6.归并排序

一开始,一个元素看作一个排好序的段,相邻段合并成包含两个元素的有序段,以此类推。直至段的长度等于序列长度。时间复杂度O(nlogn),这里使用了辅助列表,长度和原序列相同,因此,空间复杂度为O(n)。

def merge_sort(lst):
    """负责合并两个子序列"""
    def merge(lfrom, lto, low, mid, high):
        i, j, k = low, mid, low
        while i < mid and j < high:
            if lfrom[i] <= lfrom[j]:
                lto[k] = lfrom[i]
                i += 1
            else:
                lto[k] = lfrom[j]
                j += 1
            k += 1
        # 复制第一段剩余记录
        while i < mid:
            lto[k] = lfrom[i]
            i += 1
            k += 1
        # 复制第二段剩余记录
        while j < high:
            lto[k] = lfrom[j]
            j += 1
            k += 1
    """完成一遍合并,将子序列两两合并"""
    def merge_pass(lfrom, lto, llen, slen):
        i = 0
        while i + 2 * slen < llen:
            merge(lfrom, lto, i, i+slen, i+2*slen)
            i += 2*slen
        # 剩两段,最后一段长度小于slen
        if i + slen < llen:
            merge(lfrom, lto, i , i+slen, llen)
        # 只剩一段,直接复制
        else:
            for j in range(i, llen):
                lto[j] = lfrom[j]

    slen, llen = 1, len(lst)
    # 和原表等长的辅助表
    tmp_lst = [None] * llen
    while slen < llen:
        merge_pass(lst, tmp_lst, llen, slen)
        slen *= 2
        # 在原表和辅助表之间来回进行归并
        merge_pass(tmp_lst, lst, llen, slen)
        slen *= 2


7.总结

各种排序算法各有适用场景,实际使用中也并非只用一种。比如在使用快速排序和归并排序时,针对短序列(比如当被排序序列长度小于15)可以使用直接插入排序这样的简单稳定算法。



版权声明:本文为qq_45055172原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。