sort算法排序 std_leetcode 归并排序

leetcode中有几道关于归并排序的题目,本文主要介绍归并排序及其非递归实现,并解决148,315和493三道mid和hard级别的题目。

归并排序简介

归并排序是一种分治算法,它主要基于merge操作(详见let题目88),将两个有序数组合并成一个大的有序数组。所以我们可以将一个数组分割成两部分,分别递归的排序,再将两个排序结果归并成一个大的有序数组,完成排序。该算法能保证长度为N的数组,排序所消耗的时间与NlgN成正比,唯一不足的是,需要额外的N空间。

_merge函数的声明如下,result是归并排序所需的额外空间,[start, mid),[mid,end)都是nums中的有序区间,归并为一个大的有序区间[start1, end2),临时存放在result中;

所以merge完还需要把result中的数据倒腾回nums;

void _merge(vector<int>& result, const vector<int>& nums, size_t start, size_t mid, size_t end);

稳定性

归并排序是稳定的。

三个优化点

1 merge优化

上述merge函数有一个优化点:如果两个子数组没有交集,即第一个数组的最后一个元素<=第二个数组的第一个元素,那么不需要归并,可以直接返回。

2 小数组排序优化

当子数组足够小的时候,直接使用插入排序。避免来回的merge拷贝。

3 编程技巧

不要让辅助数组result和nums之间来回copy,而是利用语言特性避免拷贝。对于c++来说,就是利用vector的swap操作来消除拷贝;

实现

常见的归并排序实现是自顶向下的递归实现,本文要介绍的是自底向上的迭代实现,不使用递归。

以长度为8的数组 2 7 8 0 3 6 1 9 为例,

方法:

初始化step为1,

第一步:俩俩合并长度为step的子数组,第一次合并后,结果为:

2 7 0 8 3 6 1 9

第二部,step翻倍,即step为2,回到第一步,继续合并,结果为:

0 2 7 8 1 3 6 9

现在,这个数组的两个大小为4的子数组都各自有序了。

继续step翻倍,合并,结果为:

0 1 2 3 6 7 8 9

继续step翻倍,现在是8,达到了数组的长度,归并排序结束了。

代码如下:

void mergeSort(vector<int>& nums) {
    if (nums.size() <= 1)
        return;

    vector<int> result(nums.size()); // 额外的辅助空间
    for (size_t step = 1; step < nums.size(); step *= 2) {
        // nums[i + step , i + step * 2), nums[i + step * 2, i + step * 3) is sorted,
        // now merge them, i is 0 to nums.size() by 2*step
        for (size_t i = 0; i < nums.size(); i += 2 * step) {
            size_t start1 = i, mid = std::min(i + step, nums.size());
            size_t end2 = std::min(mid + step, nums.size());

            _merge(result, nums, start1, mid, end2);
        }

        nums.swap(result); // 利用swap避免来回拷贝
    }
}

2019.08.09补充优化点:

对于自底向上的迭代归并排序,有几个优化方法:

1 自然归并排序,timsort中有类似思想:

普通自底向上算法,有序子序列大小是从1开始的,浪费了数组中的有序信息,比如:

对于序列 1 3 5 7 2 4 6 8

按照普通做法,会先merge 13, 57, 24, 68,再merge 1357, 2468。。。

事实上,该序列已经是两个有序子序列组成的: 1357 + 2468

所以应该先预处理数组,得到每个有序子序列的结束点:4(元素2)和8(数组尾),意思是[0,4),[4,8)这些序列是有序的,直接两两归并。

不过尝试实现了一下,可能代码写的有问题?性能并没有什么变化。

    vector<int> result(nums.size()); 
    queue<int> sorted_index
    // 先O(N)预处理,记录有序子序列的终点。每一个终点是下一个子序列的起点。
    for (size_t i = 0; i < nums.size(); ) {
        size_t sorted = i+1;
        for (; sorted < nums.size(); sorted++) {
            if (nums[sorted] < nums[sorted - 1])
                break;
        }

        if (i == 0 && sorted == nums.size())
            return;

        sorted_index.push(sorted);
        i = sorted;
    }

    while (sorted_index.size() >= 2) {
        size_t start = 0;
        std::queue<int> new_sorted_index;
       // 两两归并子序列
        while (sorted_index.size() >= 2) {
            size_t mid = sorted_index.front();
            sorted_index.pop();
            size_t end = sorted_index.front();
            sorted_index.pop();

            _merge(result, nums, start, mid, end);
            std::copy(result.begin() + start,
                      result.begin() + end,
                      nums.begin() + start);

            new_sorted_index.push(end);
            start = end;
        }

        if (!sorted_index.empty()) {
            new_sorted_index.push(sorted_index.front());
        }

        sorted_index.swap(new_sorted_index);
    }

2 利用插入排序预先优化

依然是这个问题:普通自底向上算法,有序子序列大小是从1开始的;

为了提高归并时初始的子序列长度,先用插入排序分段处理整个数组,然后再执行归并;

简单测试了一下,在debug模式下性能提高了30%,release模式下,提高了7%左右,效果还是比较明显。

// 先插入排序优化,提高初始归并的子序列长度。
// 处理后,数组的[0,16), [16,32)...的子序列已经有序了。
    int initStep = 16;
    for (size_t i = 0;  i < nums.size(); i += initStep) {
        size_t end = i + initStep;
        if (end > nums.size())
            end = nums.size();
        _insertSort(nums, i, end);
    }
    // 开始归并,初始长度为16,不再是1.
    for (size_t step = initStep; step <= nums.size(); step *= 2) {
        // nums[i + step , i + step * 2), nums[i + step * 2, i + step * 3) is sorted,
        // now merge them, i is 0 to nums.size() by 2*step
        for (size_t i = 0; i < nums.size(); i += 2 * step) {
            size_t start1 = i, end1 = std::min(i + step, nums.size());
            size_t start2 = end1, end2 = std::min(end1 + step, nums.size());
            (void)start2;

            _merge(result, nums, start1, end1, end2);
        }

        nums.swap(result);
    }

let相关题目

148. 链表的排序

Loading...​leetcode.com
7b95c0208975b472c6d41d9c3e8f10be.png

该题目要求以nlgn的复杂度排序链表。很容易想到快速排序或归并排序,但是链表不像数组,切分是有复杂度的,需要遍历。所以我参考了一下std::list的sort实现,发现它正是使用自底向上的归并排序解决的。试了一下,通过测试只用了28ms,beat 100%~好吧,STL的算法就是屌。

和数组归并排序不同的是,链表归并排序只需要lgn的空间,就能实现nlgn的排序。也就是说基本可以认为不需要额外空间(例如就算链表100万个节点,也只需要20个的额外节点,忽略不计)。

还是先举个例子便于理解:

以下描述中,额外空间固定的是ListNode* tmp[64],也就是64个指针,可以为长度为2的64次方个节点的链表进行排序。并设置一个toFill变量,表示tmp的下标,它是一个优化,toFill之后的tmp不需要考虑,初始化为0。

假设待排序链表是: list "5,3,1,4,2"

第1步: 将链表头节点 5挪到tmp[0],更新toFill = 1

第2步: 继续将链表新头节点 3放到tmp[0], 因为tmp[0]上已经挂着一个节点,直接归并,得到了子链表"3,5", 现在将它挪到tmp[1],更新toFill = 2

第3步: 继续将链表新头节点 1 放到tmp[0], 因为tmp[0]是空的,啥都不做。

第4步: 继续将链表新头节点 4放到tmp[0], 因为tmp[0]上已经挂着一个节点1,直接归并,得到了子链表"1,4", 现在将它挪到tmp[1],而在第二步tmp[1]已经挂了链表“3,5”,所以再归并,得到了一个有序子链表:"1,3,4,5",挪到tmp[2],更新toFill = 3

第5步: 继续将链表的最后一个节点 2 放到tmp[0], 因为tmp[0]是空的,啥都不做,toFill还是3

好了,现在,待排序链表空了,而tmp是这样的:

tmp[0] 保存了长度为1的链表: "2"

tmp[2]保存了长度为4的链表:"1,3,4,5"。

现在toFill = 3。

所以直接将tmp[1],tmp[2]归并到tmp[0],那么tmp[0]最终是"1,2,3,4,5",直接返回头指针即可。

代码不贴了,有兴趣可以直接查看STL的list源码。

let315 统计右边比自己小的元素个数。

待写。我的实现提交后比较低效,beat 36%。。。 优化后把这道题补上

let493

理由同上。

扩展练习

1.以nlgn的复杂度,lgn的额外空间,随机打乱一个链表。

2.编写一个不改变数组的归并排序,返回一个int perm[]数组,其中perm[i]表示原数组中第i小的元素的位置。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值