|
|

) L j3 ]& g, ]) ?; {普通程序员,不学算法,也可以成为大神吗?对不起,这个,绝对不可以。1 e( E7 M5 a# z f5 h( ?
; s ]1 _. L4 k2 R5 Y可是算法好难啊~~看两页书就想睡觉……所以就不学了吗?就一直当普通程序员吗?
6 [' Z# \% j, x# Y8 V" ~+ h3 w! C) w如果有一本算法书,看着很轻松……又有代码示例……又有讲解……9 L" r! @3 A/ B
怎么会有那样的书呢?哎呀,最好学了算法人还能变得很萌……! }7 K0 s8 @! Z
这个……要求是不是太高了呀?哈哈,有的书真的能满足所有这些要求哦!7 ~; N( Z3 @0 `$ s9 _& m) |, A+ f
来,看看这本书有多可爱——
" I G" l3 C/ f' a, ?4 Q1 W. a) P2 z$ j( W! N" ]2 V
二分查找
$ \' @ h( G2 [" d& n假设要在电话簿中找一个名字以K打头的人,(现在谁还用电话簿!)可以从头开始翻页,直到进入以K打头的部分。但你很可能不这样做,而是从中间开始,因为你知道以K打头的名字在电话簿中间。! o0 B/ P$ z2 m: d M7 g( j

$ ]6 [+ k% r8 p, O( X/ w0 b! F0 C又假设要在字典中找一个以O打头的单词,你也将从中间附近开始。
+ V: V. o! x; D& m6 v现在假设你登录Facebook。当你这样做时,Facebook必须核实你是否有其网站的账户,因此必须在其数据库中查找你的用户名。如果你的用户名为karlmageddon,Facebook可从以A打头的部分开始查找,但更合乎逻辑的做法是从中间开始查找。- w% `* h6 l1 j7 [$ l: C+ C$ H
这是一个查找问题,在前述所有情况下,都可使用同一种算法来解决问题,这种算法就是二分查找。
5 ?. ^" j2 s0 S3 K/ @' z9 C( G2 f
& `3 e6 B6 e/ Y) Y6 ^9 E二分查找是一种算法,其输入是一个有序的元素列表(必须有序的原因稍后解释)。如果要查找的元素包含在列表中,二分查找返回其位置;否则返回null。7 |4 u9 D* Y" c( X
下图是一个例子。
) w0 a# E2 k& O; i( ~/ u/ m6 w
- @0 Z. f8 D8 R' W下面的示例说明了二分查找的工作原理。我随便想一个1~100的数字。3 u+ ]* e' `% ?
! @( j j+ z7 g, m7 B
你的目标是以最少的次数猜到这个数字。你每次猜测后,我会说小了、大了或对了。7 n2 A6 A" i) P' R2 k! t
假设你从1开始依次往上猜,猜测过程会是这样。
6 `9 P7 _$ |3 N. W* Y; l; q
6 F4 i5 q. T2 T0 E) ?* z6 u6 Z这是简单查找,更准确的说法是傻找。每次猜测都只能排除一个数字。如果我想的数字是99,你得猜99次才能猜到!
( ~+ q" Z0 ~5 ]/ L0 n4 v更佳的查找方式
9 W y/ P- |& F/ D9 T C
! J- ?9 \" @# x; V, x; A% ^; S: O# L1 j+ Y下面是一种更佳的猜法。从50开始。, T3 n0 _6 F3 o& m9 y0 J$ p& h

9 N- G8 @% b: r小了,但排除了一半的数字!至此,你知道1~50都小了。接下来,你猜75。
1 X# z( b, e) Z
9 b( s5 W& P& @+ H( {( P3 Y9 t大了,那余下的数字又排除了一半!使用二分查找时,你猜测的是中间的数字,从而每次都将余下的数字排除一半。接下来,你猜63(50和75中间的数字)。5 J* i- @1 _# ^* D$ O* ?

! q/ k, i9 H# t7 F1 X这就是二分查找,你学习了第一种算法!每次猜测排除的数字个数如下。9 p* M8 a8 M0 r% O8 Q, C4 X

: t1 D7 V k3 X" E0 J( k不管我心里想的是哪个数字,你在7次之内都能猜到,因为每次猜测都将排除很多数字!
1 q6 B4 K k- `. f" o3 g( H. V+ G4 R假设你要在字典中查找一个单词,而该字典包含240 000个单词,你认为每种查找最多需要多少步?
% i8 I" Z/ {0 G" G2 F ' X% h! [, h& G' a9 [! _8 L3 ?. ~
如果要查找的单词位于字典末尾,使用简单查找将需要240 000步。使用二分查找时,每次排除一半单词,直到最后只剩下一个单词。
/ D. c; M2 V/ Z$ F {6 P$ w0 s 1 d. T. `' o) Q2 l
因此,使用二分查找只需18步——少多了!一般而言,对于包含n个元素的列表,用二分查找最多需要log2n步,而简单查找最多需要n步。
0 V, V1 B/ u/ i4 x7 x4 L: b0 u对数
3 B* W% P9 _% o你可能不记得什么是对数了,但很可能记得什么是幂。log10100相当于问“将多少个10相乘的结果为100”。答案是两个:10 × 10 = 100。因此,log10100 = 2。对数运算是幂运算的逆运算。& A0 i; [% s0 `4 K

6 W0 q1 s$ U& G' a! c1 }& f, g" B* B对数是幂运算的逆运算
: h3 b; s' b& R0 r本书使用大O表示法(稍后介绍)讨论运行时间时,log指的都是log2。使用简单查找法查找元素时,在最糟情况下需要查看每个元素。因此,如果列表包含8个数字,你最多需要检查8个数字。而使用二分查找时,最多需要检查log n个元素。如果列表包含8个元素,你最多需要检查3个元素,因为log 8 = 3(23 = 8)。如果列表包含1024个元素,你最多需要检查10个元素,因为log 1024 = 10(210 =1024)。
: \1 h5 O/ ^. i9 L下面来看看如何编写执行二分查找的Python代码。这里的代码示例使用了数组。如果你不熟悉数组,也不用担心,下一章就会介绍。你只需知道,可将一系列元素存储在一系列相邻的桶(bucket),即数组中。这些桶从0开始编号:第一个桶的位置为#0,第二个桶为#1,第三个桶为#2,以此类推。% ]7 i+ D" e6 h {
函数binary_search接受一个有序数组和一个元素。如果指定的元素包含在数组中,这个函数将返回其位置。你将跟踪要在其中查找的数组部分——开始时为整个数组。* P* w: q. n. }2 s9 S6 D
low = 0high = len(list) - 1
: u7 V7 J0 E% U5 J7 \$ i你每次都检查中间的元素。# M" z) S1 m* [- n2 R$ d" M. y
mid = (low + high) / 2 ←---如果(low + high)不是偶数,Python自动将mid向下取整。guess = list[mid]ifguess < item: low = mid + 1 如果猜的数字大了,就修改high。完整的代码如下。
# q6 Y! V6 D6 Ywhilelow 1 ←别忘了索引从0开始,第二个位置的索引为1print binary_search(my_list, -1) # => None ←在Python中,None表示空,它意味着没有找到指定的元素运行时间+ Q: T5 P* X- i
) k0 D. F7 |3 y$ O% f1 w' Y
每次介绍算法时,我都将讨论其运行时间。一般而言,应选择效率最高的算法,以最大限度地减少运行时间或占用空间。
/ k9 e/ v! y5 n( W' V / }: J5 h" o0 @- b _
回到前面的二分查找。使用它可节省多少时间呢?简单查找逐个地检查数字,如果列表包含100个数字,最多需要猜100次。如果列表包含40亿个数字,最多需要猜40亿次。换言之,最多需要猜测的次数与列表长度相同,这被称为线性时间(linear time)。
4 c1 x4 p* h7 D* S$ |9 M! e二分查找则不同。如果列表包含100个元素,最多要猜7次;如果列表包含40亿个数字,最多需猜32次。厉害吧?二分查找的运行时间为对数时间(或log时间)。下表总结了我们发现的情况。; q4 k& E3 {% U# `! p7 Y% U

( \* e/ ^. {& j ~# [* B4 e* \8 K/ S2 U+ V8 Y
大O表示法
# s- J5 I- y9 F大O表示法是一种特殊的表示法,指出了算法的速度有多快。谁在乎呢?实际上,你经常要使用别人编写的算法,在这种情况下,知道这些算法的速度大有裨益。本节将介绍大O表示法是什么,并使用它列出一些最常见的算法运行时间。
0 F* z6 P7 Y( z9 C r% L6 |算法的运行时间以不同的速度增加
) s4 _ ]2 ]+ j4 N0 x; |0 b! f* {+ y; B* J8 m
Bob要为NASA编写一个查找算法,这个算法在火箭即将登陆月球前开始执行,帮助计算着陆地点。
$ X5 _# E# M5 D6 w
5 w5 x- v6 P2 N( ?这个示例表明,两种算法的运行时间呈现不同的增速。Bob需要做出决定,是使用简单查找还是二分查找。使用的算法必须快速而准确。一方面,二分查找的速度更快。Bob必须在10秒钟内找出着陆地点,否则火箭将偏离方向。另一方面,简单查找算法编写起来更容易,因此出现bug的可能性更小。Bob可不希望引导火箭着陆的代码中有bug!为确保万无一失,Bob决定计算两种算法在列表包含100个元素的情况下需要的时间。
1 ]' q3 T/ A) g) t假设检查一个元素需要1毫秒。使用简单查找时,Bob必须检查100个元素,因此需要100毫秒才能查找完毕。而使用二分查找时,只需检查7个元素(log2100大约为7),因此需要7毫秒就能查找完毕。然而,实际要查找的列表可能包含10亿个元素,在这种情况下,简单查找需要多长时间呢?二分查找又需要多长时间呢?请务必找出这两个问题的答案,再接着往下读。. C- N1 D6 F) i9 V/ |; ]
6 A7 P& E9 D; r: H9 \
Bob使用包含10亿个元素的列表运行二分查找,运行时间为30毫秒(log21 000 000 000大约为30)。他心里想,二分查找的速度大约为简单查找的15倍,因为列表包含100个元素时,简单查找需要100毫秒,而二分查找需要7毫秒。因此,列表包含10亿个元素时,简单查找需要30 × 15 = 450毫秒,完全符合在10秒内查找完毕的要求。Bob决定使用简单查找。这是正确的选择吗?
% L0 ^( r$ E& e3 U$ o5 ]2 L& E不是。实际上,Bob错了,而且错得离谱。列表包含10亿个元素时,简单查找需要10亿毫秒,相当于11天!为什么会这样呢?因为二分查找和简单查找的运行时间的增速不同。( p2 J, a% J& A* E. g; M6 I

$ ?6 n8 y) T q( {也就是说,随着元素数量的增加,二分查找需要的额外时间并不多,而简单查找需要的额外时间却很多。因此,随着列表的增长,二分查找的速度比简单查找快得多。Bob以为二分查找速度为简单查找的15倍,这不对:列表包含10亿个元素时,为3300万倍。有鉴于此,仅知道算法需要多长时间才能运行完毕还不够,还需知道运行时间如何随列表增长而增加。这正是大O表示法的用武之地。6 c; b) U% g, o0 W

6 C# ^( Q# o# E大O表示法指出了算法有多快。例如,假设列表包含n 个元素。简单查找需要检查每个元素,因此需要执行n 次操作。使用大O表示法,这个运行时间为O(n)。单位秒呢?没有——大O表示法指的并非以秒为单位的速度。大O表示法让你能够比较操作数,它指出了算法运行时间的增速。4 `+ \7 l3 _% |/ P3 H
再来看一个例子。为检查长度为n 的列表,二分查找需要执行log n 次操作。使用大O表示法,这个运行时间怎么表示呢?O(log n)。一般而言,大O表示法像下面这样。
4 N) i6 U1 d/ _' B. A& Q+ x " r! s! ?+ }' \( G' h5 L. n
这指出了算法需要执行的操作数。之所以称为大O表示法,是因为操作数前有个大O。这听起来像笑话,但事实如此!
4 K' K. A" j/ d# _" M# s# V7 r* [下面来看一些例子,看看你能否确定这些算法的运行时间。
5 j$ l4 _* J0 |7 P6 H; r) ]4 Z" b6 q理解不同的大O运行时间
7 S+ R# @; S. }% |/ F$ n* H! R
! g) `! C; U$ K* I3 g- b8 y下面的示例,你在家里使用纸和笔就能完成。假设你要画一个网格,它包含16个格子。
7 ]7 U) [8 X. \( j# {
% B4 T) T% `1 V; K& q( B算法1
/ k \; \, c" J( J( ~ D6 A/ y一种方法是以每次画一个的方式画16个格子。记住,大O表示法计算的是操作数。在这个示例中,画一个格子是一次操作,需要画16个格子。如果每次画一个格子,需要执行多少次操作呢?) V6 m/ q5 D9 v2 ^# ^, P
. s- R8 a- s; Z# t2 ~+ {0 l% V
画16个格子需要16步。这种算法的运行时间是多少?
' q; r" g% k7 B4 D7 j1 j算法2
2 O3 ~6 r: G" I* h; t- r% ^( s5 u请尝试这种算法——将纸折起来。
% L/ e; r* k7 [, T( a
3 r: p! g G) i& A! @在这个示例中,将纸对折一次就是一次操作。第一次对折相当于画了两个格子!8 l7 R9 c1 W$ O$ P, ?- x
再折,再折,再折。
4 i# \- r" p, j t
/ @3 L) A* G6 C$ T8 a折4次后再打开,便得到了漂亮的网格!每折一次,格子数就翻倍,折4次就能得到16个格子!
/ P B8 g1 l" o. b7 N" L, }: f
8 f9 d5 F U, |/ Z% a# }你每折一次,绘制出的格子数都翻倍,因此4步就能“绘制”出16个格子。这种算法的运行时间是多少呢?请搞清楚这两种算法的运行时间之后,再接着往下读。1 T" _8 R% E# M) K+ I
答案如下:算法1的运行时间为O(n),算法2的运行时间为O(log n)。
: ^) U, s$ k2 ?; ?5 m大O表示法指出了最糟情况下的运行时间
) \! `9 L5 w5 i2 m9 q0 u% C. i7 S- ^# D0 B2 G: H; o4 o' ^2 i
假设你使用简单查找在电话簿中找人。你知道,简单查找的运行时间为O(n),这意味着在最糟情况下,必须查看电话簿中的每个条目。如果要查找的是Adit——电话簿中的第一个人,一次就能找到,无需查看每个条目。考虑到一次就找到了Adit,请问这种算法的运行时间是O(n)还是O(1)呢?4 k/ K2 g! r: W1 j6 k
简单查找的运行时间总是为O(n)。查找Adit时,一次就找到了,这是最佳的情形,但大O表示法说的是最糟的情形。因此,你可以说,在最糟情况下,必须查看电话簿中的每个条目,对应的运行时间为O(n)。这是一个保证——你知道简单查找的运行时间不可能超过O(n)。2 m, I2 {4 V9 N# A" k( S H
说明
( O: y* L$ s' v/ |$ g除最糟情况下的运行时间外,还应考虑平均情况的运行时间,这很重要。最糟情况和平均情况将在第4章讨论。
{! ~& x' N' I& y3 K1 ? 一些常见的大O运行时间
7 p0 ?& K1 w% r5 |+ k1 ~" E
0 t9 P a5 _; v! g! D) a下面按从快到慢的顺序列出了你经常会遇到的5种大O运行时间。' P: u& i# B6 n2 x
! |! W" d3 V, r {1 N% z |
- O(log n),也叫对数时间,这样的算法包括二分查找。
% ? G$ Q+ j, r1 R: Z* G9 p - O(n),也叫线性时间,这样的算法包括简单查找。7 O6 h+ z! f' l3 h3 d
- O(n * log n),这样的算法包括第4章将介绍的快速排序——一种速度较快的排序算法。
2 C% X- W* o* V! \# `* G7 i' Y - O(n2),这样的算法包括第2章将介绍的选择排序——一种速度较慢的排序算法。7 |" q* ^6 ^! R1 s2 H) X6 s9 M
- O(n!),这样的算法包括接下来将介绍的旅行商问题的解决方案——一种非常慢的算法。! L' E; L4 d0 z0 M& i$ Y \
假设你要绘制一个包含16格的网格,且有5种不同的算法可供选择,这些算法的运行时间如上所示。如果你选择第一种算法,绘制该网格所需的操作数将为4(log 16 = 4)。假设你每秒可执行10次操作,那么绘制该网格需要0.4秒。如果要绘制一个包含1024格的网格呢?这需要执行10(log 1024 = 10)次操作,换言之,绘制这样的网格需要1秒。这是使用第一种算法的情况。
6 S! y* U$ q8 P5 Z: \, k) o第二种算法更慢,其运行时间为O(n)。即要绘制16个格子,需要执行16次操作;要绘制1024个格子,需要执行1024次操作。执行这些操作需要多少秒呢?
0 l; }+ U2 y& G7 M3 V下面按从快到慢的顺序列出了使用这些算法绘制网格所需的时间:
7 h4 U* r( |7 I* V" R
) p T7 `5 N7 }9 _ u% b. l' I还有其他的运行时间,但这5种是最常见的。1 f7 w) k! X6 F* v" Q6 K3 Y7 M
这里做了简化,实际上,并不能如此干净利索地将大O运行时间转换为操作数,但就目前而言,这种准确度足够了。等你学习其他一些算法后,第4章将回过头来再次讨论大O表示法。当前,我们获得的主要启示如下。' G, O0 }8 b6 I" G* J
# E# G/ z, X" s+ p4 f% W- 算法的速度指的并非时间,而是操作数的增速。* ] k* g1 K# }' K
- 谈论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加。' L) I# V# q7 h# n# k
- 算法的运行时间用大O表示法表示。* k- T) s0 q4 K3 M$ K2 L8 N% b6 S! z
- O(log n)比O(n)快,当需要搜索的元素越多时,前者比后者快得越多。& L* O8 N( m+ I& G; |4 Y3 D( e2 c
以上内容来自《算法图解》: ?4 q# n) k K, H9 ?

4 ~0 s" w' Q9 ]% K$ Z# ]# W8 f《算法图解》2 B' e8 _+ h0 |+ s0 ^. \1 U
0 G% m8 ~0 D' g' c( I7 \
扫码查看详情/ b7 L5 q! H# N5 ?0 L# M
* h5 ?: Y _; _! E2 g编辑推荐:
; ^1 @8 l; D6 A本书示例丰富,图文并茂,以让人容易理解的方式阐释了算法,旨在帮助程序员在日常项目中更好地发挥算法的能量。书中的前三章将帮助你打下基础,带你学习二分查找、大O表示法、两种基本的数据结构以及递归等。余下的篇幅将主要介绍应用广泛的算法,具体内容包括:面对具体问题时的解决技巧,比如,何时采用贪婪算法或动态规划;散列表的应用;图算法;K最近邻算法。
" x# U( h0 |6 S# ^( ^+ A( N+ {- J6 o扫码或者点击阅读原文购买& v! Q. y, ~+ N
% m# j" \5 i7 q# s
+ O4 Q/ k. ]; l6 b) a1 Y9 K% W" u' M作为码书商店的运营人员,诚邀你们进入我们的“CSDN码书福利群”,群里会不定时的给大家赠书书籍、优惠券等,有书籍推荐或者物流方面信息也可群里咨询~目前群已满100人,需要加群的请扫下方二维码添加微信,拉你入群哦~对此次活动不了解的也可咨询~5 p; n9 t6 m. c: E% [
2 ~! h5 G4 K/ _2 m8 @- t& B9 L7 S( `) c. |
! p' j5 j9 }' s/ ]/ l6 o5 Z& z来源:http://www.yidianzixun.com/article/0MIo0Rxx5 c5 c3 k: A! N4 G0 D) ^( X* e2 ?
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|