归并排序(Merge Sort)

theory



基本思想

  归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

分而治之

   可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。

合并相邻有序子序列

  再来看看阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

code

<1> 递归

(a)使用vector

/*
 * @Author: 鱼香肉丝没有鱼
 * @Date: 2021-11-13 19:19:48
 * @Last Modified by: 鱼香肉丝没有鱼
 * @Last Modified time: 2021-11-13 19:40:56
 */

#include <algorithm>
#include <iostream>
#include <limits>
#include <vector>
using namespace std;

//合并
void Merge(vector<int>& Array, int front, int mid, int end)
{
    // preconditions:
    // Array[front...mid] is sorted
    // Array[mid+1 ... end] is sorted
    // Copy Array[front ... mid] to LeftSubArray
    // Copy Array[mid+1 ... end] to RightSubArray
    vector<int> LeftSubArray(Array.begin() + front, Array.begin() + mid + 1);  // vector初始化是一个左闭右开区间 [a,b)
    vector<int> RightSubArray(Array.begin() + mid + 1, Array.begin() + end + 1);
    int idxLeft = 0, idxRight = 0;
    LeftSubArray.insert(LeftSubArray.end(), numeric_limits<int>::max());//在leftsubarray尾部插入一个INT_MAX
    RightSubArray.insert(RightSubArray.end(), numeric_limits<int>::max());

    // Pick min of LeftSubArray[idxLeft] and RightSubArray[idxRight], and put into Array[i],
    //每次从左或者右子串中挑选最小的放到array中
    for(int i = front; i <= end; i++) {
        if(LeftSubArray[idxLeft] < RightSubArray[idxRight]) {
            Array[i] = LeftSubArray[idxLeft];
            idxLeft++;
        }
        else {
            Array[i] = RightSubArray[idxRight];
            idxRight++;
        }
    }
}

void MergeSort(vector<int>& Array, int front, int end)
{
    if(front >= end)
        return;
    int mid = (front + end) / 2;
    MergeSort(Array, front, mid);
    MergeSort(Array, mid + 1, end);
    Merge(Array, front, mid, end);
}
int main()
{
    int n = 12;
    int num[n] = {23, 45, 17, 11, 13, 89, 72, 26, 3, 17, 11, 13};
    vector<int> Array(num, num + n);  //利用num中的第1到12个元素来初始化向量

    int i;

    MergeSort(Array, 0, 11);

    cout << "排序后的数组为:" << endl;
    for(int i = 0; i < n; i++)
        cout << Array[i] << ' ';  // vector可以直接当做数组来输出
    cout << endl;

    return 0;
}

(b)使用一般数组

/*
 * @Author: 鱼香肉丝没有鱼
 * @Date: 2021-11-13 19:19:48
 * @Last Modified by: 鱼香肉丝没有鱼
 * @Last Modified time: 2021-11-14 09:51:24
 */

#include <algorithm>
#include <iostream>
#include <limits>

using namespace std;

//合并
void Merge(int* num, int front, int mid, int end)
{
    int l, r;  //下标
    int i;
    int* left = NULL;  //存储左边的子串
    int* right = NULL;  //存储右边的子串
    int len = (end - front) / 2;  //确定声明子数组的长度
    left = new int[len + 2];
    right = new int[len + 2];
    for(i = 0; i < len + 2; i++) {//本来想用memset的,但是在动态数组上面使用容易出错
        left[i] = right[i] = INT_MAX;  //先初始化一下,设为最大值,可能会有一些空位用不上
    }

    for(i = 0; i <= mid - front; i++)
        left[i] = num[front + i];
    for(i = 0; i <= end - mid - 1; i++)
        right[i] = num[mid + i + 1];
    l = r = 0;
    for(i = front; i <= end; i++) {
        if(left[l] < right[r])
            num[i] = left[l++];
        else
            num[i] = right[r++];
    }

    delete[] left;  //回收这块内存
    delete[] right;
}

void MergeSort(int* num, int front, int end)
{
    if(front >= end)  //递归结束条件
        return;
    int mid = (front + end) / 2;
    MergeSort(num, front, mid);
    MergeSort(num, mid + 1, end);
    Merge(num, front, mid, end);
}
int main()
{
    int n = 12;

    int num[n] = {23, 45, 19, 11, 13, 89, 72, 26, 3, 17, 33, 13};

    int i;

    MergeSort(num, 0, 11);

    cout << "排序后的数组为:" << endl;
    for(int i = 0; i < n; i++)
        cout << num[i] << ' ';  // vector可以直接当做数组来输出
    cout << endl;

    return 0;
}

( c)不需要建立数组

老师说可以不建立数组,先占个坑,等到后面再写

填坑 只需要一开始建立两个数组,然后交替使用就好
/******************************************
 * @Author       : 鱼香肉丝没有鱼
 * @Date         : 2021-09-20 12:55:54
 * @LastEditors  : 鱼香肉丝没有鱼
 * @LastEditTime : 2021-12-10 09:44:18
 ******************************************/

#include <iostream>

using namespace std;
int* a = NULL;
int* b = NULL;
int cnt = 0;

void Merge(int left, int mid, int right) {
    int i = left, j = mid + 1, k = left;
    while(i <= mid && j <= right) {
        cnt++;  // the number of comparisons
        if(a[i] <= a[j])
            b[k++] = a[i++];
        else
            b[k++] = a[j++];
    }
    while(i <= mid)
        b[k++] = a[i++];
    while(j <= right)
        b[k++] = a[j++];
}

void Copy(int left, int right) {
    while(left <= right) {
        // a[left] = b[left++];  //wrong way
        a[left] = b[left];
        left++;
    }
}

void MergeSort(int left, int right) {
    if(left < right) {
        int mid = (left + right) >> 1;
        MergeSort(left, mid);
        MergeSort(mid + 1, right);
        Merge(left, mid, right);
        Copy(left, right);
    }
}

int main() {
    // freopen("test data.in", "r", stdin);
    //  freopen("file in.txt", "r", stdin);
    freopen("file.txt", "r", stdin);
    int n;
    cin >> n;
    a = new int[n];
    b = new int[n];
    for(int i = 0; i < n; i++) {
        scanf("%d", &a[i]);
    }

    MergeSort(0, n - 1);

    cout << "排序后的数组为:" << endl;
    for(int i = 0; i < n; i++) {
        if(i) printf(" ");
        printf("%d", a[i]);
    }

    cout << endl;
    printf("%d\n", cnt);

    return 0;
}

<2> 迭代

  • 递归会占用大量的内存,使用迭代可以提升效率,大部分的递归都可以写成迭代
  • 直接从最底层开始,两个一组,然后四个一组,八个一组,最后完成
/*
 * @Author: 鱼香肉丝没有鱼
 * @Date: 2021-11-14 15:32:21
 * @Last Modified by: 鱼香肉丝没有鱼
 * @Last Modified time: 2021-11-14 15:33:58
 */

#include <iostream>

using namespace std;

template <typename T>
void PrintArray(T arr[], int n)
{
    int i;
    for(i = 0; i < n; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;
}

//使用模板,不止适用于整型数组,浮点型等其他也可以
template <typename T>  //在这了typename和class没有区别
void MergeSort(T arr[], int n)
{
    int step, left_min, left_max, right_min, right_max;  //步长,两个子串的左右边界,区间是左开右闭
    int cnt;  // temp数组的下标
    T* temp = new T[n];  //辅助数组,和原数组一样长

    for(step = 1; step < n; step += step) {  // step是步长,也就是区间长度
        for(left_min = 0; left_min < n - step; left_min = right_max) {
            right_min = left_max = left_min + step;  //右边界等于左边界+区间长度
            right_max = right_min + step;

            if(right_max > n)
                right_max = n;  //不能超出数组原本的长度嘛
            cnt = 0;
            while(left_min < left_max && right_min < right_max) {  //只要有一边遍历完就退出
                // 左右两边谁小取谁存入temp中,然后各自下标递增
                if(arr[left_min] < arr[right_min])
                    temp[cnt++] = arr[left_min++];
                else
                    temp[cnt++] = arr[right_min++];
            }
            /*上面的while循环如果退出那么一定是左右两边其中一个子串遍历完了
            但是另一个子串可能还有剩余元素没有遍历。假如右边剩,因为每次取的都是较小的值,
            如果右边剩的话那就不用管了,本来就是应该放在右边的,只用考虑左边剩的情况
            */
            while(left_min < left_max)  //右边剩的话不会进入这个while循环
                arr[--right_min] = arr[--left_max];  //把左边的剩下元素放在右边,注意这时候rightmin指向
                                                     // 的是rightmax的位置,使用前都要自减
            while(cnt > 0)
                arr[--right_min] = temp[--cnt];  // tmep中的元素是有序的,现在还原到arr中,注意下标是先自减
        }
    }

    delete[] temp;
}

int main()
{
    int n = 12;

    int arr[n] = {23, 45, 19, 11, 13, 89, 72, 26, 3, 17, 33, 13};

    int i;

    MergeSort(arr, n);

    cout << "排序后的数组为:" << endl;
    PrintArray(arr, n);

    return 0;
}

summary

  1. 归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。每一层合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。

  2. 熟悉了Vector的使用https://blog.csdn.net/qq_41680771/article/details/121314596

  3. 递归版本中建立数组的一个致命bug记录
    我刚把其中一个len+2,写成了mid+2,如下
    在这里插入图片描述
    在这里插入图片描述
    比如到了后面mid为4,len为1,我就会给本来没有申请的空间赋值,造成后delete回收出现问题
    这是vscode运行时的报错
    在这里插入图片描述
    但是这样也能计算出最后正确的结果(因为后面只取front到end个值放在array里面,不会访问到后面那些内存不匹配的值),并且在调试窗口看数组的值的话是看不出来的,没有申请的内存也会赋值并且显示,运行exe文件的时候就出错了(当然exe文件不会报错,只会什么也没有,就消失了,或许这就是闪退(*^﹏^*)),system(“pause”)都没有用,我检查了好几遍都没有发现,最后还是请教了我的老师才发现了这个问题,他并没有直接看调试控制台,而是用了最朴素的方法,每次递归的时候打印输出front,mid和end的值,中途发现卡注了,然后发现了这个问题。

  4. new和delete一般配对使用,如果没有手动delete的话,程序结束后系统也会自动回收这些内存。

  5. new和delete属于c++的关键字,不需要头文件,但是使用c语言的malloc和free的话需要包含头文件 #include<stdlib.h>

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值