[译文]我在科技公司工作中实际用到的数据结构和算法

英文版权归Gergely Orosz所有,中译文作者yangyang(aka davidkoree)。双语版可用于非商业传播,但须注明英文版作者、版权信息,以及中译文作者。翻译水平有限,请广大读者指正。

你在日常工作中会实际用到算法和数据结构吗?我注意到一个趋势,就是在科技公司里,人们越发觉得算法是毫无意义的问题,纯粹是出于主观考虑而提出这些问题。我听到大多数人抱怨这纯粹是一种学术活动。这种观点在Homebrew作者Max Howell发表了其面试Google的经历之后变得更加流行:

尽管我也从来不需要使用二叉树反转,但是在Skype/Microsoft,Skyscanner和Uber日常工作时,我也遇到了数据结构和算法,包括编写代码并根据这些概念做出决策。但是在大多数情况下,我使用这些知识来了解如何以及为何构建业务逻辑。了解了这些概念后,就很容易理解用到这些概念的设计和实现。

本文讨论了很多生产环境中的实际情况,诸如树,图和各种算法之类的数据结构。我希望说明一下,通用的数据结构和算法知识并非“仅仅适合面试”,而是在快速发展的创新型技术公司工作时可能会用到的。

我使用过很少一部分算法,但几乎使用过所有数据结构。我不喜欢算法繁琐且不切实际的面试问题,这些问题涉及诸如红黑树或AVL树之类的奇异数据类型。我从来没有问过这些算法,也永远不会。这毫不奇怪。你可以在本文结尾处了解我对这些评价的看法。话说回来,能知道选择各种基本数据类型来解决某些问题,仍有很大价值。下面就让我们切入正题。

树和树的遍历:Skype, Uber和UI框架

当我们在构建Xbox One版本的Skype时(作为该平台上构建首批成熟的应用程序之一),我们使用的是准系统Xbox OS,但缺少关键库。我们需要一个导航解决方案,并将其集成到触摸手势和语音命令上。

我们在WinJS之上构建了一个通用的导航框架。为此,我们需要维护一个类似DOM的图(Graph)来跟踪可操作的元素。为了找到这些元素,我们对现有DOM进行了DOM遍历——基本上是树(tree)遍历。这是BFSDFS(广度优先搜索或深度优先搜索)的经典案例。

如果你是搞Web开发的,那么你已经在跟树结构(DOM)打交道了。所有DOM节点都可以有子节点,并且浏览器在遍历DOM树后将其渲染到屏幕上。如果要搜索特定元素,则可以使用内置的DOM方法找到它(例如getElementById),也可以实现BFS或DFS搜索以遍历所有节点,类似于本示例中的操作。

许多框架在底层使用树结构来渲染UI元素。React维护一个虚拟DOM,使用「智能协调」(smart reconciliation,一种“差异”算法)通过仅重新渲染屏幕上有变更的部分来提供出色的性能。Ryan Bas 在关于React协调的文章中将这一过程可视化。

在Uber的移动架构中,RIBs(译注:RIBs即Router, Interactor and Builder的缩写,为一款在Uber内部孵化并开源的跨平台移动端应用架构框架)也使用树——与大多数呈现层次结构中的元素的UI框架相似。RIBs维护着一个用于状态管理的RIB树,附加和分离需要呈现的RIB构件。在使用RIBs时,有时我们会勾勒出新RIB在层次结构中适合的位置,并讨论目标构件是否应具有视图(使其成为视图层次结构的一部分)或仅管理状态。

一个与RIB用例中的状态转换有关的例子。请参阅此处的RIB文档和代码。

如果你发现自己需要构建层次结构元素的可视化,一种常见的方法是使用树状结构,遍历树并渲染你访问的元素。我见过许多使用这种方法的内部工具。Uber移动平台团队构建的RIBs可视化工具就是一个例子,你可以在此视频的演示中看到该工具。

权重图和最短路径:Skyscanner

Skyscanner的功能是发掘最优惠的机票。为此,它会扫描全球所有路线,然后将它们组织在一起。虽然问题的本质更多地是在爬取网络数据而非缓存数据,但当航空公司计算中转路线时,多城市规划选项却成为最短路径问题

「多城市」(Multi-city)是Skyscanner花费大量时间来构建的功能之一。但公平地说,它在产品侧的实现难度大于其他方面。最佳的多城市交易是通过使用最短路径算法(例如Dijkstra或A*)来计算的。飞行路线以有向图表示,每个边均具有机票价格的权重。通过在每条路线上实施经过修改的A*搜索算法 ,可以计算出两个城市之间的最便宜价格。如果你对航班和最短路径感兴趣,那么Sachin Malhotra撰写的有关使用BFS实现最短航班搜索路径的文章是不错的阅读参考。

但是,使用Skyscanner,实际算法就不那么重要了。而缓存、爬取,和处理各种网站负载是更棘手的事情。尽管如此,最短路径问题仍会在许多旅行社基于组合进行价格优化的情况下出现,并发生变异。毫不奇怪,我们在这里讨论的这些,也只是泛泛而谈(译注:原文「a source of hallway discussions」)。

排序:Skype(一丁丁点儿)

排序是一个算法系列,我很少有借口实现或需要深入使用它们。弄懂气泡排序,插入排序,合并排序,选择排序以及最复杂的一种——快速排序这些不同类型的排序方式,是一件很有趣的事情。不过,我发现很少有理由要实现这些功能,尤其是当你不需要把排序函数编写为库的一部分时。

不过,在Skype上,我对此有一些经验。团队中的一位工程师决定实现插入排序以列出联系人。在2013年,当用户将Skype账号连接到网络时,相关的联系人信息会突然到达,并且加载所有联系人都需要花费一些时间。因此,这位工程师认为使用插入排序来建立按姓名组织的联系人列表更加有效。关于为什么不只使用默认排序算法,我们进行了反复讨论。最后,我们在适当地测试实现并进行基准测试上又花费了更多精力。我个人认为这样做没有多大意义:但那个阶段,项目拥有充裕的时间。

在现实世界中肯定有一些用例,其中有效的排序很重要,并且根据数据特征(译注:如数据规模、数据结构等)控制使用的排序类型可以有所作为。当以大块流传输实时数据并为这些数据源建立实时可视化时,插入排序会很有用。如果涉及到存储在不同节点上的大量数据,则合并排序可以很好地与分而治之方法一起使用。我没有使用这些算法,因此除了对各种方法的欣赏之外,我仍将排序算法标记为日常很少使用的东东。

哈希表和哈希:无处不在

我经常使用的最常见的数据结构是哈希表和哈希函数。从计数,检测重复,到缓存,再到分片之类的分布式系统用例,它都是一个便捷的工具。在数组之后,它很容易成为我在无数场合使用的最常见的数据结构。几乎所有编程语言都带有此数据结构,如果需要,可以轻松实现。

堆栈和队列:时不时地

堆栈(stack)的数据结构,对于那些调试过具有堆栈跟踪功能的语言的开发者来说,是再熟悉不过的。作为数据结构,我在使用它时遇到了一些问题,但是调试和性能分析让我变得手到擒来。在深度优先遍历树时,这也是一个显而易见的选择。

我很少需要选择队列(queues)作为代码的数据结构,但是我在代码库,代码弹出或向其中推送值时遇到了很多次。一个常见的用例是实现树的BFS(译注:广度优先搜索)遍历,其中队列数据结构作用其中。你还可以将队列用于其他各种用例。我曾经读过一些代码调度作业,这些作业充分利用了优先级队列,首先使用Python堆队列算法运行最短的作业。

加密:Uber

来自移动或Web客户端的用户输入的敏感信息在通过网络发送之前需要进行加密,仅在特定服务上进行解密。为此,需要在客户端和后端实施加密方法。

了解加密很有趣。你不用创建一种新的算法——这可能是最糟糕的想法之一。取而代之的是,你采用现有的标准,其拥有良好的描述文档,并使用框架内置的原语。选择的标准通常是AES标准的一部分。如果可以安全地加密美国机密信息,并且对该协议没有已知的有效攻击,那么对于大多数使用情况,AES192或AES256通常是足够安全的选择。

当我加入Uber时,移动端和Web端已经在这些原语的基础上实现了加密,这为我查找高级加密标准(AES),哈希消息身份验证码(HMAC),RSA公钥加密和其他相关实现提供了借口。弄懂一系列加密步骤如何证明是安全的是另一个有趣的领域。在“加密和MAC”,“ MAC然后加密”和“加密然后MAC”之间,只有其中一个是可证明的安全性 ——即使这并不意味着其他都不安全。

我们几乎不需要实现加密原语(crypto primitives)——除非建立一个全新的核心框架。但是,决定使用哪种原语,如何组合原语是你可能要面对的决定——或必须了解为什么采用某种方法。

决策树:Uber

在Uber的一个项目中,我们必须在移动端应用程序中实现复杂的业务逻辑。基于诸多规则(原文:half a dozen rules),我们必须从几个不同的屏幕之中选择一个来展示给用户。由于需要进行一系列合规性检查和用户选择,因此变得异常复杂。

构建此功能的工程师首先尝试使用一系列if-else语句对规则进行编码,这太复杂了。最后,他们决定采用「决策树」,因为它很容易通过产品和合规性进行验证,合理实施且易于更改(如果需要)。我们需要为边缘构建一个实现来执行规则,但仅此而已。尽管我们可以用另一种方法来节省工时,但团队发现,此解决方案易于维护和扩展。决策树如下所示:边缘是执行规则的结果(二进制结果或值范围),叶节点标记需要展示(给用户)的屏幕。

我们构建了一个决策树的结构,以遵循一组复杂的规则来决定展示哪个屏幕。

Uber的移动端构建系统称为SubmitQueue,它也利用动态构建的决策树。「开发人员体验团队」必须解决每天发生数百个移动端代码合并的难题,每个构建大约需要30分钟才能运行,包括编译、单元测试,集成和UI测试。并行化构建在当时不是一个很好的解决方案,因为两个构建经常会有重叠的更改,并且会发生合并冲突。在实践中,这意味着有时工程师将不得不等待2-3个小时,重新评估并盯着合并过程,并祈求不会发生代码合并冲突。

开发者体验团队采用了一种创新的方法,即通过「推测图」来预测合并冲突并相应地对构建进行排队。推测图非常类似于「二进制决策树」:

SubmitQueue使用一个推测树——一个二进制决策树——用每个边缘的预测概率进行注释。该方法确定要并行运行的构建集,以改善周转时间和吞吐量,同时保持Master主干的畅通。在这里阅读全文

由开发者体验团队的工程师们撰写的这份白皮书中有很多构建该解决方案的细节,值得阅读。Adrian Colyer也对该方法进行了很好的可视化分析。结果是建立了更快的构建系统,优化了构建时间,并使数百名移动端工程师脱离苦海。

正六边形网格化(Hexagonal Grids),层次索引:Uber

最后一个项目是我纯粹硬啃过的。我了解了一种新的数据结构:具有层次结构索引的六边形网格。

在优步要解决的最困难和最有趣的问题之一是如何优化旅行定价以及司机派单。价格可能是动态的,司机也在不断变化。「H3」是一个网格系统,工程师们旨在以越来越细化的方式在其上建立可视化和分析整个城市数据的能力。该套系统的数据和可视化结构,是具有层次结构索引的六边形网格,其上构建了一系列内部可视化工具。

用六边形细分区域。在这里阅读详尽的文章。

该数据结构具有特定的索引,遍历,分层网格,区域和单向边缘功能,详细信息请参见API参考。有关更深入的了解,请参见H3库上的文章源代码或有关如何以及为何构建此工具的演示

我真的很喜欢这种经验,因为发现自己创建专门的数据结构在特定领域很有意义。在没有很多用例的情况下,具有层次结构索引的六边形网格比将映射与每个单元中的各种数据级别组合起来更有意义。不过,如果你熟悉某些数据结构,则了解此新数据结构要容易得多(译注:所谓「触类旁通」),因为你可以为满足类似的特殊需求而设计另外的数据结构。

评价,算法和数据结构

这些是我多年来在多家公司工作遇到的实际且专业化的数据结构和算法。因此,让我们回到最初的推文中(译注:见图1),该推文抱怨询问诸如反转白板上的二叉树之类的问题。我认可Max的想法。

在一家科技公司工作,你不需要了解流行算法或外来数据结构的工作原理。 你应该知道什么是算法,并且应该能够自己运用一些简单的算法,例如贪婪的算法。你还应该了解非常常见的基本数据结构,例如哈希表,队列或堆栈。但是你不需要死记硬背诸如Dijkstra,A*和更高级的算法之类的特定算法:你会有相应的参考(译注:原文「you'll have a reference for this」)。除了排序以外,我在算法上所做的最大努力就是查找它们并自己理解它们。对于奇异的数据结构(如红黑树或AVL树),也是如此。我从来没有使用过这些数据结构。即使我这样做,我也会再次回顾它们。我从未问过需要这种知识来解决的问题,也永远不会问。

我只是问一些实用的编码练习,包括许多不错的解决方案,从蛮力或贪婪的方法到可能更复杂的方法。例如,要求实现一个像这样「排齐文本功能」是一个很合理的问题:这是我在Windows Phone上构建文本呈现组件时所做的事情。你只需使用一个数组和一些if/else语句即可解决此问题,而无需任何复杂的数据结构。

现实情况是,许多团队和公司都面临算法挑战。我明白算法问题的吸引力:它们在45分钟或更短的时间内抛给你,面试题也可以轻松调换。因此,如果题被泄漏了,危害也不大。在招聘时,它们也很容易扩展,因为你可以拥有100多道问题的题库,任何面试官都可以评估其中的任何一个。特别是在硅谷,听到针对动态编程或奇异数据结构的问题越来越普遍。这些问题将有助于聘请强大的工程师,但同时也会导致那些本可以在不需要高级算法知识的工作上表现出色的人被拒之门外。

对于那些听说一些公司要招募精通高级算法人员的家伙们,我的建议是:请重新考虑,你是否需要这样。我在Skyscanner伦敦分部和Uber阿姆斯特丹分部雇用了出色的团队,没有任何棘手的算法问题,仅涉及数据结构和问题解决。你不需要完全了解算法。你需要做的是了解最常见的数据结构,将之作为工具集,并具备应用简单算法来解决当前问题的能力。

数据结构和算法是一个工具集

如果你在一家快速发展的创新技术公司中工作,几乎可以肯定会在代码库中遇到各种数据结构和不同的算法实现。当你构建新的创新解决方案时,通常会发现你要独自选择恰当的数据结构。这时你要意识到各种选项,做出权衡取舍。

数据结构和算法是构建软件时应放心使用的工具。了解这些工具,你便能熟练使用它们的代码库。你还能对解决复杂问题充满信心。你知晓理论上的极限,可以进行的优化,并且能提出尽可能好的解决方案——即做到「通盘考虑」。

为此,我推荐下述资源:

  • 阅读哈希表,链表,树,图,堆,队列和堆栈数据结构。尝试如何用你熟悉的编程语言使用它们。GeeksforGeeks(译注:有移动客户端,可在Google Play上下载) 对这些内容有很好的概述。对于编码实践,我建议使用HackerRank数据结构集合
  • Aditya Bhargava所著的Grokking算法是一本唾手可得的算法指南,从初学者到经验丰富的工程师都适用。这是一个非常易懂且直观的指南,涵盖了大多数人在该主题上需要了解的所有内容。我坚信,你不需要了解更多本书未涵盖的算法知识。
  • 《算法设计手册》《算法:第四版》都是我在重温大学算法课程内容时的手头书。但我中途放弃了,发现它们很生涩,不适用于我的日常工作。

原文链接:https://blog.pragmaticengineer.com/data-structures-and-algorithms-i-actually-used-day-to-day/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值