【优选算法篇】:分而治之--揭秘分治算法的魅力与实战应用

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh--CSDN博客
✨ 文章所属专栏:优选算法篇--CSDN博客

文章目录

一.什么是分治算法

1.分治算法的基本概念

分治算法(Divide-and-Conquer Algorithm)是一种重要的算法设计范式。它的核心思想是将一个复杂的,规模较大的问题分解成若干个规模比较小,相互独立且与原问题类型相干的子问题,然后逐个解决这些子问题,最后再将子问题的解合并成原问题的解。

2.分治算法的三个步骤

  • 分解(Divide
    • 这一步是将原问题分解成多个规模的更小的子问题。例如,对于一个数组排序问题,可以将数组不断地分成两半,直到子数组的规模足够小(例如,在归并排序中,只有一个元素时就认为是已经有序的)。
    • 分解的方式需要根据具体的问题而定。在计算矩阵乘法时,可以将大矩阵分解为多个小矩阵;在处理图像时,可以将大图像分解为多个小区域。
  • 解决(Conquer
    • 对于分解后的子问题,如果子问题的规模足够小,就可以直接求解。比如,当子数组只有一个元素时,他本是就是有序的,这就是直接求解的情况。
    • 如果子问题任然比较复杂,就递归的调用分治算法来继续分解和求解子问题。例如,在归并排序中,不断将数组分解后,对每个子数组继续使用归并排序,直到子数组规模为1。
  • 合并(Combine
    • 在子问题得到解决后,需要将子问题的解合并起来,已得到原始问题的解。例如在归并排序中,将两个有序的子数组合并成一个更大的有序数组。
    • 合并的操作也需要根据具体问题进行设计。在计算大整数乘法时,将子问题计算得到的部分乘积合并起来得到最终的结果;在处理图像分割问题时,将各个小区域的处理结果合并成完整的图像结果。

二.分治算法的应用实例

1.快速排序(Quick Sort

  • 分解
    • 快速排序选择一个基准元素,将数组分成两部分,一部分的元素都小于基准元素,另一部分的元素都大于基准元素。
  • 解决
    • 对这两部分子数组递归地进行快速排序。
  • 合并
    • 由于快速排序是原地排序,不需要额外的合并操作,子数组排序完成后,整个数组也就排序完成了。

接下来通过例题来了解如何使用快速排序。

1.颜色划分

题目:

算法原理:

为了更好地理解分治思想快速排序,建议先理解颜色划分这道题,后面的快速排序就是再次基础上进行递归操作。

代码实现:

cpp 复制代码
void sortColors(vector<int>& nums){
    //三个指针,left指针指向0区间最右侧,right指针指向2区间最左侧
    int left = -1, right = nums.size();
    //i指针用来遍历数组
    int i = 0;
    while(i<right){
        if(nums[i]==0){
            swap(nums[++left], nums[i++]);
        }
        else if(nums[i]==2){
            swap(nums[--right], nums[i]);
        }
        else{
            i++;
        }
    }
}

2.排序数组

题目:

算法原理:

代码实现:

cpp 复制代码
//随机取基准值
int getkey(int left,int right,vector<int>& nums){
    int r = rand();
    return nums[r % (right - left + 1) + left];
}
//分治递归快速排序
void quicksort(int l,int r,vector<int>& nums){
    if(l>=r){
        return;
    }

    int key = getkey(l, r, nums);

    int left = l - 1, right = r + 1;
    int i = l;

    while(i<right){
        if(nums[i]<key){
            swap(nums[++left], nums[i++]);
        }
        else if(nums[i]>key){
            swap(nums[--right], nums[i]);
        }
        else{
            i++;
        }
    }

    quicksort(l, left, nums);
    quicksort(right, r, nums);
}
vector<int> sortArray(vector<int>& nums){
    srand(time(NULL));

    quicksort(0, nums.size()-1, nums);

    return nums;
}

3.数组中的第K个最大元素

题目:

算法原理:

代码实现:

cpp 复制代码
//分治递归快速选择
int quicksort(int l,int r,vector<int>&nums,int k){
    if(l==r){
        return nums[l];
    }
    int key = getkey(l, r, nums);

    int left = l - 1, right = r + 1;
    int i = l;

    while(i<right){
        if(nums[i]<key){
            swap(nums[++left], nums[i++]);
        }
        else if(nums[i]>key){
            swap(nums[--right], nums[i]);
        }
        else{
            i++;
        }
    }

    //c是大于基准值区间的个数
    int c = r - right + 1;
    //b是等于基准值区间的个数
    int b = right - left - 1;

    //如果k小于c,则递归到大于基准值的子区间查找
    if(c>=k){
        return quicksort(right, r, nums, k);
    }
    //如果k小于b+c,则直接返回基准值
    else if((c+b)>=k){
        return key;
    }
    //都不满足,则递归到小于基准值的子区间查找
    else{
        return quicksort(l, left, nums, k - b - c);
    }
}
int findKthLargest(vector<int>& nums, int k){
    srand(time(NULL));

    return quicksort(0, nums.size() - 1, nums,k);
}

4.最小的k个数

题目:

算法原理:

本道题也属于topK问题,和上面的题是类似题型,不同的是,上一道题是排升序从后往前找第K个最大元素;而本题中同样也可以排升序,但是是从前往后找前K个最小的数,因此这里就要查找小于基准值区间的个数和等于基准值区间的个数,然后比较K值。

代码实现:

cpp 复制代码
//分治递归快速选择
void _quicksort(int l,int r,vector<int>& nums,int k){
    if(l==r){
        return;
    }

    int key = getkey(l, r, nums);

    int left = l - 1, right = r + 1;
    int i = l;

    while(i<right){
        if(nums[i]<key){
            swap(nums[++left], nums[i++]);
        }
        else if(nums[i]>key){
            swap(nums[--right], nums[i]);
        }
        else{
            i++;
        }
    }

    //a表示小于基准值区间的个数
    int a = left - l + 1;
    //b表示等于基准值区间的个数
    int b = right - left - 1;

    if(a>=k){
        //当小于基准值区间的值大于等于K个数时,继续到对应的子区间递归查找
        quicksort(l, left, nums, k);
    }
    else if((a+b)>=k){
        //当小于等于基准值区间的值大于等于k个数时,直接结束返回
        return;
    }
    else{
        //当上面两种情况都不满足时,继续到大于基准值区间中查找k-a-b个数
        quicksort(right, r, nums, k);
    }

}
vector<int> smallestK(vector<int>& arr, int k){
    srand(time(NULL));

    _quicksort(0, arr.size() - 1, arr, k);

    return (arr.begin(), arr.begin() + k);
}

2.归并排序(Merge Sort

  • 分解
    • 归并排序将待排序的数组不断的分成两半,知道每个子数组只有一个元素。
  • 解决
    • 当子数组只有一个元素时,他就是有序的。对于其他子数组,递归地调用归并排序进行排序。
  • 合并
    • 采用合并操作,将两个有序的子数组合并成一个有序数组。例如,有两个有序子数组[1,3,5]和[2,4,6],通过比较元素大小,逐步地合并得到[1,2,3,4,5,6]。

接下来通过几道例题来讲解如何使用归并排序。

1.排序数组

题目:

算法原理:

第一道题就是练习如何使用归并排序,这里可以直接看我之前的关于排序算法二的博客中的归并排序,里面有详细的讲解归并排序,这里直接展示代码实现。

一定要先理解本道题之后再尝试写下面的三道题,下面的题都是在此基础上的变形。

代码实现:

cpp 复制代码
void mergesort(int left,int right,vector<int>& nums){
    //区间不存在,直接结束返回
    if(left>=right){
        return;
    }

    //1.分区间
    int mid = (right + left) / 2;
    //[left,mid],[mid+1,right]

    //2.递归子区间
    mergesort(left, mid, nums);
    mergesort(mid + 1, right, nums);

    //3.排序
    int cur1 = left, cur2 = mid + 1;
    int i = 0;
    //辅助数组
    vector<int> tmp(right - left + 1);
    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];
    }
}
vector<int> sortArray(vector<int>& nums){
    mergesort(0, nums.size() - 1, nums);

    return nums;
}

2.交易逆序对的总数

题目:

算法原理:

代码实现:

cpp 复制代码
//通过归并排序查找逆序对数
int mergesort1(int left,int right,vector<int>& nums){
    //区间不存在,直接结束返回0
    if(left>=right){
        return 0;
    }

    //1.分区间 [left,mid]  [mid+1,right]
    int mid = (left + right) / 2;

    //2.递归左右子区间
    int Lret = mergesort1(left, mid, nums);
    int Rret = mergesort1(mid + 1, right, nums);

    //3.排序+左右查找逆序对个数
    int cur1 = left, cur2 = mid + 1;
    int i = 0;
    int ret = 0;
    vector<int> tmp(right - left + 1);
    //升序,找出该数之前有多少个比自己大的
    while(cur1<=mid&&cur2<=right){
        if(nums[cur1]>nums[cur2]){
            ret += mid - cur1 + 1;
            tmp[i++] = nums[cur2++];
        }
        else{
            tmp[i++] = nums[cur1++];
        }
    }
    /*//降序,找出该数之后有多少个比自己小的
    while(cur1<=mid&&cur2<=right){
        if(nums[cur1]>nums[cur2]){
            ret += right - cur2 + 1;
            tmp[i++] = nums[cur1++];
        }
        else{
            tmp[i++] = 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];
    }

    return ret + Lret + Rret;
}

int reversePairs(vector<int>& record){
    return mergesort1(0, record.size() - 1, record);
}

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

题目:

算法原理:

这道题和上一道逆序对相似,仔细看可以发现不同点,上一题是查找数组中所有地逆序对总数,而这道题则是查找数组中每一个元素的逆序对个数并用数组输出结果,因此,这道题的找逆序对大致思路还是和上一题一样,只不过这次不在累加,而是单独存放。

本道题的重点就是,如何使输出的逆序对个数一一对应原始数组中的位置?

我刚开始想到的是通过哈希表建立原始数组中的元素和逆序对个数的映射关系,通过当前数组元素来找到逆序对个数,最后再通过哈希表中的值拷贝到统计数组中。但是最后发现,这里忽略了一个问题,就是如果存在相同元素时,就会导致两个相同元素的逆序对个数累加,比如第一个10的逆序对个数是5,而第二个10的逆序对个数是1,使用哈希表就会导致,两个10的逆序对个数都是6(5+1)。

因此在这之后,我又从新想到一种方法,直接建立一个新的数组,里面存放的是一个键值对,一个值表示原始数组中的值,用来进行排序;而另一个值用来表示元素在原始数组中的下标,这样在访问当前元素时就可以直接找到下标进而修改统计数组中对应的逆序对个数。

注意这里还有一个注意点就是,在递归函数中,因为使用的是新创建的键值对数组,因此辅助数组tmp也要变成键值对数组,这样在排序时才不会导致下标的丢失。

代码实现:

cpp 复制代码
//归并统计每个数
void mergesort2(int left,int right,vector<pair<int,int>>& nums,vector<int>& counts){
    //区间不存在,直接结束返回
    if(left>=right){
        return;
    }

    //1.分区间
    int mid = (left + right) / 2;

    //2.递归左右子区间
    mergesort2(left, mid, nums, counts);
    mergesort2(mid + 1, right, nums, counts);

    //3.排降序,统计个数
    int cur1 = left, cur2 = mid + 1;
    int i = 0;
    vector<pair<int,int>> tmp(right - left + 1);

    while(cur1<=mid&&cur2<=right){
        if(nums[cur1].first>nums[cur2].first){
            counts[nums[cur1].second] += right - cur2 + 1;
            tmp[i].first = nums[cur1].first;
            tmp[i++].second = nums[cur1++].second;
        }
        else{
            tmp[i].first = nums[cur2].first;
            tmp[i++].second = nums[cur2++].second;
        }
    }
    while(cur1<=mid){
        tmp[i].first = nums[cur1].first;
        tmp[i++].second = nums[cur1++].second;
    }
    while(cur2<=right){
        tmp[i].first = nums[cur2].first;
        tmp[i++].second = nums[cur2++].second;
    }

    //还原
    for(int i=left;i<=right;i++){
        nums[i].first = tmp[i - left].first;
        nums[i].second = tmp[i - left].second;
    }
}
vector<int> countSmaller(vector<int>& nums){
    //注意这里不能用哈希表建立元素和下标的映射关系,因为可能存在相同的元素
    //使用哈希表会导致相同的元素统计个数相加
    //重新建立一个数组,存放原数组中的元素以及对应的下标
    vector<pair<int, int>> newnums(nums.size());
    for(int i=0;i<nums.size();i++){
        newnums[i].first = nums[i];
        newnums[i].second = i;
    }
    //通过下标找到统计数组中对应元素的位置
    vector<int> counts(nums.size());

    mergesort2(0, newnums.size()-1, newnums, counts);

    return counts;
}

4.翻转对

题目:

算法原理:

本道题也是逆序对的变形,不同的是,这次不再是直接通过归并排序的性质就能直接找到,因此,本道题要在原本的归并排序基础上加上查找符合要求的个数,具体的查找方法就是通过双指针来实现。

因为归并排序在排序前(本道题是降序),左子区间是降序,右子区间也是降序,我们需要找到左子区间中是否存在值的二分之一大于右子区间的值,直接通过双指针来实现查找即可,查找完之后再进行左右子区间的归并排序。

代码实现:

cpp 复制代码
int mergesort3(int left,int right,vector<int>& nums){
    //区间不存在,直接结束返回0
    if(left>=right){
        return 0;
    }

    //1.分区间
    int mid = (left + right) / 2;

    //2.递归左右子区间
    int Lret=mergesort3(left,mid,nums);
    int Rret = mergesort3(mid + 1, right, nums);

    //3.排序,统计符合的个数
    int ret = 0;
    int cur1 = left, cur2 = mid + 1;
    int i = 0;
    vector<int> tmp(right - left+1);
    
    //重点,通过双指针找到符合要求的个数
    while(cur1<=mid){
        while(cur2<=right&&nums[cur1]/2.0<=nums[cur2]){
            cur2++;
        }
        if(cur2>right){
            break;
        }
        ret += right - cur2 + 1;
        cur1++;
    }

    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];
    }

    return ret + Lret + Rret;
}
int reversePairs1(vector<int>& nums){
    return mergesort3(0, nums.size() - 1, nums);
}

以上就是关于分治算法中快速排序和归并排序的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!

相关推荐
廖显东-ShirDon 讲编程22 分钟前
《零基础Go语言算法实战》【题目 4-1】返回数组中所有元素的总和
算法·程序员·go语言·web编程·go web
.Vcoistnt38 分钟前
Codeforces Round 976 (Div. 2) and Divide By Zero 9.0(A-E)
数据结构·c++·算法·贪心算法·动态规划·图论
轻口味41 分钟前
【HarmonyOS NAPI 深度探索4】安装开发环境(Node.js、C++ 编译器、node-gyp)
c++·node.js·harmonyos·harmonyos next·napi
pursuit_csdn1 小时前
LeetCode 916. Word Subsets
算法·leetcode·word
TU.路1 小时前
leetcode 24. 两两交换链表中的节点
算法·leetcode·链表
捕鲸叉1 小时前
C++并发编程之跨应用程序与驱动程序的单生产者单消费者队列
c++·并发编程
0xCC说逆向1 小时前
Windows图形界面(GUI)-QT-C/C++ - QT控件创建管理初始化
c语言·开发语言·c++·windows·qt·mfc·sdk
qingy_20462 小时前
【算法】图解排序算法之归并排序、快速排序、堆排序
java·数据结构·算法
CountingStars6192 小时前
梯度下降算法的计算过程
深度学习·算法·机器学习
lisanndesu3 小时前
栈 (算法十二)
算法·