【算法】递归与分治策略——二分查找、归并排序、快排、最接近点对问题

递归

递归:直接或间接的调用自身的算法称为递归算法
实现递归调用的关键是为算法建立递归调用工作栈。通常,在一个算法中调用另一算法时,系统需要在运行被调用算法之前先完成3件事:

  1. 将所有实参指针,返回地址等信息传递给被调用算法;
  2. 为被调用算法的局部变量分配存储区;
  3. 将控制转移到被调用算法的入口;

在从被调用算法返回调用算法时,系统也相应地要完成3件事:

  1. 保存被调用算法的计算结果;
  2. 释放分配给被调用算法的数据区;
  3. 依照被调用算法保存的返回地址将控制转移到调用算法;

原则: 后调用先返回
上述算法之间的信息传递和控制转移必须通过栈来实现,即系统将整个程序运行时所需的数据空间安排在一个栈中,每调用一个算法,就为它在栈顶分配一个存储区,每退出一个算法,就释放它在栈顶的存储区。当前正在运行的算法的数据一定在栈顶。

由于递归算法结构清晰,可读性强,而且容易用数学归纳法来证明算法的正确性,因此它为设计算法、调试程序带来很大方便,然而,递归算法的运行效率较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多。若在程序中消除算法的递归调用,使其转化为非递归算法。通常,消除递归采用一个用户定义的栈来模拟系统的递归调用工作栈,从而达到将递归算法改为非递归算法的目的。仅仅是机械地模拟还不能达到减少计算时间和存储空间的目的。因此,还需要根据具体程序的特点对递归调用工作栈进行简化,尽量减少栈操作,压缩栈存储空间以达到节省计算时间和存储空间的目的。

分治

分治法:分治法的基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题互相独立且与原问题相同。递归地解这些子问题,然后将各子问题的解合并得到原问题的解。

二分搜索技术

二分搜索算法时运用分治策略的典型例子。
给定已排好序的n个元素a[0:n-1],现要在这n个元素中找出一特定元素x

二分搜索算法很好的利用了“已排序好”的这个条件,每次查找根据条件缩小范围,递归求解

/**
     * 给定已排好序的n个元素a[0:n-1],现要在这n个元素中找出一特定元素x
     * @param arr
     * @param x
     * @param n
     * @return
     */
    public static int binarySearch(int []arr,int x,int n){
        int left=0;
        int right=n-1;
        while (left <=right ){
            int middle=(right -left +1 )/2+left ;
            if(x==arr[middle ]) return middle ;
            if(x>arr[middle ])  left =middle +1;
            else
                right =middle -1;
        }
        return -1;
    }

可以看出,每执行一次算法的while循环,待搜索数组的大小减少一半。因此,在最坏情况下,while循环被执行了O(logn)次,循环体内运算需要O(1)时间,因此,整个算法在最坏情况下的计算时间复杂性为O(logn)

归并排序

归并排序很好的利用了递归,将待排序集合一分为二,直至待排序集合只剩下1个元素为止。然后不断合并2个排好序的数组段。

 /**
     * 归并排序
     * 平均时间复杂度:O(nlog2 n) 最坏时间复杂度:O(nlog2 n)
     * 空间复杂度:0(n)
     * @param <T>
     */
    private static <T extends Comparable <T>> void merge(T []arr,int gap) {
        int left1 = 0;
        int right1 = left1 + gap;
        int left2 = right1 + 1;
        int right2 = left2 + gap - 1 > arr.length - 1 ? arr.length - 1 : left2 + gap - 1;
        T[] brr = (T[]) new Comparable[arr.length];
        int j = 0;//控制新数组的下标
        //有两个归并段
        while (left2 < arr.length) {
            while (left1 <= right1 && left2 <= right2) {
                if (arr[left1].compareTo(arr[left2]) < 0) {
                    brr[j++] = arr[left1++];
                } else {
                    brr[j++] = arr[left2++];
                }
            }
            if (left1 > right1) {
                while (left2 <= right2) {
                   brr[j++] = arr[left2++];
                }
            }
            if (left2 > right2) {
                while (left1 <= right1) {
                   brr[j++] = arr[left1++];
               }
            }
            left1 = right2 + 1;
            right1 = left1 + gap - 1;
            left2 = right1 + 1;
            right2 = left2 + gap - 1 > arr.length - 1 ? arr.length - 1 : left2 + gap - 1;
        }
        //只有一个归并段
        while (left1 <=arr .length -1){
            brr [j++]=arr [left1 ++];
        }
        System .arraycopy(brr ,0,arr ,0,arr .length );
    }
    public static<T extends Comparable <T>> void mergeSort(T []arr){
        for(int i=1;i<arr .length ;i*=2){
            merge(arr ,i);
        }

    }

上周在java算法与分析中看到另一种解法;

递归排序法
MergeSort

这是调用函数递归口,复制出一个和ar一样的br,然后开始调用函数

public static void MergeSort(int []ar)
    {
        int []br = new int[ar.length];
        MergePass(ar,br,0,ar.length-1);
    }
MergePass

并归函数的递归调用函数:
找到中位数,然后将原数组划分成两部分,左边先进行递归,右边再递归,然后调用Merge函数,然后拷贝到原数组就好了。

乍一看似乎很简单的实现了,仔细一想为什么
MergePass(ar,br,left,mid); // left
MergePass(ar,br,mid+1,right); // right;
这样简单的两句函数就可以实现递归排序,递归思想一直是我觉得比较难理解的思想;再进行调试时,我终于理解了是如何递归再排序的。

整个函数中只有Merge函数进行了大小比较排序,所以MergePass只是起到了划分区域的作用,每个区域的个数为1时,停止划分,递归结束,开始进行归并,然后再进行下一次的递归调用。先调用的后递归。

  public static void MergePass(int []ar,int []br,int left,int right)
    {
        if(left < right)// left = 1  , right = 1
        {
            int mid = (right - left)/2 + left;
            MergePass(ar,br,left,mid); // left
            MergePass(ar,br,mid+1,right); // right;
            Merge(br,ar,left,mid,right);
            Copy_Ar(ar,br,left,right);
        }
    }
Merge

Merge函数做的事情可以理解为合并,将左半部分和右半部分合并;
合并的规则就是比较大小,按照大小顺序进行排序,
然后将没排到的元素顺次放进去

第一个循环退出的条件就是i>m或者j>right
如果是i>m退出,就说明左边先遍历完;则将右边没遍历完的再放到dsi数组
如果是j>right退出,就说明右边先遍历完;则将左边没遍历完的放到dsi数组

 public static void Merge(int []dsi,int []src,int left ,int m, int right)
    {
        int i = left, j = m+1;
        int k = left;
        while(i<=m && j<=right)
        {
            dsi[k++] = src[i] < src[j]? src[i++]:src[j++];
        }
        while(i<=m)
        {
            dsi[k++] = src[i++];
        }
        while(j<=right)
        {
            dsi[k++] = src[j++];
        }
    }

非递归法

非递归法可以说是上下来回倒:
创建一个br数组,和ar来回倒,
具体怎么个倒法?
可以从递归法来获取灵感:递归法后调用的先递归,所以当划分成规模只有一个个数时,开始执行;所以非递归法我们可以由小到大来划区域排序。

第一次将元素个数设为1(s);
这个元素个数指的是半区域,比如左(右)半部分区域为1;
然后进行NiceMergePass,将排序好的元素复制到br中。

第二次将元素个数设为2;
这次是从br复制到ar;

(4、8、16……)是2的幂
……

NiceMergePass具体是怎么排序的呢?

一个数组的个数不是奇数就是偶数,所以可能在第一次两两划分时剩下一个或者没有剩下;
当划分完后,两两组队就可以直接进行Merge排序,剩下的元素则需要复制到新数组里。

问题又来了,怎么复制,最后的一直复制吗?
1,2,3,4,5,6 假如有六个元素,是2的幂,在第二次s=2时,【1,2 3,4】这两组拼成一组,余下的另一组直接复制;
这就是我第一次没想通的问题,我觉得数不是奇数就是偶数,而肯定最后剩下的是一个元素,只需要复制一个就好了,而有可能有一组数组剩下,所以这里的复制要用for循环。

我们可以看到下一次合并时,【1,2,3,4】【5,6】是这两个数组合并,因为数据不够所以不是四个四个合并了,所以当n(最后一个元素的下标)》=i+s时,就是要剩下的数组一起合并。

 public static void NiceMergeSort(int []arr){
        int []br=new int[arr .length ];
        int n=arr .length -1;
        int s=1;
        while (s<n){
            NiceMergePass(br,arr,s,n);
            s+=s;
            NiceMergePass(arr ,br ,s,n);
            s+=s;
        }
    }

    public static void NiceMergePass(int []dis,int []src,int s,int n){
        System.out.printf("s=%d\n",s);
        int i=0;
        for(i=0;i+2*s-1<=n;i=i+2*s){//是二次幂
            Merge(dis ,src ,i,i+s-1,i+2*s-1);
        }
        if(n>=i+s){//当剩下的集合要和后面的一起合并
            Merge(dis ,src ,i,i+s-1,n);
        }else {//
            for(int j=i;j<=n;j++){
                dis [j]=src [j];
            }
        }

    }

快排

排序的基本思想是:

  1. 分解:以ar[0]为基准元素,将数组化为三个部分,{a[0,p-1],a[p],a[p+1,ar.length-1]},其中p为ar[0]元素的下标
    特点:
  • 使得第一部分的任何元素小于等于a[0];
    第三部分的任何元素大于a[0];
  • a[0]的位置确定,再进行后续的递归排序位置不会变动。
  1. 递归求解:通过递归调用快速排序算法,分别第一部分 盒第三部分排序
  2. 合并:将三个部分合并

递归法

分解函数

让第一个元素a0先和最右边的比较,直到有小于等于a0的元素ar,把ar的值赋给al,也就是把它放到左边,然后比较左边直到有大于a0的元素,把它放到右边。最后left下标和right下标都指向a0元素的下标。
**两头开花,**左右两边都进行比较。

   public static int Partition(int []ar,int left,int right){
        int tmp=ar[left ];
        while (left <right ) {
            while (left < right && ar[right] > tmp) --right;
           // if (left < right) {
                ar[left] = ar[right];
            //}
            while(left <right && ar [left ]<=tmp ) ++left ;
           // if(left <right ){
                ar [right  ]=ar [left ];
           // }
        }
        ar[left ]=tmp ;
        return left ;//right
    }
合并
public static void QuickSort(int ar[],int left,int right){
        if(left <right ){
            int pos=Partition(ar ,left ,right );
            QuickSort(ar ,left ,pos-1);
            QuickSort(ar, pos+1, right);
        }
    }

非递归法

在设计非递归法时,要记得可以借助一些栈,队列等帮助我们实现

在递归算法时,发现每次的递归调用只是改变左右坐标的值,所以在非递归的算法时要着重关注坐标的变化。

下面算法将借助队列来实现,将左右坐标分辨入栈,然后在循环中出栈,队列的性质先进先出刚好符合我们左右坐标的顺序,然后调用分解函数,继续将两个部分的坐标入栈

public static void NiceQuickSort(int []ar,int left,int right){
        Queue <Integer > qu=new LinkedList<Integer>();
        qu.offer(left );
        qu.offer(right );
        while (!qu.isEmpty() ){
            left =((LinkedList<Integer>) qu).pollFirst() ;
            right =((LinkedList<Integer>) qu).pollFirst() ;
            int pos=Partition(ar, left, right);
            if(left <pos-1){
                qu.offer(left );
                qu.offer(pos-1);
            }
            if(pos+1<right ){
                qu.offer(pos+1);
                qu.offer(right );
            }
        }
    }

递归找第k小

找出数组中的第一小和第二小想必是简单的,遍历可得

 /**
     * 找数组中的第一小和第二小
     * @param ar
     */
    public static void SelectMin(int []ar){
        if(ar .length <2)return ;
        int min1=ar[0]<ar[1]?ar [0]:ar [1];
        int min2=ar[0]>ar[1]?ar [0]:ar[1];
        for(int i=2;i<ar .length ;i++){
            if(ar [i]<min1 ){
                min2 =min1 ;
                min1 =ar [i];
            }else if(ar [i]<min2 ){
                min2 =ar [i];
            }
        }
        System.out.println(min1 );
        System.out.println(min2 );
    }

那么如何找到第k小

第一个想到的方法应该是找第几小就遍历几次,而这样的时间复杂度挺大,效率不高

下面我们根据上面分解算法的实现,来实现递归求解
分解算法可以将数组按照大小分为两部分,然后当k>pos时只需要在后半部分查找;小于等于时在前半部分查找。
而要实现这种算法我们需要关注k,因为在后半部分时控制台传入的k值和我们右半部分实际的k值不等了,这个k是逻辑下标,比如在原本的大数组是第七小,在后半段数组中就是第二小
当数组个数为1且k==1时返回

 public static int Select_KMin(int []ar,int left,int right,int k){

        return Select_K(ar ,0,ar .length -1,k);
    }
    //k是逻辑下标,index是物理坐标
    //这个方法的时间复杂度很小《O(n)
    //因为化区域查找
    public static int Select_K(int []ar,int left,int right,int k){
        if(left ==right && k==1)return ar [left ];
        int index=Partition(ar ,left ,right ) ;
        int pos=index -left +1;
        if(k<=pos){
            return Select_K(ar ,left ,index ,k);
        }else {
            return Select_K(ar ,index +1,right ,k-pos);
        }
    }
第二种分解函数

和上面的i,j控制的分解函数不一样,这个是只用i控制,所以不同于上一个两头开花,这个是单向遍历
为什么要写这个方法呢?数组可以从i++,j–就是每个元素有前驱和后继,而只有i++控制后继,什么结构有这个性质呢?单链表,所以可以用到单链表排序

 public static int OnePartition(int []ar,int left,int right){
        int temp=ar[left ];
        int i=left ,j=i+1;
        while (j<=right ){
            if(ar [j]<=temp ){
                i++;
                if(i!=j){
                    swap_ar(ar,i,j);
                }
            }
            j++;
        }
        swap_ar(ar, left, i);
        return i;
    }

链表中实现

public static ListNode ListPartition(ListNode left, ListNode right) {
        int tmp = left.Value;
        ListNode low = left, h = low.Next;
        while (h != right) {
            if (h.Value <= tmp) {
                low = low.Next;
                if (low != h) {
                    swap_list(low, h);
                }
            }
            h = h.Next;
        }
        swap_list(left, low);
        reutrn low;
    }

    public static void ListQuickSort(ListNode left, ListNode right) {
        if (left != right) {
            ListNode pos = ListPartition(left, right);
            ListQuickSort(left, pos);
            ListQuickSort(pos.Next, right);
        }
    }
随机数分解函数
 public static int RandPartition(int[] ar, int left, int right) {
        Random rad = new Random();
        int index = rad.nextInt(right - left + 1) + left;
        int tmp = ar[left];
        ar[left] = ar[index];
        ar[index] = tmp;
        return Partition(ar, left, right);
    }

最接近点对问题

给定平面n个点,找其中的一对点,使得在n个点组成的所有点对中,该点对间距离最小。
用遍历求出每两个点之间的距离的时间复杂度是O(n^2)
分治思想求解的时间复杂度是O(nlogn)

一维的情况下:可以由之前的分解函数继续想
算出中位数,把空间分成三个区域:中位数之前,中位数,中位数之后
把第一个和第三个区域的最小值算出,然后将第一个区域的最大值与第二个区域的最小值相减,比较这三个之间的差哪个更小。

 static final int maxint = 0x7fffffff;
    public static int Cpair(int []ar,int left,int right){
        if(right -left <1)return maxint ;//当只有一个或者没有元素时直接返回
        int k=(right -left +1)/2+left ;
        Select_K(ar ,left ,right ,k);
        int index=left +k-1;

        int d1=Cpair(ar ,left ,index );
        int d2=Cpair(ar ,index +1,right );
        int maxs1=MaxS1(ar ,left ,index );
        int mins2=MinS2(ar,index +1,right );
        return Min3C(d1,d2,mins2 -maxs1 );
    }
    private static int MaxS1(int[] ar, int left, int index) {
        return ar[index ];
    }

    private static int MinS2(int[] ar, int index, int right) {
        int temp=ar [index ];
        for(int i=index +1;i<right ;i++){
            if(ar[i]<temp ){
                temp =ar[i];
            }
        }
        return temp ;
    }
    private static int Min2C(int a,int b){
        return a<b?a:b;
    }

    private static int Min3C(int d1, int d2, int c) {
        return Min2C(Min2C(d1,d2),c );

    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值