算法的快慢不是一眼看上去就能决定的(一)

  发出标题这样的感慨的原因是这两天做的两个小算法题,从我狠狠被打脸的经历中感受到了编程的确是一门需要演算与实践的科学,单凭直觉与经验根本无法判定一个算法的优劣。废话不多说了,今天就先写一下这两个题中的第一个吧。

题目:

输入一个数字N,求小于等于N的所有质数。(真的是非常非常基础的题)

思考

  这道题我总共写了三种方法五种形式(第二三四个是同一种方法),前两种方法是自己想的,最后一种是参照的网上的思路,代码在分析的最后面(我用的C++,求不吐槽,C用的实在不好。因为数据量较大,所以没有显示部分,只打印一下素数的数量来进行核对)。

分别说一下这三种方法的思路:

①   最粗暴简单法:是从2遍历到n,每个数字从2到√n求一遍余数,如果有任意一次整除,就直接进行下一个数字的计算,若遍历到最后还未有整除,则计入素数表。

②   除素数法:准备一个数组用于存放素数,从2开始每个数字a除以素数数组中的不大于√a的每个素数,一旦出现整除就直接跳到下一个数,如果没有能整除的数,就将a加入素数数组。

③   标志位法:准备一个n大小的bool型数组,全部标志为true。进入i的循环(i的范围从2到n),循环中有另一个j的循环(j的范围从2到n/i),每次循环都将数组的i*j位置上的值置为false。整个程序运行完后,只需要统计值为true的元素的下标即可。

(主观臆断的)时间复杂度分析

①   粗暴简单法:公式是1+√2+√3+……√n,估计应该在O(nlog2n)附近,这已经很高了。

②   除素数法:因为质数数组大小的变化没有规律,因而没有准确的公式,但也应该在O(nlog2n)附近,而且绝对比①要小。

③   标志位法:应该是n(1/1+1/2+1/3+……+1/n)是一个数项级数,表示完全不知道该怎么求了,而且公式输入到mathematics里面之后直接算不出东西来,凭直觉觉得应该会比第二种方法时间复杂度高,而且还有频繁的赋值操作,速度肯定慢。(就是在这里逗比的)

实现代码

代码自带计时功能。

  1 #include <iostream>
  2 #include <ctime>
  3 #include <vector>
  4 #include <memory.h>
  5 #include <cmath>
  6
  7 #define TOP_NUM 1500000
  8 #define ARRAY_SIZE 150000
  9
 10 using namespace std;
 11
 12 int find_by_normal_loop()
 13 {
 14     int prime_count = 0;
 15     bool isPrime = true;
 16
 17     for(int i=2; i<TOP_NUM; ++i)
 18     {
 19         isPrime = true;
 20
 21         for(int j=2; j<=sqrt(i); ++j)
 22         {
 23             if(0 == i%j)
 24             {
 25                 isPrime = false;
 26                 break;
 27             }
 28         }
 29
 30         if(isPrime)
 31         {
 32             ++prime_count;
 33         }
 34     }
 35
 36     return prime_count;
 37 }
 38
 39 int find_by_division_vector()
 40 {
 41     vector<int> res_vec;
 42     bool isPrime = true;
 43
 44     for(int i=2; i<TOP_NUM; ++i)
 45     {
 46         isPrime = true;
 47
 48         for(unsigned int j=0; j<res_vec.size()&&res_vec[j]<=sqrt(i); ++j)
 49         {
 50             if(0 == i%res_vec[j])
 51             {
 52                 isPrime = false;
 53                 break;
 54             }
 55         }
 56
 57         if(isPrime)
 58         {
 59             res_vec.push_back(i);
 60         }
 61     }
 62
 63     return res_vec.size();
 64 }
 65
 66 int find_by_division_vector_iterator()
 67 {
 68     vector<int> res_vec;
 69     vector<int>::iterator itor;
 70     bool isPrime = true;
 71
 72     for(int i=2; i<TOP_NUM; ++i)
 73     {
 74         isPrime = true;
 75
 76         for(itor=res_vec.begin(); res_vec.end()!=itor&&(*itor)<=sqrt(i); ++itor)
 77         {
 78             if(0 == i%(*itor))
 79             {
 80                 isPrime = false;
 81                 break;
 82             }
 83         }
 84
 85         if(isPrime)
 86         {
 87             res_vec.push_back(i);
 88         }
 89     }
 90
 91     return res_vec.size();
 92 }
 93
 94 int find_by_division_array()
 95 {
 96     int res_vec[ARRAY_SIZE] = {0};
 97     bool isPrime = true;
 98     int current_prime_count = 0;
 99
100     for(int i=2; i<TOP_NUM; ++i)
101     {
102         isPrime = true;
103
104         for(int j=0; j<current_prime_count&&res_vec[j]<=sqrt(i); ++j)
105         {
106             if(0 == i%res_vec[j])
107             {
108                 isPrime = false;
109                 break;
110             }
111         }
112
113         if(isPrime)
114         {
115             res_vec[current_prime_count] = i;
116             ++current_prime_count;
117         }
118     }
119
120     return current_prime_count;
121 }
122
123 int find_by_signal_array()
124 {
125     bool numArray[TOP_NUM];
126     int prime_count = 0;
127     memset(numArray, true, sizeof(numArray));
128
129     numArray[0] = false;
130     numArray[1] = false;
131
132     for(int i=2; i<TOP_NUM; ++i)
133     {
134         for(int j=2; i*j<TOP_NUM; ++j)
135         {
136             numArray[i*j] = false;
137         }
138     }
139
140     for(int i=0; i<TOP_NUM; ++i)
141     {
142         if(numArray[i])
143         {
144             ++prime_count;
145         }
146     }
147
148     return prime_count;
149 }
150
151 int main()
152 {
153     cout << "Hello world!" << endl;
154
155     int result_count = 0;
156     clock_t t_begin,t_end;
157
158     t_begin = clock();
159     result_count = find_by_normal_loop();
160     t_end = clock();
161
162     cout << "粗暴法:" << endl << "All count : " << result_count << endl << "Time : " << ( double)( ( t_end-t_begin)/1000.0) << endl;
163
164     t_begin = clock();
165     result_count = find_by_division_vector();
166     t_end = clock();
167
168     cout << "除素数法:" << endl << "All count : " << result_count << endl << "Time : " << ( double)( ( t_end-t_begin)/1000.0) << endl;
169
170     t_begin = clock();
171     result_count = find_by_division_vector_iterator();
172     t_end = clock();
173
174     cout << "除素数法(with iterator):" << endl << "All count : " << result_count << endl << "Time : " << ( double)( ( t_end-t_begin)/1000.0) << endl;
175
176
177     t_begin = clock();
178     result_count = find_by_division_array();
179     t_end = clock();
180
181     cout << "除素数法(with array):" << endl << "All count : " << result_count << endl << "Time : " << ( double)( ( t_end-t_begin)/1000.0) << endl;
182
183     t_begin = clock();
184     result_count = find_by_signal_array();
185     t_end = clock();
186
187     cout << "标志位法:" << endl << "All count : " << result_count << endl << "Time : " << ( double)( ( t_end-t_begin)/1000.0) << endl;
188
189     return 0;
190 }

实现代码

实验环境是Code::Blocks + GCC

  在这里再解释一下三四这两种方法。方法三将遍历vector的方法由下标变成了迭代器iterator。而方法四则是抛弃了vector,直接使用一个数组来存放素数(数组大小图方便直接随手定了个当前数据环境下不会溢出的大小)。

实验结果

下面就是喜闻乐见的被打脸过程了。

数字上限为500000时:

数字上限为1000000时:

数字上限为1500000时:

  被现实狠狠地抽了一巴掌。

  第一种方法确实是最慢的这点毫无疑问。但第二三种方法的效率差别之大还是让人揪心。并且第三种方法的耗时似乎是随数据量增长而线性增长的,而第二种方法似乎并不是这样。。。

  哦,不对,发错了,应该是这个表情,刚才那个是什么我也不知道。

自我检讨

  现在再重新检视代码。

  先比较第二种方法的三个不同形式,首当其冲的就是vector会比普通array效率低大概30%。Vector比直接用数组慢是公认的,但是毕竟不是所有的工程都能提前预知结果的数量,所以vector方法的实用性还是比数组方式高的。而使用迭代器iterator的方法又比普通的vector法慢了40%,这效率简直不能忍了。

  回头再看第一种方法,它比第二种方法慢是必然的,效率大约只有第二种方法的第三种形式的20%多一点。原因就出在它需要比第二种方法多除的那一部分合数上。差距看起来还没有拉到非常夸张的样子,不能说这种方法效率不是太慢,只能说是因为数组的遍历速度拖慢了第二种方法的速度,所以显得差距没有这么大了而已。

  最后就是狠狠打脸的第三种方法。效率高得令人发指,速度是第二种方法最快的第三种形式的三倍。总结问题根源,第三种方法如此之快应该有这么两点原因:①循环中的运算式为乘法,而不是之前几种方法的取余;②函数的时间复杂度计算还是不太熟,这谁主要原因,其次是数项级数到底什么情况还是没有搞清楚,跟头就栽这里了。

  经验教训就是:①赋值操作多的方法不一定效率慢;②除法实在比乘法慢太多,能不用就不用;③数学一定要学好;④不要总是瞧不起空间复杂度很高的方法。

时间: 2024-08-15 05:17:31

算法的快慢不是一眼看上去就能决定的(一)的相关文章

2、数据结构与算法之大O表示法

一.大O表示法 大O表示法不是一种算法.它是用来表示一个算法解决问题的速度的快慢.一般我们描述一件事情完成的快慢是用时间描述的,比如说我完成一道计算题用了多少分钟.但算法的运算是很难用准确的时间来描述的,所以我们就用算法解决问题一共用了多少步来表示算法的快慢. 用第一篇的两种查找方法来举例,简单查找我们要用列表中的每一个元素逐一去比较,如果有n个元素,那么简单查找最多需要n步找到数据(数据在列表末尾).而二分查找一次只用中位数去作比较,当查找有n个元素的数组时,最多需要log2n次. 用大O表示

算法第四章实践报告

1.选择第二题进行分析. 给定n位正整数a,去掉其中任意k≤n 个数字后,剩下的数字按原次序排列组成一个新 的正整数.对于给定的n位正整数a和正整数 k,设计一个算法找出剩下数字组成的新数最 小的删数方案. 输入 178543 4 输出132.问题描述:就是删掉指定数字中的一些数字,然后重新组成一个新的数使得这个数达到最小.3.算法描述:这个问题第一眼看下去容易想到对数组不停的排序然后不断删掉最大的,但是按着这个思路去做却发现,不断提取整型数组然后排序会比较复杂,因为要不断求余数等等,而且代码不

算法图解之大O表示法

什么是大O表示法 大O表示法可以告诉我们算法的快慢. 大O比较的是操作数,它指出了算法运行时间的增速. O(n) 括号里的是操作数. 举例 画一个16个格子的网格,下面分别列举几种不同的画法,并用大O表示法表示 1.  一次画一个格子.O(n) 2. 折叠纸张,折叠四次就能出现16个格子.O(log n) 大O表示法所表示的是一个算法在最糟糕情况下的运行时间. 一些常见的大O运行时间 O(log n),也叫对数时间,二分查找. O(n),也叫线性时间,简单查找. O(n * log n),快速排

判断单向链表中是否有环和查找环的入口

快慢指针 算法描述 定义两个指针slow, fast.slow指针一次走1个结点,fast指针一次走2个结点.如果链表中有环,那么慢指针一定会再某一个时刻追上快指针(slow == fast).如果没有环,则快指针会第一个走到NULL. 实现 结点定义如下: class Node { public Node next; public Object data; public static int sequence = 0; } 算法: /** * 快慢指针 * @param head * @ret

推断单向链表中是否有环和查找环的入口

快慢指针 算法描写叙述 定义两个指针slow, fast. slow指针一次走1个结点,fast指针一次走2个结点.假设链表中有环,那么慢指针一定会再某一个时刻追上快指针(slow == fast).假设没有环,则快指针会第一个走到NULL. 实现 结点定义例如以下: class Node { public Node next; public Object data; public static int sequence = 0; } 算法: /** * 快慢指针 * @param head *

快速找到未知长度的单链表的中间结点

问题描述:快速找到未知长度的单链表的中间结点 普通方法:首先遍历一遍单链表,以确定单链表的长度L,然后再从头结点出发,循环L/2次,找到单链表的中间结点. 高效算法(快慢指针):设置两个指针,*search,*mid都指向单链表的头结点.其中*search指针的移动速度是*mid指针移动速度的2倍.当*search移动到链表末尾时,*mid刚好在中间结点.(标尺的思想) //快速得到单链表的中间结点 Status GetMidNode(LinkList &L,int &e){ LinkLi

287. 寻找重复数

题目描述 给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数.假设只有一个重复的整数,找出这个重复的数. 示例 1: 输入: [1,3,4,2,2] 输出: 2 示例 2: 输入: [3,1,3,4,2] 输出: 3 说明: 不能更改原数组(假设数组是只读的). 只能使用额外的 O(1) 的空间. 时间复杂度小于 O(n2) . 数组中只有一个重复的数字,但它可能不止重复出现一次. 题意 审题可以发现两个关键点: num

java数据结构知识点自我总结

课前复习:二分查找 时间复杂度(O(N)) 空间复杂度:范围最大的长度复杂度:粗略衡量算法好坏的刻度尺(工具)两个维度:快慢 时间复杂度(重点)使用空间的情况 空间复杂度时间复杂度:直接利用允许时间衡量不现实,测试环境多变,不好控制变量前提:如果指定cpu的情况下,单位时间内运行的基本指令个数是固定的如果一个算法需要的指令比另一个算法需要的指令个数小,就可以推出算法A运行的时间更快前提:算法计算的快慢和输入的数据的规模是有关系的粗略计算算法的快慢:n:数据的规模f(n): n的数据规模情况下,需

算法复习:双指针(对撞指针、快慢指针)

一.快慢指针: leedcode 142. 环形链表 II 快慢指针的思想是设置慢指针slow和快指针fast,slow每次走一步,fast每次走两步,如果有环fast指针和slow指针必然相遇,相遇时 定义新的指针p从head开始和slow从当前位置起每次都走一步,直到相遇,相遇的位置就是环的入口. class Solution { public: ListNode *detectCycle(ListNode *head) { int lable=0; struct ListNode *slo