探索十大经典排序算法之美(基于Python)

在计算机科学的世界中,排序算法无疑是最为经典和基础的主题之一。排序不仅是解决各种计算问题的基础,而且在日常编程中也是必不可少的一环。Python这一富有表达力的编程语言,提供了许多强大的工具和库,使得实现和理解排序算法变得更加直观和有趣。

本篇博客将带领大家探索Python中一些常见的排序算法,从简单到复杂,深入剖析它们的工作原理、性能特点以及在实际应用中的巧妙之处。无论你是初学者还是有一定经验的开发者,通过本文的学习,你将更好地理解排序算法的本质,并能够在实际项目中明智地选择和应用合适的算法。

让我们开始这次关于Python排序算法之旅吧!

一、排序算法种类

在学习具体的排序算法之前,我们先简单了解一下,排序算法都有哪些吧!

我认为排序算法可以大致分为以下几类:

 大致了解了排序算法有哪些后,我们来聊聊具体算法的实现过程以及排序的思想

二、简单排序算法

1.冒泡排序

  • 算法实现过程

通过比较相邻的元素,如果前一个比后一个大,就把它们两个对调位置。

一趟排序完成后,则无序区减少一个数,有序区增加一个数。

对序列中的每一对相邻元素做同样的工作,直到序列全部完成。

冒泡循环需要遍历n-1次(n为序列的长度) 。

  • 时间复杂度 o(n2)
  • 代码实现
def bubble_sort(li):
    for i in range(len(li)-1):
        exchange = False
        for j in range(len(li)-i-1):
            if li[j] > li[j+1]:
                li[j], li[j+1] = li[j+1], li[j]
                exchange = True
        if not exchange:
            return

最外层循环指的是冒泡循环需要遍历n-1次(n为列表长度),内层循环到n-i-1的原因是比如第一趟过后,顶端最大的数字已经找出,并且这个最大的数已经在有序区中了,只需要用同样的方法遍历剩下的无序区,找出无序区中最大的数即可。

这里也介绍了交换两个数的方法,即  li[i], li[i+1] = li[i+1], li[i] 。

exchange是一个标志位,他的作用是如果在某一趟排序中,没有发生任何交换,此时可以说明无序区本来就是已经排列好的,不需要进行后续的排序。这里每一趟之后都需要检验,所以注意缩进。

这里默认是升序排列,若要改为降序排列,只需要将判断语句中的‘>’改为‘<’即可。

2.选择排序

  • 算法实现过程

每一轮从待排序的记录中选出最小的元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小元素,然后放到已排序的序列的末尾。

也可以每一轮找出数值最大的元素,这样的话,排序完毕后的数组最终是从大到小排列。

选择排序每次选出最小(最大)的元素,因此需要遍历 n-1 次。

  • 时间复杂度 o(n2)

注意:该选择排序的时间复杂度是o(n2),而不是o(n),这是因为在循环体中内置函数min()和remove()的复杂度都是o(n),而不是o(1),这两个函数的本质都是将整个序列遍历一遍,从而找出最小值和要删除的值

  • 代码实现
def select_sort_simple(li):
    li_new = []
    for i in range(len(li)):
        min_val = min(li)
        li_new.append(min_val)
        li.remove(min_val)
    return li_new

建立一个新的列表,遍历原列表,用内置函数min()找出列表中的最小值,把他存储在变量min_val中,在新建列表中加入这个值,并且将这个值从原始列表中删除,重复以上操作。 

  • 代码优化

由于以上代码需要再申请一块新的内存来存放排好序的列表,浪费空间,所以我们想办法对代码进行优化,我们可以把选择出来的最小的数与列表第一个位置的数进行交换,后面我只需要找无序区数中的最小值与无序区的第一个数进行比较交换,这样就能实现原地排序,节省了大量内存。由于要交换位置,那么就要记录下标,由于上面说到要与无序区的第一个数进行交换,所以min_loc的下标要记作i,后面只需要遍历无序区即可,所以j的范围是从i+1到n。

def select_sort(li):
    for i in range(len(li)):
        min_loc = i  # 遍历的是无序区的位置
        for j in range(i + 1, len(li)):  # 前包后不包,所以这里后面应当写列表长度,从i+1开始可以省去自己和自己比较的那一步
            if li[j] < li[min_loc]:
                min_loc = j
        li[i], li[min_loc] = li[min_loc], li[i]
        print(li)

3.插入排序 

  • 算法实现过程

插入排序就是每一步都将一个需要排序的数据按其大小插入到已经排序的数据序列中的适当位置,直到全部插入完毕。插入排序就如同打扑克牌一样,每次将后面抽到的牌按顺序插到前面已经排好序的牌中。

  • 时间复杂度 o(n2) 
  • 代码实现
def insert_sort(li):
    for i in range(1, len(li)):  # 前包后不包
        tmp = li[i]
        j = i - 1  # j指的是手里的牌的下标
        while j >= 0 and li[j] > tmp:
            li[j + 1] = li[j]
            j = j - 1
        li[j + 1] = tmp

最外层的循环i的范围是从1到n(n为序列的长度),这是因为我的目的是遍历所有牌和我手中的作比较,相当于每次抽取一张牌,和我手中已有的牌数进行比较,然后把它们插入在合适的位置中,我抽取的第一张牌下标为0,所以我只需要从1开始比较就可以,所以i的范围从1开始。 

j表示我手里被比较的那张牌,如果j这张牌,比我新抽到的牌数值大,那么就需要j这张牌往右挪一个;如果j这张牌比我新抽到的牌数值小,我就应当把新抽到的牌插在j这张牌的右边。

tmp变量中存储的是我新抽到的牌。

三、高效排序算法

1.快速排序

  • 算法实现过程

 先从数据序列中取出一个数作为基准数(习惯取第一个数)。

分区过程,将比基准数小的数全放到它的左边,大于或等于它的数全放到它的右边。

再对左右区间递归重复第二步,直到各区间只有一个数。

  • 时间复杂度 o(nlogn)
  • 代码实现
def partition(li, left, right):
    tmp = li[left]
    while left < right:
        while left < right and li[right] >= tmp:  # 从右边开始找比tmp小的数
            right -= 1  # right往左走一步
        li[left] = li[right]  # 把右边的值写在左边的空位上
        while left < right and li[left] < tmp:
            left += 1
        li[right] = li[left]  # 把左边的值写在右边的空位上
    li[left] = tmp  # 把tmp归位
    return left
def quick_sort(data, left, right):
    if left < right:  # 说明至少两个元素
        mid = partition(data, left, right)  # 说明第一个元素已经归位,mid把列表分成两部分
        quick_sort(data, left, mid - 1)  # 递归调用这两部分
        quick_sort(data, mid + 1, right)

2.堆排序

  • 算法实现过程

1. 建立大根堆

2. 得到堆顶元素,为最大元素

3. 去掉堆顶,将堆顶最后一个元素放到堆顶,此时可以通过一次调整使堆有序

4. 堆顶元素为第二大元素

5. 重复步骤三,直到堆变空

  • 时间复杂度 nlg(n)
  • 代码实现
def sift(li, low, high):
    """

    :param li: 列表
    :param low: 堆的根部节点位置(堆顶)
    :param high: 堆的最后一个节点的位置

    """
    i = low                    # i最开始指向根节点
    j = 2 * i + 1              # j开始指向根节点的左孩子
    tmp = li[low]              # 把堆顶元素存起来
    while j <= high:           # 只要j位置有数则一直循环
        if j + 1 <= high and li[j+1] > li[j]:    # 如果右孩子比左孩子大,并且要保证右孩子要有(不越界)
            j = j+1            # 把就指向右孩子
        if li[j] > tmp:
            li[i] = li[j]
            i = j              # 往下看一层
            j = 2 * i + 1
        else:                  # 说明tmp更大,把tmp放在i的位置上
            li[i] = tmp        # 把tmp防灾某一级领导的位置上
            break
    else:
        li[i] = tmp            # 把tmp放到叶子节点上

def heap_sort(li):
    n = len(li)
    for i in range((n-2)//2, -1, -1):# i表示建堆的时候调整的部分的根的下标
        sift(li, i, n-1)             # 建堆完成
    for i in range(n-1, -1, -1):     # i指向当前堆的最后一个元素
        li[0], li[i] = li[i], li[0]  # 让堆顶元素和最后一个元素做交换
        sift(li, 0, i-1)             # i-1是新的high
  • 堆排序的应用——topk问题

现有n个数,设计算法得到前K大的数

解题思路:取列表前K个元素建立一个小根堆。堆顶就是目前第K大的数。依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整,遍历列表所有元素后,倒叙弹出堆顶

def sift(li, low, high):
    i = low
    j = 2 * i + 1
    tmp = li[low]
    while j <= high:
        if j+1 <= high and li[j+1] < li[j]:
            j = j+1
        if li[j] < tmp:
            li[i] = li[j]
            i = j
            j = 2 * i + 1
        else:
            break
        li[i] = tmp

def topk(li, k):
    heap = li[0:k]        # 把0到K的数取出来
    for i in range((k-2)//2, -1, -1):
        sift(heap, i, k-1)
    for i in range(k, len(li)-1):
        if li[i] > heap[0]:
            heap[0] = li[i]
            sift(heap, 0, k-1)

    for i in range(k - 1, -1, -1):
        heap[0], heap[i] = heap[i], heap[0]
        sift(heap, 0, i - 1)
    return heap

 3.归并排序

  • 算法实现过程

1.分解:将列表越分越小,直到分成一个元素 2.终止条件:一个元素是有序的 3.合并:将两个有序列表合并,列表越来越大。

  •  时间复杂度 o(nlgn)
  • 代码实现
def merge(li, low, mid, high):
    i = low
    j = mid + 1
    ltmp = []
    while i <=mid and j <= high: # 只要左右两边都有数
        if li[i] < li[j]:        # 比较i和j指向的两个数的大小关系
            ltmp.append(li[i])
            i += 1
        else:                    # j指向的比i指向的大
            ltmp.append(li[j])
            j += 1
    # while循环结束的时候,两部分肯定有一部分没数了
    while i <= mid:              # 当j没有剩余时
        ltmp.append(li[i])
        i += 1
    while j <= high:             # 当i没有剩余时
        ltmp.append(li[j])
        j += 1
    li[low:high+1] = ltmp

def merge_sort(li, low, high):
      if low < high: # 至少有两个元素,递归
          mid = (low + high)//2
          merge_sort(li, low, mid)
          merge_sort(li, mid+1, high)
          merge(li, low, mid, high)

四、其他排序算法

1.希尔排序

  • 算法实现过程

希尔排序是一种分组插入插入排序算法,首先取一个整数d1=n/2,将元素分为d1个组,每组相邻量元素之间的距离为d1,在各组内进行直接插入排序;取第二个整数d2=d/2,重复上述分组排序过程,直到d1=1,即所有元素在同一组内进行直接插入排序;希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序;最后一趟使得所有数据有序。

  • 时间复杂度

希尔排序1的时间复杂度讨论比较复杂,并且和选取的gap序列有关

  • 代码实现
def insert_sort_gap(li, gap):
    for i in range(gap, len(li)):
        tmp = li[i]
        j = i - gap
        while j >= 0 and li[j] > tmp:
            li[j+gap] = li[j]
            j -= gap
        li[j+gap] = tmp

def shell_sort(li):
    d = len(li) // 2
    while d >= 1:
        insert_sort_gap(li, d)
        d //= 2

2.计数排序

  • 算法实现过程

假设列表中的数字都在0到100之间,计算数字出现的次数,然后按顺序列出这些数字。

  • 时间复杂度 o(n)
  • 代码实现
def count_sort(li, max_count=100):
    count = [0 for _ in range(max_count+1)]
    for val in li:
      count[val] += 1
    li.clear()
    for ind, val in enumerate(count):
        for i in range(val):
            li.append(ind)

3.计数排序的引申——桶排序

  • 算法实现过程

在计数排序中,如果元素的范围比较大(比如在1到1亿之间),我们应当如何改进算法?

桶排序:首先将元素分在不同的桶中,在每个桶中的元素排序,eg:我有以下这些数,我讲他们按大小放在下面五个桶中

  • 时间复杂度 

桶排序的时间复杂度取决于数据的分布,也就是需要对不同数据排序时采取不同的分同策略

  • 代码实现
def bucket_sort(li, n=100, max_num=10000):
    buckets = [[] for _ in range(n)]        # 创建n个桶
    for var in li:
        i = min(var // (max_num // n), n-1)           # i 表示var放到几号桶里
        buckets[i].append(var)              # var加在桶里面
        for j in range(len(buckets[i])-1, 0, -1):
            if buckets[i][j] < buckets[i][j-1]:
                buckets[i][j], buckets[i][j-1] = buckets[i][j-1], buckets[i][j]
            else:
                break
    sort_li = []
    for buc in buckets:
        sort_li.extend(buc)
    return sort_li

4.基数排序

  • 算法实现过程

排序过程中,将元素分层为多个关键码进行排序(一般按照数值的个位、十位、百位、…… 进行区分),多关键码排序按照从最主位关键码到最次位关键码或从最次位到最主位关键码的顺序逐次排序。

  • 时间复杂度 o(kn)
  • 代码实现
def radix_sort(li):
    max_num = max(li)
    it = 0
    while 10 ** it <= max_num:
        buckets = [[] for _ in range(10)]
        for var in li:
            digit = (var // 10 ** it) % 10
            buckets[digit].append(var)
        li.clear()
        for buc in buckets:
            li.extend(buc)

        it += 1

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值