【优选算法】分治:快速排序与归并排序

文章目录

1. 颜色划分(LC75)

颜色划分

题目描述

解题思路

使用三个指针:i遍历数组,left表示0区域的最右端,right表示2区域的最左端。三个指针把数组划分为4个区域:

  • 当前值为0:交换nums[left+1] nums[i],接着lefti向后走一步
  • 当前值为1:i向后走一步
  • 当前值为2:交换nums[right-1]nums[i]right向左走一步,i不可以移动,因为nums[right-1]是未被扫描的元素

代码实现

java 复制代码
void swap(int a,int b,int[] nums){
        int tmp = nums[a];
        nums[a] = nums[b];
        nums[b] = tmp;
    }

    public void sortColors(int[] nums) {
        int i=0,left=-1,right=nums.length;
        while(i<right){
            if(nums[i]==0){
                swap(i,left+1,nums);
                left++;
                i++;
            }else if(nums[i]==2){
                swap(i,right-1,nums);
                right--;
            }else
                i++;
        }
   }

2. 快速排序(LC912)

排序数组

题目描述

解题思路

当元素都相同时,时间复杂度会大打折扣,因此优化成数组分三块。(O(n)采用上个题的思想:

选定一个基准值,左边是小于基准值,右边大于基准值,中间是基准值。

  • 当前值小于基准值:交换nums[left+1] nums[i],接着lefti向后走一步
  • 当前值等于基准值:i向后走一步
  • 当前值大于基准值:交换nums[right-1]nums[i]right向左走一步,i不可以移动,因为nums[right-1]是未被扫描的元素

接着递归排序子区间

优化点:随机取基准值:

代码实现

java 复制代码
class Solution {
    Random random = new Random();
    public int[] sortArray(int[] nums) {
        sort(nums,0,nums.length-1);
        return nums;
    }

    void sort(int[] nums,int l,int r){
        if(l>=r)
            return;
        int key = nums[random.nextInt(r - l +1)+l];
        int i = l,left = l-1,right = r+1;
        while(i<right){
            if(nums[i]<key){
                swap(nums,i,left+1);
                i++;
                left++;
            }else if(nums[i]>key){
                swap(nums,i,right-1);
                right--;
            }else
                i++;
        }
        //[l,left] [left+1,right-1] [right,r]
        sort(nums,l,left);
        sort(nums,right,r);
    }
    void swap(int[] nums,int a,int b){
        int tmp = nums[a];
        nums[a] = nums[b];
        nums[b] = tmp;
    }
}

3. 快速选择算法(LC215)

数组内第K个大的数

题目描述

解题思路

  • 思路一:堆排序,建立容量为K的小根堆,堆顶元素就是第K大的元素。时间复杂度O(n*log2n)
  • 思路二:快速选择算法,数组分三块+随机选择基准值 时间复杂度O(n)
    • 随机选定一个随机值,数组分为小于,等于,大于基准值三部分:
    • 假设三个部分的元素个数分别是abc
      • 如果c>=k,说明第K大的元素在右区间,在[right,r]里找第K个大的元素
      • 如果b+c>=k,说明第K大的元素在中间区间,直接返回当前的基准值
      • 前两种情况都不成立:说明第K大的元素在左区间, 在[l,left]找第k-b-c大的元素

代码实现

java 复制代码
class Solution {
    Random random = new Random();
    public int findKthLargest(int[] nums, int k) {
        return  find(nums,0,nums.length-1,k);
    }
    int find(int[] nums,int l,int r,int k){
        if(l>=r)
            return nums[l];
        int key = nums[random.nextInt(r-l+1)+l];
        int i=l,left=l-1,right=r+1;
        while(i<right){
            if(nums[i]<key){
                swap(nums,i,left+1);
                left++;
                i++;
            }else if(nums[i]>key){
                swap(nums,i,right-1);
                right--;
            }else
                i++;
        }

        //[l,left] [left+1,right-1] [right,r]
        int a = left-l+1;
        int b = right - left -1;
        int c = r - right +1;

        if(c>=k)
            return find(nums,right,r,k);
        else if(c+b>=k)
            return key;
        else
            return find(nums,l,left,k-b-c);
    }
    void swap(int[] nums,int a,int b){
        int tmp = nums[a];
        nums[a] = nums[b];
        nums[b] = tmp;
    }
}

4. 最小的K个数(LCR159)

库存管理

题目描述

解题思路

  • 解法一:堆排序,建立容量为K的大根堆,堆内的元素就是最小的K个数。时间复杂度O(n*log2k)
  • 解法二:快速选择算法,数组分三块+随机选择基准值。时间复杂度O(n)
    • 随机选定一个随机值,数组分为小于,等于,大于基准值三部分:
    • 假设三个部分的元素个数分别是abc
      • 如果a>k,说明最小的k个元素在左区间,在[l,left]里找最小的k个元素
      • 如果b+a>=k,说明最小的K个元素右端点在中间区间,直接返回当前的基准值
      • 前两种情况都不成立:说明最小的K个元素右端点在右区间, 在[l,left]找前k-b-a个小的元素

代码实现

java 复制代码
class Solution {
    Random random = new Random();
    public int[] inventoryManagement(int[] stock, int cnt) {
        int index = find(stock,0,stock.length-1,cnt);
        return Arrays.copyOfRange(stock,0,index+1);
    }
    int find(int[] stock,int l,int r,int cnt){
        int left = l-1;
        int right = r+1;
        int i = l;
        int key = stock[random.nextInt(r-l+1)+l];
        while(i<right){
            if(stock[i]<key){
                swap(stock,i,left+1);
                left++;
                i++;
            }else if(stock[i]>key){
                swap(stock,i,right-1);
                right--;
            }else
                i++;
        }
        //[l,left] [left+1,right-1] [right,r]
        int a = left - l + 1;
        int b = right - left - 1;
        if(a>cnt)
            return find(stock,l,left,cnt);
        else if(a+b>=cnt)
            return cnt - a + left;
        else 
            return find(stock,right,r,cnt-a-b);

    }
    void swap(int[] nums,int a,int b){
        int tmp = nums[a];
        nums[a] = nums[b];
        nums[b] = tmp;
    }
}

5. 归并排序(LC912)

排序数组

题目描述

解题思路

  1. 把数组分解到两个长度为两个元素
  2. 有序数组合并

优化点:把临时数组改成全局变量,减少频繁创建和销毁数组的时间花销

代码实现

java 复制代码
class Solution {
    int[] tmp;
    public int[] sortArray(int[] nums) {
        tmp = new int[nums.length];
        sort(nums,0,nums.length-1);
        return nums;
    }
    void sort(int[] nums,int left,int right){
        if(left>=right)
            return;
        int mid = left + (right-left)/2;
        sort(nums,left,mid);
        sort(nums,mid+1,right);
        
        //合并有序数组
        int i = left;
        int j = left;
        int k = mid+1;
        while(j<=mid&&k<=right)
            tmp[i++] = nums[j]<nums[k]?nums[j++]:nums[k++];
        while(j<=mid)
            tmp[i++] = nums[j++];
        while(k<=right)
            tmp[i++] = nums[k++];

        //拷贝到原数组
        for (i = left; i <= right; i++) 
            nums[i] = tmp[i];
    }
}

6. 数组中的逆序对(LCR170)

交易逆序对的总数

题目描述

解题思路

  • 思路一:暴力枚举,双重循环
  • 思路二:利用归并排序
    1. 把数组分为两部分,左半部找到a个逆序对,右半部分找到b个逆序对
    2. 左右两部分分别排序,在左区间和右区间分别找出一个数,构成逆序对
    3. 整个数组的逆序对总个数就是左区间的逆序对个数+右区间的逆序对个数+左右区间各取一个得到的逆序对个数

使用归并过程中,第一步可以在递归过程中实现,最关键是处理第二步:

  • 两个区间分别排序后,j指针遍历右区间,在左区间(i指针维护)找比nums[j]大的个数
    • nums[i] <=nums[j] ,说明不可以构成逆序对,则i++
    • nums[i]>nums[j] 说明左区间中从nums[i]到结尾,都可以与nums[j]构成逆序,ret += mid - i + 1j++
    • 当某个区间指针走到头,说明已经找完所有的逆序对,接下来只需要完成排序,ret不需要改动

拓展:

降序数组解决问题:在左区间内找到比右区间当前值小的个数

代码实现

java 复制代码
class Solution {
    int[] tmp ;
    public int reversePairs(int[] record) {
        tmp = new int[record.length];
        return merge(record,0,record.length-1);
    }
    int merge(int[] nums,int left,int right){
        if(left>=right)
            return 0;

        int ret = 0;
        
        //左半部分的逆序对+右半部分的逆序对+左右各取一个构成的逆序对
        int mid = left + (right-left)/2;
        ret += merge(nums,left,mid);
        ret += merge(nums,mid+1,right);

        int i = left;
        int j = mid+1;
        int k = left;
        while(i<=mid && j<=right){
            if(nums[i]<=nums[j])
                tmp[k++] = nums[i++];
            else {
                ret+=mid-i+1;
                tmp[k++] = nums[j++];
            }
        }
        //此时逆序对已经找全,只需要完善排序
        while(i<=mid)
            tmp[k++] = nums[i++];
        
        while(j<=right)
            tmp[k++] = nums[j++];

        for(k = left;k <= right;k++)
            nums[k] = tmp[k];
        return ret;
    }
}

7. 计算右侧小于当前元素的个数(LC315)

计算右侧小于当前元素的个数

题目描述

解题思路

本质上是找逆序对,采用归并排序做逆序排序,i遍历左区间,在右区间内(j维护)找到比nums[i]小的个数

  • nums[i] <= nums[j],则tmp[k]=nums[j],j++k++
  • nums[i] > nums[j],说明nums[i]大于j后面所有的值,数量为right-j+1tmp[k] = nums[i]i++k++
  • 注意:
    • cur1cur2是排序后的结果,不是原始的下标。要想找到原始下标,需要创建index数组,大小与原数组相等,存放0到n-1,表示原数组的下标。
    • 原数组排序时,index对应下标跟随原数组交换位置。

      =400x)
    • 因为要同时给两个数组排序,临时数组tmp也要创建两个。
    • 找到原始下标,ret对应位置加上count就是最终结果。

代码实现

java 复制代码
class Solution {
    int[] ret;
    int[] index;
    int[] tmpNums;
    int[] tmpIndex;

    public List<Integer> countSmaller(int[] nums) {
        int n = nums.length;
        ret = new int[n];
        index = new int[n];
        tmpNums = new int[n];
        tmpIndex = new int[n];

        //初始化index
        for(int i = 0;i<n;i++)
            index[i] = i;

        merge(nums,0,n-1);
        List<Integer> list = new ArrayList<>();
        for(int x:ret)
            list.add(x);

        return list;
    }

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

        int i = left;
        int j = mid+1;
        int k = left;

        //降序排序
        while(i<=mid && j<=right){
            if(nums[i]<=nums[j]){
                tmpNums[k] = nums[j];
                tmpIndex[k++] = index[j++];
            }
            else{
                ret[index[i]] += right - j + 1;
                tmpNums[k] = nums[i];
                tmpIndex[k++] = index[i++];
            }            
        }

        while(i<=mid){
            tmpNums[k]=nums[i];
            tmpIndex[k++] = index[i++];

        }
        while(j<=right){
            tmpNums[k]=nums[j];
            tmpIndex[k++] = index[j++];
        }

        for(i = left;i <= right;i++){
            nums[i] = tmpNums[i];
            index[i] = tmpIndex[i];
        }
    }
}

8. 翻转对(LC493)

翻转对

题目描述

解题思路

与计算逆序对类似,采用分治的思想,分别计算左半部分,右半部分内部翻转对,再从左右两数组挑出一个元素找到翻转对,结果相加。

与逆序对的解法不同之处:不可以直接排序,而是找2倍关系。利用单调性使用同向双指针

  1. 计算翻转对
    • 策略一:计算当前元素后面有多少元素的2倍比当前值小(降序排序)
      • 如果nums[i] < nums[j]*2j++
      • 如果nums[i] >= nums[j]*2ret+=right - j + 1i++;接下来j不需要回退,因为mid+1j之间元素比i对应的元素大,一定比imid之间元素大。
    • 策略二:计算当前元素之前有多少元素的一半比当前值大(升序排序)
      • 如果nums[i]/2 < nums[j]*i++;
      • 如果nums[i]/2 >= nums[j]ret+=mid - i + 1j++
  2. 合并两个有序数组。前面的题排序和具体操作可以合并写,而这个题两者需要分开写

代码实现

java 复制代码
class Solution {
    int[] tmp;

    public int reversePairs(int[] nums) {
        int n = nums.length;
        tmp = new int[n];
        return merge(nums, 0, n - 1);
    }

    int merge(int[] nums, int left, int right) {
        if (left >= right)
            return 0;

        int ret = 0;
        int mid = left + (right - left) / 2;

        ret += merge(nums, left, mid);
        ret += merge(nums, mid + 1, right);

        int i = left;
        int j = mid + 1;

        //计算翻转对
        while (i <= mid) {
            while(j<=right && nums[i] <= (long)nums[j] * 2)//防止溢出
                j++;
            if(j>right)
                break;
            ret += right - j + 1;
            i++;
        }

        //降序排序
        i = left;
        j = mid + 1;
        int k = left;

        while (i <= mid && j <= right) {
            if (nums[i] > nums[j])
                tmp[k++] = nums[i++];
            else
                tmp[k++] = nums[j++];
        }
        while (i <= mid)
            tmp[k++] = nums[i++];
        while (j <= right)
            tmp[k++] = nums[j++];

        for(k = left;k<=right;k++)
            nums[k] = tmp[k];

        return ret;
    }
}
相关推荐
江畔柳前堤3 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore4 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考6 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX7 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面7 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号8 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽8 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI8 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者10 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者10 小时前
【征程6】校准量化中HistogramObserver解析
算法