zoukankan      html  css  js  c++  java
  • 归并排序和快速排序的衍生问题

    前面两篇总结了常见的几种排序算法的主要思想以及C++与python两种方式的实现过程, 几种排序算法中比较重要的就是归并排序和快速排序,这两种方法的相同点就是都使用了分治的思想,现在用来解决两个具体问题。

    1.分治法

      分治法就是将原问题分割成同等结构的子问题,之后将子问题逐一解决后,原问题也就得到了解决。 需要注意的是归并排序和快速排序虽然都使用了分治的思想,但它们分别代表了分治算法的两类基本思想。对于归并排序而言,它对“分”这个过程没有做太多操作,只是简单的将数组分为两部分然后递归的进行归并排序,而归并排序的关键是这样分完之后如何将它们归并起来,即merge()操作。 
      而对于快速排序来说,则是废了很大功夫放在了如何“分”这个问题上,我们是选取了一个标定点,然后使用partition()这个子过程将这个标定点移到了合适的位置,当它移到了合适的位置之后才将整个数组分成了两部分,而这样分完之后,在“合”的时候就不用做过多的考虑了,只需要一步一步递归下去就好了。 
      下面解决两个直接从归并排序和快速排序中衍生出来的具体问题的。

    2.求逆序对数量

    • 问题描述:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。给定一个数组,求出这个数组中的逆序对的总数。
    • 这个问题直接的做法是逐个统计,复杂度是O(n^2),但显然是很愚蠢的方法,这里可以利用归并排序的思想,在归并排序过程中统计逆序对的个数。
    • 实现思想: 
        归并排序是将数列a[l,r]分成两部分,a[l,mid]和a[mid+1,r]分别进行归并排序,然后再将这两半合并起来。在合并的过程中(设l<=i<=mid,mid+1<=j<=r),当a[i]<=a[j]时,并不产生逆序数;当a[i]>a[j]时,在前半部分中比a[i]大的数都比a[j]大,将a[j]放在a[i]前面的话,逆序数要加上mid+1-i。因此,可以在归并排序中的合并过程中计算逆序数。
    • C++实现:
    #include <iostream>
    using namespace std;
    // 计算逆序数对的结果以long long返回
    // 因为对于一个大小为n的数组, 其最大的逆序数对个数为 n*(n-1)/2, 非常容易产生整型溢出
    // __merge函数求出在arr[l,mid]和arr[mid+1,r]有序的基础上, arr[l,r]的逆序数对个数
    long long __merge(int arr[],int l,int mid,int r){
    long long res = 0;// 初始化逆序数对个数 res = 0
    int aux[r - l + 1];
    for(int i = l;i <= r;i++)
    aux[i - l] = arr[i];
    int i = l;
    int j = mid + 1;
    for(int k = l;k <= r;k++){
    if(i > mid){
    arr[k] = aux[j - l];
    j ++;
    }
    else if(j > r){
    arr[k] = aux[i - l];
    i ++;
    }
    else if(aux[i - l] < aux[j - l]){
    arr[k] = aux[i - l];
    i ++;
    }
    else{// 左半部分所指元素 > 右半部分所指元素
    arr[k] = aux[j - l];
    j ++;
    res += (long long) (mid - i + 1);
    // 此时, 因为右半部分所指的元素小
    // 这个元素和左半部分的所有未处理的元素都构成了逆序数对
    // 左半部分此时未处理的元素个数为 mid - j + 1
    }
    }
    return res;
    }
    // 求arr[l,r]范围的逆序数对个数
    long long __inversionCount(int arr[],int l,int r){
    if(l >= r)
    return 0;
    int mid = l + (r - l) / 2;
    long long res1 = __inversionCount(arr,l,mid);// 求出 arr[l,mid] 范围的逆序数
    long long res2 = __inversionCount(arr,mid + 1,r);// 求出 arr[mid+1,r] 范围的逆序数
    return res1 + res2 + __merge(arr,l,mid,r);
    }
    long long inversionCount(int arr[],int n){
    return __inversionCount(arr,0,n - 1);
    }
    int main(){
    int a[10] = {10,9,8,7,6,5,4,3,2,1};
    int n = 10;
    cout<< "逆序对数量为:" << inversionCount(a,n)<<endl;
    }
    • Python实现:
     
    def __merge(arr,l,mid,r):
    res = 0
    aux = []
    for x in range(l,r + 1):
    aux.append(arr[x])
    i = l
    j = mid + 1
    for k in range(l,r + 1):
    if i > mid:
    arr[k] = aux[j - l]
    j += 1
    elif j > r:
    arr[k] = aux[i - l]
    i += 1
    elif aux[i - l] < aux[j - l]:
    arr[k] = aux[i - l]
    i += 1
    else:
    arr[k] = aux[j - l]
    j += 1
    res += mid - i + 1
    return res
    def __inversionCount(arr,l,r):
    if l >= r:
    return 0
    mid = l + (r - l) // 2
    res1 = __inversionCount(arr,l,mid)
    res2 = __inversionCount(arr,mid + 1,r)
    return res1 + res2 + __merge(arr,l,mid,r)
    def inversionCount(arr):
    n = len(arr)
    return __inversionCount(arr,0,n - 1)
    arr = [10,9,8,7,6,5,4,3,2,1]
    res = inversionCount(arr)
    print('逆序对数量为:' + str(res))

    3.取数组中第k大的元素

    • 题目很简单,给定一个数组,求出该数组中第k大的元素。最简单的做法是直接进行排序,算法复杂度是O(nlogn),但是这么做很明显比较低效率,因为只要求出第k大的元素,并不需要别的信息。
    • 另一种方法是用快速排序的思想。快速排序每次把一个元素交换到正确的位置,同时把左边的都放上小的,右边都放上大的。这个算法每一次选取一个中心点,排序之后,查看中心点的位置。如果它的位置大于K,就说明,要在前面一个子序列中找出第K大的元素。反之,如果小于K,就说明要在后面一个序列中找出第k大的元素。
    • 复杂度分析: 
        这种算法的复杂度分析稍微复杂。第一次交换,算法复杂度为O(n),接下来的过程和快速排序不同,快速排序是要继续处理两边的数据,再合并,合并操作的算法复杂度是O(1),于是总的算法复杂度是O(nlogn)(可以这么理解,每次交换用了n,一共logn次)。但是这里在确定中心点的相对位置(在K的左边或者右边)之后不用再对剩下的一半进行处理。也就是说第二次插入的算法复杂度不再是O(n)而是O(n/2),然后接下来的过程是1+1/2+1/4+........<2,换句话说就是一共是O(2n)的算法复杂度也就是O(n)的算法复杂度。 
        这个算法是一种很经典的算法。原因是因为它通过努力把算法复杂度在每次递归中下降一些,最终让整个算法的复杂度下降极多,算是一种十分聪明的做法。
    • C++实现:
    #include <iostream>
    #include <cstdlib>
    #include <ctime>
    using namespace std;
    int __partition(int arr[],int l,int r){
    swap(arr[l],arr[rand()%(r - l + 1) + l]);
    int v = arr[l];
    int j = l;
    for(int i = l + 1;i <= r;i++){
    if(arr[i] > v){
    swap(arr[i],arr[j + 1]);
    j ++;
    }
    }
    swap(arr[l],arr[j]);
    return j;
    }
    int __selection(int arr[],int l,int r,int k){
    if(l == r)
    return arr[l];
    int p = __partition(arr,l,r);
    if(p == k)
    return arr[p];
    else if(p > k)
    return __selection(arr,l,p - 1,k);
    else
    return __selection(arr,p + 1,r,k);
    }
    int selection(int arr[],int n,int k){
    srand(time(NULL));
    return __selection(arr,0,n - 1,k - 1);//索引是从0开始的
    }
    int main(){
    int arr[10] = {10,9,8,7,6,5,4,3,2,1};
    int n = 10;
    cout<<"数组中第3大的元素为:"<<selection(arr,n,3)<<endl;
    }
    • Python实现:
    import random
    def __partition(arr,l,r):
    k = random.randint(l,r)
    arr[l],arr[k] = arr[k],arr[l]
    v= arr[l]
    j = l
    for i in range(l + 1,r + 1):
    if arr[i] < v:
    arr[i],arr[j + 1] = arr[j + 1],arr[i]
    j += 1
    arr[l],arr[j] = arr[j],arr[l]
    return j
    def __selection(arr,l,r,k):
    if l == r:
    return arr[l]
    p = __partition(arr,l,r)
    if p == k:
    return arr[p]
    elif p > k:
    return __selection(arr,l,p - 1,k)
    else:
    return __selection(arr,p + 1,r,k)
    def selection(arr,k):
    n = len(arr)
    return __selection(arr,0,n - 1,k - 1)
    arr = [10,9,8,7,6,5,4,3,2,1]
    print('数组中第2小的元素为'+ str(selection(arr,2)))

    推荐一个良心公众号【IT资源社】:

    本公众号致力于免费分享全网最优秀的视频资源,学习资料,面试经验等,前端,PHP,JAVA,算法,Python,大数据等等,你想要的这都有

    IT资源社-QQ交流群:625494093

    也可添加微信拉你进微信群: super1319164238

    微信搜索公众号:ITziyuanshe 或者扫描下方二维码直接关注,

     
  • 相关阅读:
    flutter sdk版本升级或者降级到指定版本
    flutter upgrage到1.17版本 android打包报错 Could not determine the dependencies of task ':app:lintVitalRelease'.
    小程序 自定义alert弹窗 带动画
    小程序 自定义cell 自定义group
    小程序 自定义弹窗禁止底部内容滚动 textarea内容上移
    css fixed 强制居中
    小程序 正则表达式
    小程序 自定义导航栏
    小程序 swiper自定义dot
    java 中锁的分类
  • 原文地址:https://www.cnblogs.com/Lovebugs/p/8778671.html
Copyright © 2011-2022 走看看