您的位置:首页 > 其它

算法学习:起点

2015-05-24 21:23 183 查看
从接触软件开始,老师就教过程序=数据结构+算法。这一著名的公式概括了程序的本质。可是我在工作并没有用到什么算法,现在仔细想想就是不停的堆砌已有的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也能够妥善应对了。

四、小结

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

完整代码:抽签算法代码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: