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

文章目录

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;
    }
}
相关推荐
0 0 02 小时前
CCF-CSP 32-2 因子化简(prime)【C++】考点:素数因子分解(试除法)
开发语言·数据结构·c++·算法
yyy(十一月限定版)2 小时前
图论——最短路Dijkstra算法
算法·图论
专注VB编程开发20年2 小时前
早期的redis是进程内的字典列表操作,后面改成TCP网络调用
数据库·redis·算法·缓存
仰泳的熊猫2 小时前
题目1545:蓝桥杯算法提高VIP-现代诗如蚯蚓
数据结构·c++·算法·蓝桥杯
TracyCoder1232 小时前
LeetCode Hot100(57/100)——5. 最长回文子串
算法·leetcode·职场和发展
载数而行5202 小时前
复杂度问题
c语言·数据结构·c++·算法·排序算法
WZ188104638692 小时前
LeetCode第20题
算法·leetcode
像素猎人2 小时前
字符串/字符与整型数据的相互转换stoi/stol()和to_string()
c++·算法
吕司2 小时前
LeetCode Hot Code——三数之和
数据结构·算法·leetcode