归并排序、随机快排
1 归并排序、随机快排
转载注明出处,源码地址: https://github.com/Dairongpeng/algorithm-note ,欢迎star
1.1 归并排序
1、 整体是递归的,左边排好序右边排好序,最后merge让整体有序,merge过程需要申请和被排序数组等长度的辅助空间
2、 让其整体有序的过程里用了排外序的方法
3、 利用master公式来求解归并的时间复杂度
4、 归并排序可改为非递归实现
1.1.1 递归思路:
主函数希望一个数组的0~3位置排序f(arr, 0, 3)
第一层递归希望f(arr, 0, 1)和f(arr, 2, 3)分别有序。
第二层递归:f(arr, 0, 1)希望f(arr, 0, 0)和f(arr, 1, 1)有序, f(arr, 2, 3)希望f(arr, 2, 2)和f(arr, 3, 3)分别有序。
f(arr, 0, 0)和f(arr, 1, 1)已经有序,回到第一层递归f(arr, 0, 1)中去merge0位置的数和1位置的数后刷回元素组的0到1位置,0到1位置变为有序; f(arr, 2, 2)和f(arr, 3, 3)已经有序,回到f(arr, 2, 3)中去merge2位置的数和3位置的数后刷回原数组的2到3位置,2到3位置变为有序。
f(arr, 0, 3)需要merge f(arr, 0, 1)和f(arr, 2, 3)此时f(arr, 0, 1)和f(arr, 2, 3)已经有序merge后copy到原数组的0到3位置。于是f(arr, 0, 3)整体有序
1.1.2 非递归思路
对于一个给定长度为n的数组arr,我们希望arr有序
初始分组为a=2,我们让每两个有序,不够一组的当成一组
分组变为a=2*2=4,由于上一步已经保证了两两有序,那么我们可以当前分组的四个数的前两个和后两个数merge使得每四个数有序
分组变为a=2*4=8,...直至a>=n,整体有序
package class03;
public class Code01_MergeSort {
// 递归方法实现
public static void mergeSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 传入被排序数组,以及左右边界
process(arr, 0, arr.length - 1);
}
// arr[L...R]范围上,变成有序的
// L...R N T(N) = 2*T(N/2) + O(N) ->
public static void process(int[] arr, int L, int R) {
if (L == R) { // base case
return;
}
// >> 有符号右移1位,相当于除以2
int mid = L + ((R - L) >> 1);
process(arr, L, mid);
process(arr, mid + 1, R);
// 当前栈顶左右已经排好序,准备左右merge,注意这里的merge递归的每一层都会调用
merge(arr, L, mid, R);
}
public static void merge(int[] arr, int L, int M, int R) {
// merge过程申请辅助数组,准备copy
int[] help = new int[R - L + 1];
// 用来标识help的下标
int i = 0;
// 左边有序数组的指针
int p1 = L;
// 右边有序数组的指针
int p2 = M + 1;
// p1和p2都没越界的情况下,谁小copy谁
while (p1 <= M && p2 <= R) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
// 要么p1越界了,要么p2越界了,谁没越界把谁剩下的元素copy到help中
while (p1 <= M) {
help[i++] = arr[p1++];
}
while (p2 <= R) {
help[i++] = arr[p2++];
}
// 把辅助数组中整体merge后的有序数组,copy回原数组中去
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
// 非递归方法实现
public static void mergeSort2(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int N = arr.length;
// 当前有序的,左组长度,那么实质分组大小是从2开始的
int mergeSize = 1;
while (mergeSize < N) { // log N
// L表示当前分组的左组的位置,初始为第一个分组的左组位置为0
int L = 0;
// 0....
while (L < N) {
// L...M 当前左组(mergeSize)
int M = L + mergeSize - 1;
// 当前左组包含当前分组的所有元素,即没有右组了,无需merge已经有序
if (M >= N) {
break;
}
// L...M为左组 M+1...R(mergeSize)为右组。右组够mergeSize个的时候,右坐标为M + mergeSize,右组不够的情况下右组边界坐标为整个数组右边界N - 1
int R = Math.min(M + mergeSize, N - 1);
// 把当前组进行merge
merge(arr, L, M, R);
// 下一个分组的左组起始位置
L = R + 1;
}
// 如果mergeSize乘2必定大于N,直接break。防止mergeSize溢出,有可能N很大,下面乘2有可能范围溢出(整形数大于21亿)
if (mergeSize > N / 2) {
break;
}
// 无符号左移,相当于乘以2
mergeSize <<= 1;
}
}
// for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// for test
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// for test
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// for test
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// for test
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
mergeSort1(arr1);
mergeSort2(arr2);
if (!isEqual(arr1, arr2)) {
succeed = false;
printArray(arr1);
printArray(arr2);
break;
}
}
System.out.println(succeed ? "Nice!" : "Oops!");
}
}
1.1.3 归并排序时间复杂度
递归复杂度计算,用master公式带入,子问题规模N/2,调用2次,除了递归之外的时间复杂度为merge的时间复杂度,为O(N)。a=2,b=2,d=1满足master第一条logb^a == d规则
T(N) = 2T(N/2) + O(N) => O(N*logN)
非递归复杂度计算,mergeSize2等于分组从2->4->8->...,每个分组下执行merge操作O(N)。所以非递归和递归的时间复杂度相同,也为O(N)O(logN) = O(NlogN)
递归和非递归的归并排序时间复杂度都为O(NlogN)
Tips: 为什么选择,冒泡,插入排序的时间复杂度为O(N^2)而归并排序时间复杂度为O(NlogN),因为选择,冒泡,插入排序的每个元素浪费了大量的比较行为N次。而归并没有浪费比较行为,每次比较的结果有序后都会保存下来,最终merge
1.1.4 归并面试题
1、在一个数组中,一个数左边比它小的数的总和,叫做小和,所有数的小和累加起来,叫做数组的小和。求数组的小和。例如[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
暴力解法,每个数找之前比自己小的数,累加起来,时间复杂度为O(N^2),面试没分。但是暴力方法可以用来做对数器
归并排序解法思路:O(NlogN)。在递归merge的过程中,产生小和。规则是左组比右组数小的时候产生小和,除此之外不产生;当左组和右组数相等的时候,拷贝右组的数,不产生小和;当左组的数大于右组的时候,拷贝右组的数,不产生小和。实质是把找左边比本身小的数的问题,转化为找这个数右侧有多少个数比自己大,在每次merge的过程中,一个数如果处在左组中,那么只会去找右组中有多少个数比自己大
package class03;
public class Code02_SmallSum {
public static int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return process(arr, 0, arr.length - 1);
}
// arr[L..R]既要排好序,也要求小和返回
// 所有merge时,产生的小和,累加
// 左 排序 merge
// 右 排序 merge
// arr 整体 merge
public static int process(int[] arr, int l, int r) {
// 只有一个数,不存在右组,小和为0
if (l == r) {
return 0;
}
// l < r
int mid = l + ((r - l) >> 1);
// 左侧merge的小和+右侧merge的小和+整体左右两侧的小和
return process(arr, l, mid) + process(arr, mid + 1, r) + merge(arr, l, mid, r);
}
public static int merge(int[] arr, int L, int m, int r) {
// 在归并排序的基础上改进,增加小和res = 0
int[] help = new int[r - L + 1];
int i = 0;
int p1 = L;
int p2 = m + 1;
int res = 0;
while (p1 <= m && p2 <= r) {
// 当前的数是比右组小的,产生右组当前位置到右组右边界数量个小和,累加到res。否则res加0
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
// 只有左组当前数小于右组copy左边的,否则copy右边的
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];
}
return res;
}
// for test
public static int comparator(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
int res = 0;
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < i; j++) {
res += arr[j] < arr[i] ? arr[j] : 0;
}
}
return res;
}
// for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// for test
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// for test
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// for test
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// for test
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
if (smallSum(arr1) != comparator(arr2)) {
succeed = false;
printArray(arr1);
printArray(arr2);
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");
}
}
类似题目:求一个数组中的所有降序对,例如[3,1,7,0,2]降序对为:(3,1), (3,0), (3,2), (1,0), (70), (7,2)。也可以借助归并排序来解决。实质就是要求一个数右边有多少个数比自身小
什么样的题目以后可以借助归并排序:纠结每个数右边(左边)有多少个数比自身大,比自身小等。求这种数的数量等等
1.2 快排
1.2.1 Partion过程
给定一个数组arr,和一个整数num。请把小于等于num的数放在数组的左边,大于num的数放在数组的右边(不要求有序)。要求额外空间复杂度为O(1),时间复杂度为O(N)。例如[5,3,7,2,3,4,1],num=3,把小于等于3的放在左边,大于3的放在右边
思路:设计一个小于等于区域,下标为-1。
1、 开始遍历该数组,如果arr[i]<=num,当前数和区域下一个数交换,区域向右扩1,i++
2、 arr[i] > num, 不做操作,i++
给定一个数组,和一个整数num。请把小于num的数放在数组的左边,等于num的放中间,大于num的放右边。要求额外空间复杂度为O(1),时间复杂度为O(N)。[3,5,4,0,4,6,7,2],num=4。实质是经典荷兰国旗问题
思路:设计一个小于区域,下标为-1。设计一个大于区域,下表为arr.length,越界位置。
1、 如果arr[i]当前位置的数==num, i++直接跳下一个
2、 如果arr[i]当前位置的数< num,当前位置的数arr[i]和小于区域的右一个交换,小于区域右扩一个位置,当前位置i++
3、 如果arr[i]当前位置的数> num,当前位置的数arr[i]与大于区域的左边一个交换,大于区域左移一个位置,i停在原地不做处理,这里不做处理是因为当前位置的数是刚从大于区域交换过来的数,还没做比较
4、i和大于区域的边界相遇,停止操作
1.2.2 快排1.0:每次partion搞定一个位置
思路:在给定数组上做partion,选定数组最右侧的位置上的数作为num,小于num的放在该数组的左边,大于num的放在该数组的右边。完成之后,把该数组最右侧的数组num,交换到大于num区域的第一个位置,确保了交换后的num是小于等于区域的最后一个数(该数直至最后可以保持当前位置不变,属于已经排好序的数),把该num左侧和右侧的数分别进行同样的partion操作(递归)。相当于每次partion搞定一个数的位置,代码实现quickSort1
1.2.3 快排2.0:每次partion搞定一批位置
思路:借助荷兰国旗问题的思路,把arr进行partion,把小于num的数放左边,等于放中间,大于放右边。递归时把小于num的区域和大于num的区域做递归,等于num的区域不做处理。相当于每次partion搞定一批数,与标记为相等的数。代码实现quickSort2
第一版和第二版的快排时间复杂度相同O(N^2):用最差情况来评估,本身有序,每次partion只搞定了一个数是自身,进行了N次partion
1.2.4 快排3.0:随机位置作为num标记位
随机选一个位置i,让arr[i]和arr[R]交换,再用=arr[R]作为标记位。剩下的所有过程跟快排2.0一样。即为最经典的快排,时间复杂度为O(NlogN)
为什么随机选择标记为时间复杂度就由O(N^2)变为O(NlogN)?如果我们随机选择位置那么就趋向于标记位的左右两侧的递归规模趋向于N/2。那么根据master公式,可以计算出算法复杂度为O(NlogN)。实质上,在我们选择随机的num时,最差情况,最好情况,其他各种情况的出现概率为1/N。对于这N种情况,数学上算出的时间复杂度最终期望是O(NlogN),这个数学上可以进行证明,比较复杂
例如我们的num随机到数组左侧三分之一的位置,那么master公式为
T(N) = T((1/3)N) + T((2/3)N) + O(N)
对于这个递归表达式,master公式是解不了的,master公式只能解决子问题规模一样的递归。对于这个递归,算法导论上给出了计算方法,大致思路为假设一个复杂度,看这个公式是否收敛于这个复杂度的方式,比较麻烦
1.2.5 快排的时间复杂度与空间复杂度
时间复杂度参考上文每种的复杂度
空间复杂度:O(logN)。空间复杂度产生于每次递归partion之后,我们需要申请额外的空间变量保存相等区域的左右两侧的位置。那么每次partion需要申请两个变量,多少次partion?实质是该递归树被分了多少层,树的高度,有好有坏,最好logN,最差N。随机选择num之后,期望仍然是概率累加,收敛于O(logN)。
package class03;
public class Code03_PartitionAndQuickSort {
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// partion问题
public static int partition(int[] arr, int L, int R) {
if (L > R) {
return -1;
}
if (L == R) {
return L;
}
int lessEqual = L - 1;
int index = L;
while (index < R) {
if (arr[index] <= arr[R]) {
swap(arr, index, ++lessEqual);
}
index++;
}
swap(arr, ++lessEqual, R);
return lessEqual;
}
// arr[L...R] 玩荷兰国旗问题的划分,以arr[R]做划分值
// 小于arr[R]放左侧 等于arr[R]放中间 大于arr[R]放右边
// 返回中间区域的左右边界
public static int[] netherlandsFlag(int[] arr, int L, int R) {
// 不存在荷兰国旗问题
if (L > R) {
return new int[] { -1, -1 };
}
// 已经都是等于区域,由于用R做划分返回R位置
if (L == R) {
return new int[] { L, R };
}
int less = L - 1; // < 区 右边界
int more = R; // > 区 左边界
int index = L;
while (index < more) {
// 当前值等于右边界,不做处理,index++
if (arr[index] == arr[R]) {
index++;
// 小于交换当前值和左边界的值
} else if (arr[index] < arr[R]) {
swap(arr, index++, ++less);
// 大于右边界的值
} else {
swap(arr, index, --more);
}
}
// 比较完之后,把R位置的数,调整到等于区域的右边,至此大于区域才是真正意义上的大于区域
swap(arr, more, R);
return new int[] { less + 1, more };
}
public static void quickSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process1(arr, 0, arr.length - 1);
}
public static void process1(int[] arr, int L, int R) {
if (L >= R) {
return;
}
// L..R上partition 标记位为arr[R] 数组被分成 [ <=arr[R] arr[R] >arr[R] ],M为partion之后标记位处在的位置
int M = partition(arr, L, R);
process1(arr, L, M - 1);
process1(arr, M + 1, R);
}
public static void quickSort2(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process2(arr, 0, arr.length - 1);
}
public static void process2(int[] arr, int L, int R) {
if (L >= R) {
return;
}
// 每次partion返回等于区域的范围
int[] equalArea = netherlandsFlag(arr, L, R);
// 对等于区域左边的小于区域递归,partion
process2(arr, L, equalArea[0] - 1);
// 对等于区域右边的大于区域递归,partion
process2(arr, equalArea[1] + 1, R);
}
public static void quickSort3(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process3(arr, 0, arr.length - 1);
}
public static void process3(int[] arr, int L, int R) {
if (L >= R) {
return;
}
// 随机选择位置,与arr[R]上的数做交换
swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
int[] equalArea = netherlandsFlag(arr, L, R);
process3(arr, L, equalArea[0] - 1);
process3(arr, equalArea[1] + 1, R);
}
// for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// for test
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// for test
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// for test
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// for test
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
int[] arr3 = copyArray(arr1);
quickSort1(arr1);
quickSort2(arr2);
quickSort3(arr3);
if (!isEqual(arr1, arr2) || !isEqual(arr2, arr3)) {
succeed = false;
break;
}
}
System.out.println(succeed ? "Nice!" : "Oops!");
}
}