【leetcode学习笔记】递归、归并排序、快速排序

一、递归

1.1 定义

递归是指将一个(总)问题拆分成更小规模的子问题,通过求解子问题,并将子问题的解合并就能得到(总)问题的解。

例:求解一个数组的最大值。

可以把数组分成两半,分别求解最大值和最小值

class Solution(){
    public int max(int[] nums){
        return maxRecur(nums, 0, nums.length - 1);
    }

    private int maxRecur(int[] nums, int i, int j){
        if(i == j){return nums[j];}
        int mid = (i+j)/2;
        int max1 = maxRecur(nums, i, mid);
        int max2 = maxRecur(nums, mid+1, j);
        return (max1 > max2) ? max1 : max2;
    }
}

1.2 递归函数的时间复杂度:master’s theorem

对于上述例子来说,如果规模为n的问题时间复杂度为T(n),那么T(n)将分为三部分:求解左侧最大值,时间复杂度为T(n/2);求解右侧最大值,时间复杂度为T(n/2);第三步为整合,时间复杂度为O(1),那么我们得到

T(n)=2T(n/2)+O(1)

这种形如T(n) = aT(n/b)+O(n^k)的形式,均可以用master公式求解复杂度。

master公式:(非原版,这里针对多数情况做了简化)

形如T(n) = aT(n/b)+O(n^k)的递推算法,求解时间复杂度时只需要比较log_b ak

  • 如果\log_ba>k,则T(n)=O(n^{\log_ba})
  • 如果\log_ba=k,则T(n)=O(n^{\log_ba}\log n)
  • 如果\log_ba<k,则T(n)=O(n^k)

上例中,a=2,b=2,k=0,满足第一种情况,得到T(n)=O(n)

二、归并排序

2.1 核心思想

将数组排序问题转化为递归问题,对于一个数组,分为三步:

第一步:使数组的左半部分有序

第二步:使数组的右半部分有序

第三步:将两个有序数组合并

我们看到,前两步都是递归的子问题,因此只需要考虑第三步。具体的方式为,使用两个指针,分别指向两个有序数组的第一个元素,哪个元素小,就将哪个元素移到一个新的数组里,并将对应指针右移一位,如果有一个指针已经越过边界,则默认选取另一个指针。直至两个指针都越过边界为止。

2.2 java实现

class Solution {
    private int[] temp;

    public void mergeSort(int[] nums, int left, int right){
        if(left >= right){return ;}
        int mid = (left + right) / 2;
        mergeSort(nums, left, mid);
        mergeSort(nums, mid+1, right);
        merge(nums, left, mid, right);
    }

    public void merge(int[] nums, int left, int mid, int right){
        int i = left;
        int j = mid+1;
        int p = left;
        while(i <= mid && j <= right){
            if(nums[i] <= nums[j]){
                temp[p] = nums[i];
                ++i;
            }
            else{
                temp[p] = nums[j];
                ++j;
            }
            ++p;
        }

        while (i <= mid)  {
            temp[p] = nums[i];
            ++i;
            ++p;
        }
        while (j <= right) {
            temp[p] = nums[j];
            ++j;
            ++p;
        }

        for(int k = left; k <= right; k++){
            nums[k] = temp[k];
        }
    }
}

2.3 归并排序的时间负责度

主要分析第三步,合并两个有序数组需要多少次常数操作?对于两个指针来说,无论什么顺序,他们都只会从左向右走,同时一次只走一个,因此所需的时间复杂度为O(n),于是我们得到T(n)=2T(n/2)+O(n),应用master公式情形2,我们得到T(n)=O(n\log n)

2.4 归并排序与选择排序

为什么归并排序比选择排序的时间复杂度低?我们回忆一下选择排序,第一步遍历n个数,确认第一个数;第二步遍历n-1个数,确认第二个数;这样下去,问题就在于每一步的比较行为是独立的,第一步比较的很多中间信息,在第二步被浪费了。我们以[2,4,3,1,5]为例,第一步先选出最小值,放到首位,我们得到[1,4,3,2,5]。第二步开始,我们从第二个数开始向后比较,比较4和3,但实际上这两个数在第一步已经比较过了,这就属于比较信息的浪费,也可以理解为重复/无效比较。

我们再看归并排序,以[1,4,3,2,5]为例。对于数字3来说,当第一步中,3和1、4完成了比较,经过第二步,数组变成[1,3,4]和[2,5],而在第三步合并时,3无需再和1、4做比较了,只需要和右边的数比较,这就大大减少了比较信息的浪费,前两步的比较信息在第三步被有效的利用了起来。因此归并排序的时间复杂度优于选择排序。

2.5 leetcode第315题

https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self/

给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是  nums[i] 右侧小于 nums[i] 的元素的数量。

示例 1:

输入:nums = [5,2,6,1]
输出:[2,1,1,0] 
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素

分析:本题考虑归并排序,考虑数组[5,2,6,1],,在归并排序的前两步,我们得到[2,5]和[1,6],那么在2的右侧,比它小的数,只可能有两种,一种是在[2,5]里面的数,另一种是在在[1,6]中找到最后一个比2小的数,它左边的数均满足要求。所以我们只需要在归并排序的第三步里顺便把对应的数更新。

class Solution {
    private int[] temp;
    private int[] tempIndex;
    private int[] index;
    private int[] res;

    public List<Integer> countSmaller(int[] nums) {
        this.temp = new int[nums.length];
        this.tempIndex = new int[nums.length];
        this.index = new int[nums.length];
        this.res = new int[nums.length];

        for (int i = 0; i < nums.length; ++i) {
            index[i] = i;
        }

        mergeSort(nums, 0, nums.length-1);

        List<Integer> ans = new ArrayList<>();
        for(int num: res){
            ans.add(num);
        }
        return ans;
    }

    public void mergeSort(int[] nums, int left, int right){
        if(left >= right){return ;}
        int mid = (left + right) / 2;
        mergeSort(nums, left, mid);
        mergeSort(nums, mid+1, right);
        merge(nums, left, mid, right);
    }

    public void merge(int[] nums, int left, int mid, int right){
        int i = left;
        int j = mid+1;
        int p = left;
        while(i <= mid && j <= right){
            if(nums[i] <= nums[j]){
                temp[p] = nums[i];
                tempIndex[p] = index[i];
                res[index[i]] += (j - mid - 1);
                ++i;
            }
            else{
                temp[p] = nums[j];
                tempIndex[p] = index[j];
                ++j;
            }
            ++p;
        }

        while (i <= mid)  {
            temp[p] = nums[i];
            tempIndex[p] = index[i];
            res[index[i]] += (j - mid - 1);
            ++i;
            ++p;
        }
        while (j <= right) {
            temp[p] = nums[j];
            tempIndex[p] = index[j];
            ++j;
            ++p;
        }

        for(int k = left; k <= right; k++){
            nums[k] = temp[k];
            index[k] = tempIndex[k];
        }
    }
}

2.6 leetcode剑指offer第51题

https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:

输入: [7,5,6,4]
输出: 5

分析:同样是在归并排序的第三步中,顺便更新一下逆序对的数量

class Solution {
    private int[] temp;
    private int res;

    public int reversePairs(int[] nums) {
        this.temp = new int[nums.length];
        this.res = 0;
        mergeSort(nums, 0, nums.length-1);
        return this.res;
    }

    public void mergeSort(int[] nums, int left, int right){
        if(left >= right){return ;}
        int mid = (left + right) / 2;
        mergeSort(nums, left, mid);
        mergeSort(nums, mid+1, right);
        merge(nums, left, mid, right);
    }

    public void merge(int[] nums, int left, int mid, int right){
        int i = left;
        int j = mid+1;
        int p = left;
        while(i <= mid && j <= right){
            if(nums[i] <= nums[j]){
                temp[p] = nums[i];
                res += (j - mid - 1);
                ++i;
            }
            else{
                temp[p] = nums[j];
                ++j;
            }
            ++p;
        }

        while (i <= mid)  {
            temp[p] = nums[i];
            res += (j - mid - 1);
            ++i;
            ++p;
        }
        while (j <= right) {
            temp[p] = nums[j];
            ++j;
            ++p;
        }

        for(int k = left; k <= right; k++){
            nums[k] = temp[k];
        }
    }
}

3 快速排序

在介绍快速排序之前,我们先看一下荷兰国旗问题

3.1 leetcode第75题

https://leetcode-cn.com/problems/sort-colors/

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

必须在不使用库的sort函数的情况下解决这个问题。

示例 1:

输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]

分析:这个问题要用双指针,用p1指向第一个元素,用p2指向最后一个元素。然后对数组从左到右遍历。如果当前数是0,就和p1对应的数交换,然后p1右移。如果当前数是2,就和p2对应的数交换,然后p2左移,再判断交换过来的数是不是1,如果不是1的话,需要将i(当前遍历索引)回退一位。直到i和p2相遇为止。

class Solution {
    public void sortColors(int[] nums) {
        if(nums.length <= 1){return ;}
        int p1 = 0;
        int p2 = nums.length-1;
        for(int i = 0; i <= p2; i++){
            if(nums[i] == 0){
                nums[i] = nums[p1];
                nums[p1] = 0;
                p1++;
            }
            if(nums[i] == 2){
                nums[i] = nums[p2];
                nums[p2] = 2;
                p2--;
                if(nums[i] != 1){
                    i--;
                }
            }
        }
    }
}

3.2 快速排序的核心思想

快速排序同样是递归算法,排序将分为三步:

第一步:随机选取数组中的一个数(不妨认为选第一个数),先将数组排序,使得比该数小的数在左侧,和该数相等的数在中间,比该数大的数在右侧。这其实就是我们3.1中讨论的荷兰国旗问题。

例:[4,6,2,4,5,3,1,5] --> [2,1,3],[4,4],[5,6,7]

第二步:对左半部分快速排序

第三步:对右半部分快速排序

3.3 快速排序的时间复杂度

最好情况:第一步能够将数组恰好等分,此时T(n)=2T(n/2)+O(n),即T(n)=O(n\log n)

最差情况:第一步以后,右半部分为空,此时T(n)=T(n-1)+O(n),即T(n)=O(n^2)

平均情况:我们将第一步完成后,分割点所处的位置视为一个随机变量,然后求数学期望(过程略),可知平均复杂度为T(n)=O(n\log n)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值