排序算法总结(基于Python)

排序算法总结(基于Python)

比较排序

(基于比较的排序)给定一个包含 n n n个对象的待排序序列 a 1 , a 2 , . . . , a n {a_1},{a_2},...,{a_n} a1,a2,...,an。假如已知如何比较任意两个对象的大小关系,以及如何对这一序列排序。对于自定义对象,比较规则应当满足两个性质:传递性全序性

冒泡排序

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
特点:调整相邻两个对象的位置,每进行一次内循环,将最大值调整到最后。
时间复杂度: O ( n 2 ) {\rm{O}}\left( {{{\rm{n}}^2}} \right) O(n2)

def bubble_sort(arr):
    n = len(arr)

    # 遍历所有数组元素
    for i in range(n):
        for j in range(0, n - i - 1):
            # 比较大小
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]

梳排序

梳排序是改良自冒泡排序,其要旨在于消除乌龟,亦即在阵列尾部的小数值,这些数值是造成冒泡排序缓慢的主因。相对地,兔子,亦即在阵列前端的大数值,不影响冒泡排序的效能。

冒泡排序总是比较相邻的值。所以一次交换中只能删除一个逆序计数。而梳式排序通过使用大于1的间隔来改进冒泡排序。间隔从一个较大的值开始,并在每次迭代中缩小1.3倍,直到达到值1。因此,梳式排序在一次交换中删除了多个逆序计数,比冒泡排序性能上更好。

收缩系数的设定影响着梳排序的效率。根据经验,收缩系数是1.3 (通过在超过20万个随机列表上测试梳排序)。

虽然平均情况下,它比冒泡排序性能更好,但在最坏的情况下仍然是 O ( n 2 ) {\rm{O}}\left( {{{\rm{n}}^2}} \right) O(n2)

def comb_sort(arr):
    the_len = len(arr)
    if the_len < 2:  # 0和1
        return arr
    else:
        i = int(the_len / 1.3)
        while i >= 1:
            for j in range(the_len):
                if i + j >= the_len:
                    i = int(i / 1.3)
                    break
                else:
                    if arr[j] >= arr[j + i]:
                        arr[j], arr[j + i] = arr[j + i], arr[j]
        return arr

堆排序

堆排序(Heap Sort)是利用堆这种数据结构所设计的一种排序算法。堆的结构是一棵完全二叉树的结构,并且满足堆积的性质:每个节点(叶节点除外)的值都大于等于(或都小于等于)它的子节点。
堆排序先按从上到下、从左到右的顺序将待排序列表中的元素构造成一棵完全二叉树,然后对完全二叉树进行调整,使其满足堆积的性质:每个节点(叶节点除外)的值都大于等于(或都小于等于)它的子节点。构建出堆后,将堆顶与堆尾进行交换,然后将堆尾从堆中取出来,取出来的数据就是最大(或最小)的数据。重复构建堆并将堆顶和堆尾进行交换,取出堆尾的数据,直到堆中的数据全部被取出,列表排序完成。总的时间复杂度: O ( n + k log ⁡ n ) {\rm{O}}\left( {n + k\log n} \right) O(n+klogn)

堆结构分为大顶堆小顶堆

  • 大顶堆:每个节点(叶节点除外)的值都大于等于其子节点的值,根节点的值是所有节点中最大的,所以叫大顶堆,在堆排序算法中用于升序排列。
    -小顶堆:每个节点(叶节点除外)的值都小于等于其子节点的值,根节点的值是所有节点中最小的,所以叫小顶堆,在堆排序算法中用于降序排列。

堆排序的原理如下:

  1. 将待排序列表中的数据按从上到下、从左到右的顺序构造成一棵完全二叉树。
  2. 将完全二叉树中每个节点(叶节点除外)的值与其子节点(子节点有一个或两个)中较大的值进行比较,如果节点的值小于子节点的值,则交换他们的位置(大顶堆,小顶堆反之)。
  3. 将节点与子节点进行交换后,要继续比较子节点与孙节点的值,直到不需要交换或子节点是叶节点时停止。比较完所有的非叶节点后,即可构建出堆结构。
  4. 将数据构造成堆结构后,将堆顶与堆尾交换,然后将堆尾从堆中取出来,添加到已排序序列中,完成一轮堆排序,堆中的数据个数减1。
  5. 重复步骤2,3,4,直到堆中的数据全部被取出,列表排序完成。
def heap_sort(array):
    first = len(array) // 2 - 1
    for start in range(first, -1, -1):
        # 从下到上,从右到左对每个非叶节点进行调整,循环构建成大顶堆
        big_heap(array, start, len(array) - 1)
    for end in range(len(array) - 1, 0, -1):
        # 交换堆顶和堆尾的数据
        array[0], array[end] = array[end], array[0]
        # 重新调整完全二叉树,构造成大顶堆
        big_heap(array, 0, end - 1)
    return array


def big_heap(array, start, end):
    root = start
    # 左孩子的索引
    child = root * 2 + 1
    while child <= end:
        # 节点有右子节点,并且右子节点的值大于左子节点,则将child变为右子节点的索引
        if child + 1 <= end and array[child] < array[child + 1]:
            child += 1
        if array[root] < array[child]:
            # 交换节点与子节点中较大者的值
            array[root], array[child] = array[child], array[root]
            # 交换值后,如果存在孙节点,则将root设置为子节点,继续与孙节点进行比较
            root = child
            child = root * 2 + 1
        else:
            break

归并排序

归并排序(Merge Sort)是一种非常高效的排序方式,它用了分治的思想,基本排序思想是:先将整个序列两两分开,然后每组中的两个元素排好序。接着就是组与组和合并,只需将两组所有的元素遍历一遍,即可按顺序合并。以此类推,最终所有组合并为一组时,整个数列完成排序。
时间复杂度: O ( n log ⁡ n ) {\rm{O}}\left( {n\log n} \right) O(nlogn)

def merge(s1,s2,s):
    i = j = 0
    while i+j<len(s):
        # j==len(s2)时说明s2走完了,或者s1没走完并且s1中该位置是最小的
        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):
    n = len(s)
    # 剩一个或没有直接返回
    if n < 2:
        return
    # 拆分
    mid = n // 2
    s1 = s[0:mid]
    s2 = s[mid:n]
    # 子序列递归调用排序
    merge_sort(s1)
    merge_sort(s2)
    # 合并
    merge(s1,s2,s)

快速排序

快速排序(Quicksort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
时间复杂度: O ( n log ⁡ n ) {\rm{O}}\left( {n\log n} \right) O(nlogn)
稳定性:不稳定

步骤为:

  1. 从数列中挑出一个元素,称为**“基准”(pivot)**。
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
    递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

基准点的选择对快速排序的性能有极大的影响。基准点若选成区间的最大/最小值会导致退化(遍历区间时pivot始终大于或小于其他所有元素,这会导致第6和7行的循环把区间遍历两遍);最好的情况应选择区间内元素的中位数(每次恰好将区间内元素分割成大于、小于基准点的两部分,只需遍历一遍),但这通常无法实现,会导致时间常数大幅增高。
区间最左侧区间中央区间最右侧取中位数做基准点(三数取中法),这种方法只需少量比较即可获得(不那么准确的)中位数,在序列已有序时获得良好的效果。
快速排序的分治算法在大多数情况下内存访问效率极高,虽然其他算法都是 O ( n log ⁡ n ) {\rm{O}}\left( {n\log n} \right) O(nlogn),但是快排比其他的快。

def quick_sort(alist, start, end):
    if start >= end:  # 递归的退出条件
        return
    mid = alist[start]  # 设定起始的基准元素
    low = start  # low为序列左边在开始位置的由左向右移动的游标
    high = end  # high为序列右边末尾位置的由右向左移动的游标
    while low < high:
        # 如果low与high未重合,high(右边)指向的元素大于等于基准元素,则high向左移动
        while low < high and alist[high] >= mid:
            high -= 1
        alist[low] = alist[high]  # 走到此位置时high指向一个比基准元素小的元素,将high指向的元素放到low的位置上,此时high指向的位置空着,接下来移动low找到符合条件的元素放在此处
        # 如果low与high未重合,low指向的元素比基准元素小,则low向右移动
        while low < high and alist[low] < mid:
            low += 1
        alist[high] = alist[low]  # 此时low指向一个比基准元素大的元素,将low指向的元素放到high空着的位置上,此时low指向的位置空着,之后进行下一次循环,将high找到符合条件的元素填到此处

    # 退出循环后,low与high重合,此时所指位置为基准元素的正确位置,左边的元素都比基准元素小,右边的元素都比基准元素大
    alist[low] = mid  # 将基准元素放到该位置,
    # 对基准元素左边的子序列进行快速排序
    quick_sort(alist, start, low - 1)  # start :0  low -1 原基准元素靠左边一位
    # 对基准元素右边的子序列进行快速排序
    quick_sort(alist, low + 1, end)  # low+1 : 原基准元素靠右一位  end: 最后

内省排序

**内省排序(Introsort)**是快速排序和 堆排序 的结合,由 David Musser 于 1997 年发明。内省排序其实是对快速排序的一种优化,保证了最差时间复杂度为 2 log ⁡ n 2\log n 2logn

内省排序将快速排序的**最大递归深度限制为 2 log ⁡ n 2\log n 2logn **,超过限制时就转换为堆排序。这样既保留了快速排序内存访问的局部性,又可以防止快速排序在某些情况下性能退化为 n 2 {\rm{n}}^2 n2

heap排序在平均时间复杂度是 O ( n log ⁡ n ) {\rm{O}}\left( {n\log n} \right) O(nlogn),最坏情况也是 O ( n log ⁡ n ) {\rm{O}}\left( {n\log n} \right) O(nlogn),看起来要比快排要快。但是实际上,快排是要比heap排序要快,第一个原因是:heap排序虽然和快排在平均情况下的时间复杂度是 O ( n log ⁡ n ) {\rm{O}}\left( {n\log n} \right) O(nlogn),但是heap排序的时间常数要比快排的时间常数要大。第二个原因是:据统计,快排的最坏情况在是很少发生的。第三个原因是:快排能够比较好的吻合程序的空间局部性原理,因为它操作的基本都是相邻的元素(虚拟存储器的设计理论基础中就有程序的时间局部性和空间局部性),能够减少内存缺页中断的发生次数。

import random
from math import log2


def percolateDown(heap, idx, maxIdx=None, *, reverse=False):
    if maxIdx is None:
        maxIdx = len(heap) - 1
    while idx < maxIdx:
        largestIdx = idx
        if 2 * idx + 1 <= maxIdx and (reverse ^ (heap[2 * idx + 1] > heap[largestIdx])):
            largestIdx = 2 * idx + 1
        if 2 * idx + 2 <= maxIdx and (reverse ^ (heap[2 * idx + 2] > heap[largestIdx])):
            largestIdx = 2 * idx + 2

        if largestIdx != idx:
            heap[idx], heap[largestIdx] = heap[largestIdx], heap[idx]
            idx = largestIdx
        else:
            break


def heapify(heap, maxIdx=None, *, reverse=False):
    if maxIdx is None:
        maxIdx = len(heap) - 1
    for idx in range(maxIdx // 2, -1, -1):
        percolateDown(heap, idx, reverse=reverse)


def heapSort(heap, reverse=False):
    heapify(heap, reverse=reverse)
    for idx in range(len(heap) - 1, 0, -1):
        heap[0], heap[idx] = heap[idx], heap[0]
        percolateDown(heap, 0, idx - 1, reverse=reverse)
    return heap


def findPivot(begin, end):
    return random.randint(begin, end)


def partition(array, begin, end, *, reverse=False):
    pivotIdx = findPivot(begin, end)
    pivot = array[pivotIdx]

    array[end], array[pivotIdx] = array[pivotIdx], array[end]
    firstLarger = begin
    for idx in range(begin, end):
        if reverse ^ (array[idx] <= pivot):
            array[idx], array[firstLarger] = array[firstLarger], array[idx]
            firstLarger += 1

    array[end], array[firstLarger] = array[firstLarger], array[end]
    return firstLarger


def introSort(array, begin=0, end=None, depth=0, *, reverse=False):
    if end is None:
        end = len(array) - 1

    if depth < log2(len(array)):
        if begin < end:
            mid = partition(array, begin, end, reverse=reverse)
            introSort(array, begin, mid - 1, depth + 1, reverse=reverse)
            introSort(array, mid + 1, end, depth + 1, reverse=reverse)
    else:
        array[begin:end + 1] = heapSort(array[begin:end + 1], reverse=reverse)

Timesort

Timsort是一种混合、稳定高效的排序算法,源自合并排序和插入排序,旨在很好地处理多种真实数据。它由Tim Peters于2002年实施使用在Python编程语言中。该算法查找已经排序的数据的子序列,并使用该知识更有效地对其余部分进行排序。这是通过将已识别的子序列(称为运行)与现有运行合并直到满足某些条件来完成的。从版本2.3开始,Timsort一直是Python的标准排序算法。如今,Timsort 已是是 Python、 Java、 Android平台 和 GNU Octave 的默认排序算法。
针对现实中需要排序的数据分析看,大多数据通常是有部分已经排好序的数据块,Timsort 就利用了这一特点。Timsort 称这些已经排好序的数据块为 “run”,我们可以将其视为一个一个的“分区”。在排序时,Timsort迭代数据元素,将其放到不同的 run 里,同时针对这些 run ,按规则进行合并至只剩一个,则这个仅剩的 run 即为排好序的结果。
换句话说,就是分析待排序数据,根据其本身的特点,将排序好的(不管是顺序还是逆序)子序列的分为一个个run分区,当然,这个分区run也存在一定的约束,即根据序列会产生一个minrun,如果原始的run小于minrun的长度,用插入排序扩充run,直到达到条件,之后使用归并排序来合并多个run。

def binary_search(arr, left, right, value):
    if left >= right:
        if arr[left] <= value:
            return left + 1
        else:
            return left
    elif left < right:
        mid = (left + right) // 2
        if arr[mid] < value:
            return binary_search(arr, mid + 1, right, value)
        else:
            return binary_search(arr, left, mid - 1, value)


def insertion_sort(arr):
    length = len(arr)
    for index in range(1, length):
        value = arr[index]
        pos = binary_search(arr, 0, index - 1, value)
        arr = arr[:pos] + [value] + arr[pos:index] + arr[index + 1:]
    return arr


def merge(l1, l2):
    if not l1:
        return l2
    if not l2:
        return l1
    if l1[0] < l2[0]:
        return [l1[0]] + merge(l1[1:], l2)
    else:
        return [l2[0]] + merge(l1, l2[1:])


def timsort(arr):
    if not arr:  # 空列表
        return
    runs, sorted_runs = [], []
    new_run = [arr[0]]
    length = len(arr)
    # 划分run区,并存储到runs里,这里简单的按照升序划分,没有考虑降序的run
    for index in range(1, length):
        if arr[index] < arr[index - 1]:
            runs.append(new_run)
            new_run = [arr[index]]
        else:
            new_run.append(arr[index])
        if length - 1 == index:
            runs.append(new_run)
            break

    for run in runs:
        insertion_sort(run)

    # 合并runs
    sorted_arr = []
    for run in runs:
        sorted_arr = merge(sorted_arr, run)
    return sorted_arr

非比较排序

桶排序

桶排序(Bucket Sort)是计数排序的推广,其主要思想是:将待排序集合中处于同一个值域的元素存入同一个桶中,也就是根据元素值特性将集合拆分为多个区域,则拆分后形成的多个桶,从值域上看是处于有序状态的。对每个桶中元素进行排序,则所有桶中元素构成的集合是已排序的。
算法实现步骤:

  1. 根据待排序集合中最大元素和最小元素的差值范围和映射规则,确定申请的桶个数;
  2. 遍历排序序列,将每个元素放到对应的桶里去;
  3. 对不是空的桶进行排序;
  4. 按顺序访问桶,将桶中的元素依次放回到原序列中对应的位置,完成排序。
def bucket_sort(array):
    min_num, max_num = min(array), max(array)
    # 桶的大小
    bucket_num = (max_num - min_num) // 3 + 1
    # 桶数组
    buckets = [[] for _ in range(int(bucket_num))]
    # 向桶数组填数
    for num in array:
        buckets[int((num - min_num) // 3)].append(num)
    new_array = list()
    for i in buckets:
        for j in sorted(i):
            new_array.append(j)
    return new_array

基数排序

基数排序(Radix Sort)是一种非比较型整数排序算法,是桶排序的扩展。基本思想是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。按照低位先排序,分别放入10个队列中,然后采用先进先出的原则进行收集;再按照高位排序,然后再收集;依次类推,直到最高位,最终得到排好序的数列。对于数值偏小的一组序列,其速度是非常快的,时间复杂度达到了线性,而且思想也非常的巧妙。
算法实现步骤

  1. 取得数组中的最大数,并取得位数;
  2. 对数位较短的数前面补零;
  3. 分配,先从个位开始,根据位值(0-9)分别放到0~9号桶中;
  4. 收集,再将放置在0~9号桶中的数据按顺序放到数组中;
  5. 重复3~4过程,直到最高位,即可完成排序。
def radix_sort(arr):
    n = len(str(max(arr)))  # 记录最大值的位数
    for k in range(n):  # n轮排序
        # 每一轮生成10个列表
        bucket_list = [[] for i in range(10)]  # 因为每一位数字都是0~9,故建立10个桶
        for i in arr:
            # 按第k位放入到桶中
            bucket_list[i // (10 ** k) % 10].append(i)
        # 按当前桶的顺序重排列表
        arr = [j for i in bucket_list for j in i]
    return arr
  • 30
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

煌澄艾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值