一、 总体是递归的,左边排好序右边排好序,最后merge让总体有序,merge过程须要申请和被排序数组等长度的辅助空间面试
二、 让其总体有序的过程里用了排外序的方法算法
三、 利用master公式来求解归并的时间复杂度数组
四、 归并排序可改成非递归实现less
主函数但愿一个数组的0~3位置排序f(arr, 0, 3)dom
第一层递归但愿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)分别有序。ui
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)总体有序指针
对于一个给定长度为n的数组arr,咱们但愿arr有序code
初始分组为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!"); } }
递归复杂度计算,用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, 3, 4, 2, 5]
1左边比1小的数:没有 3左边比3小的数:1 4左边比4小的数:一、3 2左边比2小的数为:1 5左边比5小的数为:一、三、四、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)。也能够借助归并排序来解决。实质就是要求一个数右边有多少个数比自身小
什么样的题目之后能够借助归并排序:纠结每一个数右边(左边)有多少个数比自身大,比自身小等。求这种数的数量等等
给定一个数组arr,和一个整数num。请把小于等于num的数放在数组的左边,大于num的数放在数组的右边(不要求有序)。要求额外空间复杂度为O(1),时间复杂度为O(N)。例如[5,3,7,2,3,4,1],num=3,把小于等于3的放在左边,大于3的放在右边
思路:设计一个小于等于区域,下标为-1。
一、 开始遍历该数组,若是arr[i]<=num,当前数和区域下一个数交换,区域向右扩1,i++
二、 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,越界位置。
一、 若是arr[i]当前位置的数==num, i++直接跳下一个
二、 若是arr[i]当前位置的数< num,当前位置的数arr[i]和小于区域的右一个交换,小于区域右扩一个位置,当前位置i++
三、 若是arr[i]当前位置的数> num,当前位置的数arr[i]与大于区域的左边一个交换,大于区域左移一个位置,i停在原地不作处理,这里不作处理是由于当前位置的数是刚从大于区域交换过来的数,还没作比较
四、i和大于区域的边界相遇,中止操做
思路:在给定数组上作partion,选定数组最右侧的位置上的数做为num,小于num的放在该数组的左边,大于num的放在该数组的右边。完成以后,把该数组最右侧的数组num,交换到大于num区域的第一个位置,确保了交换后的num是小于等于区域的最后一个数(该数直至最后能够保持当前位置不变,属于已经排好序的数),把该num左侧和右侧的数分别进行一样的partion操做(递归)。至关于每次partion搞定一个数的位置,代码实现quickSort1
思路:借助荷兰国旗问题的思路,把arr进行partion,把小于num的数放左边,等于放中间,大于放右边。递归时把小于num的区域和大于num的区域作递归,等于num的区域不作处理。至关于每次partion搞定一批数,与标记为相等的数。代码实现quickSort2
初版和第二版的快排时间复杂度相同O(N^2):用最差状况来评估,自己有序,每次partion只搞定了一个数是自身,进行了N次partion
随机选一个位置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公式只能解决子问题规模同样的递归。对于这个递归,算法导论上给出了计算方法,大体思路为假设一个复杂度,看这个公式是否收敛于这个复杂度的方式,比较麻烦
时间复杂度参考上文每种的复杂度
空间复杂度: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!"); } }