p3-归并+快排

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 为参数的对数

举例说明:

  1. 如果我用递归方法寻找数组最大值,每次递归寻找从中间位置划分的左右两个数组,那么这个问题的a就是2,b就是2,额外复杂度为O(1)。
  2. 假设我们在1的基础上,不二分查找,而是左侧算2/3,右侧2/3,依然满足 master 公式
  3. 如果我们选择左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);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值