本人大概五年前作为初学者接触过时间复杂度的相关概念,但是实际写代码的时候,可能因为领域不同,一直都没有碰到需要设计时间复杂度的问题,都只是需要实现功能就行了,所以一直以来,脑海中的印象仅限于二分法这个词,只记得二分法查找比普通方法更好(你是不是也是这样?)。
近期本人碰到了大规模计算的实际问题,需要考虑算法性能,才切实体会到了合理的时间复杂度确实能极大降低计算时间。
这篇博客偏向于深入理解,帮助初学者避开一些理解误区,不提供代码(因为网上代码太多了)。
那么,回到问题,二分法一定优于遍历吗?
首先,二分法依赖于顺序存储结构,原因在于二分法的执行过程中需要不断通过索引取值,取出的值将作为要查找的值的比较对象。
其次,典型的实际问题通常是,给定一个数组(数组是无序的),需要在数组中查找某个值。
如果用普通方法,那就遍历一遍。如果用二分法,那就先排序,再查找,较好的排序方法的时间复杂度是O(nlogn),查找的时间复杂度是O(logn)(关于两项时间复杂度,这里不展开说明)。
(注意,以下的计算并不是严格的数学推导,只是便于理解两种方法的区别)
假设在n个数中查找一个数,那么,两种方法的时间复杂度分别是
普通方法:遍历O(n)
二分法:排序O(nlogn) + 查找O(logn)
假设这样查找的次数为x次,而且假设x次的时间复杂度可以累计,那么,两种方法累计的时间复杂度分别是
普通方法:xO(n)
二分法:O(nlogn) + xO(logn)
两种方法累计的计算耗时可以近似为
普通方法:xn
二分法:nlogn + xlogn
这时,对于特定的n,将查找次数x视为自变量,计算耗时视为函数值,则普通方法的斜率为n,二分法的斜率为logn,显然普通方法的斜率大于二分法的斜率。
当x=1时,普通方法耗时n < 二分法耗时nlogn + logn
当x=n时,普通方法耗时n*n > 二分法耗时nlogn + nlogn
可以理解,当x等于1~n之间的某个数x’时,如果查找次数再增大,那么二分法才会体现出计算耗时更短的优势。
也就是说,如果查找次数较多(也就是频繁查找),更适合用二分法(先排序一遍,再查找多遍),如果只查找一次,那还不如直接遍历一遍,毕竟二分法的排序耗时就摆在那里。如果加强约束条件,数组自身就是排序好的,那么二分法省去了排序的时间,基本上可以认为二分法比普通方法耗时更短了。
另外,从实际运用角度来说,问题规模需要达到一定数量,才能体现出二分法的优势。
这里做一个测试:
设定数组的长度为val,数组存的值依次为0~val-1,而在数组中,要查找的数值为val-9。例如,val=10^2=100时,数组存的值为0~99,要查找的数值为91。设定查找次数为100次,记录val从10^2~10^8的情况下,两种方法查找的耗时,单位为s。(代码由c++编写,而且这里忽略二分法排序的耗时,数组就是排序好的)
val | 10^2 | 10^3 | 10^4 | 10^5 | 10^6 | 10^7 | 10^8 |
普通方法 | 0.000 | 0.000 | 0.001 | 0.008 | 0.086 | 0.894 | 8.973 |
二分法 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 |
可以看出,在val为10^2~10^6时,两种方法几乎体现不出计算时间的差别,当val=10^8时,才显现出明显的差别。
所以,对于小规模问题,普通方法和二分法,都能快速运行,当问题规模增大到一定程度,二分法才会体现出计算耗时更短的优势,才更适合用二分法。