python 归并排序

归并排序的原理
先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。
在这里插入图片描述
分治思想,分治算法一般都是用递归来实现的。写递归代码的技巧就是:
1)分析递推公式
2)找到终止条件
3)将递推公式翻译成递归代码

递推公式
merge_sort(p...r) = merge(merge_sort(p...q), merge_sort(q...r))
终止条件
p >= r, 不用再继续分解

根据递推公式,转化成代码:

 def merge_sort(data_list):
     length = len(data_list)
     merge_sort_c(data_list, 0, length)
 
 def merge_sort_c(data_list, p, r): 
     if p+1 >= r: return
     else:
         q = (p+r)/2
         merge_sort_c(data_list, p, q)
         merge_sort_c(data_list, q, r)
         merge(data_list, p, q, r)
  
  def merge(data_list, p, q, r):
      tmp = []
      i = p
      j = q
      while i < q and j < r:
          if data_list[i] <= data_list[j]:
              tmp.append(data_list[i])
              i += 1
          else:
              tmp.append(data_list[j])
              j += 1
      while i < q:
          tmp.append(data_list[i])
          i += 1
      while j < r:
          tmp.append(data_list[j])
          j += 1
  
      for tmp_index, index in enumerate(range(p, r)):
          data_list[index] = tmp[tmp_index]
  
  if __name__ == "__main__":
      data_list = [38, 50, 63, 27, 89, 94, 11, 82, 9, 13]
      print(data_list)
      merge_sort(data_list)
      print(data_list)

性能分析:

  1. 时间复杂度:归并排序不关心数组的初始状态,因此最好、最坏、平时时间复杂度都是一样的,为O(nlogn),关于专栏中是这样求解时间复杂度的,非常有学习的价值。
    不仅递归求解的问题可以写成递推公式,递归代码的时间复杂度也可以写成递推公式。
    我们假设对 n 个元素进行归并排序需要的时间是 T(n),那分解成两个子数组排序的时间都是 T(n/2)。我们知道,merge() 函数合并两个有序子数组的时间复杂度是 O(n)。所以,套用前面的公式,归并排序的时间复杂度的计算公式就是:
 T(1) = C;   n=1 时,只需要常量级的执行时间,所以表示为 C。 
 T(n) = 2*T(n/2) + n; n>1

通过这个公式,如何来求解 T(n) 呢?还不够直观?那我们再进一步分解一下计算过程。

T(n) = 2*T(n/2) + n
     = 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n
     = 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
     = 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
     ......
     = 2^k * T(n/2^k) + k * n
     ......

通过这样一步一步分解推导,我们可以得到 T(n) = 2^k T(n/2^k)+kn。当 T(n/2^k)=T(1) 时,也就是 n/2^k=1,我们得到 k=log2n 。我们将 k 值代入上面的公式,得到 T(n)=Cn+nlog2n 。如果我们用大 O 标记法来表示的话,T(n) 就等于 O(nlogn)。所以归并排序的时间复杂度是 O(nlogn)。

2、空间复杂度:O(n),因此它不是一个原地排序算法。递归代码的空间复杂度并不能像时间复杂度那样累加。尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)。

3、稳定性:稳定。我们对数组分成左右两部分,对于两边相同的值,我们可以选择将右部分的值归并后放在左边相同值的后面,因此它是稳定的排序算法。

使用哨兵优化性能

在上述 merge 函数中有三处使用了 while 循环,第一个 while 循环条件中还有两个范围判断语句,当数据量非常大时,这些过多的判断势必会影响算法的性能。

我们知道,在编程中可以借助哨兵来简单条件判断,从而可以写出 bug 更少的代码,进而优化性能。

上述中的 merge 函数主要目的主是合并两个有序数组,但是为了在比较的过程中防止越界,加入了 i < r 和 j < q 来防止左右部分越界,最后防止某部分有剩余元素从而多写了两个 while 循环。

其实在大多数情况下,越界的情况是非常少的,那么每一次循环对越界的检查也会浪费 CPU 资源,而哨兵就是优化条件判断的。

思考:
1、如果左右部分的最后一个元素都是最大且相等,那么当左边的元素循环结束时,右边也必定结束,这样只用一个 while 就可以搞定,而且只需要一个 i < r 就够了,节省一个条件判断。

2、范围比较 i < r 需要 cpu 对每个二进制位进行比较,如果换成不等于判断,只要一个二进制位不同,就可以得出结果,理论上也可以更快些。

使用哨兵的 merge 函数如下所示:

def merge2(data_list,p,r,q):
    '''
    利用哨兵的归并函数
    例如 data_list[p:q] = [...,1,4,2,3,...]
    part_left = [1,4]
    part_right = [2,3]
    归并后 data_list[p:q] = [...,1,2,3,4,...] 
    '''
    part_left = [data_list[index] for index in range(p,r)]  #临时数组保存左部分
    part_right = [data_list[index] for index in range(r,q)] #临时数组保存右部分

    #对左边部分或右边部分增加哨兵,哨兵为待排序数据的最大值如99999
    max_value = 99999
    part_left.append(max_value)
    part_right.append(max_value)
    i = 0
    j = 0
    k = p
    # while i != r-p: # 也可以这样写,看自己喜好
    while part_left[i] != max_value:
        if part_left[i] <= part_right[j]:
            data_list[k] = part_left[i]
            i += 1
        else:
            data_list[k] = part_right[j]
            j += 1
        k +=1 #依次从左边部分和右边部分按顺序抽取数据  

分别在左部分和右部分的最后加入最大值的哨兵,可以减化 merge 函数的编码,使用哨兵有以下三点优化:

1、减少了 while 的个数,简化了编码过程
2、减少了 while 循环的条件判断
3、将范围判断改为不等于判断

小结:分治是一种解决问题的处理思想,递归是一种编程技巧。哨兵是一种不错的编程技巧,使用它也可减少 bug,某种程度上,也提高了代码的性能。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值