This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,225 @@
<audio id="audio" title="《数据结构与算法之美》学习指导手册" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4c/80/4cb2010b28b77652de60e0d9b1aa6480.mp3"></audio>
你好,我是王争。
在设计专栏内容的时候为了兼顾不同基础的同学我在内容上做到了难易结合既有简单的数组、链表、栈、队列这些基础内容也有红黑树、BM、KMP这些难度较大的算法。但是对于初学者来说一下子面对这么多知识可能还是比较懵。
我觉得,对于初学者来说,先把最简单、最基础、最重要的知识点掌握好,再去研究难度较高、更加高级的知识点,这样由易到难、循序渐进的学习路径,无疑是最合理的。
基于这个路径,我对专栏内容,重新做了一次梳理,希望给你一份具体、明确、有效的学习指导。我会写清楚**每个知识点的难易程度、需要你掌握到什么程度、具体如何来学习**。
如果你是数据结构和算法的初学者,或者你觉得自己的基础比较薄弱,希望这份学习指导,能够让你学起来能更加有的放矢,能把精力、时间花在刀刃上,获得更好的学习效果。
下面,我先给出一个大致的学习路线。
<img src="https://static001.geekbang.org/resource/image/54/48/54163f16e152f71b8f91d3fba652cf48.jpg" alt="">
现在,针对每个知识点,我再给你逐一解释一下。我这里先说明一下,下面标记的难易程度、是否重点、掌握程度,都只是针对初学者来说的,如果你已经有一定基础,可以根据自己的情况,安排自己的学习。
## 1.复杂度分析
尽管在专栏中,我只用了两节课的内容,来讲复杂度分析这个知识点。但是,我想说的是,它真的非常重要。你必须要牢牢掌握这两节,基本上要做到,简单代码能很快分析出时间、空间复杂度;对于复杂点的代码,比如递归代码,你也要掌握专栏中讲到的两种分析方法:递推公式和递归树。
对于初学者来说,光看入门篇的两节复杂度分析文章,可能还不足以完全掌握复杂度分析。不过,在后续讲解每种数据结构和算法的时候,我都有详细分析它们的时间、空间复杂度。所以,你可以在学习专栏中其他章节的时候,再不停地、有意识地去训练自己的复杂度分析能力。
难易程度Medium
是否重点10分
掌握程度:在不看我的分析的情况下,能自行分析专栏中大部分数据结构和算法的时间、空间复杂度
## 2.数组、栈、队列
这一部分内容非常简单,初学者学起来也不会很难。但是,作为基础的数据结构,数组、栈、队列,是后续很多复杂数据结构和算法的基础,所以,这些内容你一定要掌握。
难易程度Easy
是否重点8分
掌握程度:能自己实现动态数组、栈、队列
## 3.链表
链表非常重要虽然理论内容不多但链表上的操作却很复杂。所以面试中经常会考察你一定要掌握。而且我这里说“掌握”不只是能看懂专栏中的内容还能将专栏中提到的经典链表题目比如链表反转、求中间结点等轻松无bug地实现出来。
难易程度Medium
是否重点9分
掌握程度:能轻松写出经典链表题目代码
## 4.递归
对于初学者来说,递归代码非常难掌握,不管是读起来,还是写起来。但是,这道坎你必须要跨过,跨不过就不能算是入门数据结构和算法。我们后面讲到的很多数据结构和算法的代码实现,都要用到递归。
递归相关的理论知识也不多,所以还是要多练。你可以先在网上找些简单的题目练手,比如斐波那契数列、求阶乘等,然后再慢慢过渡到更加有难度的,比如归并排序、快速排序、二叉树的遍历、求高度,最后是回溯八皇后、背包问题等。
难易程度Hard
是否重点10分
掌握程度轻松写出二叉树遍历、八皇后、背包问题、DFS的递归代码
## 5.排序、二分查找
这一部分并不难,你只需要能看懂我专栏里的内容即可。
难易程度Easy
是否重点7分
掌握程度:能自己把各种排序算法、二分查找及其变体代码写一遍就可以了
## 6.跳表
对于初学者来说,并不需要非得掌握跳表,所以,如果没有精力,这一章节可以先跳过。
难易程度Medium
是否重点6分
掌握程度:初学者可以先跳过。如果感兴趣,看懂专栏内容即可,不需要掌握代码实现
## 7.散列表
尽管散列表的内容我讲了很多,有三节课。但是,总体上来讲,这块内容理解起来并不难。但是,作为一种应用非常广泛的数据结构,你还是要掌握牢固散列表。
难易程度Medium
是否重点8分
掌握程度:对于初学者来说,自己能代码实现一个拉链法解决冲突的散列表即可
## 8.哈希算法
这部分纯粹是为了开拓思路,初学者可以略过。
难易程度Easy
是否重点3分
掌握程度:可以暂时不看
## 9.二叉树
这一部分非常重要!二叉树在面试中经常会被考到,所以要重点掌握。但是我这里说的二叉树,并不包含专栏中红黑树的内容。红黑树我们待会再讲。
难易程度Medium
是否重点9分
掌握程度:能代码实现二叉树的三种遍历算法、按层遍历、求高度等经典二叉树题目
## 10.红黑树
对于初学者来说,这一节课完全可以不看。
难易程度Hard
是否重点3分
掌握程度:初学者不用把时间浪费在上面
## 11. B+树
虽然B+树也算是比较高级的一种数据结构了,但是对初学者来说,也不是重点。有时候面试的时候还是会问的,所以这一部分内容,你能看懂专栏里的讲解就可以了。
难易程度Medium
是否重点5分
掌握程度:可看可不看
## 12.堆与堆排序
这一部分内容不是很难,初学者也是要掌握的。
难易程度Medium
是否重点8分
掌握程度能代码实现堆、堆排序并且掌握堆的三种应用优先级队列、Top k、中位数
## 13.图的表示
图的内容很多但是初学者不需要掌握那么多。一般BAT等大厂面试不怎么会面试有关图的内容因为面试官可能也对这块不会很熟悉哈。但是最基本图的概念、表示方法还是要掌握的。
难易程度Easy
是否重点8分
掌握程度:理解图的三种表示方法(邻接矩阵、邻接表、逆邻接表),能自己代码实现
## 14.深度广度优先搜索
这算是图上最基础的遍历或者说是搜索算法了,所以还是要掌握一下。这两种算法的原理都不难哈,但是代码实现并不简单,一个用到了队列,另一个用到了递归。对于初学者来说,看懂这两个代码实现就是一个挑战!可以等到其他更重要的内容都掌握之后,再来挑战,也是可以的。
难易程度Hard
是否重点8分
掌握程度:能代码实现广度优先、深度优先搜索算法
## 15.拓扑排序、最短路径、A*算法
这几个算法稍微高级点。如果你能轻松实现深度、广度优先搜索,那看懂这三个算法不成问题。不过,这三种算法不是重点。面试不会考的。
难易程度Hard
是否重点5分
掌握程度:有时间再看,暂时可以不看
## 16.字符串匹配BF、RK
BF非常简单RK稍微复杂点但都不难。这个最好还是掌握下。
难易程度Easy
是否重点7分
掌握程度能实践BF算法能看懂RK算法
## 17.字符串匹配BM、KMP、AC自动机
这三个算法都挺难的,对于算法有一定基础的人来说,看懂也不容易。所以,对于初学者来说,千万别浪费时间在这上面。即便有余力,看懂就好了,不用非得能自己实现。
难易程度Hard
是否重点3分
掌握程度:初学者不用把时间浪费在上面
## 18.字符串匹配Trie
这个还是要能看懂不过不需要能代码实现。有些面试官喜欢考这个东西主要是结合应用场景来考察只是看你知不知道要用Trie树这个东西。
难易程度Medium
是否重点7分
掌握程度:能看懂,知道特点、应用场景即可,不要求代码实现
## 19.位图
位图不是重点,如果有余力最好掌握一下。
难易程度Easy
是否重点6分
掌握程度:看懂即可,能自己实现一个位图结构最好
## 20.四种算法思想
这个是重点也是难点。贪心、分治、回溯、动态规划每一个都不简单其中动态规划又是最难、最烧脑的。要应付FLAG这样公司的面试必须拿下这块内容。但是呢学习要循序渐进这块内容的学习可以放到最后做个长时间的学习计划来攻克。
这块内容理论的东西不多,要想真的掌握,还是要大量刷题。
难易程度Hard
是否重点10分
掌握程度可以放到最后但是一定要掌握做到能实现Leetcode上Medium难度的题目
学而时习之,专栏虽然已经结束,但是学习的同学和留言依旧源源不断。希望这份学习指导手册对你有帮助,也欢迎你继续给我留言,和大家一起交流、学习、进步。

View File

@@ -0,0 +1,73 @@
<audio id="audio" title="不定期福利第一期 | 数据结构与算法学习书单" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5c/9a/5c38a858306b7555f1a5e758ef48739a.mp3"></audio>
你好,我是王争。欢迎来到不定期更新的周末福利时间。
专栏已经上线两周了,看到这么多人在留言区写下自己的疑惑或者观点,我特别开心。在留言里,很多同学让我推荐一些学习数据结构与算法的书籍。因此我特意跟编辑商量了,给你一个周末福利。所以这一期呢,我们就来聊一聊数据结构和算法学习过程中有哪些必读书籍。
有的同学还在读大学,代码还没写过几行;有的同学已经工作数十年,这之间的差别还是挺大的。而不同基础的人,适宜看的书是完全不一样的。因此,**针对不同层次、不同语言的同学,我分别推荐了不同的书**。希望每个同学,都能找到适合自己的学习资料,都能在现有水平上有所提高。
## 针对入门的趣味书
入门的同学,我建议你不要过度追求上去就看经典书。像《算法导论》《算法》这些书,虽然比较经典、比较权威,但是非常厚。初学就去啃这些书肯定会很费劲。而一旦啃不下来,挫败感就会很强。所以,入门的同学,我建议你找一些比较容易看的书来看,比如《大话数据结构》和《算法图解》。**不要太在意书写得深浅,重要的是能不能坚持看完。**
《**大话数据结构**》 这本书最大的特点是它把理论讲得很有趣不枯燥。而且每个数据结构和算法作者都结合生活中的例子进行了讲解能让你有非常直观的感受。虽然这本书有400多页但是花两天时间读完应该是没问题的。如果你之前完全不懂数据结构和算法可以先从这本书看起。
《**算法图解**》 跟《大话数据结构》走的是同样的路线就像这本书副标题写的那样“像小说一样有趣的算法入门书”主打“图解”通俗易懂。它只有不到200页所以内容比较少。作为入门看看这本书能让你对数据结构和算法有个大概的认识。
这些入门书共同的问题是,缺少细节,不够系统,也不够严谨。所以,如果你想要系统地学数据结构和算法,看这两本书肯定是不够的。
## 针对特定编程语言的教科书
讲数据结构和算法肯定会跟代码实现挂钩。所以很多人就很关心某某书籍是用什么语言实现的是不是自己熟悉的语言。市面大部分数据结构和算法书籍都是用C、C++、Java语言实现的还有些是用伪代码。而使用Python、Go、PHP、JavaScript、Objective-C这些编程语言实现的就更少了。
我这里推荐《数据结构和算法分析》。国内外很多大学都拿这本书当作教材。这本书非常系统、全面、严谨,而且又不是特别难,适合对数据结构和算法有些了解,并且掌握了至少一门编程语言的同学。而且,这个作者也很用心。他用了三种语言,写了三个版本,分别是:《**数据结构与算法分析 C语言描述**》《**数据结构与算法分析C++描述**》《**数据结构与算法分析Java语言描述**》。
如果你熟悉的是Python或者JavaScript可以参考《**数据结构与算法JavaScript描述**》《**数据结构与算法Python语言描述**》 。至于其他语言的算法书籍,确实比较少。如果你有推荐,可以在留言区补充一下。
## 面试必刷的宝典
算法对面试很重要很多人也很关心。我这里推荐几本有益于面试的书籍分别是《剑指offer》《编程珠玑》《编程之美》。
从《**剑指offer**》这本书的名字就可以看出,作者的写作目的非常明确,就是为了面试。这本书几乎包含了所有常见的、经典的面试题。如果能搞懂这本书里的内容,应付一般公司的面试应该不成问题。
《**编程珠玑**》这本书的豆瓣评分非常高有9分。这本书最大的特色就是讲了很多针对海量数据的处理技巧。这个可能是其他算法书籍很少涉及的。面试的时候海量数据处理的问题也是经常会问的特别是校招面试。不管是开拓眼界还是应付面试这本书都很值得一看。
《**编程之美**》这本书有多位作者其中绝大部分是微软的工程师所以书的质量很有保证。不过这里面的算法题目稍微有点难也不是很系统这也是我把它归到面试这一部分的原因。如果你有一定基础也喜欢钻研些算法问题或者要面试Google、Facebook这样的公司可以拿这本书里的题先来自测一下。
## 经典大部头
很多人一提到算法书就会搬出《算法导论》和《算法》。这两本确实非常经典,但是都太厚了,看起来比较费劲,我估计很少有人能坚持全部看下来。如果你想更加深入地学一学数据结构和算法,我还是强烈建议你看看。
我个人觉得,《**算法导论**》这本书的章节安排不是循序渐进的,里面充斥着各种算法的正确性、复杂度的证明、推导,数学公式比较多,一般人看起来会比较吃力。所以,作为入门书籍,并不是很推荐。
《**算法**》这本书也是一本经典大部头,不过它比起《算法导论》来要友好很多,更容易看懂,更适合初学者入门。但是这本书的缺点也很明显,就是内容不够全面,比如动态规划这么重要的知识点,这本书就没有讲。对于数据结构的东西,它讲的也不多,基本就是偏重讲算法。
## 殿堂级经典
说到殿堂级经典书,如果《**计算机程序设计艺术**》称第二,我想没人敢称第一。这本书包括很多卷。说实话,我也只看过比较简单的几卷,比如《基本算法》《排序和查找》。
这套书的深度、广度、系统性、全面性是其他所有数据结构和算法书籍都无法相比的。但是,如果你对算法和数据结构不是特别感兴趣,没有很好的数学、算法、计算机基础,想要把这套书读完、读懂是比较难的。你可以把它当作你算法学习的终极挑战。
## 闲暇阅读
算法无处不在。我这里再推荐几本适合闲暇时间阅读的书:《**算法帝国**》《**数学之美**》《**算法之美**》。
这些书共同的特点是都列举了大量的例子非常通俗易懂。夸张点说像《算法帝国》文科生都能读懂。当你看这些书的时候你常常会深深感受到算法的力量被算法的优美之处折服。即便不是从事IT工作的看完这几本书也可以开拓眼界。
<img src="https://static001.geekbang.org/resource/image/1e/b8/1e306ffd0d56facbda45f413bc27a4b8.jpg" alt="">
书籍差不多就是这些。除此之外留言区很多人问到算法的实现语言。我这里也解释一下。因为我现在比较常用的编程语言是Java。所以在专栏里特别简单的、不涉及高级语法的我会用Java或者C、C++来实现。稍微复杂的,为了让你能看懂,我会用伪代码。所以你完全不用担心语言的问题。
每节课中有需要代码实现的数据结构和算法我都另外用Java语言实现一遍然后放到Github上供你参考。Github的地址我放在这里你可以收藏一下[https://github.com/wangzheng0822/algo](https://github.com/wangzheng0822/algo)。
至于其他语言的同学比如C、C++、Python、Go、PHP、JavaScript、Objective-C等我想了一个crowd sourcing的方法。
我希望基础较好的同学参照我的Java实现用你熟悉的编程语言再实现一遍并且将代码留言给我。如果你写得正确我会将你的代码上传到Github上分享给更多人。
还有人问,我学完这个专栏,就可以拿下数据结构和算法吗?我想说的是,**每个人的基础、学习能力都不一样,掌握程度取决于你的努力程度**。除了你之外,没有人能百分之百保证你能掌握什么知识。
有的同学只是把每一节课听下来、看下来,就束之高阁,也不求甚解,那效果肯定会很差。而有些同学除了听、看之外,遇到不懂的会自己去查资料、看参考书籍,还会把我讲的数据结构和算法都认真地实现一遍,这样的学习效果自然就比只听一遍、看一遍要好很多。即便我已经尽我所能把这些知识讲得深入浅出,通俗易懂,但是学习依然还是要靠你自己啊。
这种答疑的方式也会成为我们之后的固定动作,我会把留言里有价值的问题和反馈沉淀下来,希望对你的日常学习起到补充作用。**如果你有什么看不懂、听不懂的地方,或者工作中有遇到算法问题、技术难题,欢迎写在留言区。**(我发现留言区里卧虎藏龙啊,没事儿可以多扫扫留言区。)
这次的周末福利时间就到这啦,我们下次见!

View File

@@ -0,0 +1,183 @@
<audio id="audio" title="不定期福利第三期 | 测一测你的算法阶段学习成果" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9a/57/9a180cbd39ac63b44dddcdd3c6f47f57.mp3"></audio>
专栏最重要的基础篇马上就要讲完了,不知道你掌握了多少?我从前面的文章中挑选了一些案例,稍加修改,组成了一套测试题。
你先不要着急看答案,自己先想一想怎么解决,测一测自己对之前的知识掌握的程度。如果有哪里卡壳或者不怎么清楚的,可以回过头再复习一下。
正所谓温故知新,这种通过实际问题查缺补漏的学习方法,非常利于你巩固前面讲的知识点,你可要好好珍惜这次机会哦!
## 实战测试题(一)
假设猎聘网有10万名猎头顾问每个猎头顾问都可以通过做任务比如发布职位来积累积分然后通过积分来下载简历。**假设你是猎聘网的一名工程师如何在内存中存储这10万个猎头ID和积分信息让它能够支持这样几个操作**
<li>
根据猎头的ID快速查找、删除、更新这个猎头的积分信息
</li>
<li>
查找积分在某个区间的猎头ID列表
</li>
<li>
查询积分从小到大排在第x位的猎头ID信息
</li>
<li>
查找按照积分从小到大排名在第x位到第y位之间的猎头ID列表。
</li>
### 相关章节
[17 | 跳表为什么Redis一定要用跳表来实现有序集合](https://time.geekbang.org/column/article/42896)
[20 | 散列表(下):为什么散列表和链表经常会一起使用?](https://time.geekbang.org/column/article/64858)
[25 | 红黑树:为什么工程中都用红黑树这种二叉树?](https://time.geekbang.org/column/article/68638)
### 题目解析
这个问题既要通过ID来查询又要通过积分来查询所以对于猎头这样一个对象我们需要将其组织成两种数据结构才能支持这两类操作。
我们按照ID将猎头信息组织成散列表。这样就可以根据ID信息快速地查找、删除、更新猎头的信息。我们按照积分将猎头信息组织成跳表这种数据结构按照积分来查找猎头信息就非常高效时间复杂度是O(logn)。
我刚刚讲的是针对第一个、第二个操作的解决方案。第三个、第四个操作是类似的,按照排名来查询,这两个操作该如何实现呢?
我们可以对刚刚的跳表进行改造每个索引结点中加入一个span字段记录这个索引结点到下一个索引结点的包含的链表结点的个数。这样就可以利用跳表索引快速计算出排名在某一位的猎头或者排名在某个区间的猎头列表。
实际上这些就是Redis中有序集合这种数据类型的实现原理。在开发中我们并不需要从零开始代码实现一个散列表和跳表我们可以直接利用Redis的有序集合来完成。
## 实战测试题(二)
电商交易系统中订单数据一般都会很大我们一般都分库分表来存储。假设我们分了10个库并存储在不同的机器上在不引入复杂的分库分表中间件的情况下我们希望开发一个小的功能能够快速地查询金额最大的前K个订单K是输入参数可能是1、10、1000、10000假设最大不会超过10万。**如果你是这个功能的设计开发负责人,你会如何设计一个比较详细的、可以落地执行的设计方案呢?**
为了方便你设计,我先交代一些必要的背景,在设计过程中,如果有其他需要明确的背景,你可以自行假设。
<li>
数据库中订单表的金额字段上建有索引我们可以通过select order by limit语句来获取数据库中的数据
</li>
<li>
我们的机器的可用内存有限比如只有几百M剩余可用内存。希望你的设计尽量节省内存不要发生Out of Memory Error。
</li>
### 相关章节
[12 | 排序如何用快排思想在O(n)内查找第K大元素](https://time.geekbang.org/column/article/41913)
[28 | 堆和堆排序:为什么说堆排序没有快速排序快?](https://time.geekbang.org/column/article/69913)
[29 | 堆的应用如何快速获取到Top 10最热门的搜索关键词](https://time.geekbang.org/column/article/70187)
### 题目解析
解决这个题目的基本思路我想你应该能想到,就是借助归并排序中的合并函数,这个我们在排序(下)以及堆的应用那一节中讲过。
我们从每个数据库中通过select order by limit语句各取局部金额最大的订单把取出来的10个订单放到优先级队列中取出最大值也就是大顶堆堆顶数据就是全局金额最大的订单。然后再从这个全局金额最大订单对应的数据库中取出下一条订单按照订单金额从大到小排列的然后放到优先级队列中。一直重复上面的过程直到找到金额前KK是用户输入的大订单。
从算法的角度看起来这个方案非常完美但是从实战的角度来说这个方案并不高效甚至很低效。因为我们忽略了数据库读取数据的性能才是这个问题的性能瓶颈。所以我们要尽量减少SQL请求每次多取一些数据出来那一次性取出多少才合适呢这就比较灵活、比较有技巧了。一次性取太多会导致数据量太大SQL执行很慢还有可能触发超时而且我们题目中也说了内存有限太多的数据加载到内存中还有可能导致Out of Memory Error。
所以一次性不能取太多数据也不能取太少数据到底是多少还要根据实际的硬件环境做benchmark测试去找最合适的。
## 实战测试题(三)
我们知道CPU资源是有限的任务的处理速度与线程个数并不是线性正相关。相反过多的线程反而会导致CPU频繁切换处理性能下降。所以线程池的大小一般都是综合考虑要处理任务的特点和硬件环境来事先设置的。
**当我们向固定大小的线程池中请求一个线程时,如果线程池中没有空闲资源了,这个时候线程池如何处理这个请求?是拒绝请求还是排队请求?各种处理策略又是怎么实现的呢?**
### 相关章节
[09 | 队列:队列在线程池等有限资源池中的应用](https://time.geekbang.org/column/article/41330)
### 题目解析
这个问题的答案涉及队列这种数据结构。队列可以应用在任何有限资源池中,用于排队请求,比如数据库连接池等。实际上,对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队。
这个问题的具体答案,在队列那一节我已经讲得非常详细了,你可以回去看看,这里我就不赘述了。
## 实战测试题(四)
通过IP地址来查找IP归属地的功能不知道你有没有用过没用过也没关系你现在可以打开百度在搜索框里随便输一个IP地址就会看到它的归属地。
这个功能并不复杂它是通过维护一个很大的IP地址库来实现的。地址库中包括IP地址范围和归属地的对应关系。比如当我们想要查询202.102.133.13这个IP地址的归属地时我们就在地址库中搜索发现这个IP地址落在[202.102.133.0, 202.102.133.255]这个地址范围内那我们就可以将这个IP地址范围对应的归属地“山东东营市”显示给用户了。
```
[202.102.133.0, 202.102.133.255] 山东东营市
[202.102.135.0, 202.102.136.255] 山东烟台
[202.102.156.34, 202.102.157.255] 山东青岛
[202.102.48.0, 202.102.48.255] 江苏宿迁
[202.102.49.15, 202.102.51.251] 江苏泰州
[202.102.56.0, 202.102.56.255] 江苏连云港
```
在庞大的地址库中逐一比对IP地址所在的区间是非常耗时的。**假设在内存中有12万条这样的IP区间与归属地的对应关系如何快速定位出一个IP地址的归属地呢**
### 相关章节
[15 | 二分查找(上):如何用最省内存的方式实现快速查找功能?](https://time.geekbang.org/column/article/42520)
[16 | 二分查找如何快速定位IP对应的省份地址](https://time.geekbang.org/column/article/42733)
### 题目解析
这个问题可以用二分查找来解决不过普通的二分查找是不行的我们需要用到二分查找的变形算法查找最后一个小于等于某个给定值的数据。不过二分查找最难的不是原理而是实现。要实现一个二分查找的变形算法并且实现的代码没有bug可不是一件容易的事情不信你自己写写试试。
关于这个问题的解答以及写出bug free的二分查找代码的技巧我们在二分查找那一节有非常详细的讲解你可以回去看看我这里就不赘述了。
## 实战测试题(五)
假设我们现在希望设计一个简单的海量图片存储系统最大预期能够存储1亿张图片并且希望这个海量图片存储系统具有下面这样几个功能
<li>
存储一张图片及其它的元信息主要的元信息有图片名称以及一组tag信息。比如图片名称叫玫瑰花tag信息是{红色,花,情人节}
</li>
<li>
根据关键词搜索一张图片,比如关键词是“情人节 花”“玫瑰花”;
</li>
<li>
避免重复插入相同的图片。这里,我们不能单纯地用图片的元信息,来比对是否是同一张图片,因为有可能存在名称相同但图片内容不同,或者名称不同图片内容相同的情况。
</li>
我们希望自主开发一个简单的系统不希望借助和维护过于复杂的三方系统比如数据库MySQL、Redis等、分布式存储系统GFS、Bigtable等并且我们单台机器的性能有限比如硬盘只有1TB内存只有2GB**如何设计一个符合我们上面要求,操作高效,且使用机器资源最少的存储系统呢?**
### 相关章节
[21 | 哈希算法(上):如何防止数据库中的用户信息被脱库?](https://time.geekbang.org/column/article/65312)
[22 | 哈希算法(下):哈希算法在分布式系统中有哪些应用?](https://time.geekbang.org/column/article/67388)
### 题目解析
这个问题可以分成两部分,第一部分是根据元信息的搜索功能,第二部分是图片判重。
第一部分,我们可以借助搜索引擎中的倒排索引结构。关于倒排索引我会在实战篇详细讲解,我这里先简要说下。
如题目中所说,一个图片会对应一组元信息,比如玫瑰花对应{红色,花,情人节},牡丹花对应{白色,花},我们可以将这种图片与元信息之间的关系,倒置过来建立索引。“花”这个关键词对应{玫瑰花,牡丹花},“红色”对应{玫瑰花},“白色”对应{牡丹花},“情人节”对应{玫瑰花}。
当我们搜索“情人节 花”的时候,我们拿两个搜索关键词分别在倒排索引中查找,“花”查找到了{玫瑰花,牡丹花},“情人节”查找到了{玫瑰花},两个关键词对应的结果取交集,就是最终的结果了。
第二部分关于图片判重,我们要基于图片本身来判重,所以可以用哈希算法,对图片内容取哈希值。我们对哈希值建立散列表,这样就可以通过哈希值以及散列表,快速判断图片是否存在。
我这里只说说我的思路,这个问题中还有详细的内存和硬盘的限制。要想给出更加详细的设计思路,还需要根据这些限制,给出一个估算。详细的解答,我都放在哈希算法(下)那一节里了,你可以自己回去看。
## 实战测试题(六)
我们知道散列表的查询效率并不能笼统地说成是O(1)。它跟散列函数、装载因子、散列冲突等都有关系。如果散列函数设计得不好,或者装载因子过高,都可能导致散列冲突发生的概率升高,查询效率下降。
在极端情况下有些恶意的攻击者还有可能通过精心构造的数据使得所有的数据经过散列函数之后都散列到同一个槽里。如果我们使用的是基于链表的冲突解决方法那这个时候散列表就会退化为链表查询的时间复杂度就从O(1)急剧退化为O(n)。
如果散列表中有10万个数据退化后的散列表查询的效率就下降了10万倍。更直观点说如果之前运行100次查询只需要0.1秒那现在就需要1万秒。这样就有可能因为查询操作消耗大量CPU或者线程资源导致系统无法响应其他请求从而达到拒绝服务攻击DoS的目的。这也就是散列表碰撞攻击的基本原理。
**如何设计一个可以应对各种异常情况的工业级散列表,来避免在散列冲突的情况下,散列表性能的急剧下降,并且能抵抗散列碰撞攻击?**
### 相关章节
[18 | 散列表Word文档中的单词拼写检查功能是如何实现的](https://time.geekbang.org/column/article/64233)
[19 | 散列表(中):如何打造一个工业级水平的散列表?](https://time.geekbang.org/column/article/64586)
### 题目解析
我经常把这道题拿来作为面试题考察候选人。散列表可以说是我们最常用的一种数据结构了,编程语言中很多数据类型,都是用散列表来实现的。尽管很多人能对散列表都知道一二,知道有几种散列表冲突解决方案,知道散列表操作的时间复杂度,但是理论跟实践还是有一定距离的。光知道这些基础的理论并不足以开发一个工业级的散列表。
所以,我在散列表(中)那一节中详细给你展示了一个工业级的散列表要处理哪些问题,以及如何处理的,也就是这个问题的详细答案。
这六道题你回答得怎么样呢或许你还无法100%回答正确,没关系。其实只要你看了解析之后,有比较深的印象,能立马想到哪节课里讲过,这已经说明你掌握得不错了。毕竟想要完全掌握我讲的全部内容还是需要时间沉淀的。对于这门课的学习,你一定不要心急,慢慢来。只要方向对了就都对了,剩下就交给时间和努力吧!
通过这套题,你对自己的学习状况应该有了一个了解。从专栏开始到现在,三个月过去了,我们的内容也更新了大半。**你在专栏开始的时候设定的目标是什么?现在实施得如何了?<strong>你可以在留言区给这三个月的学习做个**阶段性学习复盘</strong>。重新整理,继续出发!

View File

@@ -0,0 +1,89 @@
<audio id="audio" title="不定期福利第二期 | 王争:羁绊前行的,不是肆虐的狂风,而是内心的迷茫" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/08/4d/0805719a7b73708138d360c24aa6c14d.mp3"></audio>
你好,我是王争。
专栏更新过半,我发现有些小伙伴已经掉队,虽然有人掉队也挺正常,但是我还是想尽量拉一把。于是,周末的时间,我就在想,究竟是什么原因让有些小伙伴掉队了?是内容本身太难了吗?是我讲得不够清楚吗?还是小伙伴本身基础太差、不够努力、没有掌握学习方法?
我觉得都不是,让你掉队的原因,从根儿上讲,是你内心的迷茫。**如果我们不那么确信能不能看懂、能不能学会的时候,当面对困难的时候,很容易就会否定自己,也就很容易半途而废**。
这就好比你迷失在沙漠中,对你来说,肆虐的狂风并不可怕,可怕的是,你不知道该努力多久才能走出沙漠,不知道到底能不能走出沙漠。这种对结果的未知、不确定,导致了你内心的恐惧,最后就差那么一点点就可以走出沙漠的时候,你放弃了。
学习也是同样的道理。所以,**我今天不打算讲学习方法,也不打算给你灌输心灵鸡汤,我就讲讲,对这个专栏的学习,或者对于任何学习来说,我觉得你应该建立的一些正确认知。**有了这些认知,希望你能在后面的专栏学习中,少一点迷茫,多一份坚持。
## 没有捷径,没有杀手锏,更没有一招致胜的“葵花宝典”
<img src="https://static001.geekbang.org/resource/image/21/68/2138b83c6c35881807d00eac47c02f68.jpg" alt="">
有小伙伴给我留言说:“看书五分钟,笔记两小时,急求学霸的学习方法”,还有人问,“数据结构和算法好难,到底该怎么学?是我的学习方法不对?还是我太笨?”
我想说,并没有什么杀手锏的学习方法,更没有一招致胜的“葵花宝典”。不知道这么说有没有让你失望。**如果你真要“求”一个学习方法,那就再看看我在专栏开始写的“[如何抓住重点,系统高效地学习数据结构与算法](https://time.geekbang.org/column/article/40011)”那篇文章吧**。
说实话,我也挺想知道学霸的学习方法的,所以,在求学路上,每当有学霸来分享学习方法,我都要去听一听。但是,听多了之后,我发现其实并没有太多用。因为那些所谓学霸的学习方法,其实都很简单,比如“认认真真听讲”“认认真真做每一道题”等等。
也不是他们说的不对,但是这种大实话,我总有一种领会不了的感觉,更别说真正指导我的学习了。而且,我觉得,很多时候,**这些方法论的难点并不在于能不能听懂,而是在于能不能执行到位**。比如很多人都听过“一万小时定律”,坚持一万个小时,你就能成为大牛,但有多少人能坚持一万个小时呢?
所以,这里我要纠正一个认知,那就是,学习没有“杀手锏”似的方法论。不要怀疑是不是自己的学习方法不对,不要在开始就否定自己。因为否定得越多,你就越迷茫,越不能坚持。
## 不要浮躁,不要丧失思考能力,不要丧失学习能力
<img src="https://static001.geekbang.org/resource/image/8c/ca/8c1963cc2a871f4c2de8631d4f8684ca.jpg" alt="">
有小伙伴给我留言说:“老师,这个地方看不懂,你能不能再解释一下”,还有小伙伴留言说:“《红黑树(上)》里的图为什么跟你的定义不相符?”
对于留言的问题,我都挺重视的,但是当仔细看这些问题的时候,我发现,实际上文章里已经有答案了,他根本没有认真看、认真思考,更别说去自己搜搜资料,再研究下,就来提问了。
一般情况下,我都会回复“你自己再认真看一遍”或者“你自己先去网上搜一下,研究研究,如果还不懂再给我留言”。告诉你答案,并不会花费我太长时间,但是,这样会让你丢失最宝贵的东西,那就是,你自己的思考能力、学习能力,能自己沉下心来研究的能力。这个是很可怕的。
现在,互联网如此发达,我们每天都会面对各种各样的信息轰炸,人也变得越来越浮躁。很多人习惯看些不动脑子就能看懂的东西,看到稍微复杂的东西,就感觉脑子转不动了。
上学的时候还好要考试有老师督促还能坚持学习。但是工作之后没有人监督很多人陷入各种手机App中不能自拔学一会儿就想玩会儿手机想静下心来学上半个小时都无比困难。无法自律沉不下心来那你就基本可以跟学习说拜拜了。
## 只有做好打硬仗的心理准备,遇到困难才能心态平和
<img src="https://static001.geekbang.org/resource/image/d3/42/d3c715012b855aaca2b186b5cf862642.jpg" alt="">
还有小伙伴给我留言说“看不懂一个4000多字的文章、10分钟的音频反复看了、听了2个小时都没怎么看懂”。我给他的回复是“**如果之前没有基础或者基础不好的话看2个小时还不懂很正常看一个礼拜试试。**”
“一个礼拜”的说法我一点都不是夸张。虽然专栏的每篇文章都只有三四千字10分钟左右的音频但是知识点的密度还是很高的。如果你潜意识里觉得应该一下子就能看懂就会出现这样的情况看了一遍不懂又看了一遍还是不怎么懂然后就放弃了。
数据结构和算法就是一个非常难啃的硬骨头,可以说是计算机学科中最难学的学科之一了。**我当时学习也费了老大的劲,能做到讲给你听,我靠的也是十年如一的积累和坚持**。如果没有基础、或者基础不好你怎能期望看2个小时就能完全掌握呢
面对这种硬骨头,我觉得我们要有打硬仗、打持久战的心理准备。只有这样,在学习的过程中遇到困难的时候,心态才能更加平和,才能沉下心来有条不紊地去解决一个个的疑难问题。这样,碰到问题,你可能还会“窃喜”,我又遇到了一个之前不怎么懂的知识点了,看懂它我又进步了一点。甚至你还会“坏坏地”想,又多了一个拉开我跟其他人距离的地方了。跨过这些点,我就能比别人更厉害。
一口吃不成胖子,如果你基础不好,那就从长计议吧,给自己定一个长一点的“死磕”计划,比如一年。面对不懂的知识点,沉下心来逐个突破,这样你的信心慢慢也就建立了。
## “放弃”的念头像是一个心魔,它会一直围绕着你
<img src="https://static001.geekbang.org/resource/image/61/f4/6118ba4f07e5c1f8f1a7a0a18ba6f7f4.jpg" alt="">
还有小伙伴给我留言说:“开始没怎么看懂,看了一下午,终于看懂了”。看到这样的留言,我其实挺为他感到庆幸的,庆幸他没有中途放弃。因为,放弃的念头就像一个心魔,在我们的学习过程中,它会一直围绕着我们,一旦被它打败一次,你就会被它打败很多次,掉队就不可避免了。
我分享一个我最近思考比较多的事情。前一段时间我在研究多线程方面的东西它涉及一块比较复杂的内容“Java内存模型”。虽然看懂并不难但是要透彻、无盲点地理解并不容易。本来以为半天就能看懂的东西结果我从周一一直看到周五下午断断续续花了5天的时间才把它彻底搞懂。回忆起这5天我有不下10次都想放弃每次心里都在想“算了先放一放以后再说吧”“太难了啃不下来算了。”“就这样吧反正也用不到没必要浪费时间”等等。这种放弃的念头就像一个邪恶的魔鬼一样一直围绕着我这5天的研究中。
现在回想起来,我很庆幸我当时没有放弃,多坚持了几天。如果当时我放弃了,那之后再遇到技术难题时,“放弃”的心魔还会再来拜访我,潜意识里我还是会认输。
之所以没有放弃,我自己总结了两点原因。
第一,我对学习这件事情认识得比较清楚,我一直觉得,没有学不会的东西,没有攻克不了的技术难题,如果有,那就说明时间花得还不够多。
第二,我之前遇到卡壳的时候,几乎从来没有放弃过,即便短暂地停歇,我也会继续拎起来再死磕,而且每次都能搞定,正是这种正向的激励,给了我信心,让我再遇到困难的时候,都能坚信自己能搞定它。
## 入门是一个非常漫长和煎熬的过程,谁都逃不过
<img src="https://static001.geekbang.org/resource/image/c3/67/c3db74036668f2d279e9a4a7b8468167.jpg" alt="">
还有小伙伴留言说:“看到有小伙伴有很多疑问,我来帮作者说句话,文章写得很好,通俗易懂,如果有一定基础,看懂还是不成问题的。”
我觉得,有些小伙伴的觉悟还是挺高的:)。我文章写得再通俗易懂,对于之前没有任何基础的人来说,看起来还是挺费劲的。
第一,数据结构和算法这门课程本身的难度摆在那里,想要轻松看懂,本身就不太现实。第二,对于任何新知识的学习,入门都是一个非常漫长和煎熬的过程。但是这个过程都是要经历的,谁都逃不过。只要你挺过去,入了门,再学习更深的知识就简单多了。
我大学里的第一堂课是C语言现在回想起来当时对我来说简直就是听天书。因为之前没有接触过计算机更别说编程语言对我来说C语言就像另一个世界的东西。从完全看不懂到慢慢有点看懂再到完全看懂不夸张地讲我花了好几年的时间但是当掌握了之后我发现这个东西其实也不难。但是如果没有度过漫长和煎熬的入门的过程如果没有一点韧性没有一点点信念那可能也没有现在的我了。
其实我一直觉得**情商比智商更重要**。对于很多学科的学习,智商并不是瓶颈,最终能够决定你能达到的高度的,还是情商,而情商中最重要的,我觉得就是**逆商**逆境商数Adversity Quotient也就是**当你遇到困难时,你会如何去面对,这将会决定你的人生最终能够走多远。**
<img src="https://static001.geekbang.org/resource/image/56/c2/56db1ff64199a020ef376187f75304c2.jpg" alt="">
好了,今天我想分享的关于学习的几个认知就讲完了。现在,你有没有对学习这件事有更加清晰的认识呢?能不能让你少一点迷茫,多一份坚持呢?
最后,我有一句送给你:吃得苦中苦,方为人上人。耐得住寂寞,才能守得住繁华。

View File

@@ -0,0 +1,81 @@
<audio id="audio" title="不定期福利第四期 | 刘超:我是怎么学习《数据结构与算法之美》的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bc/a2/bc691b254ecbd8b3fd2ddcadac9267a2.mp3"></audio>
你好,我是刘超,是隔壁《趣谈网络协议》专栏的作者。今天来“串个门儿”,讲讲我学习《数据结构与算法之美》这个专栏的一些体会和感受。
《数据结构与算法之美》是目前“极客时间”订阅量最多的专栏,我也是其中最早购买的一员。我之所以一看就心动了,源于王争老师在开篇词里面说的那段话:
>
基础知识就像是一座大楼的地基,它决定了我们的技术高度。那技术人究竟都需要修炼哪些“内功”呢?我觉得,无外乎就是大学里的那些基础课程,操作系统、计算机网络、编译原理等等,当然还有数据结构和算法。
这个也是我写《趣谈网络协议》的时候,在开篇词里反复强调的观点。我为什么这么说呢?因为,我们作为面试官,在招人的时候,往往发现,使用框架速成的人很多,基础知识扎实的人少见,而基础不扎实会影响你以后学习新技术的速度和职业发展的广度。
和“极客时间”编辑聊的时候,我也多次表达,希望我们讲的东西和一般的培训机构有所区别,希望“极客时间”能做真正对程序员的技能提升和职业发展有价值的内容,希望“极客时间”能够成为真正帮助程序员成长的助手。
所以当“极客时间”相继推出《Java核心技术36讲》《零基础学Python》《从0开始学架构》《MySQL实战45讲》这些课程的时候我非常开心。我希望将来能够继续覆盖到编译原理、操作系统、计算机组成原理等等。在这些课程里算法是基础的基础也是我本人很想精进的部分。
当然,除了长远的职业发展需要,搞定算法还有一个看得见、摸得着的好处,面试。
我经常讲,越是薪资低的企业,面试的时候,它们往往越注重你会不会做网站,甚至会要求你现场做出个东西来。你要注意了,这其实是在找代码熟练工。相反,越是薪资高的企业,越是重视考察基础知识。基础好,说明可塑性强,培养起来也比较快。**而最牛的公司,考的往往是算法和思路。**
相信很多购买《数据结构与算法之美》专栏的同学,下单的时候,已经想象自己面试的时候,在白板上挥洒代码,面试官频频点头的场景,想着自己马上就能“进驻牛公司,迎娶白富美”了。
然而,事实却是,**武功套路容易学,扎马步基本功难练**,编程也是一样。框架容易学,基本功难。你没办法讨巧,你要像郭靖学习降龙十八掌那样,一掌一掌劈下去才行。
于是咱们这个专栏就开始了你见到的仍然是困难的复杂度计算指针指来指去烧脑的逻辑小心翼翼的边界条件判断。你发现数据结构和算法好像并不是你上下班时间顺便听一听就能攻克的问题。你需要静下心来仔细想拿个笔画一画甚至要写一写代码Debug一下才能够理解。是的的确不轻松那你坚持下来了吗
我在这里分享一下我的学习思路,我将这个看起来困难的过程分成了几部分来完成。
第一部分,数据结构和算法的基础知识部分。如果在大学学过这门课,在专栏里,你会看到很多熟悉的描述。有些基础比较好的同学会质疑写这些知识的必要性。这大可不必,因为每个人的基础不一样,为了专栏内容的系统性和完整性,老师肯定要把这些基础知识重新讲述一遍的。对于这一部分内容,如果你的基础比较好,可以像学其他课程一样,在上下班或者午休的时候进行学习,主要是起到温习的作用。
第二部分,需要代码练习的部分。由于王争老师面试过很多人,所以在专栏里,他会列举一些他在面试中常常会问的题目。很多情况下,这些题目需要当场就能在白板上写出来。这些问题对于想要提升自己面试能力的同学来说,应该是很有帮助的。
我这里列举几个,你可以看看,是不是都能回答出来呢?
<li>
在链表这一节单链表反转链表中环的检测两个有序的链表合并删除链表倒数第n个结点求链表的中间结点等。
</li>
<li>
在栈这一节,在函数调用中的应用,在表达式求值中的应用,在括号匹配中的应用。
</li>
<li>
在排序这一节如何在O(n)的时间复杂度内查找一个无序数组中的第 K大元素
</li>
<li>
在二分查找这一节,二分查找的四个变体。
</li>
这些问题你都应该上手写写代码,或者在面试之前拿来练练手,而且,不仅仅只是实现主要功能。大公司的面试很多情况下都会考虑边界条件。只要被面试官抓住漏洞,就会被扣分,所以你最好事先写写。
第三部分,对于海量数据的处理思路问题。现在排名靠前的大公司,大都存在海量数据的处理问题。对于这一类问题,在面试的时候,也是经常会问到的。由于这类问题复杂度比较高,很少让当场就写代码,但是基本上会让你说一个思路,或者写写伪代码。想要解决海量数据的问题,你会的就不能只是基础的数据结构和算法了,你需要综合应用。如果平时没有想过这部分问题,临时被问,肯定会懵。
在专栏里,王争老师列举了大量这类问题,你要重点思考这类问题背后的思路,然后平时自己处理问题的时候,也多想想,如果这个问题数据量大的话,应该怎么办。这样多思考,面试的时候,思路很容易就来了。
比如,我这里随便列了几个,都是很经典的问题。你要是想不起来,就赶紧去复习吧!
<li>
比如说我们有10GB的订单数据我们希望按订单金额假设金额都是正整数进行排序但是我们的内存有限只有几百MB没办法一次性把10GB的数据都加载到内存中。这个时候该怎么办呢
</li>
<li>
如果你所在的省有50万考生如何通过成绩快速排序得出名次呢
</li>
<li>
假设我们有10万个手机号码希望将这10万个手机号码从小到大排序你有什么比较快速的排序方法呢
</li>
<li>
假设我们有1000万个整型数据每个数据占8个字节如何设计数据结构和算法快速判断某个整数是否出现在这1000万数据中 我们希望这个功能不要占用太多的内存空间最多不要超过100MB你会怎么做呢
</li>
第四部分,工业实践部分。在每种数据结构的讲解中,老师会重点分析一些这些数据结构在工业上的实践,封装在库里面的,一般人不注意的。
我看王争老师也是个代码分析控。一般同学可能遇到问题,查一查有没有开源软件或者现成的库,可以用就完了。而王争老师会研究底层代码的实现,解析为什么这些在工业中大量使用的库,应该这样实现。这部分不但对于面试有帮助,对于实际开发也有很大的帮助。普通程序员和高手的差距,就是一个用完了就完了,一个用完了要看看为啥这样用。
例如老师解析了Glibc中的qsort() 函数Java中的HashMap如何实现工业级的散列表Redis中的有序集合Sorted Set的实现工程上使用的红黑树等等。
尤其是对于哈希算法,老师解析了安全加密、数据校验、唯一标识、散列函数,负载均衡、数据分片、分布式存储等应用。如果你同时订阅了架构、微服务的课程,你会发现这些算法在目前最火的架构设计中,都有使用。
师傅领进门,修行在个人。尽管老师只是解析了其中一部分,但是咱们在平时使用开源软件和库的时候,也要多问个为什么。写完了程序,看看官方文档,看看原理解析的书,看看源代码,然后映射到算法与数据结构中,你会发现,这些知识和思路到处都在使用。
最后,我还想说一句,坚持,别放弃,啃下来。基础越扎实,路走得越远,走得越宽。加油!

View File

@@ -0,0 +1,85 @@
<audio id="audio" title="总结课 | 在实际开发中,如何权衡选择使用哪种数据结构和算法?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d9/28/d999636bdf48284b2b3fe20b2d881b28.mp3"></audio>
你好,我是王争,今天是一篇总结课。我们学了这么多数据结构和算法,在实际开发中,究竟该如何权衡选择使用哪种数据结构和算法呢?今天我们就来聊一聊这个问题,希望能帮你把学习带回实践中。
我一直强调,学习数据结构和算法,不要停留在学院派的思维中,只把算法当作应付面试、考试或者竞赛的花拳绣腿。作为软件开发工程师,我们要把数据结构和算法,应用到软件开发中,解决实际的开发问题。
不过,要想在实际的开发中,灵活、恰到好处地应用数据结构和算法,需要非常深厚的实战经验积累。尽管我在课程中,一直都结合实际的开发场景来讲解,希望带你真枪实弹地演练算法如何解决实际的问题。但是,在今后的软件开发中,你要面对的问题远比我讲的场景要复杂、多变、不确定。
要想游刃有余地解决今后你要面对的问题,光是熟知每种数据结构和算法的功能、特点、时间空间复杂度,还是不够的。毕竟工程上的问题不是算法题。算法题的背景、条件、限制都非常明确,我们只需要在规定的输入、输出下,找最优解就可以了。
而工程上的问题往往都比较开放,在选择数据结构和算法的时候,我们往往需要综合各种因素,比如编码难度、维护成本、数据特征、数据规模等,最终选择一个**工程的最合适解**,而非**理论上的最优解**。
为了让你能做到活学活用,在实际的软件开发中,不生搬硬套数据结构和算法,今天,我们就聊一聊,在实际的软件开发中,如何权衡各种因素,合理地选择使用哪种数据结构和算法?关于这个问题,我总结了六条经验。
## 1.时间、空间复杂度不能跟性能划等号
我们在学习每种数据结构和算法的时候,都详细分析了算法的时间复杂度、空间复杂度,但是,在实际的软件开发中,复杂度不能与性能简单划等号,不能表示执行时间和内存消耗的确切数据量。为什么这么说呢?原因有下面几点。
**复杂度不是执行时间和内存消耗的精确值**
在用大O表示法表示复杂度的时候我们会忽略掉低阶、常数、系数只保留高阶并且它的度量单位是语句的执行频度。每条语句的执行时间并非是相同、确定的。所以复杂度给出的只能是一个非精确量值的趋势。
**代码的执行时间有时不跟时间复杂度成正比**
我们常说时间复杂度是O(nlogn)的算法比时间复杂度是O(n^2)的算法,执行效率要高。这样说的一个前提是,算法处理的是大规模数据的情况。对于小规模数据的处理,算法的执行效率并不一定跟时间复杂度成正比,有时还会跟复杂度成反比。
**对于处理不同问题的不同算法,其复杂度大小没有可比性**
复杂度只能用来表征不同算法,在处理同样的问题,以及同样数据类型的情况下的性能表现。但是,对于不同的问题、不同的数据类型,不同算法之间的复杂度大小并没有可比性。
## 2.抛开数据规模谈数据结构和算法都是“耍流氓”
在平时的开发中,在数据规模很小的情况下,普通算法和高级算法之间的性能差距会非常小。如果代码执行频率不高、又不是核心代码,这个时候,我们选择数据结构和算法的主要依据是,其是否简单、容易维护、容易实现。大部分情况下,我们直接用最简单的存储结构和最暴力的算法就可以了。
比如对于长度在一百以内的字符串匹配我们直接使用朴素的字符串匹配算法就够了。如果用KMP、BM这些更加高效的字符串匹配算法实际上就大材小用了。因为这对于处理时间是毫秒量级敏感的系统来说性能的提升并不大。相反这些高级算法会徒增编码的难度还容易产生bug。
## 3.结合数据特征和访问方式来选择数据结构
面对实际的软件开发场景,当我们掌握了基础数据结构和算法之后,最考验能力的并不是数据结构和算法本身,而是对问题需求的挖掘、抽象、建模。**如何将一个背景复杂、开放的问题,通过细致的观察、调研、假设,理清楚要处理数据的特征与访问方式,这才是解决问题的重点。**只有理清楚了这些东西,我们才能将问题转化成合理的数据结构模型,进而找到满足需求的算法。
比如我们前面讲过Trie树这种数据结构是一种非常高效的字符串匹配算法。但是如果你要处理的数据并没有太多的前缀重合并且字符集很大显然就不适合利用Trie树了。所以在用Trie树之前我们需要详细地分析数据的特点甚至还要写些分析代码、测试代码明确要处理的数据是否适合使用Trie树这种数据结构。
再比如,图的表示方式有很多种,邻接矩阵、邻接表、逆邻接表、二元组等等。你面对的场景应该用哪种方式来表示,具体还要看你的数据特征和访问方式。如果每个数据之间联系很少,对应到图中,就是一个稀疏图,就比较适合用邻接表来存储。相反,如果是稠密图,那就比较适合采用邻接矩阵来存储。
## 4.区别对待IO密集、内存密集和计算密集
如果你要处理的数据存储在磁盘比如数据库中。那代码的性能瓶颈有可能在磁盘IO而并非算法本身。这个时候你需要合理地选择数据存储格式和存取方式减少磁盘IO的次数。
比如我们在[递归](https://time.geekbang.org/column/article/41440)那一节讲过最终推荐人的例子。你应该注意到了,当时我给出的代码尽管正确,但其实并不高效。如果某个用户是经过层层推荐才来注册的,那我们获取他的最终推荐人的时候,就需要多次访问数据库,性能显然就不高了。
不过,这个问题解决起来不难。我们知道,某个用户的最终推荐人一旦确定,就不会变动。所以,我们可以离线计算每个用户的最终推荐人,并且保存在表中的某个字段里。当我们要查看某个用户的最终推荐人的时候,访问一次数据库就可以获取到。
刚刚我们讲了数据存储在磁盘的情况现在我们再来看下数据存储在内存中的情况。如果你的数据是存储在内存中那我们还需要考虑代码是内存密集型的还是CPU密集型的。
所谓CPU密集型简单点理解就是代码执行效率的瓶颈主要在CPU执行的效率。我们从内存中读取一次数据到CPU缓存或者寄存器之后会进行多次频繁的CPU计算比如加减乘除CPU计算耗时占大部分。所以在选择数据结构和算法的时候要尽量减少逻辑计算的复杂度。比如用位运算代替加减乘除运算等。
所谓内存密集型简单点理解就是代码执行效率的瓶颈在内存数据的存取。对于内存密集型的代码计算操作都比较简单比如字符串比较操作实际上就是内存密集型的。每次从内存中读取数据之后我们只需要进行一次简单的比较操作。所以内存数据的读取速度是字符串比较操作的瓶颈。因此在选择数据结构和算法的时候需要考虑是否能减少数据的读取量数据是否在内存中连续存储是否能利用CPU缓存预读。
## 5.善用语言提供的类,避免重复造轮子
实际上对于大部分常用的数据结构和算法编程语言都提供了现成的类和函数实现。比如Java中的HashMap就是散列表的实现TreeMap就是红黑树的实现等。在实际的软件开发中除非有特殊的要求我们都可以直接使用编程语言中提供的这些类或函数。
这些编程语言提供的类和函数都是经过无数验证过的不管是正确性、鲁棒性都要超过你自己造的轮子。而且你要知道重复造轮子并没有那么简单。你需要写大量的测试用例并且考虑各种异常情况还要团队能看懂、能维护。这显然是一个出力不讨好的事情。这也是很多高级的数据结构和算法比如Trie树、跳表等在工程中并不经常被应用的原因。
但这并不代表,学习数据结构和算法是没用的。深入理解原理,有助于你能更好地应用这些编程语言提供的类和函数。能否深入理解所用工具、类的原理,这也是普通程序员跟技术专家的区别。
## 6.千万不要漫无目的地过度优化
掌握了数据结构和算法这把锤子不要看哪里都是钉子。比如一段代码执行只需要0.01秒你非得用一个非常复杂的算法或者数据结构将其优化成0.005秒。即便你的算法再优秀,这种微小优化的意义也并不大。相反,对应的代码维护成本可能要高很多。
不过度优化并不代表,我们在软件开发的时候,可以不加思考地随意选择数据结构和算法。我们要学会估算。估算能力实际上也是一个非常重要的能力。我们不仅要对普通情况下的数据规模和性能压力做估算,还需要对异常以及将来一段时间内,可能达到的数据规模和性能压力做估算。这样,我们才能做到未雨绸缪,写出来的代码才能经久可用。
还有当你真的要优化代码的时候一定要先做Benchmark基准测试。这样才能避免你想当然地换了一个更高效的算法但真实情况下性能反倒下降了。
## 总结
工程上的问题,远比课本上的要复杂。所以,我今天总结了六条经验,希望你能把数据结构和算法用在刀刃上,恰当地解决实际问题。
<img src="https://static001.geekbang.org/resource/image/f7/f4/f7125dded207f84d3e363ef1603b03f4.jpg" alt="">
我们在利用数据结构和算法解决问题的时候,一定要先分析清楚问题的需求、限制、隐藏的特点等。只有搞清楚了这些,才能有针对性地选择恰当的数据结构和算法。这种灵活应用的实战能力,需要长期的刻意锻炼和积累。这是一个有经验的工程师和一个学院派的工程师的区别。
好了,今天的内容就到这里了。最后,我想听你谈一谈,你在实际开发选择数据结构和算法时,有什么感受和方法呢?
欢迎在留言区写下你的想法,也欢迎你把今天的文章分享给你的朋友,帮助他在数据结构和算法的实际运用中走得更远。