细聊排序算法

常见的排序算法:选择排序,插入排序,冒泡排序,快速排序,堆排序,桶排序,基数排序。

一个一个来聊吧。

目录

冒泡排序:

选择排序:

插入排序:

归并排序:

快速排序:

堆排序:

桶排序和基数排序:

总结


冒泡排序:

'''
冒泡排序
它重复地遍历要排序的队列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。
这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端
'''

A = [12,5,2,4,6,2,1,26,36,30,5]

for i in range(len(A)):    
    for j in range(len(A)-i-1):  # 控制每轮相邻比较的次数
        if A[j] > A[j+1]:  # 如果前面的元素的值大于后面一个元素 互换值
            tmp = A[j]
            A[j] = A[j+1]
            A[j+1] = tmp
    

选择排序:

'''
选择排序
从所有序列中先找到最小的,然后放到第一个位置。之后把剩余元素中最小的,放到第二个位置 …… 以此类推
'''

for i in range(len(A)):  
    min_index = i
    for j in range(i+1, len(A)):  
        if A[j] < A[min_index]:  
            min_index=j
    tmp = A[i]
    A[i] = A[min_index]
    A[min_index] = tmp

这两种排序平均时间复杂度都很高,经常讨论和优化的是以下几个:

插入排序:

"""
插入排序:通过遍历,和前边元素依次比较,然后插入到合适位置
"""
def insert_sort1(A):
    # 基于数组
    for k in range(1, len(A)):
        tmp = A[k]
        j = k
        while j > 0 and A[j-1] > tmp:
            A[j] = A[j-1]
            j -= 1
        A[j] = tmp


def insert_sort2(L):
    # 基于链表, 对于链表内部实现细节改日再聊
    if len(L) > 1:
        marker = L.first()  # marker标记序列排序部分最右边的位置
    while marker != L.last():
        pivot = L.after(marker)
        value = pivot.element()
        if value > marker.element():
            marker = pivot
        else:
            walk = marker
            while walk != L.first() and L.before(walk).element() > value:
                walk = L.before(walk)
            L.delete(pivot)
            L.add_before(walk, value)

归并排序:

说归并排序之前先聊聊分治法:

在分治法的算法设计模式当中,使用了递归的方法,递归可以十分简练的描述一个算法。主要分三步:

  1. 分解:输入规格小于确定的阈值(比如一个或两个元素), 我们就可以直接处理后返回答案。否则,我们把输入值分解为两个或更多的互斥子集。
  2. 解决子问题:递归地解决这些与子集相关的子问题。
  3. 合并:整理这些子问题的解,然后把它们合并成一个整体用以解决最开始的问题。

可以用一个二叉树T形象化一个递归排序算法的执行过程,称这个二叉树为归并排序树,T的每一个节点表示归并排序算法的一个递归调用。归并排序树算法的可视化容易理解分析整个归并排序的运行时间,归并排序树的高度大约是h <= logn(证明忽略)。归并排序的时间复杂度为O(nlogn) (树的每一层运行总时间为O(n), 最后乘以树的高度,至于每一层时间怎么得来,不详细描述)

"""
归并排序: 使用分治法,先拆分,然后递归解决每个小序列,最后合并得到结果。基于数组实现。
"""
def merge(S1, S2, S):
    # 把两个有序序列S1, S2合并成一个有序序列S
    i = j = 0
    while i + j < len(S):
        if j == len(S2) or (i < len(S1) and S1[i] < S2[j]):
            S[i+j] = S1[i]
            i += 1
        else:
            S[i+j] = S2[j]
            j += 1

def merge_sort(S):
    # 结合merge方法, 对S进行排序
    n = len(S)
    if n < 2:
        return
    mid = n // 2  # 根据mid将S一分为二
    S1 = S[0:mid]
    S2 = S[mid:n]
    merge_sort(S1)  # 对每个子序列递归调用
    merge_sort(S2)
    merge(S1, S2, s)  # 从树的最底层开始,由下往上, 由小往大合并子序列。最后得到总序列

A = [1, 2, 4, 6, 3, 0, 2]
merge_sort(A)

以上归并排序实现是基于数组,下面换链表实现。

"""
基于链表实现归并排序
"""
def merge(S1, S2, S):
    while not S1.is_empty() and not S2.is_empty():
        if S1.first() < S2.first():
            S.enqueue(S1.dequeue())
        else:
            S.enqueue(S2.dequeue())
    while not S1.is_empty():
        S.enqueue(S1.dequeue())
    while not S2.is_empty():
        S.enqueue(S2.dequeue())


def merge_sort(S):
    n = len(S)
    if n < 2:
        retrun 
    S1 = LinkedQueue()  # 实例化一个链表
    S2 = LinkedQueue()
    while len(S1) < n // 2:
        S1.enqueue(S.dequeue())
    while not S.is_empty():
        S2.enqueue(S.dequeue())
    merge_sort(S1)
    merge_sort(S2)
    merge(S1, S2, S)

以上两种实现都是基于递归,下面还有一种非递归方式实现归并排序,运行时间同为O(nlogn)。实际中,比递归还要快些,回避了递归调用的额外开销并且在每一级都有临时存储器。这种算法主要思想是自底向上,即对归并排序树自底向上逐层逐行执行合并。

"""
二维数组实现归非递归并排序
"""
def merge(src, result, start, inc):
    # 把src[start: start+inc]和src[start+inc: start+2*inc]合并成result
    end1 = start + inc
    end2 = min(start = 2*inc, len(src))
    x, y, z = start, start+inc, start
    while x < end1 and y < end2:
        if src[x] < src[y]:
            result[z] = src[x]
            x += 1
        else:
            result[z] = src[y]
            y += 1
        z += 1
    if x < end1:
        result[z:end2] = src[x:end1]
    elif y < end2:
        result[z:end2] = src[y:end2]

def merge_sort(S):
    n = len(S)
    logn = math.ceil(math.log(n, 2))
    src, dest = S, [None] * n
    for i in (2**k for k in range(logn)):
        for j in range(0, n, 2*i):
            merge(src, dest, j, i)
        src, dest = dest, src
    if S is not src:
        S[0:n] = src[0:n]

快速排序:

同样基于分治法,不同于归并排序,快速排序的实现使用了相反的方式

  1.  分解:选择一个基准值,以此分成三个序列
  2.  解决子问题
  3.  按顺序合并放回
"""
基于链表实现快速排序
"""
def quick_sort(S):
    n = len(S)
    if n < 2:
        return 
    p = S.first()  # 以P为基准值,把序列分成三部分
    L = LinkedQueue()
    E = LinkedQueue()
    G = LinkedQueue()
    while not S.is_empty():
        if S.first() < P:
            L.enqueue(S.dequeue())
        elif p < S.first():
            G.enqueue(S.dequeue())
        else:
            E.enqueue(S.dequeue())
    quick_sort(L)
    quick_sort(G)
    while not L.is_empty():
        S.enqueue(L.dequeue())
    while not E.is_empty():
        S.enqueue(E.dequeue())
    while not G.is_empty():
        S.enqueue(G.dequeue())

快速排序的额外优化:就地算法, 仅仅使用少量的内存

"""
实现就地快速排序
"""
def partition(A, p, r):
    x = A[r]  # 把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):
    # p, r  分别为起始、终止索引
    if p < r:
        q = partition(A, p, r)
        quickSort(A, p, q - 1)
        quickSort(A, q + 1, r)
 
A = [2, 8, 7, 1, 3, 5, 6, 4]
quickSort(A, 0, 7)

有两点对于快速排序经常提到:

1. 与归并排序不同的是, 当序列已经是有序的,快速排序的复杂度将达到O(n**2)。在这种情况下,把最后一个数作为基准值,会产生长度为n-1的字序列L, 长度为1的子序列E和长度为0的子序列G。那么递归下去,L的长度每减1,都会产生三个类似的序列。快速排序树的高度将达到n-1。

2. 基准值的选择。盲目使用最后一个数作为基准值,使其会受到O(n**2)最坏情况的影响。那么最合适的就是取头部、中部、尾部三数的中位数,这种三位取中的启发式搜索法将更多地选择到好的基准值。

堆排序:

首先这里提到的堆是一颗二叉树,与内存堆毫无关系。详细描述堆,就得聊聊二叉树。在堆T中,对于除了根T的每个位置P, 存储在p中的键值大于或等于存储在p的父节点的键值。所以最小键总是存于树的顶部,因此命名为堆。

因为涉及到二叉树,改篇再细致描述。这里简单提下。

桶排序和基数排序:

如果一个应用程序用小的整数键、字符串、和离散范围的d元祖键对条目进行排序,那么桶排序和基数排序是很好的选择。运行时间为O(d(n+N)), [0,N-1]是整形键的范围(对于桶排序来说,d = 1)。所以效率明显高于nlogn的运行复杂度。但是篇幅有限而且思路与该篇不一致,这里简单一提。

总结

1,对于插入排序,对于小序列是非常好的选择(将近几十个元素),适用于几乎已经排好序的序列。但是对于其他情况O(n**2)的复杂度不可取。

2,对于堆排序,最坏情况下运行时间是O(nlogn),当输入的数据可以适应主存时,堆排序很容易执行。

3,对于快速排序,最坏情况时间复杂度也达到O(n**2),但是正常情况O(nlogn)的复杂度也是期待得。几十年来,快速排序是一种通用的内存排序算法的默认选择,某些方面还是优于其他排序的。

4,对于归并排序,最坏情况下时间复杂度为O(nlogn),但是对于数组的合并排序的就地操作很难,并且对于分配临时数组的额外开销无法实现最优化。但是即便如此,计算机的各级存储器结构(告诉缓存,主存储器,外部存储器)之间被分层的情况,归并排序仍然是一个优秀的算法考虑。归并排序在很长的合并流中处理数据的方法,最好地利用了在各级存储器中以块存储的所有数据,因此减少了内存交换的总数。

Python的list类中sort方法,已经成为Tim-sort的混合方法。本质上是由自下而上的归并排序,之后进入额外的插入排序。所以实际情况中混合排序的选择还是很多的。

5,对于桶排序和基数排序,使用场景和以上几种方法就不同了,就不一起讨论了。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值