《计算之魂》读书笔记

1.3 怎样寻找最好的算法

方法1,做一次三重循环,其实就是中学里学的排列组合的方法。 我们假设这个序列有K个数,依次是a1,a1,a2,…,aK。假定区间起始的数字序号为p,结束的数字序号为q,这些数字的总和为S(p,q),则S(p,q)=ap+ap+1+…+aq。 p可以从1一直到K,q可以从p一直到K,这是两重循环了,因此区间一头一尾的组合有O(K2)种。在每一种组合中,计算S(p,q)平均要做K/4次加法,这是又一重循环。因此这种算法的复杂度是O(K3)。 这个方法虽然完成了任务,但是做了太多的无用功。如果遇上通用电气(GE)这样的百年老店,即使只考虑每天的收盘数据,也有几万个数据点,几万的三次方可是几十万亿,计算量非常大。如果你只能想出这种方法,那还没有达到五级工程师的要求,因为你还完全没有计算机科学的概念。当然,这种方法稍加改进就能快很多,于是就有了下面的方法2。

伪代码

    public static void main(String[] args) {

        // 假设的数组
        double[] nums = {1.5, -12.3, 3.2, -5.5, 23.2, 3.2, -1.4, -12.2, 34.2, 5.4, -7.8, 1.1, -4.9};
        // 总的组合数
        int combination = 0;
        // 总的计算次数
        int calculateNum = 0;
        // K就是这个数组的长度,也就是文中的序列个数
        int k = nums.length;
        // 最大组合的总和
        double maxSpq = 0;
        // 组合的总和
        double spq = 0;
        // 最大组合总和中p在数组中下标
        int minNum = 0;
        // 最大组合总和中q在数组中下标
        int maxNum = 0;
        // p可以从1到K,数组是以0开始计位
        for (int p = 0; p < k; p++) {
            // 相当于p-q
            for (int q = p; q < k; q++) {
                double pValue = nums[p];
                spq = nums[q];
                combination += 1;
                // 相当于计算S(p,q)总和
                for (int sum = p + 1; sum <= q; sum++) {
                    // spq = S(p,q)
                    pValue = pValue + nums[sum];
                    spq = pValue;
                    calculateNum += 1;
                }
                // 比较出最大的spq
                if (spq > maxSpq) {
                    minNum = p;
                    maxNum = q;
                    maxSpq = spq;
                }
            }
        }

        double[] maxNums = Arrays.copyOfRange(nums, minNum, maxNum + 1);
        System.out.println("K = " + k + ",总计算次数 = " + calculateNum + ",总组合数 = " + combination + ",最大总和数 = " + maxSpq
        + ",最大的区间 = " + Arrays.toString(maxNums) + ",区间中开始下标: = " + minNum +",区间结束下标 = " + maxNum);
    }

在这里插入图片描述

笔记:
为什么文中说一头一尾的组合有K2种,但是我写的伪代码中组合才91,远远达不到132 = 169种,因为他是算正反向组合,就是说我代码中的91 = 13(K) + 78(正向两个数或以上个数的元素组成),还需要91 + 78(反向元素组成数) = 169
平均要做K/4次加法,实际并不等于K/4,而是在K假定是无限大的时候,无限接近于K/4
文中组合有O(K2) 种我觉得写得并不严谨,多少种应该是一个值,并不是复杂度,所以这里应该直接用K^2就可以,容易误导读者。

方法2,做两重循环。 方法1效率不高的原因是做了太多的无用功,比如当我们把区间的起点定在了位置p之后,如果已经计算了从p到q之间的数字的总和S(p,q),下次再计算从p到q+1之间的数字的总和S(p,q+1)时,只需要在原来的基础上再做一次加法,而不需要再来一次循环。当然有人可能会担心,这样是否需要占用额外的存储空间来保留所有的中间结果S(p,q)。其实这种担心是不必要的,因为我们只需要记录这样三个中间值。 第一个值是从p开始到当前位置q为止的总和S(p,q),因为我们接下来计算S(p,q+1)时要用到它。 第二个值则是从p开始到当前位置q为止所有总和中最大的那个值,我们假定为Max。有了这个值之后,如果S(p,q+1)≤Max,则Max维持不变;如果S(p,q+1)>Max,则要更新Max,当然,我们也要记录下来Max是在区间[p,q+1]取得的。 因此,第三个要记录的值就是区间结束的位置,我们不妨以r来表示。如果Max的值更新了,相应的区间结束位置也要更新为q+1。 我们不妨看这样一个具体的例子。 假定区间的起始点是p=500,这时S(500,500)=a500,Max=a500,r=500。接下来,遇到了第501个数字,如果a501>0,显然S(500,501)>S(500,500),于是记下到当前为止最大的区间总和Max=S(500,501),r=501;如果a501≤0,我们知道到当前为止最大的区间总和依然是Max=S(500,500),不需要做任何改变。再往后,遇到第502个数字时,我们只需算出S(500,502),如果S(500,502)>Max,则更新Max,并且记录下r=502,否则维持原来的Max和r,然后继续往后扫描。 对于给定的p,需要从头到尾试K−p次,也就是O(K)的复杂度。而p可以从1到K,有K种可能性,二者的组合就是O(K2)。如果K有好几万,计算量是十几亿,而方法1的计算量是它的上万倍。如果你能想到这种方法,那就基本上达到了五级工程师的要求,因为你已经搞清楚哪些计算是重复计算了。 当然,O(K2)的答案远非最优,这个问题还有复杂度更低的解法。

伪代码

    public static void main(String[] args) {


        // 假设的数组
        double[] nums = {1.5, -12.3, 3.2, -5.5, 23.2, 3.2, -1.4, -12.2, 34.2, 5.4, -7.8, 1.1, -4.9};
        // 总的组合数
        int combination = 0;
        // 总的计算次数
        int calculateNum = 0;
        // K就是这个数组的长度,也就是文中的序列个数
        int k = nums.length;
        // 最大组合的总和
        double maxSpq = 0;
        // 组合的总和
        double spq = 0;
        // 最大组合总和中p在数组中下标
        int minNum = 0;
        // 最大组合总和中q在数组中下标
        int maxNum = 0;
        // p可以从1到K,数组是以0开始计位
        for (int p = 0; p < k; p++) {
            double pValue = nums[p];
            // 相当于p-q
            for (int q = p; q < k; q++) {
                spq = nums[q];
                combination += 1;
                if(q != p){
                    // spq = S(p,q)
                    pValue = pValue + nums[q];
                    spq = pValue;
                    calculateNum += 1;
                }
                // 比较出最大的spq
                if (spq > maxSpq) {
                    minNum = p;
                    maxNum = q;
                    maxSpq = spq;
                }
            }
        }

        double[] maxNums = Arrays.copyOfRange(nums, minNum, maxNum + 1);
        System.out.println("K = " + k + ",总计算次数 = " + calculateNum + ",总组合数 = " + combination + ",最大总和数 = " + maxSpq
        + ",最大的区间 = " + Arrays.toString(maxNums) + ",区间中开始下标: = " + minNum +",区间结束下标 = " + maxNum);
    }

在这里插入图片描述
笔记:
从结果上对比方法1,总计算次数明显减少了很多,如果让K是无穷的时候,这个效率就天差地别了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值