算法学习:起点

从接触软件开始,老师就教过程序=数据结构+算法。这一著名的公式概括了程序的本质。可是我在工作并没有用到什么算法,现在仔细想想就是不停的堆砌已有的API,编写一堆所谓的代码而已,在发现身边的人工作了几年却还在和我做同样的事情之后,我开始越来越不安。尽管现在我对自己也谈不上有什么规划,但我清楚这绝不是我想做的工作。所以现在通过一个简单的算法优化过程,再次学习这程序的二分之一。
闲话少说,进入正题。
话说如果有个需求:将写有数字k1,k2…kn的n个纸片放入口袋中,可以从口袋中去4次纸片,每次几下纸片上数字后将纸片再放回口袋。如果四个数字的和是m,就输出true,否则就输出false。检查是否有输出true的可能性。
样例1:输入{ 1, 3, 5 },10,则输出true
样例2:输入{ 1, 3, 5 },9,则输出false

一、O(n4)

对于这个问题,最简单的就是采用多重循环来完成,也是做容易的想到的。

private static boolean drawMethod(int[] n, int m) {
        boolean result = false;
        // 循环枚举所有可能
        for (int a = 0; a < n.length; a++) {
            for (int b = 0; b < n.length; b++) {
                for (int c = 0; c < n.length; c++) {
                    for (int d = 0; d < n.length; d++) {
                        if (n[a] + n[b] + n[c] + n[d] == m) {
                            result = true;
                        }
                    }
                }
            }
        }
        return result;
    }

以上解法的时间复杂度可以很容易得出—O(n4)。
可是,程序的运行都是有时间限制的。对于以上的解决方法n<50不用1秒应该就能运行完成。但是如果n<1000呢?将1000带入n4得到1012,可见四重循环还是需要运行一段时间的。这个时候我们就应该思考是否有更高效的方法来实现需求。
通过观察发现,上述四重循环最内侧做的事情就是:
检查是否有d使得n[a] + n[b] + n[c] + n[d] = m
通过对式子进行移向,得到:
检查是否有d使得n[d] = m - n[a] - n[b] - n[c]
换句话说就是,检查数组中是否有元素m - n[a] - n[b] - n[c]。
这时候,我们就可以考虑一种快速检查的方法了。

二、O(n3logn)

关于在查找,比较容易想到,速度也比较快的就是二分查找。二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

    /**
     * 递归二分查找
     * 
     * @param Array
     * @param low
     * @param high
     * @param key
     * @return
     */
    private static boolean binarySearch(int Array[], int low, int high, int key) {
        if (low <= high) {
            int mid = (low + high) / 2;
            if (key == Array[mid])
                return true;
            else if (key < Array[mid])
                // 移动low和high
                return binarySearch(Array, low, mid - 1, key);
            else if (key > Array[mid])
                return binarySearch(Array, mid + 1, high, key);
        }
        return false;
    }

二分查找每次将查找的区域缩小至原来的一半,因此,要判断长度为n的有序数列k中是否有x,只要反复执行log2n次就可以完成了。所以二分查找的时间复杂度是O(logn),这种阶的运行时间被称为对数时间。这样即使n变得恨到,对数时间也会非常迅速。

private static boolean drawMethod2(int[] n, int m) {
        boolean result = false;
        Arrays.sort(n); // 进行排序

        for (int a = 0; a < n.length; a++) {
            for (int b = 0; b < n.length; b++) {
                for (int c = 0; c < n.length; c++) {
                    if (binarySearch(n, 0, n.length - 1, m - n[a] - n[b] - n[c])) {
                        result = true;
                    }
                }
            }
        }

        return result;
    }

这样的话,将内侧循环替换为二分查找之后,就变成
排序时间:O(nlogn)
循环时间:O(n3logn)
O(n3logn)比O(nlogn)大,所以算法的时间复杂度就是O(n3logn)。

三、O(n2logn)

但是,将1000带入O(n3logn),会发现这依然是有很大的时间开销,必须对算法进一步优化。
以上,是对四重循环的最内侧循环,如果着眼于内层的两个循环。
一刚才同一思路,
检查是否有c和d使得n[d] + n[c] = m - n[a] - n[b]
很明显,这样不能直接使用刚才的二分查找了。但是,如果可以预先将n[d] + n[c]的结果枚举并排好序,刚才的思路就行的通了。
将n[d] + n[c]结果枚举,去除重复后个数为n(n+1)/2个。

private static boolean drawMethod3(int[] n, int m) {
        boolean result = false;
        int nn[] = new int[n.length*n.length] ;
        // 枚举n[c]+n[d]的和
        for (int c = 0; c < n.length; c++) {
            for (int d = 0; d < n.length; d++) {
                nn[c * n.length + d] = n[c] + n[d];
            }
        }
        Arrays.sort(nn);
        for (int a = 0; a < n.length; a++) {
            for (int b = 0; b < n.length; b++) {
                if (binarySearch(nn, 0, nn.length - 1, m - n[a] - n[b])) {
                    result = true;
                }
            }
        }

        return result;
    }

这时:
排序时间:O(n2logn)
循环时间:O(n2logn)
总的来说,算法的时间复杂度就是O(n2logn)。这样的话,n=1000也能够妥善应对了。

四、小结

通过以上一步步的优化,总的来说在时间复杂度上已经下降很多,也算是我的第一步,当然很多事情都不能一蹴而就,积跬步才能至千里,希望可以在这条路上走的更远。

完整代码:抽签算法代码

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值