归并排序的优化

归并排序的时间复杂度为\Theta(n\log_2n), 插入排序的时间复杂度为\Theta(n ^{^{2}}), 当需要排序的数组足够长时,归并排序肯定比插入排序更快,但当数组长度比较小的时候,常量因子起主导作用,由于插入排序的常量因子比较小,插入排序比归并排序更快。

所以有一个常见的对归并排序的优化:当递归排序的子问题变得足够小时,不继续递归调用归并排序,而是直接调用插入排序。

有两种做法:

  1. 自顶向下,指定需要使用插入排序的子问题的大小。
  2. 自底向上。把数组均分为k份,每一份用插入排序进行排序,然后对这k个排好序的子数组进行归并操作。

伪代码

merge_ins_sort(array, subarray_length)
    # 自顶向下
    n = array.length
    merge_ins_sort_aux(array, 1, n, subarray_length)

merge_ins_sort_aux(array, left, right, subarray_length)
    length = right - left + 1
    if subarray_length >= length
        insertion_sort(array, left, right)
    else
        mid = floor((left + right) / 2)
        merge_ins_sort_aux(array, left, mid, subarray_length)
        merge_ins_sort_aux(array, mid + 1, right, subarray_length)
        merge(array, left, mid, right)
merge_ins_sort(array, partition)
    # 自底向上
    n = array.length
    subarray_length = ceil(n / partition)
    subarray_number = partition
    for i = 1 to subarray_number
        left = sublist_length * (i - 1) + 1
        right = min(subarray_length * i, n)
        insertion_sort(array, left, right)
    while subarray_number > 1
        for i = 1 to subarray_number by 2
            merge(array, subarray_length * (i - 1) + 1, subarray_length * i,
                        min(subarray_length * (i + 1), n))
            subarray_length *= 2
            subarray_number = ceil(subarray_number / 2)

 

 时间复杂度

自底向上

数组被分成k个子数组,则每个子数组的长度为n / k, 所以对所有子数组执行插入排序操作的总时间为k * \Theta((n / k) ^ 2) = \Theta(n ^ 2/ k),

然后相邻的子数组两两进行归并, 这样,子数组数目变为k / 2, 每个子数组的长度为2n/k, 一轮归并的总时间为\Theta(n), 继续归并相邻的子数组, 直到子数组的数目变为1, 也就是数组被排好序。由于每轮归并相邻的子数组,子数组的数目变为原来的1 / 2, 所以,需要执行\Theta(\log_2k), 所以归并的总时间为\Theta(nlog_2k)

所以算法的时间复杂度为\Theta(nlog_2k+ n^2/k)

当k = n时,对每个长度为1的子数组进行插入排序,时间为\Theta(n^2/k) = \Theta(n), 然后对n个长度为1的子数组进行归并,时间为\Theta(nlog_2k) = \Theta(nlog_2n)

当k = 1时,只有一个长度为n的子数组,所以只有插入排序,没有归并操作,时间为\Theta(n ^ 2/k) + \Theta(nlog_2k) = \Theta(n^2) + \Theta(nlog_21) = \Theta(n ^ 2)

自顶向下

该问题形成一个递归树,第一层是长度为n的问题,第二层是两个长度为n/2的子问题,也就是说,每一层比前一层子问题数目增倍,每个子问题长度减倍,每一层的归并操作总时间为\Theta(n), 如果是完全的归并排序,子问题的长度变为1时才停止递归,递归树的高度为log_2n+1, 算法复杂度为\Theta(nlog_2n), 在我们这个归并排序的变形中,当子问题的长度小于等于指定长度l时,就不再继续向下递归,而是直接使用插入排序,所以递归树的高度为log_2(n / l) + 1, 归并的总时间为\Theta(nlog_2(n / l)), 需要执行插入排序的子数组有n/l个,每个子数组长度为l,插入排序的总时间为n / l * \Theta(l ^ 2) = \Theta(nl), 所以总的时间复杂度为\Theta(nl + nlog_2(n/l)).

当l=n时,只有插入排序,没有归并操作,时间为\Theta(n ^ 2)

当l=1时,归并的时间为\Theta(nlog_2(n/l)) = \Theta(nlog_2n), 插入排序的时间为\Theta(nl) = \Theta(n), 总时间为\Theta(nlog_2n)

 

总结及讨论

自顶向下和自底向上的时间复杂度是相同的,都是\Theta(nlog_2(n/k)+ nk), k为子数组长度

问题1

当k趋近于1,时间复杂度趋近于归并排序的时间复杂度,当k趋近于n时,时间复杂度趋近于插入排序的时间复杂度。那么把k作为n的函数并且使用渐进符号表示,那么使得时间复杂度仍然渐进等于归并排序的时间复杂度的k的最大值是多少呢?

首先我们观察到,k肯定不能大于\Theta(log_2n), 如果k = \Theta(log_2n), 那么\Theta(nlog_2(n/k)+ nk) = \Theta(nlog_2n - nlog_2k + nk) = \Theta(nlog_2n - nlog_2(log_2n) + nlog_2n) = \Theta(nlog_2n), 所以k最大值为\Theta(log_2n)

问题2

当k取何值,我们的算法真正起到了优化效果?

算法运行时间分为两部分: c_1nkc_2nlog_2(n/k), 其中c_1是插入排序的常量因子,c_2是归并排序的常量因子,c_1 < c_2

归并排序的时间为c_2nlog_2n, 所以我们的优化算法和归并排序的时间差别就是c_1nk - c_2nlog_2k, 这个时间差别是怎么来的呢。归并排序的递归树有log_2n + 1层,优化算法的递归树只有log_2(n / k) + 1层,在log_2(n / k) + 1层直接使用插入排序。所以也就是插入排序所用时间c_1nk(log_2n + 1)- (log_2(n/k) + 1) = log_2k层递归所用的时间c_2nlog_2k的区别。

插入排序和归并排序的时间差别为c_1n ^ 2 - c_2nlog_2n = n(c_1n - c_2log_2n), 所以使得归并排序的时间小于插入排序的n都是我们的优化算法中可取的k。 

c_1nk - c_2nlog_2k \le 0 \Rightarrow c_1k \le c_2log_2k \Rightarrow \frac{k}{log_2k} \le \frac{c2}{c1}, 由于\frac{k}{log_2k}是递增函数,k的范围为(1, K_0],K_0是使得\frac{k}{log_2k} \le \frac{c2}{c1}成立的最大数。

最后来看一下优化效果,时间差除以归并排序的时间\frac{c_1nk - c_2nlog_2k}{c_2nlog_2n} = \frac{c_1k - c_2log_2k}{c_2log_2n},  随着n越来越大,分子所占的比例越来越小,所以我们的优化算法有一定的效果,但并不明显.

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值