快速排序和归并排序

快速排序

快速排序的核心思想是分治,其算法大体可分为三个部分:

  • 确定一个分界点 pivot (这个分界点可以任取,一般来说取左端点中间端点);
  • 调整区间,将小于 pivot 的数和大于 pivot 的数分开
  • 递归处小于 pivot 和大于 pivot 的部分。

这里我们用到了双指针,即在待排序数组的两端定义两个指针 iji 不断向右移动直到 q[i] > pivotj 不断向左移动直到 q[j] < pivot,这时将 q[i]q[j] 交换,之后递归进行处理。

这样处理的好处就是 i 左边的数永远是小于 pivotj 右边的数永远是大于 pivot

下面给出快速排序的C++模板:

void quick_sort(int q[], int l, int r){
    if(l >= r)    return;
    int x = q[l + (r - l) / 2], i = l - 1, j = r + 1;
    while(i < j){
        do i++; while(q[i] < x);
        do j--; while(q[j] > x);
        if(i < j){
            swap(q[i], q[j]);
        }
    }
    quick_sort(q, l, j);
    quick_sort(q, j + 1, r);
}

(在使用此模板时轻易不要改动,否则容易出现边界问题。。。。)

归并排序

归并排序的核心思想也是分治,其算法大体也可以分为三个部分:

  • 确定分界点(这个分界点是数组的中点,不能改变);
  • 递归排序左半部分和右半部分;
  • 归并–将两部分数组合二为一

发现与快速排序不同的地方了吗?

快速排序先处理再递归不需要使用额外的空间,而归并排序先递归再处理,而且需要使用额外的空间

此算法的核心就是如何将两排序数组合二为一,这里我们依然采用双指针法。我们开辟一个额外的数组空间 tmp ,定义两个指针分别指向已经分割好的两数组的第一个元素,然后对指针所指向的元素的值进行比较,取较小者加入新数组 tmp ,当其中一个数组全部加入后,将另一个数组按顺序加入 tmp 即可。

下面给出归并排序的Java模板:

public static void mergeSort(int[] arr, int l, int r){
    if(l >= r){
        return;
    }
    int mid = l + (r - l) / 2;
    mergeSort(arr, l, mid);
    mergeSort(arr, mid + 1, r);
    merge(arr, l, mid, r);
}

public static void merge(int[] arr, int l, int mid, int r){
    int[] tmp = new int[r - l + 1];
    int i = l, j = mid + 1;
    int k = 0;
    while(i <= mid && j <= r){
        if(arr[i] <= arr[j]){
            tmp[k++] = arr[i++];
        }else{
            tmp[k++] = arr[j++];
        }
    }
    while(i <= mid){
        tmp[k++] = arr[i++];
    }
    while(j <= r){
        tmp[k++] = arr[j++];
    }
    for(int m = l, n = 0; m <= r; ++m, ++n){
        arr[m] = tmp[n];
    }
}

关于归并排序,有一个非常经典的小和问题:

在一个数组中, 每一个数左边比当前数小的数累加起来, 叫做这个数组的小和。 求一个数组
的小和。
例子:
[1,3,4,2,5]
1左边比1小的数, 没有;
3左边比3小的数, 1;
4左边比4小的数, 1、 3;
2左边比2小的数, 1;
5左边比5小的数, 1、 3、 4、 2;
所以小和为1+1+3+1+1+3+4+2=16  

思路:

通过归并排序进行求解,每次merge都能计算出一次小和,当要将两个排好序的两个子序列进行合并时,如果左边的当前值比右边的当前值要小,则说明这个值比右边的所有值都要小,所有就产生了arr[i] * (r - l + 1)个小和,通过这种进行计算,直到计算完成。

题解:

public static int merge(int[] arr, int l, int mid, int r){
    int[] tmp = new int[r - l + 1];
    int i = l, j = mid + 1;
    int res = 0, k = 0;
    while(i <= mid && j <= r){
        if(arr[i] < arr[j]){
            res += (r - j + 1) * arr[i];
            tmp[k++] = arr[i++];
        }else{
            tmp[k++] = arr[j++];
        }
    }
    while(i <= mid){
        tmp[k++] = arr[i++];
    }
    while(j <= r){
        tmp[k++] = arr[j++];
    }
    for(int m = l, n = 0; m <= r; ++m, ++n){
        arr[m] = tmp[n];
    }
    return res;
}

public static int mergeSort(int[] arr, int l, int r){
    if(l >= r){
        return 0;
    }
    int mid = l + (r - l) / 2;
    return mergeSort(arr, l, mid) + mergeSort(arr, mid + 1, r) + merge(arr, l, mid, r);
}


public static void main(String[] args) {
    int[] arr = {1, 3, 4, 2, 5};
    int res = mergeSort(arr, 0, arr.length - 1);
    System.out.println(res);
}

快速排序和归并排序的核心思想都是分治,从另一种层面上理解,快速排序实质上就是一个二叉树的前序遍历,读到这里相信很多小伙伴是理解不了的,我们可以这样理解:快排的步骤是先构造一个分界点,然后再递归的处理左右子数组,这不就是二叉树的前序遍历(根左右)吗?同理,我们可以得出归并排序实质上就是一个二叉树的后序遍历

这两个算法还是比较重要的,建议在理解的基础上背一手模板就很舒服。