编程珠玑第八章-性能之算法设计技术

说明:以下arr[0…i]均表示数组元素arr[0]到arr[i]的和
一.给定一个n元的向量(正负值),如何求最大的连续子向量?
首先贴一下书中伪代码的Java实现及我的理解

package chapter8;

import javax.xml.crypto.AlgorithmMethod;

//给定n个具有浮点数的向量,求出连续子向量值的最大和 
//划归为计算arr[i]到arr[j]的和问题
public class t0 {
    //第一个算法就是先用两个循环把i,j的可能变化的边界确定下来
    //  再用第三个循环将在i到j之间的值累加起来暂存到sum中 (注意此时在计算一次arr[i...j] sum的值都会先清零),再在maxSoFar和sum中取大者
    public static void alg0(float arr[]){
        long currenTime=System.currentTimeMillis();
        float sum=0;
        float maxSoFar=0;
        for(int i=0;i<arr.length;i++){
            for(int j=i;j<arr.length;j++){
                sum=0;//注意此时在计算一次arr[i...j] sum的值都会先清零
                for(int k=i;k<=j;k++){
                    sum+=arr[k];
                }
                maxSoFar=Math.max(sum, maxSoFar);
            }
        }
        System.out.println(maxSoFar);
        System.out.println(System.currentTimeMillis()-currenTime);
    }
    //优化一的算法思想就是把握住arr[i]到arr[j]的和 与 arr[i]到arr[j-1]之间的关系,即arr[i...j]=arr[i...j-1]+arr[j];
    //并且每一次都将结果暂存在sum中,避免重复地进行计算**(算法设计技术1)**
    // 所以想用一个循环确定i的值,然后在用一个循环表示j的变化范围
    //此时就要sum的值就不能在每一次循环时清零了,因为他要暂存 arr[i]到arr[j-1]的值,以便得到arr[i...j]的值
    public static void alg1(float arr[]){
        long currenTime=System.currentTimeMillis();
        float sum=0;
        float maxSoFar=0;
        for(int i=0;i<arr.length;i++){
            sum=0;
            for(int j=i;j<arr.length;j++){
                sum+=arr[j];
                maxSoFar=Math.max(sum, maxSoFar);
            }
        }
        System.out.println(maxSoFar);
        System.out.println(System.currentTimeMillis()-currenTime);
    }
    //优化二的算法就是利用arr[i...j]=arr[0...j]-arr[0,i-1]这一关系来得到
    //因为arr[0...j]及arr[0,i-1]都可以用一个循环事先计算并保存在一个数组里,
    //因而这里就用sumArr数组来讲信息预处理并且放入对应的元素下标里**(算法设计技术2)**
    //最后就只需要两个循环来分别确定i和j了;
    public static void alg2(float arr[]){
        long currenTime=System.currentTimeMillis();
        float sumArr[]=new float[arr.length];
        sumArr[0]=arr[0];
        float maxSoFar=0;
        for(int i=1;i<arr.length;i++){
            sumArr[i]=sumArr[i-1]+arr[i];
        }
        for(int i=0;i<arr.length;i++){
            for(int j=i;j<arr.length;j++){
                if(i==0){//sumArr[j]-sumArr[i-1]中i必须大于等于1,防止数组越界添加了这个if
                    maxSoFar=Math.max(maxSoFar, sumArr[j]);
                }
                else {
                    maxSoFar=Math.max(maxSoFar, sumArr[j]-sumArr[i-1]);
                }
            }
        }
        System.out.println(maxSoFar);
        System.out.println(System.currentTimeMillis()-currenTime);
    }
    //利用分治法来(**算法设计技术3**)求解,因为分治法函数一般规模为每一次的操作数*O(logn),所以只要控制每一次操作数的规模不大于O(n),就有望将整个时间复杂度降低到O(nlogn)
    //将原来数组一分为二,即arr[l...m],arr[m+1,r] m=1/2(l+r);
    //然而最大连续子向量可能在一半在此时的左部分,一半在右部分,所以此时还要算出中间点左边最大的子向量和lmax与中间点右边最大子向量和rmax
    //所以最终就在arr[l...m],arr[m+1,r],lmax+rmax三者之间取大者
    //最后考虑一下小向量的情况,如果l>r,返回0;如果只有一个数时,正数就是其本身,负数就取0;当然小向量的情况最先写
    public static float alg3(float arr[],int l,int r){
        if(l>r){
            return 0;
        }
        if(l==r){
            return Math.max(0, arr[l]);
        }
        int m=(l+r)/2;
        int lmax=0,sum,rmax=0;
        sum=0;
        for(int i=m;i>=l;i--){
            sum+=arr[i];
            lmax=Math.max(lmax, sum);
        }
        sum=0;
        for(int i=m+1;i<=r;i++){
            sum+=arr[i];
            rmax=Math.max(rmax, sum);
        }
        float temp=Math.max(alg3(arr, l, m), alg3(arr, m+1, r));
        return Math.max(temp, lmax+rmax);
    }
    //**扫描算法**,对于maxEndingHere的理解;
    //在第一个循环赋值语句进行之前,maxEndingHere实际上是以arr[i-1]为尾,向前找到的最大子向量和
    //第一条赋值语句执行完毕之后,maxEndingHere变为以arr[i]为尾,向前找到的最大子向量和
    //而后的maxSoFar就被赋值成了以arr[i]为尾,向前找到的最大子向量和之前maxSoFar的大者
    public static void alg4(float arr[]){
        long currenTime=System.currentTimeMillis();
        float maxSoFar=0;
        float maxEndingHere=0;
        for(int i=0;i<arr.length;i++){
            maxEndingHere=Math.max(arr[i]+maxEndingHere,0);
            maxSoFar=Math.max(maxSoFar, maxEndingHere);
        }
        System.out.println(maxSoFar);
        System.out.println(System.currentTimeMillis()-currenTime);
    }
    public static void main(String args[]){
        float arr[]={31,-24,97,56,-65,73,-98,24,-100,170,-180,32};
        alg0(arr);
        alg1(arr);
        alg2(arr);
        long currenTime=System.currentTimeMillis();
        System.out.println(alg3(arr, 0, arr.length-1));
        System.out.println(System.currentTimeMillis()-currenTime);
        alg4(arr);
    }
}

运行结果

170.0
3
170.0
0
170.0
0
170.0
0
170.0
0

二.对于以上算法的总结
0.算法的思想。也就是要如何解决的思路,不同的思路一般就决定了时间复杂度的大小。例如,alg0最简单,不需要付出太多的思考就可以得到,但是它的算法时间复杂度竟然达到了O(n^3),所以这时候你就必须另辟蹊径了。其他下面的算法充分利用给定n元向量之间的性质。如,优化一alg1的算法思想就是利用arr[i…j]=arr[i…j-1]+arr[j];alg2算法利用了arr[i…j]=arr[0…j]-arr[0,i-1];alg3利用可以问题经过一定处理便可以分而治之;alg4中maxEndingHere的巧妙使用等等,接下来才是下面的这些。。
1.保存状态,避免重复计算。例如优化一alg0算法的sum的使用
2.将信息进行预处理并保存到指定的数据结构。如alg1算法将arr[0…i]的值预先处理保存到sumArr里
3.分治算法。一般如果将一次算法的规模控制在n以后,并且保持基本上是将原问题对半缩小,即满足
T(n)=2T(n/2)+O(n)这样的算法一般都会达到O(nlogn)的时间渐进

可以看出,一般来讲对一个问题的解决思路的不同会导致算法的不同,更多地效率高的算法有时候并不是那么容易想到,甚至不是那么好理解。还是多编多体悟总结为好。但是可以明确时间复杂度太大的一般都必须找到优化算法,不然随着n的增长就不会好用了(当然有些问题就是难解的。。。)
10.如何查找总和最接近0的连续子向量
就是找到找到arr[0…i]-arr[0…j]最接近零的,然后我们就知道arr[j+1,i]就近似等于0,即为所求

package chapter8;

import java.math.BigDecimal;
import java.util.Arrays;

class Sum_Index implements Comparable<Sum_Index>{//类Sum_Index来存储和及下标
    public float sum;
    public  int index;
    @Override
    public int compareTo(Sum_Index other) {
        return new BigDecimal(this.sum).compareTo(new BigDecimal(other.sum));
    }
}
public class t10 {
    //找到arr[0...i]-arr[0...j]近似等于零的,然后我们就知道arr[j+1,i]就近似等于0,即为所求
    //这里因为直接对得到的arr[0...i]进行排序,无法得知他的结束下标,所以就用一个类Sum_Index来存储和及下标
    public static void getTheNearestToZero(float arr[]){
        Sum_Index sumArr[]=new Sum_Index[arr.length];
        float sum=0;
        for(int i=0;i<arr.length;i++){
            sum+=arr[i];
            sumArr[i]=new Sum_Index();
            sumArr[i].sum=sum;
            sumArr[i].index=i;
        }
        Arrays.sort(sumArr);
        int theIndex=0;
        float minSub=sumArr[1].sum-sumArr[0].sum;
        for(int i=2;i<sumArr.length;i++){
            if(minSub>sumArr[i].sum-sumArr[i-1].sum){
                minSub=sumArr[i].sum-sumArr[i-1].sum;
                theIndex=i-1;
            }
        }
        if(sumArr[theIndex].index < sumArr[theIndex+1].index){
            System.out.println("数组里的以下下标对应连 续元素之和最接近零: 从"+(sumArr[theIndex].index+1)+"到 "+sumArr[theIndex+1].index+"的和最接近0");
            for(int i=sumArr[theIndex].index+1;i<=sumArr[theIndex+1].index;i++){
                System.out.print(arr[i]+" ");   
            }
            System.out.println();

        }
        else {
            System.out.println("数组里的以下下标对应连续元素之和最接近零: 从"+(sumArr[theIndex+1].index+1)+"到 "+sumArr[theIndex].index);
            for(int i=sumArr[theIndex+1].index+1;i<=sumArr[theIndex].index;i++){
                System.out.print(arr[i]+" ");   
            }
            System.out.println();
        }
    }
    public static void main(String args[]){
        float arr[]={31,-24,97,56,-65,73,-98,24,-100,170,-180,32};
        getTheNearestToZero(arr);
    }
}

运行结果

数组里的以下下标对应连续元素之和最接近零: 从5到 7
73.0 -98.0 24.0 

11.收费公路计费问题
与上述解决问题的方法类似,假设我们为每个收费站编号0~n,同时用arr[0…i] (0<=i<=n)表示从高速公路第0个收费站到第i个收费站的费用和;所以假设我们的起点收费站是 i,终点是是j; 因为高速公路是双向的,所以i>=j或i<=j都有可能,但是我们需要的是计算费用,所以直接对arr[0…i] -arr[0…j] 取绝对值就好了.程序这样子就应该很好写,仿照上面的一中的alg2就好稍微更改就好。

package chapter8;

public class t11 {
    //feeArr[i] 保存从i收费站到i+1收费站的费用
    public static void alg2(float feeArr[],int startPoint,int endPoint){
        if(startPoint>feeArr.length||startPoint<0){
            System.out.println("Please input the right startPoint");
            return;
        }
        if(endPoint>feeArr.length||endPoint<0){
            System.out.println("Please input the right endPoint");
            return;
        }
        float sumArr[]=new float[feeArr.length+1];
        sumArr[0]=0;
        for(int i=1;i<sumArr.length;i++){
            sumArr[i]=sumArr[i-1]+feeArr[i-1];
        }
        System.out.println("从收费站"+startPoint+"到收费站"+endPoint+"的费用是"+Math.abs(sumArr[startPoint]-sumArr[endPoint])+"¥");
    }
    public static void main(String args[]){
        float feeArr[]={1,2,3,4,5,6,7,8,9};
        alg2(feeArr, 0, 9);
        alg2(feeArr, 0, 10);
        alg2(feeArr, 9, 7);
        alg2(feeArr, 7, 9);
    }
}

运行结果

从收费站0到收费站9的费用是45.0¥  
Please input the right endPoint   
从收费站9到收费站7的费用是17.0¥
从收费站7到收费站9的费用是17.0

14本题应该就是第10题的升级版,第十题是只要找到最接近0的连续子向量,而对该连续子向量的长度没有限制。而本题就是在对该子向量的长度提出了限定,必须是长度为m+1的连续子向量总和,并且然后使得该向量和尽可能靠近零。思路还是和第10题差不多,即arr[0…m+i]-arr[0…i-1]的绝对值最小的。

package chapter8;

import java.math.BigDecimal;
import java.util.Arrays;

//本题应该就是第10题的升级版,第十题是只要找到最接近0的连续子向量,而对该连续子向量的长度没有限制
//而本题就是在对该子向量的长度提出了限定,必须是长度为m+1的连续子向量总和,并且然后使得该向量和尽可能靠近零
//思路还是和第10题差不多,即arr[0...m+i]-arr[0...i-1]的绝对值最小的
public class t14 {
        //找到arr[0...i]-arr[0...j]近似等于零的,然后我们就知道arr[j+1,i]就近似等于0,即为所求
        //这里因为直接对得到的arr[0...i]进行排序,无法得知他的结束下标,所以就用一个类Sum_Index来存储和及下标
    //这里我们就取n为数组的长度减一,即n=arr.length-1; m满足0<=m<=n即可
        public static void getTheNearestToZero1(float arr[],int m){
            if(m<0||m>=arr.length){
                System.out.println("m="+m+"overflow the index of array.Please input it rightly");
            }
            float sumArr[]=new float[arr.length];
            float sum=0;
            for(int i=0;i<arr.length;i++){
                sum+=arr[i];
                sumArr[i]=sum;
            }
            int theIndex = 0;
            float minSub=Math.abs(sumArr[m]);
            for(int i=1;i<arr.length-1-m;i++){
                if(Math.abs(sumArr[i+m]-sumArr[i-1])<minSub){
                    minSub=Math.abs(sumArr[i+m]-sumArr[i-1]);
                    theIndex=i;
                }
            }
            System.out.println("取连续的"+(m+1)+"个子向量: 从下标"+theIndex+"到"+(theIndex+m)+"和最趋近0");
        }
        public static void main(String args[]){
            float arr[]={31,-24,97,56,-65,73,-98,24,2,170,-180,32};
            getTheNearestToZero1(arr,3);
        }
}

运行结果

取连续的4个子向量: 从下标5到8和最趋近0

本章总结
算法的确是一门艺术,原问题的稍微转变可能就需要重新设计算法以使算法效率达到最优化。当面对一个新的问题,首先还是看他的问题定义,理解其中的特殊要求及特性,然后利用所有的已知条件和自己的经验进行设计验证。多思考多积累多总结吧,加油!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值