算法黑书中排序算法总结

摘要:介绍几种常用的排序算法和基本思想

排序算法大致可以从两种思路入手,一种是我们常见的基于比较的排序算法。即通过比较两数大小来得到顺序排列;另一种是通过位置,比如说把数字放到对应数组下标中,用空间换取时间来避免排序(可以证明基于排序的最优只能是 O ( n ∗ l g n ) O(n*lgn) O(nlgn)的时间复杂度)。

基于比较有思路最简单的冒泡排序,表现优异的归并排序和快速排序,打扑克常用的插入排序,维护排列简单的堆排序。非比较的主要介绍计数排序和基数排序。

这篇文章主要是总结一下自己对排序算法的理解,具体每个算法的实现、讲解可以参考链接。里面还带有图片,就不盗图了。

https://www.cnblogs.com/onepixel/p/7674659.html

冒泡排序适合初入门了解一下,但是算法设计中用的并不多,毕竟这个时间复杂度有点高,而且系数也不小,优点的话我的感觉就是思路简单,原址排序(在原数组上进行更改)。大致思路是从头到尾比较,如果前一个比后面这个大(大、小、加等号都可),就交换位置,这样每次把未排的最大的放到了最后。重复操作直到排序完成。冒泡排序的简单实现如下:

def BubbleSort(A):
    for i in range(len(A)-1):
        for j in range(len(A)-1,i,-1):
            if A[j] < A[j-1]:
                A[j],A[j-1] = A[j-1],A[j]

归并排序的渐进时间是 O ( n ∗ l g n ) O(n*lgn) O(nlgn),这点比较好,而且归并排序可以作为分治算法的学习例子,能够很好的理解分治算法的条件,步骤,关键点。但是归并排序并不是原址的。Python内置的排序算法好像就是用归并实现的,所以实际写代码的时候也蛮方便的。归并排序的大致思路是:因为排序具有最优子结构性质,把n规模的数据均分成两部分,分别递归调用后,再根据两个有序的部分通过比较合并成一大个有序的部分。难点即在于合并部分,重点的优化也在这里,分治算法的讲解可以参考我的另一篇博客。分治算法的实现如下:

#n*lgn时间对集合进行排序,稳定的
def Merge(A,p,q,r):   #合并部分  pqr为列表下标
    np = q - p + 1#包括q
    nq = r - q
    L = []
    R = []
    for i in range(np):
        L.append(A[p+i])
    for j in range(nq):
        R.append(A[q+j+1])
    L.append(float("inf"))   #结尾插入两个标记阻止比较越界
    R.append(float("inf"))
    i = j = 0
    for k in range(p,r+1):
        if L[i] <= R[j]:
            A[k] = L[i]
            i += 1
        else:
            A[k] = R[j]
            j += 1
def MergeSort(A,p,r):  #归并排序主体(分治的主体)
    if p < r:
        q = (p+r)//2
        MergeSort(A,p,q)
        MergeSort(A,q+1,r)
        Merge(A,p,q,r)

快速排序在实际运行过程中通常比堆排序要快,虽然它的最坏运行时间是 O ( n 2 ) O(n^2) O(n2),但期望运行时间和堆排序一样是 O ( n ∗ l g n ) O(n*lgn) O(nlgn)。与插入排序类似,快速排序的代码非常紧凑,运行时间中隐含的常数系数较小,而且快速排序是原址排序的,它是排序大数组的时候用的最多的算法。快速排序的思路是每次选一个基准,把比它小的放前面,大的放后面,再分别排序。可以想到基准的选择对排序的性能影响比较大,所以会出现随机快排,即基准数是所有数字中随机的一个,这样就不存在特别坏的输入序列,而最坏情况的发生概率是特别小的,可以放心使用。快速排序的实现如下:

import random
def Partition(A, p, r):
    x = A[r]
    i = p - 1
    for j in range(p, r):
        if A[j] <= x:
            i = i + 1
            A[i], A[j] = A[j], A[i]
    A[i+1], A[r] = A[r], A[i+1]
    return i+1
def QuickSort(A, p, r):
    if p < r:
        q = Partition(A, p, r)
        QuickSort(A, p, q-1)
        QuickSort(A, q+1, r)

def Random_Partition(A, p, r):
    i = random.randint(p, r)
    A[r], A[i] = A[i], A[r]
    return Partition(A, p, r)
def Random_QuickSort(A, p, r):
    if p < r:
        q = Random_Partition(A, p, r)
        Random_QuickSort(A, p, q-1)
        Random_QuickSort(A, q+1, r)

插入排序和我们打扑克的时候有点像,每次都维护手中的牌是有序的,把接下来的这张插入到顺序合适的地方。插入排序的最坏运行时间是 O ( n 2 ) O(n^2) O(n2),但是它的内层循环非常紧凑,对于小规模输入,插入排序是非常适用的。插入排序也是一种原址排序算法,原址排序在有的时候非常需要,可以带来一定的便捷,而且省空间。插入排序的复杂度体现在查找新元素的合适位置,它总是从后往前比较找到自己的合适位置,这样从算法上分析查找过程可能就需要 O ( n ) O(n) O(n)的运行时间,这一点不是很划算。可以利用二分查找修改插入排序,达到总的最坏运行时间为 O ( n ∗ l g n ) O(n*lgn) O(nlgn)。算法的具体实现如下:

def InsertSort(A):
    for j in range(1, len(A)):
        key = A[j]#原地排序
        #insert A[j] into the sorted sequence A[0]toA[j-1]
        for i in range(j-1,-1,-1):
            if A[i] > key:
                A[i+1] = A[i]
                if i == 0:  #到0没法往前比了
                    A[i] = key
            else:
                A[i+1] = key

通过二分查找修改后的插入排序为:

def ModifyBinary(A,p,q,v):
    n = q - p + 1
    if n == 0:
        return p
    elif A[p+n//2] == v:
        return p+1 #不稳定排序
    elif A[p+n//2] < v:
        if q > p + n//2 and A[p + n//2+1] < v:
            return ModifyBinary(A, n//2+2, q, v)
        else:
            return p + n//2+1
    else:
        if n // 2 > 0 and A[p + n//2 - 1] > v:
            return ModifyBinary(A, p, n//2-2, v)
        else:
            return p + n//2
def ModifyInsert(A):
    for j in range(1, len(A)):
        key = A[j]#原地排序
        #insert A[j] into the sorted sequence A[0]toA[j-1]
        A.insert(ModifyBinary(A, 0, j-1, A[j]), A[j])
        A.pop(j+1)

堆排序应该是很多算法里面都需要的。尤其是优先队列的维护,每个维护操作的时间只要 O ( l g n ) O(lgn) O(lgn). 这里堆这个数据结构用的不是结构体等,就是一个数组。所以首先需要知道给定一个下标 i,如何获取它的父节点和左右孩子结点。此外,二叉堆可以分为两种形式,一种是最大堆、一种是最小堆。堆排序用的是最大堆,而优先队列通常用最小堆实现。具体的堆中的操作和堆排序算法见代码:

#heaplength = len(A) #数组长度
#heapsize = heaplength  #有效数据长
#返回父节点   O(1)
def parent(i):
    return i//2
#返回左孩子下标  O(1)
def left(i):
    return 2*i
#返回右孩子下标  O(1)
def right(i):
    return 2*i + 1
#对可能违反最大堆性质的坐标i调整以满足最大堆性质  O(lgn)
def max_heapify(A, i, heapsize):
    l = left(i)
    r = right(i)
    if l < heapsize and A[l] > A[i]:
        largest = l
    else:
        largest = i
    if r < heapsize and A[r] > A[largest]:
        largest = r
    if largest != i:
        A[i], A[largest] = A[largest], A[i]
        max_heapify(A, largest, heapsize)
# 建立最大堆  O(n) 证明见p88
def build_max_heap(A, heaplength):
    heapsize = heaplength
    for i in range(heaplength//2, -1, -1):
        max_heapify(A, i, heapsize)
#堆排序  O(n*lgn)
def heap_sort(A, heapsize):
    build_max_heap(A, heapsize)
    for i in range(heapsize-1,  0, -1):
        A[0], A[i] = A[i], A[0]
        heapsize -= 1
        max_heapify(A, 0, heapsize)
#返回优先队列最大值
def heap_maximum(A):
    return A[0]
#去掉并返回最大键字元素
def heap_extract_max(A, heapsize):
    if heapsize < 1:
        return -1
    max = A[0]
    A[1] = A[heapsize-1]
    heapsize -= 1
    max_heapify(A, 0, heapsize)
    return max
#将x关键字值增加到k 并保持性质
def heap_increase_key(A, i, key):
    if key < A[i]:
        print("error")
        return
    A[i] = key
    while i > 0 and A[parent(i)] < A[i]:
        A[i], A[parent(i)] = A[parent(i)], A[i]
        i = parent(i)
#插入一个元素  思路就是先加成负无穷 然后增加到k
def max_heap_insert(A, key):
    A.append(-float("inf"))
    heap_increase_key(A, len(A)-1, key)
A = [3, 2, 5, 6, 1]
max_heap_insert(A, 666)
heap_sort(A, len(A))
print(A)

在介绍非比较排序之前我们先探讨探讨排序算法的运行时间下界:考虑一个决策树模型。从根节点开始每个内部结点表示一次比较。左右孩子表示比较的结果后下一次进行的比较操作(不失一般性的假设元素互异)。每个叶子节点对应一个排序的结果。对于一个完整的决策树而言,应该包括 n n n个输入 n ! n! n!个排序的结果,即有 n ! n! n!个叶子节点。树的高度 h h h对应于最坏情况下的输入对应需要 h h h次比较操作。对于高度为 h h h的二叉树,叶子结点最多 2 h 2^h 2h个。所以有 n ! ≤ 2 h n!≤2^h n!2h. 所以有 l g n ! ≤ h lgn!≤h lgn!h, 即 n ∗ l g n ≤ h n*lgn≤h nlgnh. 所以基于排序的算法的时间复杂度 T ( n ) T(n) T(n)至少为 O ( h ) = O ( n ∗ l g n ) O(h)=O(n*lgn) O(h)=O(nlgn)

非比较排序
计数排序是一种新思路,通过存储数字在对应数组下标位置达到排序的目的,当数据大小和数据个数规模相当的时候,计数排序能达到 O ( n ) O(n) O(n)的时间复杂度。

def CountingSort(A, B, k): #A B 下标0~len(A)-1  k= len(A)+1
    C = [0] * k
    for j in range(len(A)):
        C[A[j]] += 1   #这个大小的元素个数
    for i in range(1, k):  #对应的就是元素在B中应该的位置
        C[i] += C[i-1]
    for j in range(len(A)-1, -1, -1):
        B[C[A[j]]-1] = A[j]
        C[A[j]] -= 1   #减小放到前一个,因此是稳定排序

计数排序相对好理解一点,但是数字比较大的时候占用空间也比较严重,所以引出更复杂一点的基数排序。

基数排序
算法很简单,从低位到高位分别以每一位作为依据进行稳定的计数排序,若有最大数据有 d d d位数字,则需要运行d次。故总时间 O ( n ∗ d ) O(n*d) O(nd). 在数据比较大的时候对每位排序起到了非常棒的效果。代码如下:

def GetNumd(num, d):  #返回num的第d位数字  最低位为1
    return (num//(10**(d-1)))%10
def dCountingSort(A, B, d): #A B 下标0~len(A)-1  k= len(A)+1
    k = 10
    C = [0] * k
    for j in range(len(A)):
        C[GetNumd(A[j], d)] += 1   #这个大小的元素个数
    for i in range(1, k):  #对应的就是元素在B中应该的位置
        C[i] += C[i-1]
    for j in range(len(A)-1, -1, -1):
        B[C[GetNumd(A[j], d)]-1] = A[j]
        C[GetNumd(A[j], d)] -= 1   #减小放到前一个,因此是稳定排序
def RadixSort(A, d):
    B = A.copy()
    for i in range(1, d+1):
        dCountingSort(A, B, i)
        A = B.copy()
    return A
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值