本人加入程序猿行列已近七年,平日工作虽繁忙,但感觉实际浑浑噩噩,提升十分有限,所以打算开始业余时间提升自己,学习算法相关知识,借此也想与各位共同成长,发表一些自己的学习所得。
学习算法之前,我们首先要知道一个知识点就是,我们该如何判断一个算法的好坏呢?
说起来很简单,好的算法无非是运行时间短、空间占用小。
不过,在没有实际运行之前,我们无法得知程序确切的运行时间,所以便需要在程序执行前,估算它的执行次数。
那么,我们应该如何去估算这个执行的次数呢?
我们先来看一道很简单的题目,给你一个不重复的有序的整型数组,要求查找某个数字在这个数组中的索引。
曾经的我:“这还不简单?瞧不起谁呢!!!看我的大力出奇迹!!!”
根据题意,我们不难得出第一个思路(算法1):
int BruteFind(int* a, int n, int nVal)
{
for (int i = 0; i < n; ++i)
{
if (nVal == a[i])
return i;
}
return -1;
}
面试官:“Emmmmmmm……如此确实可以解决我提出的需求,但你这个算法的效率可不高啊……”
曾经的我:“怕啥?现在的计算机性能越来越高,再慢也就几毫秒,权当是误差了,哈哈!”
面试官:“好吧,那你回去等十万亿毫秒的通知吧……”
我:“啊?不要啊!!!”
……
咳咳,友情提示,大家可不要学曾经的老蛋一样噢。
好,那么言归正传,面试官是如何看出我这个算法的效率不高的?
这就涉及到了我们今天的知识点,时间复杂度。
在这个算法当中,我们不难看出,如果在最坏的情况下,需要循环整个数组,才可以找出正确的数。
我们将这个,记为O(n),表示其时间复杂度。
那么有没有什么更快速的算法能够实现这个需求的呢?
当然有,这就涉及到了第二个算法,二分查找算法。
在实现算法之前,你们可以回忆一下自己有玩过这样的游戏吗?
你在心里默默想一个1000以内数字(比如是420),我来说一些数字,你只要根据你想的数字说大了还是小了,我很快就能猜出你想的那个数字。
这当然可以用我们的算法一去解决,但那并不是最优解,最优的方法应该如下:
(1)我:“500。”
你:“大了。”
(2)我:“250。”
你:“小了。”
(3)我:“375。”
你:“小了。”
(4)我:“437。”
你:“大了。”
(5)我:“406。”
你:“小了。”
(6)我:“421。”
你:“大了。”
(7)我:“413。”
你:“小了。”
(8)我:“417。”
你:“小了。”
(9)我:“419。”
你:“小了。”
(10)我:“420。”
你:“答对了。”
可以看出,这种解法,10次就已经猜出了你想的数字,那么聪明的你应该也发现了它的规律了吧?
没错,因为序列有序,所以我们可以先猜0和1000的平均值500,如果大了,我们就知道数字所处的位置应该是0-499之间,进而猜测0和499的平均值,如果小了,我们就知道数字所处的位置应该是501-1000之间,进而猜测501和1000的平均值……按照这个规律猜测下去,直到找出正确答案。
而我们的二分查找算法所用到的,就是这个原理。
事不宜迟,上代码:
int binary_search(int key, int* a, int n)
{
int low = 0, high = n - 1;
while (low <= high)
{
int mid = low + (high - low) / 2; // 防止溢出
if (key < a[mid])
high = mid - 1;
else if (key > a[mid])
low = mid + 1;
else
return mid;
}
return -1;
}
由于这个算法每次能将数组折半(逻辑折半,并不是物理折半),最坏的情况下折半到最后一个元素才完成查找,因此不难得知时间复杂度应为O(㏒₂n)(n以2为底的对数)。
㏒₂n < n,因此我们可以认为二分查找的效率优于暴力算法,这个是理论上的,实际上并不一定,但普遍是满足这个规律。
那么我们常见的时间复杂度有哪些呢?
(1)O(1),可以认为是没有时间开销,基本是只有基础运算,并未有循环、递归。
(2)O(n),如刚刚暴力查找算法,最坏情况基本是遍历整个数组。
(3)O(㏒₂n),如二分查找算法,每次循环会对数组折半(只是逻辑上的,不是物理上的折半)。
(4)O(n²),如冒泡排序、选择排序、插入排序,基本都是二层循环。
(5)O(n㏒₂n),如快速排序、堆排序、归并排序。
还有更复杂的就暂时不赘述了,这些是我们学习过程中经常遇到的,大致了解即可。
祝大家在算法路上越走越远,越走越顺!!!