图解归并排序

前言

此次咱们介绍另外一种时间复杂度为O(nlogn)的排序算法叫作归并排序。归并排序在数据量大且数据递增或递减连续性好的状况下,效率比较高,且是O(nlogn)复杂度下惟一一个稳定的排序。java

自顶向下的归并排序

归并排序是创建在归并操做上的一种有效的排序算法,该算法是采用分治法的一个很是典型的应用。将已有序的子序列合并,获得彻底有序的序列;即先使每一个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序是一种稳定的排序方法。

实现归并的一种直截了当的办法是将两个不一样的有序数组归并到第三个数组中。实现的方法很简单,建立一个适当大小的数组而后将两个输入数组中的元素一个个从小到大放入这个数组中。可是,当用归并将一个大数组排序时,咱们须要进行不少次归并,这样每次归并时都建立一个新数组来存储排序结果就会浪费空间,所以咱们可使用原地归并。算法

原地归并的思路是:一样须要建立一个新数组做为辅助空间,可是这个数组不是用于存放归并后的结果,而是存放归并前的结果,而后将归并后的结果一个个从小到大放入原来的数组中。数组

原地归并的步骤以下:函数

  1. 建立一个和须要归并的数组相同的新数组,让k指向原来数组的第一个位置,i指向新数组左半部分的第一个元素,j指向右半部分的一个元素。

  1. 若是i指向的元素ei小于j指向的元素ej,则将ei放入k指向的位置,而后i++指向下一个元素,k++指向下一个须要存放的位置。不然若是ei>ej,则将ej放入k指向的位置,而后j++指向下一个元素,k++指向下一个须要存放的位置。

  1. 若是左半部分i指向的位置已经超过中间位置,而此时右半部分j还未移动到末尾,那么将j指向位置后面的全部元素都移动到k指向位置的后面,反之相似。

下图展现了对数组[8, 7, 6, 5, 4, 3, 2, 1]进行从小到大归并排序的过程:性能

归并排序的代码:优化

public static void sort(Comparable[] arr) {
    
    int n = arr.length;
    sort(arr, 0, n - 1);
}

// 递归使用归并排序,对arr[l...r]的范围进行排序
private static void sort(Comparable[] arr, int l, int r) {
    
    if (l >= r) {
        return;
    }
    // 这种写法防止溢出
    int mid = l + (r - l) / 2;
    sort(arr, l, mid);
    sort(arr, mid + 1, r);
    merge(arr, l, mid, r);
}

// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
private static void merge(Comparable[] arr, int l, int mid, int r) {
    
    Comparable[] aux = Arrays.copyOfRange(arr, l, r + 1);
    // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
    int i = l, j = mid + 1;
    for (int k = l; k <= r; k++) {
        if (i > mid) {  // 若是左半部分元素已经所有处理完毕
            arr[k] = aux[j - l];
            j++;
        } else if (j > r) {   // 若是右半部分元素已经所有处理完毕
            arr[k] = aux[i - l];
            i++;
        } else if (aux[i - l].compareTo(aux[j - l]) < 0) {  // 左半部分所指元素 < 右半部分所指元素
            arr[k] = aux[i - l];
            i++;
        } else {  // 左半部分所指元素 >= 右半部分所指元素
            arr[k] = aux[j - l];
            j++;
        }
    }
}

优化1

和快速排序同样,对于小规模数组,咱们可使用直接插入排序。其次,对于近乎有序的数组,咱们能够减小归并的次数。spa

优化的归并排序代码:code

public static void sort(Comparable[] arr) {

    int n = arr.length;
    sort(arr, 0, n - 1);
}

private static void sort(Comparable[] arr, int l, int r) {

    // 对于小规模数组, 使用插入排序
    if (r - l <= 15) {
        InsertionSort.sort(arr, l, r);
        return;
    }
    int mid = (l + r) / 2;
    sort(arr, l, mid);
    sort(arr, mid + 1, r);
    // 对于arr[mid] <= arr[mid+1]的状况,不进行merge
    // 对于近乎有序的数组很是有效,可是对于通常状况,有必定的性能损失
    if (arr[mid].compareTo(arr[mid + 1]) > 0) {
        merge(arr, l, mid, r);
    }
}

private static void merge(Comparable[] arr, int l, int mid, int r) {

    Comparable[] aux = Arrays.copyOfRange(arr, l, r + 1);
    // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
    int i = l, j = mid + 1;
    for (int k = l; k <= r; k++) {
        if (i > mid) {  // 若是左半部分元素已经所有处理完毕
            arr[k] = aux[j - l];
            j++;
        } else if (j > r) {   // 若是右半部分元素已经所有处理完毕
            arr[k] = aux[i - l];
            i++;
        } else if (aux[i - l].compareTo(aux[j - l]) < 0) {  // 左半部分所指元素 < 右半部分所指元素
            arr[k] = aux[i - l];
            i++;
        } else {  // 左半部分所指元素 >= 右半部分所指元素
            arr[k] = aux[j - l];
            j++;
        }
    }
}

优化2

咱们对空间进行优化,上述归并排序因为每次调用merge方法都会申请新的辅助空间,递归深度过大,就会形成 OOM。blog

然而咱们能够经过参数的方式传递给子函数,这样只须要在开始的时候申请一次辅助空间。排序

优化代码:

public static void sort(Comparable[] arr) {

    int n = arr.length;
    Comparable[] aux = new Comparable[n];
    sort(arr, aux, 0, n - 1);
}

private static void sort(Comparable[] arr, Comparable[] aux, int l, int r) {

    // 对于小规模数组, 使用插入排序
    if (r - l <= 15) {
        InsertionSort.sort(arr, l, r);
        return;
    }
    int mid = (l + r) / 2;
    sort(arr, aux, l, mid);
    sort(arr, aux, mid + 1, r);
    // 对于arr[mid] <= arr[mid+1]的状况,不进行merge
    // 对于近乎有序的数组很是有效,可是对于通常状况,有必定的性能损失
    if (arr[mid].compareTo(arr[mid + 1]) > 0) {
        merge(arr, aux, l, mid, r);
    }
}

private static void merge(Comparable[] arr, Comparable[] aux, int l, int mid, int r) {

    System.arraycopy(arr, l, aux, l, r - l + 1);
    // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
    int i = l, j = mid + 1;
    for (int k = l; k <= r; k++) {
        if (i > mid) {  // 若是左半部分元素已经所有处理完毕
            arr[k] = aux[j];
            j++;
        } else if (j > r) {   // 若是右半部分元素已经所有处理完毕
            arr[k] = aux[i];
            i++;
        } else if (aux[i].compareTo(aux[j]) < 0) {  // 左半部分所指元素 < 右半部分所指元素
            arr[k] = aux[i];
            i++;
        } else {  // 左半部分所指元素 >= 右半部分所指元素
            arr[k] = aux[j];
            j++;
        }
    }
}

自底向上的归并排序

自底向上的归并排序是先归并小数组,而后成对归并获得的子数组,即先进行两两归并(把每一个元素想象成大小为 1 的数组),而后是四四归并(把两个大小为 2 的数组归并成一个有 4 个元素的数组),而后是八八归并,一直下去。在每一轮归并中,最后一次归并的第二个可能比第一个子数组要小,不然全部的归并中两个数组的大小都应该同样,而在下一轮中子数组的大小会翻倍。

过程以下图,利用迭代实现:

自底向上的归并排序代码:

public static void sort(Comparable[] arr) {

    int n = arr.length;
    // 外循环控制归并数组的大小
    for (int len = 1; len < n; len += len) {
        // 内循环根据外循环分配的大小进行两两归并
        for (int i = 0; i < n - len; i += len + len) {
            // 对 arr[i...i+len-1] 和 arr[i+len...i+2*len-1] 进行归并
            // 须要知足 i+len < n 且 i+2*len-1 < n
            merge(arr, i, i + len - 1, Math.min(i + len + len - 1, n - 1));
        }
    }
}

private static void merge(Comparable[] arr, int l, int mid, int r) {

    Comparable[] aux = Arrays.copyOfRange(arr, l, r + 1);
    // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
    int i = l, j = mid + 1;
    for (int k = l; k <= r; k++) {

        if (i > mid) {  // 若是左半部分元素已经所有处理完毕
            arr[k] = aux[j - l];
            j++;
        } else if (j > r) {   // 若是右半部分元素已经所有处理完毕
            arr[k] = aux[i - l];
            i++;
        } else if (aux[i - l].compareTo(aux[j - l]) < 0) {  // 左半部分所指元素 < 右半部分所指元素
            arr[k] = aux[i - l];
            i++;
        } else {  // 左半部分所指元素 >= 右半部分所指元素
            arr[k] = aux[j - l];
            j++;
        }
    }
}

优化

优化思路同上:

  • 对于小数组改用直接插入排序;
  • 对于有序的数组减小归并的次数;
  • 复用辅助数组空间。

优化的代码:

public static void sort(Comparable[] arr) {

    int n = arr.length;
    Comparable[] aux = new Comparable[n];
    // 对于小数组, 使用插入排序优化
    for (int i = 0; i < n; i += 16) {
        InsertionSort.sort(arr, i, Math.min(i + 15, n - 1));
    }
    for (int len = 16; len < n; len += len) {
        for (int i = 0; i < n - len; i += len + len) {
            // 对于arr[mid] <= arr[mid+1]的状况,不进行merge
            if (arr[i + len - 1].compareTo(arr[i + len]) > 0) {
                merge(arr, aux, i, i + len - 1, Math.min(i + len + len - 1, n - 1));
            }
        }
    }
}

private static void merge(Comparable[] arr, Comparable[] aux, int l, int mid, int r) {

    System.arraycopy(arr, l, aux, l, r - l + 1);
    // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
    int i = l, j = mid + 1;
    for (int k = l; k <= r; k++) {
        if (i > mid) {  // 若是左半部分元素已经所有处理完毕
            arr[k] = aux[j - l];
            j++;
        } else if (j > r) {   // 若是右半部分元素已经所有处理完毕
            arr[k] = aux[i - l];
            i++;
        } else if (aux[i - l].compareTo(aux[j - l]) < 0) {  // 左半部分所指元素 < 右半部分所指元素
            arr[k] = aux[i - l];
            i++;
        } else {  // 左半部分所指元素 >= 右半部分所指元素
            arr[k] = aux[j - l];
            j++;
        }
    }
}

总结

对比归并排序与快速排序:

  • 归并排序是先切分、后排序,快速排序是切分、排序交替进行。
  • 归并排序的递归发生在处理整个数组(先递归切分再对数组排序)以前,快速排序的递归发生在处理整个数组以后(先对数组排序再递归到子数组)。
  • 归并排序是稳定的排序,而快速排序是不稳定的排序。
  • 归并排序在最坏和最好状况下的时间复杂度均为O(nlogn),而快速排序最坏O(n^2),最好O(n)

相关文章
相关标签/搜索