算法介绍1-二分查找、大O表示法
title: 算法介绍1-二分查找、大O表示法
date: 2019-12-27 13:12:36
tags: 技术
前言
本人对算法有些兴趣,正好最近不知道写什么,所以就想介绍一些算法,内容大部分取自《算法图解》——一本很生动的算法介绍书。书里面的代码是用python所写,但因为现在大家学的几乎都是C语言,所以代码部分我将用C语言重现一遍,如果对文章内容和代码有疑问的欢迎评论或者私信我哦。
二分查找
又假设要在字典中找一个以O开头的单词,你也将从中间附近开始。
这是一个查找问题,在上述情况下,都可以使用同一种算法来解决问题,这种算法就是二分查找。
二分查找是一种算法,其输入是一个有序的元素列表。如果要查找的元素包含在列表中,二分查找返回其位置,否则返回NULL
。
下面的示例说明了二分查找的工作原理。我随便想一个1~100的数字。
你的目标是以最少的次数猜到这个数字。你每次猜测后,我会说小了、大了或对了。
假设你从1开始往上猜,猜测过程会是这样:
你:“1”, 我:“小了”
你:“2”, 我:“小了“
.
.
.
你:“7”, 我:“小了”, 你:“我佛辣...”
更佳的查找方式
下面是一种更好的猜法。从50开始。
你:“50”, 我:“小了”
小了,但排除了一半的数字!至此,你知道1~50都小了。接下来,你猜75。
你:“75”, 我:“大了”
大了!那剩余的数字又排除了一半!使用二分查找时,你猜测的是中间的数字,从而每次都将余下的数字排除一半。接下来,你猜63 (50和75中间的数字)。
你:“63”, 我:“大了”
你:“57”, 我:“对了!”
这就是二分查找!恭喜你学会了第一种算法!每次猜测排除的数字个数如下。
在1~100中,不管我心里想的是哪个数字,你在7次之内都可以猜到,因为每次猜测都可以排除很多数字!
假设你要在字典中查找一个单词,而该字典包含240 000个单词,你认为每种查找最多需要多少步?
-
如果要查找的单词位于字典末尾,使用简单查找将需要240 000步。 -
使用二分查找时,每次排除一半单词,直到最后只剩下一个单词。
每次排除一半,那就相当于除以2,如此往复除以2,显然我们使用二分查找的步数,就是log2n步,而简单查找最多需要 n 步。
说明:仅当列表是有序的时候,二分查找才有用处。例如,字典中的单词是按字母顺序排列的,因此可以使用二分查找来查找单词。如果不是按顺序排列的,结果将如何呢?
下面来看看如何编写执行二分查找的C语言代码。这里的代码示例使用了数组。如果你不熟悉数组也不用担心,下次的推文就会介绍。你只需要知道,可将一系列元素存储在一系列相邻的桶(bucket),即数组中。这些桶从0开始编号:第一个桶的位置为#0,第二个桶为#1,第三个桶为#2,以此类推。
函数binary_search
接收一个有序数组和一个元素。如果指定的元素包含在数组中,这个函数返回其位置。你将跟踪要在其中查找的数组部分——开始时为整个数组。
low = 0;
high = sizeof(arr) - 1; //arr为要查找的数组
你每次都检查中间的元素
mid = (low + (high - low)) / 2;
guess = arr[mid];
如果猜的数字guess
小了,就相应修改high
if (guess < arr[mid]) //猜的数字小于中间的数,这时修改high为中间的位置
{
high = mid - 1; //用mid - 1是因为mid这个位置的数也被排除,所以可以省去
}
如果猜的数字大了,就修改low
。完整的代码如下。
int binary_search(int* p, int guess) //传入的p是一个数组
{
int low = 0, high = sizeof(p) - 1;
int mid;
while (low <= high) //只要范围没有缩小到只含有一个元素
{
mid = (low + (high - low)) / 2; //就检查中间的元素
if (p[mid] == guess) //找到了元素
{
return mid;
}
if (guess < p[mid]) //猜的数字小了
{
high = mid - 1;
}
else //猜的数字大了
{
low = mid + 1;
}
}
return -1; //没有指定元素
}
完整代码如下(可以结合我的动态数组推文食用)
#include <stdio.h>
#include <stdlib.h>
int binary_search(int* p, int guess)
{
int low = 0, high = sizeof(p) - 1;
int mid;
while (low <= high)
{
mid = (low + (high - low)) / 2;
if (p[mid] == guess)
{
return mid;
}
if (guess < p[mid])
{
high = mid - 1;
}
else
{
low = mid + 1;
}
}
return -1;
}
int main(void)
{
int* arr;
int n;
int result, guess;
printf("Please input the size of the array\n");
scanf("%d", &n);
arr = (int*)malloc(n * sizeof(int));
printf("Please input the numbers in order:\n");
for (int i = 0; i < n; i++)
{
scanf("%d", &arr[i]);
}
printf("Input the number you want to search:\n");
scanf("%d", &guess);
if (binary_search(arr, guess) >= 0)
{
printf("Find it ! The position is : %d\n", binary_search(arr, guess));
}
else
{
printf("SORRY~I cannot find it !\n");
}
free(arr);
return 0;
}
练习
-
假设有一个包含128个名字的有序列表,你要使用二分查找在其中查找一个名字,请问最多需要几步才能找到? -
上面列表的长度翻倍后,最多需要几步?
运行时间
每次介绍算法时,我们都将讨论其运行时间。一般而言,应 选择效率最高的算法,以最大限度地减少运行时间或占用空间。
回到前面的二分查找。使用它可以节省多少时间呢?,简单查找逐个检查数字,如果列表包含100个数字,最多需要猜100次。如果列表包含40亿个数字,最多需要猜40亿次。换言之,最多需要猜测的次数与列表长度相同,这被称为线性时间(linear time)。
二分查找则不同。如果列表包含100个元素,最多要猜7次;如果列表中包含40亿个元素,最多需要猜32次。二分查找的运行时间为对数时间 (或log时间)。
大O表示法
大O表示法是一种特殊的表示法,指出了算法的速度有多快。谁在乎呢?实际上,你经常要 使用别人编写的算法,在这种情况下,知道这些算法的速度大有裨益。
算法的运行时间以不同的速度增加
以上文的二分查找法为例。假设检查一个元素需要1毫秒。使用简单查找时,你必须检查100个元素,因此需要100毫秒才能查找完毕。而使用二分查找时,只需检查7个元素 (log2100大约为7),因此需要7毫秒就能查找完毕。然而,实际要查找的列表可能包含10亿个元素,在这种情况下,简单查找需要的时间太多了。
当你使用包含10亿个元素的列表运行二分查找,运行时间为30毫秒 (log21 000 000 000大约为30)时。你可能心里想,二分查找的速度大约为简单查找的15倍,因为列表包含100个元素时,简单查找需要100毫秒,二分查找需要7毫秒。因此,列表包含10亿个元素时,简单查找需要30 x 15 = 450毫秒,甚至可以忽略!于是你决定使用简单查找。这是正确的选择吗?
不是!实际上你错的离谱。列表包含10亿个元素时,简单查找需要10亿毫秒, 相当于11天!为什么会这样呢?因为二分查找和简单查找的运行时间的增速不同。随着元素数量的增加,二分查找需要的额外时间并不多, 而简单查找需要的额外时间却很多。因此,随着列表的增长,二分查找的速度比简单查找快得多。有鉴于此,仅知道算法 需要多长时间才能运行完毕还不够,还需知道运行时间如何随列表增长 而增加。这正是大O表示法的用武之地。
大O表示法指出了算法有多快。例如,假设列表包含n个元素。简单查找需要检查每个元素,因此需要执行n次操作。使用大O表示法,这个运行时间为O(n)。单位秒呢?没有——大O表示法指的并非以秒为单位的速度。大O表示法让你能够比较操作数,它指出了算法运行时间的增速。
举个例子,检查长度为n的列表,二分查找需要执行log2n次。使用大O表示法,这个运行时间怎么表示?
O(log2n)
大O表示法指出了最糟糕情况下的运行时间
简单查找的运行时间总是为O(n),因为大O表示法说的是最糟的情形。因此,你可以说,这是一个保证——你知道简单查找的运行时间不可能超过O(n)。
一些常见的大O运行时间
下面按从快到慢的顺序列出了你经常会遇到的5种大O运行时间
-
O(log n),也叫 对数时间,这样的算法包括二分算法 -
O(n),也叫 线性时间,这样的算法包括简单查找 -
O(n * log n),这样的算法包括 快速排序——一种速度较快的排序算法 -
O(n2),这样的算法包括 选择排序——一种速度较慢的排序算法 -
O(n!),这样的算法包括下一次将介绍的 旅行商问题的解决方案——一种非常慢的算法
还有其他的运行时间,但是这5种是最常见的。
实际上,我们不能如此干净利索地将大O运行时间转换为操作数,但就目前而言,这种准确度足够了。等你学习其他一些算法后,到后面我们将回过头来再次讨论大O表示法。当前,我们获得的主要启示如下。
-
算法的速度指的并非时间,而是操作数的增速。 -
谈论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加。 -
算法的运行时间用大O表示法表示。 -
O(log n) 比 O(n) 快,当需要搜索的元素越多时,前者比后者快得越多。
练习
使用大O表示法给出下述各种情形的运行时间。
-
在电话簿中根据名字查找电话号码。 -
在电话簿中根据电话号码找人。(提示:你必须查找整个电话簿。) -
阅读电话簿中每个人的电话号码。 -
阅读电话簿中姓名以A打头的人的电话号码。这个问题比较棘手,它的答案可能让你感到惊讶!
—— E N D ——
文字:DrPirate2931
长
按
关
注
到这儿来嘛
ID : daozherlaima
爱好游戏、健身、电影、敲代码