c#sort升序还是降序_python中的sort之timsort学习

背景

      日常与工作中使用sort的场景还是蛮多,
最近突然在问自己,他是怎样实现一个工业级
的排序算法呢?

前言

TimSort是结合了插入排序和归并排序稳定的排序算法,并做了许多优化,
在实际应用中效率很高,因为大部分情况下待排序的数组都是部分有序
(升序或降序)的。对于已经部分有序的数组,时间复杂度远低于nlog(n),
最好可达到O(n);对于随机的数组,时间复杂度是nlog(n)。空间复杂度
是O(n),最好的情况下是O(1)。

TimSort主要步骤:

1、根据数组长度计算minrun(最小分区长度),run理解为分区,

2、将数组按升序(非严格)或者严格降序(需反转为升序)分割成
一个一个run(分区),长度小于minrun的分区则使用插入排序进行扩充

3、将分区的首元素下标及长度放入栈中,当栈顶run的长度满足 runLen[n-2] + runLen[n-1] >= runLen[n-3] 
或者满足runLen[n-1] >= runLen[n-2]时(n表示栈中run的个数,下标n-1就是栈顶),使用归并排序将栈顶相邻最
短的两个run进行合并,继续对剩余的数组元素进行分区。

4、数组分区完成后将栈中剩余的run全部合并

minrun是什么?作用?如何确认minrun?

含义:最小分区长度,根据数组实际长度确定

作用:为了避免合并的两个数组太短

如何确认minrun值:
1.当数组元素个数小于64时,minrun就是数组的长度,此时就采用二分查找插入排序来进行数组排序
2. 当数组元素个数大于等于64时(待确认),分区的数目等于或略小于2的幂时,合并两个数组最为
有效。所以minrun范围为 [32,64]选择数组长度的六个最高标志位,如果其余的标志存在,则加1,
比如189:10111101,取前六个最高标志位为101111(47),同时最后两位为01,所以 minrun 
为47+1=48,可以划4个分区。976:11 1101 0000,取前六个最高标志位为111101(61),同时
最后几位为0000,所以minrun为61。

​	

分区长度不够minrun时,为什么选择插入排序

  • 选择的指标:时间复杂度 、空间复杂度、稳定性、比较次数、交换或移动次数
  • 基本的排序算法,如图:

0557ddea8a0919eb737f0b4a25ff6f6c.png
  • 为什么选择归并排序而不是其他NlogN的排序算法
1. 稳定性,我们希望排序后原始相等的数据顺序不变,需要排序时稳定的。
排除快速排序  堆排序;
2. 基数排序是一种很特别的排序算法,是基于关键字各位的大小进行排序的,使用场景相比特别,
比如 当针对大量电话号码排序时
  • 为什不是多路归并排序?
  • 为什么归并排序中需要加入插入或者冒泡排序?
当数据量小时,归并与插入时间复杂度一样,但数据比较有序时,插入排序
与冒泡排序效果会更好
  • 为什么选择插入排序,而不是冒泡排序
    当我们发现数据规模比较小或者基本有序的时候,时间复杂度比二路归
并要更好,所以为了进一步优化,当数据量量较大时,先试用二路归并排序,当递归到
数据量比较少时,使用插入排序或者冒泡排序更好,为了避免归并的分区太短,出现了minrun
  • 冒泡排序
  def bubble_sort(collection):
      """冒泡排序"""
      length = len(collection)
      for i in range(length - 1):
          swapped = False
          for j in range(length - 1 - i):
              if collection[j] > collection[j + 1]:
                  swapped = True
                  collection[j], collection[j + 1] = collection[j + 1], collection[j]
          # 当某趟遍历时未发生交换,则已排列完成,跳出循环
          if not swapped:
              break
      return collection
 
  • 插入排序的改进-二分插入排序
#顾名思义,插入排序中针对插入点使用二分查找进行改进,
#将查找插入位置的时间复杂度从0(n)降到O(logn),但整体
# 时间复杂度没有变,还是代码实践如下:

def binary_insert_sort(nums):
    """折半插入排序
    :param nums:
    :return:
    """
    # 遍历数组中的所有元素,其中0号索引元素默认已排序,因此从1开始
    # 当nums元素数量为空或者1时,不会进入for循环
    for i in range(1, len(nums)):
        #  二分查找在已排序中寻找待插入位置确定待插入点
        left = 0
        right = i - 1
        target = nums[i]
        while left <= right:
            mid = (right - left) // 2 + left
            if nums[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
       
        for j in range(i, right+1, -1):
            # 交换,并由此处可见折半插入排序为稳定性排序
            nums[j]= nums[j - 1]

        nums[right+1] = target

    return nums

# 以上二分插入是不稳定的,需要加个=号,找到最右边相等的数
  • 基于比较次数与移动次数对比插入排序与冒泡排序
插入排序,最好情况比较n次,移动0次,最坏情况比较n2/2,移动n2/2,均值n2/4;
冒泡排序,最好情况比较n次,移动0次,最坏情况比较n2/2,移动3n2/2,均值3n2/4;
冒泡排序,每一次比较移动时,需要交换三次,相比插入排序需要一次,所需选择
插入排序。

timsort流程讲解:

假设minrun为5,如图:

a48c70239f55f5b0fcfed4588dc748a5.png
升序与严格降序分区
  1. 按升序分区run[0],将run[0]的起始下标和长度压入栈;
  2. 第二个分区为严格降序,反转,若此时分区比minrun小,则二分插入补全,再次压入栈;
  3. 此时runLen[1] >= runLen[0],满足合并规则,使用归并排序进行合并,合并后如下:

73a059a1304d099cc09038b957415cb7.png

4. 合并后run[0]长度变为11,压入栈,此时没有run[1];

5. 寻找新的分区,是降序,反转为[9,11,15,17,18,20],run[0]、run[1]不满足合并规则(runLen[n-2] + runLen[n-1] >= runLen[n-3] 或者runLen[n-1] >= runLen[n-2]),不合并;

6. 紧接着升序的元素(0、5、6)个数小于minrun(5),使用插入排序将元素个数扩充至minrun,组成run[2]并压入栈中,此时栈顶元素run[0]、run[1]、run[2]也不满足合并规则(runLen[n-2] + runLen[n-1] >= runLen[n-3] 或者runLen[n-1] >= runLen[n-2]),不合并;

7. 数组中还剩下两个元素,组成run[3],此时栈顶满足合并规则(runLen[n-1] >= runLen[n-2]),使用归并排序进行合并,合并结果如下

e2adabe727ae5ce1962c1ecdf88b4e77.png

8. 数组分区完成后, 依次从栈顶对分区使用归并排序进行合并, 直到数组完全有序

  • 压栈构成阶梯式

461732bdcaee2afb3181dca6c7e75f0a.png

4a71e87288720bc50394e410677d33a8.png

这样可以有效的防止 一个很长的分区与很短的分区合并。

合并中的优化-Galloping mode(倍增搜寻法)

  • 减少合并时比较的数量

e40cba8e1c71bd06d3c1dcdc22bf14bb.png

考虑到归并时两个都是有序的,查找R2左边界起始元素在R1中的位置,这样之前的元素都不用比较,以2的n次方递增做比较,然后使用二分查找

流程如下:

run1:[6,7,8,9,10]
run2:[1,2,3,4,5]


run1[0] > run2[0]
run2[0] > run2[1]
run1[0] > run2[3]

run1[0] > run2[2^n-1]
run1[0] > run2[2^(n+1)-1]

然后在2^n-1到2^(n+1)-1之间使用二分查找,

timsort总结:

b0f0fdb4ddf071630a2cb01883dcd13c.png
  1. 识别降序进行翻转,获取待比较序列中连续的升序列;
  2. 插入排序中使用二分查找快速定位插入位置;
  3. 动态计算minRun以减少合并次数;
  4. 把要合并的run模拟压栈成mergeSort的样子;
  5. 自动排除一些不需要比较的头部和尾部。
  6. 使用倍增法快速地位查找区间,在一个序列总比另外一个序列小的时候,他会猜测会有更多的数据满足这个情况,再次划出那些不用去比较的头部和尾部

扩展知识-外排序

  • 针对1亿用户 按照年龄排序 如果内存有限呢?
桶排序,假设最大年龄100岁,分成100个桶,将不同年
龄的用户写入不同的桶(文件中),如后针对每一个桶进
行排序,最后从小到大遍历桶,将内容写入最后的文件中

参考文章

Comparison between timsort and quicksort

Timsort: The Fastest sorting algorithm for real-world problems

timsort详细介绍

timsort简介

timsort流程

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值