归并排序的实现

1.概述

归并排序由冯·诺伊曼在1945年首次提出,是一种典型的分治思想:将问题拆分,递归处理,然后合并。归并排序的基础思路是,当一个数组的前后两部分都是有序的话,那么可以在O(n)时间内合并,使整个数组有序。归并排序的算法复杂度,在最好、最坏和平均的情况下,都是O(nLogn)。

2.算法

归并排序有两种思路:自顶向下和自底向上。

自顶向下就是先将数组作为整体,设数组长度为n,则先分成两个n/2的数组,递归调用归并排序,然后再合并。

按照这个思路,可以先写出以下递归代码:

void mergeSort(vector<int> &nums, int i, int j){
    if (i < j){
        int mid = (i + j) / 2;
        mergeSort(nums, i, mid);        // 使[i, mid]有序
        mergeSort(nums, mid + 1, j);    // 使(mid, j]有序
        merge(nums, i, mid, j);         // 将有序的两部分[i, mid], (mid, j]合并起来
    }
}

merge的过程也很简单:

  1. 分配一个临时数组,空间大小和待合并的数组相同:j-i+1
  2. 当左右两部分数组都有值时,对比两个数组中最小的值,将该值存放到临时数组中
  3. 当一个子数组的数据全部存放到临时数组后,将另一个子数组的数据依次放到临时数组中
  4. 将临时数组的数据替换到原数组中

在这个过程需要分配额外的空间,其大小和数组长度一致,所以归并排序的空间复杂度为O(n)

代码如下:

void merge(vector<int>& nums, int left, int mid, int right){
    vector<int> tmp(right - left + 1);      // 分配临时数组
    int i = left, j = mid + 1, k = 0;       // i,j是左右两个子数组的起点,将移动到各自的终点:mid和right
    while(i <= mid && j <= right){          // 当两个子数组都有数据时,进行对比
        if (nums[i] <= nums[j]){            // 将小的值存放到tmp中
            tmp[k++] = nums[i++];
        }else{
            tmp[k++] = nums[j++];
        }
    }
                                            
    while (i <= mid) {                      // 如果左边数组还有值,放到tmp中
        tmp[k++] = nums[i++];
    }

    while (j <= right) {                    // 如果右边数组还有值,放到tmp中
        tmp[k++] = nums[j++];
    }

    for (int p = 0; p < k; p++) {           // 将tmp的值复制到原数组
        nums[left + p] = tmp[p];
    }
}

归并排序也可以采用自底向上的思路,即先将数组n分成一个个长度为1的子数组,然后两两合并;再分成长度为2的子数组,合并;直到分成长度为n/2的子数组,再进行合并,达到最终有序。

代码如下:

void mergeSort(vector<int> &nums){
    int len = nums.size();
    int left = 0, right = len - 1;  // 左右边界
    for (int width = 1; width < len; width *= 2){           // 分组的边长,依次是1,2,4,8,...
        for (int i = 0; i < len - width; i += width * 2){   // 每个小数组的长度为width, 每次合并两个数组,i代表第1个数组的起点 
            int j = i + width * 2 - 1;                      // j是第二个数组的右边界
            if (j > len - 1){
                j = len - 1;                            // 不能超过整个数组的边界
            }
            int mid = i + width - 1;                    // mid是第一个数组的右边界
            merge(nums, i, mid, j);                     // 合并
        }
    } 
}

3.C++11代码

  • 递归版本
#include<vector>
#include<iostream>
using namespace std;

void merge(vector<int> &nums, int left, int mid, int right){
    vector<int> tmp(right - left + 1);
    int i = left, j = mid + 1, k = 0;

    while(i <= mid && j <= right){
        if (nums[i] > nums[j]){
            tmp[k++] = nums[j++];
        }else{
            tmp[k++] = nums[i++];
        }
    }

    while(i <= mid){
        tmp[k++] = nums[i++];
    }

    while(j <= right){
        tmp[k++] = nums[j++];
    }

    for (i = 0; i < k; i++){
        nums[left + i] = tmp[i];
    }
}

void mergeSort(vector<int> &nums, int i, int j){
    if (i >= j){
        return ;
    }

    int mid = (i + j) / 2;
    mergeSort(nums, i, mid);     // 对左边进行排序
    mergeSort(nums, mid + 1, j); // 对右边进行排序

    merge(nums, i, mid, j);     // 现在[i, mid]和[mid+1, j]都是有序的,将这两部分合并
}

int main(){
    // 测试用例
    vector<vector<int>> test_case = {
        {2, 2},
        {1, 2},
        {2, 1},
        {2, 2, 2, 2, 2},
        {1, 2, 3, 4, 5},
        {5, 4, 3, 2, 1},
    };

    // 加一些随机数组
    srand(time(0));
    for (int i = 0; i < 1000; i++){
        int len = rand() % 100 + 1;
        vector<int> nums(len);
        for (int j = 0; j < len; j++){
            nums.push_back(rand() % 100);
        }
        test_case.push_back(nums);
    }

    for (auto nums : test_case){
        mergeSort(nums, 0, nums.size() - 1);

        for (int i = 1; i < nums.size(); i++){
            if (nums[i - 1] > nums[i]){
                cout << "error" << endl;
                return 0;
            }
        }
    }

    cout << "all is ok" << endl;
    return 0;
}
  • 非递归版本
#include<vector>
#include<iostream>
using namespace std;

void mergeSort(vector<int> &nums){
    int len = nums.size();
    vector<int> tmp(len);

    for(int width = 1; width < len; width *= 2){                         // witdh是每个小数组的大小,1,2,4,8, ...
        for (int left = 0; left < len - width; left += 2 * width){       // left是每一对子数组的起点
            // 处理每一对子数组,此时每个子数组的左右两边都是有序的
            int mid = left + width - 1;          //左数组的边界(因为left < len - width,所以mid不会大于len - 1)
            int right = left + width * 2 - 1;   //右数组的边界(不能超过len-1)
            if (right > len - 1){
                right = len - 1;
            }
            // 开始合并过程,此时[left, mid],(mid,right]都是有序的
            int k = left;
            int i = left;
            int j = mid + 1;
            while(i <= mid && j <= right){
                if (nums[i] > nums[j]){
                    tmp[k++] = nums[j++];
                }else{
                    tmp[k++] = nums[i++];
                }
            }

            while(i <= mid){
                tmp[k++] = nums[i++];
            }

            while(j <= right){
                tmp[k++] = nums[j++];
            }
        }

        //
        for (int i = 0; i < len; i++){
            nums[i] = tmp[i];
        }
    }
}


int main(){
        // 测试用例
        vector<vector<int>> test_case = {
        {2, 2},
        {1, 2},
        {2, 1},
        {2, 2, 2, 2, 2},
        {1, 2, 3, 4, 5},
        {5, 4, 3, 2, 1},
    };

        // 加一些随机数组
        srand(time(0));
        for (int i = 0; i < 1000; i++){
                int len = rand() % 100 + 1;
                vector<int> nums(len);
                for (int j = 0; j < len; j++){
                        nums.push_back(rand() % 100);
                }
                test_case.push_back(nums);
        }

        for (auto nums : test_case){
                mergeSort(nums);

                for (int i = 1; i < nums.size(); i++){
                        if (nums[i - 1] > nums[i]){
                                cout << "error" << endl;
                                return 0;
                        }
                }
        }

        cout << "all is ok" << endl;
        return 0;
}

4.复杂度分析

归并排序其实可以看作是二叉树的后序遍历,树的深度是log2n,每一层的merge,其比较的总次数都是数组长度n,因此总体的时间复杂度是O(nlogn)。

5.对比

归并排序和快速排序都是常见的基于比较的排序算法,它们的时间复杂度都为 O(nlogn) 。
归并排序的最好、最坏和平均时间复杂度都为O(nlogn),并且它具有稳定性。
归并排序的主要缺点是需要开辟额外的空间,这可能在处理大规模数据时限制了其使用。

快速排序的优势在于它是一种原址排序算法,不需要额外的空间开销,它的平均速度比归并排序更快,特别是对于大规模数据集。但是,快速排序在最坏情况下的运行时间仍然不理想,而且它不具有稳定性,可能会改变相同元素的原始相对位置。
因此,选择归并排序或快速排序取决于具体问题和数据特征。在排序需要保持稳定性的问题上,归并排序是最好的选择;在其他问题上,特别是处理大规模数据时,快速排序通常更为高效。

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++中的归并排序是一种经典的排序算法,它采用分治的思想,将待排序的数组不断地分割成更小的子数组,然后再将这些子数组合并成有序的数组。下面是C++中归并排序实现: ```cpp #include <iostream> using namespace std; // 合并两个有序数组 void merge(int arr[], int left, int mid, int right) { int n1 = mid - left + 1; int n2 = right - mid; // 创建临时数组 int L[n1], R[n2]; // 将数据复制到临时数组 for (int i = 0; i < n1; i++) L[i] = arr[left + i]; for (int j = 0; j < n2; j++) R[j] = arr[mid + 1 + j]; // 合并临时数组 int i = 0, j = 0, k = left; while (i < n1 && j < n2) { if (L[i] <= R[j]) { arr[k] = L[i]; i++; } else { arr[k] = R[j]; j++; } k++; } // 复制剩余元素 while (i < n1) { arr[k] = L[i]; i++; k++; } while (j < n2) { arr[k] = R[j]; j++; k++; } } // 归并排序 void mergeSort(int arr[], int left, int right) { if (left < right) { int mid = left + (right - left) / 2; // 分割数组 mergeSort(arr, left, mid); mergeSort(arr, mid + 1, right); // 合并数组 merge(arr, left, mid, right); } } int main() { int arr[] = {12, 11, 13, 5, 6, 7}; int n = sizeof(arr) / sizeof(arr[0]); cout << "原始数组:"; for (int i = 0; i < n; i++) cout << arr[i] << " "; cout << endl; mergeSort(arr, 0, n - 1); cout << "排序后的数组:"; for (int i = 0; i < n; i++) cout << arr[i] << " "; cout << endl; return 0; } ``` 这段代码实现归并排序算法。首先,我们定义了一个`merge`函数,用于合并两个有序数组。然后,我们定义了`mergeSort`函数,用于递归地分割和合并数组。最后,在`main`函数中,我们创建一个待排序的数组,并调用`mergeSort`函数进行排序
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值