【排序】常见排序总结


一、Low B三人组 - O(n²)

1.冒泡排序

思路:
从第一个数开始,列表每两个相邻的数,如果前面比后面大,则交换
一趟排序过后,后面的有序区增加一个数,前面的无序区减少一个数

def bubble_sort_simpe(alist):
    for i in range(len(alist) - 1):     # 趟数
        for j in range(len(alist) - i - 1):   # 控制下标
            if alist[j] > alist[j + 1]:
                alist[j], alist[j + 1] = alist[j + 1], alist[j]

以上是基础实现,但我们仍可以对其进行改进
如图所示,问题如下:

当我们排序一个后面已经排好序的列表时,会出现排序重复进行的情况,此时我们可以定义exchange = False在每一趟的过程中来判断列表是否更改

冒泡

def bubble_sort(alist):
    for i in range(len(alist) - 1):     #趟数
        exchange = False
        for j in range(len(alist) - i - 1):
            if alist[j] > alist[j + 1]:
                alist[j], alist[j + 1] = alist[j + 1], alist[j]
                exchange = True
        if not exchange:    #如果每经过一趟列表没有更改说明已经排好序
           return None

2.选择排序

思路:
假设第一个位置的数为最小数,然后从无序区开始遍历了,每次遍历从列表中选一个最小数,随即把最小数与无序区最左边的元素交换,直至列表有序

def select_sort(alist: list):
   
    for i in range(len(alist) - 1):  # i控制无序区最左边的元素
        min_index = i
        for j in range(i + 1, len(alist)):  # j控制无序区的遍历,负责找到最小值
            if alist[j] < alist[min_index]:
                min_index = j
        alist[min_index], alist[i] = alist[i], alist[min_index]

3.插入排序

思路:
记录第第一个元素的index和value,然后依次看后面的元素,如果后面的元素大于第一个,则排在它的后面,主要是记录这个元素的位置,依次将排好序的下标右移

def insert_sort(list: list):
   
    for i in range(1, len(list)):   # 摸到的牌的下标
        tmp = list[i]
        j = i - 1       # j指的是手中最右边一张牌的下标
        # 当摸到的牌比最右边牌小时,遍历手中的牌,看摸到的牌,一直往左走,如果看到比摸到的牌小的就插入
        while list[j] > tmp and j >= 0:     #j=-1 指将牌移到最左边
            list[j + 1] = list[j]
            j -= 1
        list[j + 1] = tmp

二、NB三人组 - O(nlogn)

1.快速排序

思路:

  • 取一个元素p(第一个元素),使元素p归位(定义partition方法)
  • 列表被p分成两部分,左边都比p小,右边都比p大
  • 递归完成排序

问题:

  • 递归:最大深度问题999,会消耗系统资源
    import sys
    sys.setrecursionlimit(100000) #设置递归最大深度为100000
  • 最坏情况:时间复杂度O(n²)
    不找第一个值,随机找一个值,把第一个数跟随机选的那个数交换
def quick_sort(alist, left, right):  # left和right的值每次递归时都会更改
    """
    框架
    """
    if left < right:  # 这个区域至少有两个元素的时候递归 否则不
        mid = partition(alist, left, right)  # partition方法获取已归位的index
        quick_sort(alist, left, mid - 1)
        quick_sort(alist, mid + 1, right)


def partition(alist, left, right):
    """
    - 先拿一个变量把要归位的元素p(最左边)存起来
    - 左边有一个空位(p原来的位置),然后right-1开始找,把比p小的放在左边的空位
    - 右边现在也有上次元素移出来时的空位,然后再left+1开始找,把比p大的放在右边的空位
    - 最后直至left和right重合,说明这个位置是要归位的位置,放入p
    """
    tmp = alist[left]  # 存最左边的元素p,创建一个空位好让右边的填过来

    while left < right:  # left=right时结束,找到归位的位置
        while alist[right] >= tmp and left < right:  # 先从右边找比tmp小的数放入左边
            # 如果右边的所有数都比p大,那么right就会一直走,为了让right与left重合时退出循环限制一下
            right -= 1  # 往左走
        alist[left] = alist[right]  # 把从右边找到的比p大的值 or 自己 放在左边的空位
        print(alist, "right")
        while alist[left] <= tmp and left < right:
            left += 1
        alist[right] = alist[left]
        print(alist, "left")

    if left == right:   # left和right重合时,放入p
        alist[left] = tmp
    return left

2.堆排序

思路:
1.建立堆
2.得到堆顶元素为最大元素
3.去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序
4.堆顶元素为第二大元素
5.重复步骤 3 直至堆变空

注意事项:

  • 二叉树的顺序存储方式 - 用列表存
  • 堆:一种特殊的完全二叉树结构(大根堆、小根堆)
  • 父节点i和’左’孩子节点的编号下标关系:i -> 2i+1
    父节点i和’右’孩子节点的编号下标关系:i -> 2i+2
    孩子节点j和父节点:j -> (j-1)//2
def sift(alist: list, low, high):
    """
    调整
    :param low:堆的根节点位置(堆顶)
    :param high:堆的最后一个元素的位置(low需要一层层往下看,然后跟high比,如果low>high则越界)
    :return:
    """
    i = low     # 堆的根节点位置(父亲)
    j = 2 * i + 1   # j开始指向i的左孩子
    tmp = alist[low]    # 把堆顶的元素存起来 i的位置为空
    while j <= high:    # 只要j位置有数(high指向最后一个元素)
        if j + 1 <= high and alist[j + 1] > alist[j]:     # 如果有右孩子且右孩子比左孩子大
            j += 1              # 则j指向右孩子
        if alist[j] > tmp:  # 如果孩子比堆顶的元素大,那么孩子上去
            alist[i] = alist[j]
            i = j           # 然后要将i和j的位置同时往下移(循环判断直至tmp插入)
            j = 2 * i + 1
        else:   # 堆顶元素tmp更大,就把tmp放到i的位置然后退出循环
            alist[i] = tmp
            break
    alist[i] = tmp  # 最后把tmp放到叶子节点上


def heap_sort(alist):
    n = len(alist)

    # 先建立堆,从孩子找父亲是j->(j-1)//2,现在孩子是最后一个元素len-1,则父亲就是(n-2)//2
    for i in range((n-2) // 2, -1, -1):
        # i表示建堆的时候调整的部分的根的下标
        sift(alist, i, n - 1)   # high还是最后一个元素的下标
    # 建堆完成
    for i in range(n-1, -1, -1):
        # i指向当前堆的最后一个元素
        alist[0], alist[i] = alist[i], alist[0]     # 把堆顶元素与最后一个元素交换位置
        sift(alist, 0, i - 1)   # 由于最后一个元素已经是最大的了排好序了,所以i-1是新的high

堆的内置模块

import heapq    # q->quene 优先队列
import random

alist = list(range(100))
random.shuffle(alist)
print(alist)

heapq.heapify(alist)    # 建堆(小根堆)
print(alist)

for i in range(len(alist)):
    print(heapq.heappop(alist))    # 往外弹出一个最小的元素

top k问题及实现

问题:
现在有n个数,设计算法得到前k大的数(k<n)

解决方法:

  • 1.把所有数排序后切片 - O(nlogn)
  • 2.排序LowB三人组 - O(kn)
  • 3.堆排序思路 - O(nlogk)
    ⅰ取列表前k个元素建立一个小根堆,堆顶就是目前第k大的数(即k中最小的数)
    ⅱ依次向后遍历原列表,对于列表中的元素,如果大于堆顶,则堆顶更换为该元素,并且对堆进行一次调整,反之忽略
    ⅲ遍历列表所有元素后,倒序弹出堆顶

在原来的堆排序上加入top_k方法即可

def top_k(alist, k):
    heap =alist[0:k]    # 先进行切片
    for i in range((k-2)//2, -1, -1):
        sift(heap, i, k-1)  # 1.建堆

    for i in range(k, len(alist) - 1):
        if alist[i] > heap[0]:  # 如果大于堆顶
            heap[0] = alist[i]  # 覆盖
            sift(heap, 0, k-1)  # 调整

    for i in range(k - 1, -1, -1):
        # i指向当前堆的最后一个元素
        heap[0], heap[i] = heap[i], heap[0]  # 把堆顶元素与最后一个元素交换位置
        sift(heap, 0, i - 1)  # 由于最后一个元素已经是最大的了排好序了,所以i-1是新的high
    return heap

3.归并排序

Python的sort方法是基于归并排序的

归并:把一个两段有序的列表合为一个有序列表

  • low mid+1两个指针分别指向这两段有序列表的第一个数,比较他们,小的先排列,然后对应的指针后移
  • 后来肯定有一个列表先被遍历完,随即另一个列表的剩余元素全部排列

归并排序整体思路:

  • 分解:将列表越分越小,直至分成一个元素,因为一个元素是有序的
  • 合并:将两个有序列表合并,列表越来越大
# 假设有一个两段有序的列表
def merge(alist, low, mid, high):
    i = low         # 第一段的第一个
    j = mid + 1     # 第一段的第一个
    tmp_list = []
    while i <= mid and j <= high:   # 只要两边都有数
        if alist[i] < alist[j]:
            tmp_list.append(alist[i])
            i += 1
        else:
            tmp_list.append(alist[j])
            j += 1
    # 现在有一部分没数了
    while i <= mid:  # 第一部分有数
        tmp_list.append(alist[i])
        i += 1
    while j <= high:  # 第二部分有数
        tmp_list.append(alist[j])
        j += 1

    alist[low:high+1] = tmp_list    # 把这个临时列表替换回去

def merge_sort(alist, low, high):   # low和high分别指向第一个和最后一个元素下标
    if low < high:  # 至少有两个,递归分解,后合成
        mid = (high + low) // 2
        merge_sort(alist, low, mid)  # 递归左边
        merge_sort(alist, mid+1, high)  # 递归右边
        merge(alist, low, mid, high)    # 合并左边和右边

小结

小结

一、时间复杂度:O(nlogn)

二、运行时间的快慢:快排 > 归并排序 > 堆排序

三、缺点:
- 快排:极端情况下排序效率低
- 归并排序:需要额外的内存开销(不是原地排序)
- 堆排序:在快的排序算法中相对较慢

- 稳定性:有顺序的、挨个换的排序才具有稳定性
【Python的sort不使用快排的原因:不稳定】
稳定:排序后保证两个数的相对顺序不变
例如:
在字典中对name进行排序
{'name':'a', 'age':18}
{'name':'b', 'age':20}
{'name':'a', 'age':25}
排序后:
{'name':'b', 'age':20}
{'name':'a', 'age':18}
{'name':'a', 'age':25}
底下两个name的相对位置不更改

三、其他排序

1.希尔排序

希尔排序是一种分组插入排序算法(基于插入排序)
它的时间复杂度比较复杂,并且和选取的gap序列有关

思路:

  • 首先取一个整数d₁ = n/2,将元素分为d₁个组,每组相邻量元素之间的距离为d₁,在各族进行插入排序
  • 取第二个整数d₂ = d₁/2,重复上述分组排序过程直至dᵢ=1,即所有元素在同一组内进行插入排序

希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序,最后一趟排序使得所有元素有序

这里的insert_sort_gap方法是将插入排序代码中的1 -> gap

def insert_sort_gap(alist, gap):    # gap表示组
    for i in range(gap, len(alist)):
        tmp = alist[i]
        j = i - gap
        while alist[j] > tmp and j >= 0:
            alist[j + gap] = alist[j]
            j -= gap
        alist[j + gap] = tmp

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

2.计数排序

问题:
对列表进行排序,已知列表中的数范围都在0~100之间。设计时间复杂度为O(n)的算法

解决:创建一个临时列表统计这些数的值和个数,然后放回原列表

def count_sort(li: list, max_count=100):
    count = [0 for _ in range(max_count+1)]  # 创建一个临时列表
    for val in li:
        count[val] += 1  # 把对应的值的个数放入count中,下标对应的就是值
    li.clear()
    for ind, val in enumerate(count):   # enumerate 取出列表中对应的下标和值
        for i in range(val):
            li.append(ind)

3.桶排序

桶排序的时间复杂度取决于数据的分布 平均:O(n+k)

思路:
Bucket sort:首先将元素分在不同的桶中,在对每个桶中的元素排序


def bucket_sort(li, n=100, max_num=10000):  # 把这些元素分到100个桶里,这些元素的最大值是10000
    buckets = [[] for _ in range(n)]  # 创建一个二维列表表示100个桶
    for var in li:
        i = min(var // (max_num // n), n-1)  # i表示var放到几号桶,当n=10000放到99号桶
        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
    sorted_li = []
    for buc in buckets:
        sorted_li.extend(buc)  # extend - 追加列表
    return sorted_li

4.基数排序

思路:
radix sort:先按照个位分到0~9号桶,随后按个位依次输出,然后按十位分桶直至所有数全部排好
迭代的次数由最大的数决定
注:9的十位以上的位都看成0

def radix_sort(nums: list[int]):
    max_num = max(nums)  # 99->2    99迭代2次
    iteration = 0  # 迭代的次数
    while 10 ** iteration <= max_num:
        buckets = [[] for _ in range(10)]  # 分10个桶代表0~9
        for var in nums:  # 分桶 - var表示variable变量
            digit = (var // 10 ** iteration) % 10  # it=0取个位...
            buckets[digit].append(var)
        nums.clear()
        for buc in buckets:
            nums.extend(buc)  # 追加列表,把数重新写回nums
        iteration += 1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>