分治法(归并排序,快速排序)

分治法

    具体操作:  把原问题分成 k 个较小规模的子问题,对这 k 个子问题分别求解。如果子问题不够小,那么把每个子问题再划分为规模更小的问题。这样一直分解下去,直到问题足够小,很容易求出这些小问题的解为止

    求解的问题特征

  1. 平衡子问题:子问题的规模大致相同,能把问题划分成大小差不多相等的 k 个子问题,最好 k=2,即分成两个规模相等的子问题。子问题规模相等的处理效率比子问题规模不等的处理效率要高
  2. 独立子问题:子问题之间相互独立
    分治法能大大优化算法复杂度,一般情况下能把 O(n) 的复杂度优化到 O(log(2)n)(以 2 为底)。这是因为,局部的优化有利于全局;一个子问题的解决,其影响力扩大了 k 倍,即扩大到了全局
    分治法的思想几乎就是递归的过程

    用分治法建立模型时,解题步骤

  1. 分解:把问题分解成独立的子问题
  2. 解决:递归解决子问题
  3. 合并:把子问题的结果合并成原问题的解

归并排序

    具体操作

  1. 分解。把初始序列分成长度相同的左右两个子序列,然后把每个子序列在分成更小的两个子序列,直到子序列只包含 1 个数。这个过程用递归实现,如下图中的第 1 行是初始序列,每个数是一个子序列,可以看成递归到达的最底层
  2. 求解子问题,对子序列排序。最底层的子序列只包含 1 个数,其实不用排序
  1. 合并。归并两个有序的子序列,这是归并排序的主要操作,过程如下图所示。例如在图(a)中,i 和 j 分别指向子序列 { 13, 94, 99 } 和 { 34, 56 } 的第一个数,进行第 1 次比较,发现 a[i] < a[j],把 a[i] 放到临时空间 b[] 中。总共经过 4 次比较,得到了 b[] = { 13, 34, 56, 94, 99 }

    计算复杂度:  对 n 个数进行归并排序:(1)需要 log(2)n 趟归并; (2)在每一趟归并中有很多次合并操作,一共需要 O(n) 次比较。 所以计算复杂度是 O(nlog(2)n)
    空间复杂度:由于需要一个临时的 b[] 存储结果,所以空间复杂度是 O(n)


     逆序对问题


    问题描述

    分析
    当 k=0 时,就是求原始序列中有多少个逆序对
    考察图 6.7 所示的一次合并过程发现,可以利用这个过程记录逆序对。观察到以下现象:
    (1)在子序列内部,元素都是有序的,不存在逆序对;逆序对只存在于不同的子序列之间
    (2)在合并两个子序列时,如果前一个子序列的元素比后面子序列的元素小,那么不产生逆序对,如图 6.7(a) 所示;如果前一个子序列的元素比后面子序列的元素大,就会产生逆序对,如图 6.7(b) 所示。不过,在一次合并中,产生的逆序对不止一个,例如在图 6.7(b) 中把 34 放到 b[] 中,它与 94、99 产生了两个逆序对。在下面程序中,相关代码是 " cnt+=mid-i+1; "
    根据以上观察,只要在归并排序过程中记录逆序对就行了

    以上解决了 k=0 时原始序列的问题,现在考虑,当 k!=0 时(即把序列中任意两个相邻数交换不超过 k 次)逆序对最少有多少?注意:不超过 k 次的意思是可以少于 k 次,而不一定要 k 次
    在所有相邻数中,只有交换那些逆序的才会影响逆序对的数量。设原始序列有 cnt 个逆序对,讨论以下两种情况:

  1. 如果 cnt <= k ,总逆序数量不够交换 k 次。所以进行 k 次交换之后,最少的逆序对数为 0
  2. 如果 cnt > k,让 k 次交换都发生在逆序的相邻数上,那么剩余的逆序对是 cnt - k
    代码(Java)
import java.util.*;

public class Main
{
    final int MAXN = 100005;
    long[] a = new long[MAXN];
    long[] b = new long[MAXN];
    long cnt;

    void Merge(int L, int mid, int r){
        int i=L, j=mid+1, t=0;
        while(i<=mid && j<=r){
            if(a[i] > a[j]){
                b[t++] = a[j++];
                cnt += mid-i+1;   //记录逆序对的数量,**除去此句,便是完整的纯归并排序**
            }
            else b[t++] = a[i++];
        }
        //一个子序列中的数都处理完了,另一个还没有,把剩下的直接复制过来
        while (i<=mid)
            b[t++] = a[i++];
        while(j<=r)
            b[t++] = a[j++];
        for(i=0; i<t; i++)   //把排好序的 b[] 复制回 a[]
            a[L+i] = b[i];
        return;
    }

    void Mergesort(int L, int r){
        if(L < r){
            int mid = (L+r)/2;   //平分成两个子序列
            this.Mergesort(L,mid);
            this.Mergesort(mid+1,r);
            this.Merge(L,mid,r);   //合并
        }
        return;
    }

    public static void main(String args[]){
        int n;
        long k;
        Main m = new Main();
        Scanner sc = new Scanner(System.in);
        while (!sc.hasNext("#")){ //以 # 字符串结束输出
            n = sc.nextInt();
            k = sc.nextLong();
            m.cnt = 0;
            for(int i=0; i<n; i++)
                m.a[i] = sc.nextLong();
            m.Mergesort(0,n-1);   //归并排序
            if(m.cnt <= k) System.out.println(0);
            else System.out.println(m.cnt-k);
        }
    }
}

1

快速排序

    思路:把序列分成左、右两部分,使得左边所有的数都比右边的数小;递归这个过程,直到不能再分为止
    最简单的办法是设定两个临时空间 X,Y 和一个基准数 t;检查序列中所有的元素,比 t 小的放在 X 中,比 t 大的放在 Y 中。但其实可以直接在原序列上操作,不需要临时空间 X、Y
    以下介绍一种很容易操作的方法:

以上方法的实现如下:

    代码(Java)
import java.util.*;

public class Main{
    final int N = 10010;
    int[] data = new int[N];

    private void swap(int a, int b){   //交换
        int temp = data[a];
        data[a] = data[b];
        data[b] = temp;
    }

    private int partition(int left, int right){   //划分成左右两部分,以 i 指向的数为界
        int i = left;
        int temp = data[right];   //把尾数看成基准数
        for(int j=left; j<right; j++){
            if(data[j] < temp){  //比基准数小的放在左边
                swap(i, j);
                i++;
            }
        }
        swap(i, right);   //把基准数放在中间
        return i;    //返回基准数的位置
    }

    private void quicksort(int left, int right){
        if(left < right){
            int i = partition(left, right);   //划分
            quicksort(left, i-1);   //分治:i 左边的继续递归划分
            quicksort(i+1, right);   //分治:i 右边的继续递归划分
        }
        return;
    }

    public static void main(String[] args) {
        Main m = new Main();
        Scanner sc = new Scanner(System.in);
        int n;
        n = sc.nextInt();
        for(int i=0; i<n; i++)
            m.data[i] = sc.nextInt();
        m.quicksort(0,n-1);   //快速排序
        for(int i=0; i<n; i++)
            System.out.print(m.data[i]+",");  //输出排序后的序列
        System.out.print('\n');
        System.out.print(m.data[n/2]);
    }
}

2


  1. Java中数组下标只能是 int 型(集合也是如此),而c/c++ 是还可以是long,甚至long long类型等 ↩︎

  2. Java中:(1)基本数据类型传值,对形参的修改不会影响实参,即为值传递
                  (2)引用数据传引用(数组,类,接口),形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响实参
                  (3)String,Integer,Double等类型的特殊处理,可以理解为传值,最后的操作也不会修改实参 ↩︎

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值