一. 归并排序
归并排序是一种稳定的排序,利用了递归思想,概括来说主要思路是:保证左半边有序;保证右半边有序;让整体有序。具体步骤为:
- 将整个序列分为左右两个长度相同的子序列;
- 两个子序列分别排序;
- 合并两个子序列:创建一个与原序列等长的辅助数组;设置两个位置标记,分别指向两个子序列的起始位置;比较两个位置的元素,将较小值拷贝至辅助数组,并将标记移到下一个位置;重复上一步,直到一个序列中的位置标记即将越界,就将另一个序列中的剩余元素直接拷贝到辅助数组;将辅助数组的值拷回原数组。这个合并的过程可以看作小规模的外排序。
- 递归执行上述步骤
此过程中T(N) = 2T(N/2) + O(N),符合master公式的使用条件:T(N) = aT(N/b) + O(N^d)。此时logba = log22 = 1= d,因此时间复杂度为O(N*logN)。额外空间复杂度为O(N)。
Java
1 | public static void mergeSort(int[] arr) { |
JavaScript
1 | var a =[1,11,3,5,7,2,6,9,9]; |
二. 归并排序的扩展
先补充一些复杂度的优劣排序,常见的复杂度按照从优到劣的顺序排列,依次为:
最优 ——>
O(1) | O(logN) | O(logN^2) | O(N) | O(N*logN) | O(N^2) |
---|---|---|---|---|---|
O(N^3) | O(N^k) (k为常数) | O(2^n) (n步决定,每步都有2个决策) | O(3^n) (三叉树) | O(k^n) (k为常数 k叉树) | O(n!) (排列组合) |
——> 最劣
(1)小和问题
在一个数组中,每个数左边比它小的数相加,最后的结果称作这个数组的小和。在处理这个问题的过程中,可以先将原始的标准转化为更容易处理的等效标准。每个数被加了几次,就看它右边有多少个比它大的数。可以用归并排序的方法求小和,与归并排序的方式只有一点不同,那就是遇到相等值的情况,归并问题遇相等拷贝左部分或右部分的值都可以,而小和问题遇到相等时只能拷贝右侧的。因为要保证左侧的每个值被拷贝进来时都已经找到了比它大的。
假设数组 {1,3,4,2,5}
1×4+3×2+4×1+2×1 = 16,这个数组的小和为16,和原始标准计算出的小和结果相同。
此问题也可以直接用遍历方式求解,但是时间复杂度为O(N^2)。如果采用上述方式,时间复杂度为O(N×logN).
具体的实现思路为:
- 将数组划分为左右两部分;
- 求左半部分的小和;
- 求右半部分的小和;
- 合并左右两部分,求左部分在右部分中的小和;
- 将上三步的和相加,得到整个数组的小和;
- 递归地执行上述步骤
每个数都不会错过右侧的所有范围,不会找漏;求小和只存在于跨组行为的时候,所以也不会找重。
Java
1 | public static int smallSum(int[] arr) { |
JavaScript
1 | var a =[1,3,4,2,5]; |
(2)逆序对问题
在数组中,如果左边的数比右边的数大,那么这两个数就构成一个逆序对。比如数组{3,1,2,4,2,1},其中存在的逆序对有:2,1; 3,2; 3,1; 2,4等等。要求打印出所有逆序对。此题目也可以直接用遍历的方法解出,但是时间复杂度为O(N^2)。同样可以利用归并排序的思路求解,与小和问题类似,都是merge的活用。将标准转化为右侧有多少个数比它小。
Java
1 | // 用来记录逆序对的个数 |
JavaScript
1 | var a =[3,1,4,5,2]; |