p3 O(NlogN)的排序
master公式计算递归方法的时间复杂度
如果递归行为满足,母递归行为每次均拆分为N/b规模的子递归行为,而且总额外时间复杂度(除去调用子问题)为O(N^d),那么总问题的时间复杂度即可以计算。
公式如下:
T(N) = a*T(N/b) + O(N^d)
log(b,a) > d -> O(N ^ log(b,a))
log(b,a) = d -> O(N ^ d * log(N))
log(b,a) < d -> O(N ^ d)
注:log(b,a) 即以 b 为底,a 为参数的对数
举例说明:
- 如果我用递归方法寻找数组最大值,每次递归寻找从中间位置划分的左右两个数组,那么这个问题的a就是2,b就是2,额外复杂度为O(1)。
- 假设我们在1的基础上,不二分查找,而是左侧算2/3,右侧2/3,依然满足 master 公式
- 如果我们选择左1/3,右2/3,那么则不满足该公式
归并排序
对于每次排序而言,先二分递归左部分与右部分,此时认为左部分有序,右部分也有序,那么对这两部分进行 merge。代码如下
class MergeSort {
public static void mergeSort(int[] nums, int left, int right){
if(left==right){// 此时已经有序
return;
}
int mid = left + ((right-left)>>1);
mergeSort(nums, left, mid);
mergeSort(nums, mid+1, right);
merge(nums, left, mid, right);
}
public static void merge(int[] nums, int left, int mid, int right) {
// 对于merge而言,相当于使用双指针,从left与mid开始,依次放入一个小的数,最终达到有序
int[] help = new int[right - left +1];
int index = 0;
int p1 = left;
int p2 = mid +1;
while(p1<=mid&&p2<=right){
help[index++] = nums[p1]<=nums[p2]?nums[p1++]:nums[p2++];
}
while(p1<=mid){
help[index++] = nums[p1++];
}
while(p2<=right){
help[index++] = nums[p2++];
}
for(int i=0;i<help.length;i++){
nums[left + i] = help[i];
}
}
public static void main(String[] args) throws Exception {
int[] nums = new int[]{9,5,7,3,1,6,8,4,2};
mergeSort(nums, 0, nums.length-1);
for(int num:nums){
System.out.println(num);
}
}
}
小和问题(逆序对问题)
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和,求小和。
例如:[1,3,4,2,5], 1 左侧没有比 1 小的数;3左侧有一个小于 3 的数,即1;4 左侧有两个比4小的数,即1,3;。。。最终小和为 1+1+3+1+1+3+4+2 = 16
解:对于此问题可以转化为,对于每一个数,去寻找右侧有几个比自己大的数,如对于 1 而言,右侧有 4 个,那么1*4;对于3,右侧有2个,那么3*2;。。。最终:1*4+3*2+4+2 = 16
具体的做法即在归并排序的基础上,在merge的时刻,去双指针比较两个数,获得左侧的小数对应在右侧有几个大数。
public static int littleSum(int[] nums, int left, int right){
if(left==right){// 此时已经有序
return 0;
}
int mid = left + ((right-left)>>1);
return littleSum(nums, left, mid) + littleSum(nums, mid+1, right) + merge(nums, left, mid, right);
}
public static int merge(int[] nums, int left, int mid, int right){
int[] help = new int[right - left +1];
int index = 0;
int p1 = left;
int p2 = mid+1;
int ret = 0;
while(p1<=mid&&p2<=right){
// 此处是相比归并排序的不同之处,注意细节,是严格小于号
ret += nums[p1]<nums[p2]?(right-p2+1)*nums[p1]:0;
help[index++] = nums[p1]<nums[p2]?nums[p1++]:nums[p2++];
}
while(p1<=mid){
help[index++] = nums[p1++];
}
while(p2<=right){
help[index++] = nums[p2++];
}
for(int i=0;i<help.length;i++){
nums[left+i] = help[i];
}
return ret;
}
public static void main(String[] args) throws Exception {
int[] nums = new int[]{1,3,4,2,5};
System.out.println(littleSum(nums, 0, nums.length-1));
for(int num:nums){
System.out.println(num);
}
}
为什么是严格小于号
由于我们针对于左区间节点,得到其右区间节点中所有大于它的节点的个数,那么值必须是严格大于的,这样才能保证个数正确
如何保证不漏数重数
这个就是因为排序的结果。针对于任何一个数,它的心路历程。当它和别的数merge的时候,一定会判断其右边有谁比它大。然后构成有序部分。此后再与更大的部分merge,此时判断更大部分的有谁比它大。最终会将其右边所有的数都算进去,而已经有序的部分也不会再次计算,所以不会漏算也不会重算。
见p3的1:37:00开始。
荷兰旗问题
给定数组nums,数num,将小于num的放在数组左边,等于num放在中间,大于num放在右边,要求空间复杂度O(1),时间 O(N)。
解:双指针问题,我们划分数组为三个部分,左部分:严格小于num的数,右部分:严格大于num的数;中间部分,等于num的数及剩余没有处理的数。
我们用下标从左往右访问,遇到小的与左边界下一个数交换,左边界外扩;遇到大的和有边界前一个数交换,右边界内缩;遇到等于的,下标增加即可。
代码如下:
class NetherlandFlag {
public static void netherlandFlag(int[] nums, int num){
int left = 0, right = nums.length-1;
int index = 0;
while(index<right){
if(nums[index]<num){
swap(nums, index, left);
// 左边界的下一个,要么是index自己,要么是等于num的数,相当于已知其和num的关系了
left++;
index++;
}else if(nums[index]>num){
swap(nums, index, right);
// 注意这里,下标不用增加了,因为交换过来的那个数,我们还没判断它和num的关系
right--;
}else{
index++;
}
}
}
public static void swap(int[] nums, int index1, int index2){
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
public static void main(String[] args) throws Exception {
int[] nums = new int[]{9,5,7,3,1,6,8,4,2,5,5,5,8};
netherlandFlag(nums, 5);
for(int num:nums){
System.out.println(num);
}
}
}
快排
快排即基于荷兰旗问题,对数组进行荷兰旗,然后递归对其左右区间进行快排。
但是如果我人为构建例子,是可以让快排退化到O(N^2)的。所以利用随机选择地标,可以将其降至平均O(NlogN)。快排的空间复杂度是O(logN)
class QuickSort {
public static void quickSort(int[] nums){
if(nums==null||nums.length<2){// 此时已经有序
return;
}
quickSort(nums, 0, nums.length-1);
}
public static void quickSort(int[] nums, int left, int right){
if(left<right){
swap(nums, left + (int) (Math.random() * (right - left + 1)), right);
int[] ret = partition(nums, left, right);
quickSort(nums, left, ret[0]-1);
quickSort(nums, ret[1]+1, right);
}
}
public static int[] partition(int[] nums, int left, int right) {
int less = left -1;// 左区间的边界,闭区间
int more = right;// 右区间的边界,闭区间
while(left<more){
if(nums[left]<nums[right]){
swap(nums, ++less, left++);
}else if(nums[left]>nums[right]){
swap(nums, --more, left);
}else{
left++;
}
}
swap(nums, more, right);
return new int[]{less+1, more};
}
public static void swap(int[] nums, int index1, int index2){
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
public static void main(String[] args) throws Exception {
int[] nums = new int[]{9,5,7,3,1,6,8,4,2};
quickSort(nums);
for(int num:nums){
System.out.println(num);
}
}
}