8.1.2 排序算法概述及python基本实现(下)

归并排序(Merge Sort)

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

算法步骤

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。
def mergeSort(nums):
    # 归并过程
    def merge(left, right):
        result = []  # 保存归并后的结果
        i = j = 0
        while i < len(left) and j < len(right):
            if left[i] <= right[j]:
                result.append(left[i])
                i += 1
            else:
                result.append(right[j])
                j += 1
        result = result + left[i:] + right[j:] # 剩余的元素直接添加到末尾
        return result
    # 递归过程
    if len(nums) <= 1:
        return nums
    mid = len(nums) // 2
    left = mergeSort(nums[:mid])
    right = mergeSort(nums[mid:])
    return merge(left, right)

计数排序(Counting Sort)

计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序,要求输入数据的范围在 [0,N-1] 之间。

当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。

def countingSort(nums):
    bucket = [0] * (max(nums) + 1) # 桶的个数
    for num in nums:  # 其实就是统计频率,但是索引代表的是关键字的值,该位置的数字代表频率
        bucket[num] += 1
    i = 0  # nums 的索引
    # 再排序回去,若每个记录还有其他信息,如(1, 'z'), (2, 'q'),则需要在上面的桶数组上略微改进
    for j in range(len(bucket)):  
        while bucket[j] > 0:
            nums[i] = j
            bucket[j] -= 1
            i += 1
    return nums

桶排序(Bucket Sort)

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。

桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序。

  • 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序;
  • 从不是空的桶里把排好序的数据拼接起来。

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

def bucketSort(nums, defaultBucketSize = 5):
    maxVal, minVal = max(nums), min(nums)
    bucketSize = defaultBucketSize  # 如果没有指定桶的大小,则默认为5
    bucketCount = (maxVal - minVal) // bucketSize + 1  # 数据分为 bucketCount 组
    buckets = []  # 二维桶
    for i in range(bucketCount):
        buckets.append([])
    # 利用函数映射将各个数据放入对应的桶中
    for num in nums:
        buckets[(num - minVal) // bucketSize].append(num)
    nums.clear()  # 清空 nums
    # 对每一个二维桶中的元素进行排序
    for bucket in buckets:
        insertionSort(bucket)  # 假设使用插入排序
        nums.extend(bucket)    # 将排序好的桶依次放入到 nums 中
    return nums

基数排序(Radix Sort)

基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。

基数排序有两种方法:

  1. MSD (主位优先法):从高位开始进行排序
  2. LSD (次位优先法):从低位开始进行排序
# LSD Radix Sort
def radixSort(nums):
    mod = 10
    div = 1
    mostBit = len(str(max(nums)))  # 最大数的位数决定了外循环多少次
    buckets = [[] for row in range(mod)] # 构造 mod 个空桶
    while mostBit:
        for num in nums:  # 将数据放入对应的桶中
            buckets[num // div % mod].append(num)
        i = 0  # nums 的索引
        for bucket in buckets:  # 将数据收集起来
            while bucket:
                nums[i] = bucket.pop(0) # 依次取出
                i += 1
        div *= 10
        mostBit -= 1
    return nums

非比较排序三种方法总结 —— 基数排序 vs 计数排序 vs 桶排序

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 计数排序:每个桶只存储单一键值 0 ~ N-1的整数,索引代表数值,桶里放频率
  • 桶排序:每个桶存储一定范围的数值 (通过映射关系)
  • 基数排序:根据键值的每位数字来分配桶,索引代表某位数字的值,桶里放该位数字等于它的所有数据。

在算法实现上共同点也很多,用数组代表桶,先建立一批桶,将数据放入相应编号的桶,桶内或者要分别排序,或者取出再入桶,当完成后,各桶依次取出数据即可。


外部排序

外排序了解即可,我们简单谈一下外排序的基本方法,即归并排序法,及其基本原理和步骤。

外部排序是指大文件排序,即待排序的数据记录以文件的形式存储在外存储器上。由于文件中的记录很多、信息容量庞大,所以整个文件所占据的存储单元往往会超过了计算机的内存量,因此,无法将整个文件调入内存中进行排序。于是,在排序过程中需进行多次的内外存之间的交换。在实际应用中,由于使用的外设不一致,通常可以分为磁盘文件排序磁带文件排序两大类。

外部排序基本上由两个相对独立的阶段组成。

  1. 生成若干初始归并段(顺串)。按可用内存大小,将外存上含 N 个记录的文件分成若干长度为 L(<N) 的子文件,依次读入内存,利用内部排序算法进行排序。将排序后的文件写入外存,通常将这些文件称为归并段(Run)或“顺串”。
  2. 多路归并。对这些归并段进行逐步归并,使得有序的归并段逐渐扩大,最终在外存上形成整个文件的单一归并段,也就完成了文件的外排序。

可见外部排序的基本方法是归并排序法,底层还要依赖于内排序。

最简单的归并法如上所述,每个初始归并段只有内存那么大,各初始段归并时只能层层进行二路归并,若初始归并段为m,归并数的深度为\left \lceil log_{2} m \right \rceil + 1,排序过程要对数据扫描 \left \lceil log_{2} m \right \rceil  次。

事实上外部排序的效率还可以进一步提高。要提高外排的效率,关键要解决以下4个问题:

  • 如何减少归并轮数
  • 如何有效安排内存中的输入、输出块,使得机器的并行处理能力被最大限度利用
  • 如何有效生成归并段
  • 如何将归并段进行有效归并

针对这些问题,人们设计了多种解决方案:

  1. 在实现将初始文件分为 m 个初始归并段时,为了尽量减小 m 的值,采用置换-选择排序算法,可实现将整个初始文件分为数量较少的长度不等的初始归并段。
  2. 同时在将初始归并段归并为有序完整文件的过程中,为了尽量减少读写外存的次数,利用哈夫曼树的贪心策略选择归并次序构建最佳归并树的方式;釆用多路归并取代简单的二路归并,就可以减少归并轮数;
  3. 对初始归并段进行归并,而归并的具体实现方法是采用败者树的方式。

总之,外排序在基本思想方法之外,也有许多优化方向和技巧,一般不再要求掌握。


排序算法介绍结束了,最后再提供一个 8 大内部排序算法相关及其java实现

鉴于python语言的特性,要掌握排序代码,用一门强类型语言java 写一遍可能更好。。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值