图解算法系列之归并排序

(1)算法描述

对于给定的线性序列,将当前序列不断的进行分组,当每个分组的数据只有一个元素时,代表这个分组是有序的,那么向上合并。每一层两两合并,合并的过程是,开辟一个新空间,使用两个指针同时扫描两个有序的分组,使得较小的元素或者较大的元素先进入新空间。在不断的比较之后,如果一个分组的元素为空,直接拷贝另一个没有被用完的元素到新空间。

(2) 图解算法

归并排序的过程

《图解算法系列之归并排序》

合并函数的合并过程

《图解算法系列之归并排序》

(3) C/C++代码实现

CustomSort.h

// 归并排序
void MergeSort(int arr[], int number);
// 内部的归并排序
void __MergeSort(int arr[], int left, int right);
// 内部合并
void __Merge(int arr[], int left, int mid, int right);

CustomSort.cpp

/************************************************************
- 功能描述:实现归并排序
- int arr[]: 待排序的数组
- int number: 待排序数组中的元素
- 返回值:void
************************************************************/
void MergeSort(int arr[], int number) {
    // 数组为空或者有一个一下的元素直接返回
    if(arr == NULL || number <= 1) {
        return;
    }

    // 调用内部排序算法
    __MergeSort(arr, 0, number-1);
}

/************************************************************
- 功能描述:实现归并排序的内部排序
- int arr[]: 待排序的数组
- int left: 待排序数组的左边界
- int right: 待排序数组的右边界
- 返回值:void
************************************************************/
void __MergeSort(int arr[], int left, int right) {
    // 如果 right <= left 没有排序的必要性
    if(right <= left) {
        return;
    }

    // 计算中间值
    // 没有使用 (right + left) / 2 是为了避免数据太大导致内存溢出
    int mid = (right - left) / 2 + left;

    // 分别排序两个分组
    __MergeSort(arr, left, mid);
    __MergeSort(arr, mid+1, right);

    // 排序完成后就合并两个分组
    __Merge(arr, left, mid, right);
}

/************************************************************
- 功能描述:实现归并排序的分组合并
- int arr[]: 待排序的数组
- int left: 待合并数组的左边界
- int right: 待合并数组的右边界
- int mid: 待合并数组的中间值
- 返回值:void
************************************************************/
void __Merge(int arr[], int left, int mid, int right) {
    int len = right - left + 1;

    // 动态创建数组,因为每个分组的大小都不一样,使用完需要delete[]空间
    int *temp = new int[len];

    // 用于第一个分组的指针one
    // 用于第二个分组的指针two
    // 用于辅助数组的指针i
    int one = left;
    int two = mid + 1;
    int i = 0;

    // 判断只要one和two都没有越界就不断的进行比较
    while(one <= mid && two <= right) {
        temp[i++] = arr[one] > arr[two] ? arr[one++] : arr[two++];
    }

    // 判断哪个数组的指针还没到头就直接全都拷贝到temp数组
    while(one <= mid) {
        temp[i++] = arr[one++];
    }
    while(two <= right) {
        temp[i++] = arr[two++];
    }

    // 往回拷贝数组,注意数组的位置
    for(int j = 0; j < len; j++) {
        arr[left + j] = temp[j];
    }

    delete[] temp;
}

(4) Java代码实现

public class MergeSort {
    // 归并排序函数
    public static void sort(int[] arr) {
         // 如果数组为null或者是数组中的元素小于2, 没有排序的意义
         if (arr == null || arr.length < 2) {
               // 直接返回
               return;
         }
         // 调用排序函数
         mergeSort(arr, 0, arr.length - 1);
    }
    // 主要逻辑
    public static void mergeSort(int[] arr, int l, int r) {
         // 如果左边的指针等于右边指针, 也就是数组不能再分割
         if (l == r) {
               // 就要直接返回
               return;
         }
         // 数组中间数值的指针位置
         int mid = l + ((r - l) >> 1);
         // 排序中间位置左边的数组
         mergeSort(arr, l, mid);
         // 排序中间位置右边的数组
         mergeSort(arr, mid + 1, r);
         // 合并函数
         merge(arr, l, mid, r);
    }
     // 合并函数
    public static void merge(int[] arr, int l, int m, int r) {
         // 创建辅助数组
         int[] help = new int[r - l + 1];
         // 辅助数组的指针位置
         int i = 0;      
         // 数组1的指针位置
         int p1 = l;
         // 数组2的指针位置
         int p2 = m + 1;
         //  判断左边和右边的数组是否越界
         while (p1 <= m && p2 <= r) {
               // 如果都没有越界, 向辅助数组添加数据,类似于外排的方式
               help[i++] = arr[p1] < arr[p2] ? arr[p1++] :  arr[p2++];
         }
         // 如果第一个数组的指针还没到头, 就要拷贝数组
         while (p1 <= m) {
               help[i++] = arr[p1++];
         }
         // 如果第二个数组的指针还没到头, 就要拷贝数组
         while (p2 <= r) {
               help[i++] = arr[p2++];
         }
         // 拷贝排序后数组
         for (i = 0; i < help.length; i++) {
               arr[l + i] = help[i];
         }
    }
}

(5) 时间复杂度分析

当函数出现递归调用的时候,一个函数A调用了本身,假定是subA,那么当前的函数A将会压入系统的栈内,系统将会保存现场(包括函数执行到哪一行代码,函数当前的调用状态以及函数中变量的值),进行下一个函数的执行,经过一层层函数的调用,遇到一个返回条件,系统中的栈中保存的状态将会一个一个的弹出,也就是函数的恢复现场,最后函数调用结束。
只要符合master公式的都可以使用一下方法计算时间复杂度:
master公式:T(N)=a*T(N/b)+O(Nd)

1) log(b, a) > d 复杂度是O(N log(b,a))
2) log(b, a) = d 复杂度是O(Nd * log(2, N))
3) log(b, a) < d 复杂度是O(Nd)

估计时间复杂度:左侧部分的规模和右侧部分的规模都是N/2,在整体外排的过程中总共划过N个数,算式为:T(N)=2T(N/2)+O(N),代入master公式,复杂度就是O(N*log2N),空间复杂度是O(N)

点赞