算法——分治

思想:分而治之,将大问题转化为若干个相同或相似的子问题。快排的题目常见的方法是利用三指针法将数组分三块搭配随机选择基准元素的思想
算法——分治_第1张图片

颜色分类(分治_快排

颜色分类

题目解析

  1. 原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
  2. 我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
  3. 必须在不使用库内置的 sort 函数的情况下解决这个问题。

算法原理

  • 解法:三指针(数组分三块)
    • 将数组划分为三个区域,一部分区域全是0,一部分全是1,一部分全是2.定义i指针,用来扫描整个数组;left指针标记0区域的最右侧,right标记2区域的最左侧。算法——分治_第2张图片
    • 区间划分好,接下来我们就分类讨论每个区间应该怎么处理后,就可以写代码了。
    • 当i位置是0的情况:
      • 要把该元素加入左边的区间里,即加到left+1的位置。left+1这个位置是1,让i和left+1交换位置,然后让i++外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
      • 还有一种情况,当i的值等于left+1,即i此时所指的位置也是0,但是i是在left的后面,此时我们依旧要执行交换操作,执行交换操作的依然是left+1的位置和i的位置,但是相当于是自己和自己交换。交换完之后依旧要让left++,i++算法——分治_第3张图片
    • 当i位置是1的情况:要保证让1全部在left+1和i-1的区间内,所以让i++即可。
    • 当i位置是2的情况:此时我们要把他加入到right-1的位置,因为我们要把它加入到right最右边的区域的话,相当于是把他加入到right-1的区域。所以要i和right-1位置交换。并且要让right–。在交换完之前[i,right-1]这个区间全是待扫描的元素。当交换完之后,此时i所指的位置依旧是带扫描的元素,所以i不能++算法——分治_第4张图片
  • 当i和right相遇之后,此时没有待扫描的元素,此时停止循环。

下面模拟一下流程

  1. i所指元素是2,执行第三种情况,让i所指的元素和right-1位置的元素0交换,然后i不动,right–。

算法——分治_第5张图片

  1. 此时i指的元素是0,和left+1的位置(其实还是i所指的)元素0交换(那自己和自己交换就不动啦嘻嘻)。交换完之后让i++。left也++。交换完之后咱就发现目前而言左边区域就全是0.

算法——分治_第6张图片

3.然后i所指的元素是0,将left+1位置元素和他交换(还是自己和自己交换),交换完之后i++、left++
算法——分治_第7张图片算法——分治_第8张图片

  1. 接下来,i所指的元素是2,和right+1位置交换,交换完right–,i不动算法——分治_第9张图片
  2. 当i是1,很简单直接++

算法——分治_第10张图片

当i与right相遇停止循环。

代码实现

class Solution {
public:
    void sortColors(vector<int>& nums) 
    {
        int n = nums.size();
        int left = -1, right = n, i = 0;
        while(i < right)
        {
        if(nums[i] == 0) swap(nums[++left], nums[i++]);
        else if(nums[i] == 1) i++;
        else swap(nums[--right], nums[i]);
        }
    }
};

排序数组(分治_快排

排序数组

题目解析

用数组划分的思想实现快排从而解决这个问题

算法原理

  • 解法:快速排序:
    1. 普通版本:选择一个基准元素k,将原数组分成两部分,一部分小于等于k,另一部分全部大于k(这一步数据划分是关键,称为partation),此时选择的基准所在的位置就是排序之后所在的位置,此时只需要排序基准元素左右两边的数字即可;然后再选择一个基准元素,如此循环。但有一种极端情况,是数组元素全部重复的时候,时间复杂度就会退化为O(n2)算法——分治_第11张图片
    2. 数组分三块思想:时间复杂度O(n)
      1. 分三类情况讨论
        1. 当nums[i] < k,把当前位置元素加到左边的区域,执行交换操作,与left+1位置交换,然后left++,i也++
        2. 当nums[i] = k,i++
        3. 当nums[i] > k,交换nums,–right算法——分治_第12张图片
    3. 优化:用随机的方式选择基准元素,时间复杂度渐近到O(nlogn)
      1. 给定一个数组,一个左区间,一个右区间,要等概率的返回这个区间上的任意一个数。所以我们用随机数的方式选择基准元素。偏移量:r%(right-left+1)取值就是[0,n-1]。此时再让left加上偏移量就映射到这个区间随缘选一个点。算法——分治_第13张图片

代码实现

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        srand(time(NULL)); // 种下⼀个随机数种⼦
        qsort(nums, 0, nums.size() - 1);
        return nums;

    }

    // 快排
    void qsort(vector<int>& nums, int l, int r)
    {
        if(l >= r) return;
    // 数组分三块
        int key = getRandom(nums, l, r);
        int i = l, left = l - 1, right = r + 1;
        while(i < right)
        {
        if(nums[i] < key) swap(nums[++left], nums[i++]);
        else if(nums[i] == key) i++;
        else swap(nums[--right], nums[i]);
        }

// [l, left] [left + 1, right - 1] [right, r]
    qsort(nums, l, left);
    qsort(nums, right, r);
    }

    int getRandom(vector<int>& nums, int left, int right)
    {
        int r = rand();
        return nums[r % (right - left + 1) + left];
    }
};

数组中第k个最大元素(分治_快排)

数组中第k的最大元素

题目解析

本题是我们之前写过的TOP—K问题,共有四种问法:第K大、第K小、前K大、前K小。解决此问题有两种方法:一种是堆排序,时间复杂度O(nlogn);另一种就是这次的快速选择算法,时间复杂度O(n)。

  • 需要找的是数组排序后的第 k 个最大的元素

  • 设计并实现时间复杂度为 O(n) 的算法

算法原理

该算法是基于快排改良的
数组分三块+随机选择基准元素:

  1. 在l和r区间(区间两个端点),随机选择一个基准元素k,将区间分为三部分:k。因为题目要求找出第k大元素,所以当我们可以确定处于上面三部分中的其中一部分时,另外两部分就不用考虑,这样就提高了效率。
  2. 我们设在k区间里有c个数,此时分三种情况讨论。因为题目中说了数组已经排过序,所以按照常理我们从>k区间里先去找,概率大一点。
    1. 落在 >key 区间的判断条件为:c>=k,因为题中要第k大的元素,这个区间里都是较大的数,当k小于等于c时,该数字一定在这个区间里。
    2. 落在 =key区间的判断条件:b+c>=k,这里我们约定如果在b情况,那么a情况绝对不成立。此时k>=c(范围为蓝色箭头所指),所以,那么>key区间里的数就会符合题意,直接返回key
    3. 当前两种情况都不成立时。说明k很大(范围为红色箭头所指),后两部分区间(里面存的都是较大的数)已经找了b+c个,所以还需要在

算法——分治_第14张图片

代码实现

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k)
    {
        srand(time(NULL));
        return qsort(nums, 0, nums.size() - 1, k);
    }

    int qsort(vector<int>& nums, int l, int r, int k)
    {
        if(l == r) return nums[l];
    // 1. 随机选择基准元素
        int key = getRandom(nums, l, r);
    // 2. 根据基准元素将数组分三块
        int left = l - 1, right = r + 1, i = l;
        while(i < right)
        {
        if(nums[i] < key) swap(nums[++left], nums[i++]);
        else if(nums[i] == key) i++;
        else swap(nums[--right], nums[i]);
        }

    // 3. 分情况讨论
    int c = r - right + 1, b = right - left - 1;
    if(c >= k) return qsort(nums, right, r, k);
    else if(b + c >= k) return key;
    else return qsort(nums, l, left, k - b - c);
    }

    int getRandom(vector<int>& nums, int left, int right)
    {
    return nums[rand() % (right - left + 1) + left];
    }
};

最小的K个数(分治_快排)

最小的k个数——库存管理III

题目解析

输入整数数组 arr ,找出其中最小的 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

算法原理

  1. 解法一:排序:直接调用容器排序数组。然后把最小的k个数字摘出来。时间复杂度O(nlogn)
  2. 解法二:堆:创建一个大小为k的大根堆,存储的上限设为k,把所有的数依次丢入到大根堆里,最后堆里剩下的k个数就是最小的k个数.时间复杂度O(nlogk)
  3. 解法三:快速选择.随机选择基准元素+数组分三块。时间复杂度O(n)
    1. 题中要求第k小,所以从最左边的区间先开始分析。落在最左边区间的条件为a>k,再[l,left]区间里找
    2. (第一种情况不成立后再判断第二种)。落在=key区间的条件为a+b>=k,因为此时我们默认第一种情况不成立,说明此时k>=a,我们就在图中红线的区间里找,无论落在哪里,
    3. (此时前两种情况不成立)说明k很大,此时区间在蓝色箭头所指的范围里已经找a+b个小的数,还要去>key区间里找最小的k-a-b小的数

算法——分治_第15张图片
快速选择算法仅仅是把最小的k个数丢到了前面,并没有把前面几个数字排序
这个算法是在递归的过程中直接去对应符合条件的区间里找数字,而不是去区间里排序。所以快速选择算法比快排更快。并且在《算法导论》里有证明,当我们用随机选择基准元素的方法时,我们的三个区间都是等概率划分的,此时他的时间复杂度会逼近与O(N)。

代码实现

class Solution {
public:
    vector<int> inventoryManagement(vector<int>& stock, int cnt) {
        srand(time(NULL));
        qsort(stock, 0, stock.size() - 1, cnt);//这里是快速选择,不是排序,所以我们只需要返回前k个数就行,里面是无序的
        return {stock.begin(), stock.begin() + cnt};
    }

    void qsort(vector<int>& stock, int l, int r, int cnt)
    {
    if(l >= r) return;
    // 1. 随机选择⼀个基准元素 + 数组分三块
    int key = getRandom(stock, l, r);
    int left = l - 1, right = r + 1, i = l;
        while(i < right)
        {
          if(stock[i] < key) swap(stock[++left], stock[i++]);
          else if(stock[i] == key) i++;
          else swap(stock[--right], stock[i]);
        }

// [l, left][left + 1, right - 1] [right, r]
// 2. 分情况讨论
    int a = left - l + 1, b = right - left - 1;
    if(a > cnt) qsort(stock, l, left, cnt);
    else if(a + b >= cnt) return;
    else qsort(stock, right, r, cnt - a - b);
    }

    int getRandom(vector<int>& stock, int l, int r)
    {
    return stock[rand() % (r - l + 1) + l];
    }
};

排序数组(分治_归并)

排序数组

题目解析

整数数组 nums,请你将该数组升序排列。

归并算法回顾

用归并算法给数组排序,首先先选择mid中间点,先把左边部分排序,排左边的时候相当于又是一个归并排序的过程,直至只剩下一个元素的时候,向上返回,排右边区间,直至剩下一个元素时,开始向上返回,当这一层都排完时,合并两个有序数组。相当于二叉树中的后序遍历,快排的过程是先把数组分两块,然后把左边继续用一个key值分成左右两部分。相当于前序遍历

合并有序数组时需要创建辅助数组
算法——分治_第16张图片

代码实现

class Solution {
    vector<int>tmp; //节省时间消耗
public:
    vector<int> sortArray(vector<int>& nums) {
        tmp.resize(nums.size());  //在归并前更改大小
        srand(time(NULL)); // 种下⼀个随机数种⼦
        mergeSort(nums, 0, nums.size() - 1);
        return nums;

    }

    void mergeSort(vector<int>& nums, int left, int right)
    {
    if(left >= right) return;
    // 1. 选择中间点划分区间
    int mid = (left + right) >> 1;
    // [left, mid] [mid + 1, right]
    // 2. 把左右区间排序
    mergeSort(nums, left, mid);
    mergeSort(nums, mid + 1, right);

    // 3. 合并两个有序数组
    int cur1 = left, cur2 = mid + 1, i = 0;
    while(cur1 <= mid && cur2 <= right)
    tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];
    
    // 处理没有遍历完的数组
    while(cur1 <= mid) tmp[i++] = nums[cur1++];
    while(cur2 <= right) tmp[i++] = nums[cur2++];

    // 4. 还原
    for(int i = left; i <= right; i++)
        nums[i] = tmp[i - left];
    }
};

数组中的逆序对

数组中的逆序对

题目解析

逆序对:前面的数大于后面的数 算法——分治_第17张图片

算法原理

  1. 暴力枚举:把所有的二元组列举出来,判断是不是逆序对。先固定其中一个数,在这个数的后面的区间找找有几个比他小的数。两层for循环。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 左半部分->右半部分->一左一右:将数组从中间劈成两部分,求整个数组的逆序对的时候,先去求左边部分有a个逆序对,右边部分有b个逆序对。也就是说求a的时候不看b。接着分别在左区间挑一个,右区间挑一个;一左一右统计有c个逆序对,这样本质其实和遍历一样。最终a+b+b就是最终个数算法——分治_第18张图片
  2. **左半部分->左排序->右半部分->右排序->一左一右+排序:**这个与2相比,只是在一左一右的时候有些不同。在左边选完一个数后,只需要看右边区间有没有比这个数小就行。(+排序是为了和前半部分+排序保持一致,因为归并的子过程要相同)

算法——分治_第19张图片

  1. **利用归并排序解决:**先去搞左半部分有多少逆序对,如果数组很大,继续拆,…这个过程非常类似我们的归并过程。所以上面两种策略刚好对应递归排序。(我们这里只需要搞定一左一右+排序,因为左半部分+排序和右半部分+排序可以在递归中完成)。我们只需要算出一左一右有多少逆序对就行。此时数组升序。时间复杂度O(NlogN)

算法——分治_第20张图片


  • 策略一:找出该数之前有多少个比我大的数字

这时候在cur1之前的数都是比它小的,所以cur1之前的数就会比cur2之前的数字小,(因为cur1比cur2位置的数字小,cur1会先归并到辅助数组中)。我们找逆序对是在找到比我大的数之前,有多少数字能和我组成逆序对。所以我们分情况讨论:

  1. 当cur1[num] <= cur2[num]:说明此时还没有比cur2位置上大的数,就继续找,直到找到cur1位置大于cur2位置的数,所以让cur1++(本质上是先把cur1位置的数放到辅助数组里面,然后让cur1++)

  2. 当cur1[num] > cur2[num]:此时cur1后面的数全都比cur2大。我们就根据归并排序的以此比较,就找出了一堆比cur2大的数,此时我们用ret+=mid-cur1+1 记录cur1后面有多少个数字。并且让cur2++算法——分治_第21张图片

  3. 处理细节问题:如果数组降序,可以怎样处理呢

    1. 先选大的数归并到辅助数组里面,此时cur1和cur2左边的数都是比他们各自大 。
    2. 当cur1[num] > cur2[num]:此时统计一下cur1左边数字的个数,然后让cur1++,但此时会面临一个问题,如果cur1往后移动的数字依然比cur2大,此时再统计个数就重复统计了。因此策略1只能降序数组。

  • **策略2:找出该数之后有多少个比我小 ** 该策略只能用降序(因为升序也会重复统计)
    • 当升序时,此时我们固定nuns1,让较大的指针cur2放入辅助数组里外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
    • 数组降序,此时各自左边部分的数字都比cur1和cur2大(cur2左边部分的数比cur1大)。当cur1的位置数比cur2大时,说明是cur1第一次比cur2大,此时比cur2后面的区间全都大.统计个数算法——分治_第22张图片

代码实现

class Solution {
    int tmp[50010];
public:
    int reversePairs(vector<int>& nums)
    {
        return mergeSort(nums, 0, nums.size() - 1);
    }

    int mergeSort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return 0;
        int ret = 0;
    // 1. 找中间点,将数组分成两部分
        int mid = (left + right) >> 1;
        
    // [left, mid][mid + 1, right]
    // 2. 左边的个数 + 排序 + 右边的个数 + 排序
    ret += mergeSort(nums, left, mid);
    ret += mergeSort(nums, mid + 1, right);
        
    // 3. ⼀左⼀右的个数
    int cur1 = left, cur2 = mid + 1, i = 0;
    while(cur1 <= mid && cur2 <= right) // 升序的时候
    {
        if(nums[cur1] <= nums[cur2])
        {
            tmp[i++] = nums[cur1++];
        }

        else
        {
            ret += mid - cur1 + 1;
            tmp[i++] = nums[cur2++];}
        }

    // 4. 处理⼀下排序
    while(cur1 <= mid) tmp[i++] = nums[cur1++];
    while(cur2 <= right) tmp[i++] = nums[cur2++];
    for(int j = left; j <= right; j++)
     nums[j] = tmp[j - left];

        return ret;
    }
};
class Solution
{
    int tmp[50010];
public:
    int reversePairs(vector<int>& nums)
    {
        return mergeSort(nums, 0, nums.size() - 1);
    }

    int mergeSort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return 0;
        int ret = 0;

    // 1. 找中间点,将数组分成两部分
    int mid = (left + right) >> 1;

    // [left, mid][mid + 1, right]
    // 2. 左边的个数 + 排序 + 右边的个数 + 排序
    ret += mergeSort(nums, left, mid);
    ret += mergeSort(nums, mid + 1, right);

    // 3. ⼀左⼀右的个数
    int cur1 = left, cur2 = mid + 1, i = 0;
    while(cur1 <= mid && cur2 <= right) // 降序的版本
    {
        if(nums[cur1] <= nums[cur2])
        {
            tmp[i++] = nums[cur2++];
        }

        else
        {
        ret += right - cur2 + 1;
        tmp[i++] = nums[cur1++];
        }
    }

// 4. 处理⼀下排序
    while(cur1 <= mid) tmp[i++] = nums[cur1++];
    while(cur2 <= right) tmp[i++] = nums[cur2++];
    for(int j = left; j <= right; j++)
        nums[j] = tmp[j - left];
    return ret;
    }
};

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

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

题目解析

  • 返回的新数组要与原数组同等规模。
  • 返回该位置之后比他小的数字的个数

算法——分治_第23张图片

算法原理

归并排序(分治):因为要找比该位置小的数,所以我们可以用上到题的策略二——当前元素后面有多少个比我小的数。数组降序

  • 将数组劈成两部分,先将左边的结果找到,再将右边的结果找到。快速找到某一个位置之后比他小的数,就盯着cur1.此时开始讨论:
    • 当nums[cur1] <= nums[cur2]:此时没找到比cur1小的,那么让cur2++,继续向后移动
    • 当nums[cur1] > nums[cur2]:此时cur1比cur2右边部分的数字都大,此时要记录个数不能用ret记录,而是cur1对应的位置里面的ret(因为返回是通过数组形式)算法——分治_第24张图片
  • 那么问题来了,如何找nums元素对应的原始下标是多少呢?因为我们将原数组分治完之后排序了,所以此时下标已经乱了,当前位置的cur1并不是真实下标。
    • 我们可以搞一个index数组,专门记录nums数组当前位置元素的原始下标。然后无论nums数组中的元素怎么变,我们让他绑定移动
    • 我们一共要搞两个辅助数组tmp,一个是合并nums两个有序数组,另一个绑定

算法——分治_第25张图片

代码实现

class Solution 
{
    vector<int> ret;
    vector<int> index; // 记录 nums 中当前元素的原始下标
    int tmpNums[500010];
    int tmpIndex[500010];
public:
    vector<int> countSmaller(vector<int>& nums)
    {
    int n = nums.size();
    ret.resize(n);
    index.resize(n);
    // 初始化⼀下 index 数组
    for(int i = 0; i < n; i++)
        index[i] = i;mergeSort(nums, 0, n - 1);
    
    return ret;
    }

void mergeSort(vector<int>& nums, int left, int right)
{
    if(left >= right) return;
// 1. 根据中间元素,划分区间
    int mid = (left + right) >> 1;

// [left, mid] [mid + 1, right]
// 2. 先处理左右两部分
    mergeSort(nums, left, mid);
    mergeSort(nums, mid + 1, right);

// 3. 处理⼀左⼀右的情况
    int cur1 = left, cur2 = mid + 1, i = 0;
    while(cur1 <= mid && cur2 <= right) // 降序
    {
    if(nums[cur1] <= nums[cur2])
        {
            tmpNums[i] = nums[cur2];
            tmpIndex[i++] = index[cur2++];
    }

    else
    {   
        ret[index[cur1]] += right - cur2 + 1; // 重点 +会覆盖,所以要+=
        tmpNums[i] = nums[cur1];
        tmpIndex[i++] = index[cur1++];
    }

    }

// 4. 处理剩下的排序过程
    while(cur1 <= mid)
    {
    tmpNums[i] = nums[cur1];
    tmpIndex[i++] = index[cur1++];
    }

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

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

翻转对

翻转对

题目解析

找两个数,使前面的数大于后面的数2倍
算法——分治_第26张图片

算法原理

分治: 将整个数组分治为两部分,求出左半部分翻转对数a,右半部分翻转对数为b,一左一右翻转对数为c,最后a+b+c即为所求。但有些细节问题

  1. 该题比较条件是前面的数大于后面的数二倍,此时就不能按照归并排序的流程进行。所以我们要在归并排序之前进行翻转对。利用两个数组有序的性质。我们可以在一次归并中用O(N)的时间复杂度搞定该层的翻转对的个数(利用单调性,使用同向双指针)算法——分治_第27张图片
    1. 策略一:计算当前元素后面有多少个比两倍还小的数,降序排列
      1. cur1不动,如果cur2当前所指的元素比cur1两倍还大,往后移。直到找到第一个比cur1两倍还小的(因为数组降序),记ret+=right-cur2+1
      2. 之后移动cur1时,不要让cur2回滚到之前的位置否则时间复杂度为O(n^2logn)。让cur2在第一个找到的比cur1小的位置继续往后移动即可。直到cur1移动到最后结束算法——分治_第28张图片
    2. 策略二:计算当前元素之前有多少个元素的一半比我大,升序排列
      1. cur2不动,如过当前cur2的位置比cur1位置的一半还要小,cur1右移。直到出现比cur1位置一半大的,记ret+=mid-cur1+1.然后cur2++,cur1同理只需要在当前位置向后移动即可,不需要会退到第一个位置。直到cur2移动到最后结束。算法——分治_第29张图片
  2. 合并两个有序数组

代码实现

class Solution 
{
    int tmp[50010];
public:
    int reversePairs(vector<int>& nums)
    {
    return mergeSort(nums, 0, nums.size() - 1);
    }

    int mergeSort(vector<int>& nums, int left, int right)
    {
    if(left >= right) return 0;
    int ret = 0;

    // 1. 先根据中间元素划分区间
    int mid = (left + right) >> 1;
    // [left, mid] [mid + 1, right]
    // 2. 先计算左右两侧的翻转对
    ret += mergeSort(nums, left, mid);
    ret += mergeSort(nums, mid + 1, right);
    
    // 3. 先计算翻转对的数量
    int cur1 = left, cur2 = mid + 1, i = left;
    while(cur1 <= mid) // 降序的情况
    {
    while(cur2 <= right && nums[cur2] >= nums[cur1] / 2.0) cur2++;
    if(cur2 > right)
     break;
    ret += right - cur2 + 1;
    cur1++;
    }

// 4. 合并两个有序数组
    cur1 = left, cur2 = mid + 1;
    while(cur1 <= mid && cur2 <= right)
        tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur2++] : nums[cur1++];
    while(cur1 <= mid) tmp[i++] = nums[cur1++];
    while(cur2 <= right) tmp[i++] = nums[cur2++];
    for(int j = left; j <= right; j++)
        nums[j] = tmp[j];
    
    return ret;
}
};

你可能感兴趣的:(算法,算法)