前言
工程学科有两种隐含的正确性原则:正确的做事(HOW)、做正确的事(WHAT)。
按照这个概念,所谓的测试驱动开发、重构、敏捷等工程性实践可以纳入HOW的范畴,而使得计算机科学永葆活力的数据结构和算法则纳入WHAT的范畴。
在一年简单的工作中一直关注的HOW,略有所得;从近一个月面试的反馈中了解到对数据结构和算法的硬性要求,而这是我没有多少脑力活动和技术沉淀的领域,所谓知耻而后勇,计划花一段时间来探索和总结。
先从一道二面题说起:
100亿个urlno-text的key-value序列,urlno已编号,每个text大约100KB(1)如何存储(2)如何保证查询效率(3)如何考虑备份?
之前也看过一些数据存储和处理的书籍和文章,像hadoop, hbase、“十道海量数据处理面试题与十个方法大总结”(http://kb.cnblogs.com/page/95701/)等。
我当时的思路是(请允许对自己有些美化):就是hbase的存储方式,主管理节点和数据节点,每个数据有3个备份,没记住hbase究竟是如何组织数据的。
吭哧半天后算出来数据量有100T,就100台机器,将urlno%100存储,记得有个B树的概念常被用于商业级数据库实现中可以优化查询,可是完全不知道这个数据结构是如何组织的。失败!
除此之外,还有那道输出集合幂的笔试题。被鄙视倒是其次,在解决问题时的无助让我感到耻辱。
参考文献
[1]Anany Levitin,Maria Levitin 著, 赵勇, 徐章宁, 高博 译. 算法谜题[M]. 人民邮电出版社,北京. 2014.
内容
编程珠玑的作者一直在"炫耀"他在算法设计、数据结构选择和程序实现技巧等方面的优势,但忽略了重要的一环:授人以鱼不如授人以渔。
Donald E. Knuth的计算机设计程序艺术则属于重剑无锋性质的飞机操作和维修详细说明书,但怎么把飞机滑翔起来呢?
还好有关算法的书籍不总是高山仰止,例如[1],让人从Ah!从Aha!的过程能够尽量有趣和平缓一点。
记性比较差,书写下来,一方面理理词汇和思路,一方面便于回顾。
书[1]给出了常见算法设计策略:
(1)穷举搜索 (2)回溯法 (3)减而治之 (4)分而治之 (5)变而治之 (6)贪心法 (7)迭代改进 (8)动态规划
策略是高层的规范性说明,一方面有趣的完整示例更能说明问题,这可以理解为策略的实例(instance);而如何从0/1、n->n+1等举一反三的策略应用需要高质量的后期脑力训练。
(1)穷举搜索(exhaustive search)
说明
该策略蛮力的尝试问题的所有可能解,直到找到解为止。
优势和局限性
不需要额外的精心设计和组织,仅需完备的表示可能解和测试可能解。
问题的可能解随问题规模的增长呈指数增长或增长很快时,会存在计算能力、存储空间的不足问题。
示例
幻方 将1~9填入3×3的表格,使得每行、每列以及每条对角线上的数字之和相等。
(2)回溯法(backtraking)
说明
该策略是对穷举搜索的改进。
求解过程分为解的构造和评估部分解。解的构造通过在现有部分解的基础上一次添加一个元素完成。
评估部分解通过评估当前部分解能够再进一步而不违背问题中的约束,如果可以则选择第一个合法元素添加到当前部分解中;
如果不可以则终止当前尝试、将当前部分解的最后一个元素替换为该元素的候选元素,继续尝试。
回溯法图形解释:状态空间树(state-space tree)。
优势和局限性
在完备可能解空间内,遇到明显的无望解时,及时的抽身而出,减少可能解测试次数。
该策略需要一定的错误尝试撤销动作,该动作越少,算法找到单个/唯一解的速度越快。最差情况下,该策略可能会遍历所有可能解,回退到穷举搜素策略。
示例
n皇后问题 将n个皇后放在n×n的国际象棋棋盘上,其中没有任何两个皇后处于同一行、同一列或同一对角线,以使得他们不能互相攻击。(对象线指或长或短的任一对角线)
(3)减而治之(decrease-and-conquer)
说明
该策略观察给定问题的解与较小规模问题的解之间的关系,以递归方式逐步削减问题规模,直到特定规模的问题可以轻松解决。
那每次将问题规模减多少呢?视问题而定,可以选择1、一半或某个常数因子。
优势和局限性
不再是尝试可能解了,上升到观察不同规模的同一问题的解之间的关系了。
递归概念的引入,便于算法设计者的抽象,而不恰当的递归实现可能消耗较多的计算资源。
示例
(减1)集合幂 输出集合{a1, a2, ..., an}的幂(power set)。
(减半)猜数字 仅通过提出答案为是/否的问题,判定[1, n]内的一个事先选定的数字。
(4)分而治之(divide-and-conquer)
说明
该策略是减而治之策略的泛化,其基本思路是将问题划分为若干较小规模的问题,分别解决每个小规模问题,必要时组合小规模问题的解构成最终问题的解。
优势和局限性
与(3)大致相同;
如何划分问题规模、如何组合小规模问题解构成最终问题的解需要算法设计者额外的精力注入。
示例
三格骨牌谜题 三格骨牌是L的相邻三格。使用适当的三格骨牌覆盖一块缺了一格的2^n×2^n棋盘,棋盘上缺的可以是任意一格。三格骨牌不许覆盖除缺格之外的全部格,且不允许有重叠。
(5)变而治之(transform-and-conquer)
说明
该策略的核心是变换,问题的求解分为两个阶段,一是变换阶段,将问题变换为另一个问题,保持两个问题有相同的解(语义保持);而求解变换后的问题。
通常变换后的问题由于具有某种特殊性质,容易求解。
究竟怎么变呢?(这里只能抄了)
(a)谜面简化(instance simplification) - 同一问题的实例转换
将问题的一个实例转换为该问题的另一个实例,后一个实例具备特殊性质容易求解;
(b)表示变更(representation change) - 问题输入转换
将问题的输入转换成另一种表示,从而有助于找到有效算法来求解;
(c)问题规约(problem reduction) - 两个问题的实例转换
将给定问题的实例整体转换为另一个问题的实例。
优势和局限性
转换(问题实例、问题输入)强调的是变通,便于算法设计者将不熟悉的问题转换为已熟悉的问题或有充分自信可以求解的问题。
转换也是双刃剑,稍不留意可能增加问题的复杂度,同样需要算法设计者对问题有足够的洞察力。
示例
(编程珠玑)变位词检测 变位词就有由相同的字母组成的单词,如果eat, ate, tea就是变位词。设计一种算法,在一个巨大的英语单词文件中找出所有的变位词集合。
(6)贪心法(greedy approach)
说明
该策略的基本思想是在不违背问题约束的前提下,选择产生最大收益的下一步。其依赖于假设:局部最优解序列最终可以生成全局最优解。
优势和局限性
该策略每一步都是“令人欢欣鼓舞的”。
证明局部最优解序列最终确实生成了全局最优解可能存在一定的困难。
示例
(可以采用)不可互攻的王 在一张8×8棋盘上放置尽可能多的王,使得它们两两互补相邻,纵向、横向和对角线方向均是如此。
(不可采用)夜过吊桥 一行A,B,C,D,只有一个手电筒,需要在夜间过一座吊桥。桥身最多承重两人的重量,过桥时需要照明。手电筒只能由人携带,不可以扔来扔去。四人过桥的事件分别是1min, 2min, 5min, 10min。如果结对过桥,就只能迁就于速度最慢的人。试求过桥的最快方案。
(7)迭代改进(iterative improvement)
说明
该策略从容易获得的估计解出发,重复的应用一些简单的步骤不断的改进估计解。
依据该策略设计的算法的正确性证明包括两部分:一为算法能够在有限步内终止,二为最终得到的估计解确实是问题的解。
优势和局限性
总是可以生成接近于问题解的估计解。
技巧性或困难点在于估计解的获取/表示、改进估计解的方法、算法正确性证明。
示例
柠檬水摊设点 A,B,C,D,E五个人想合伙摆个柠檬水摊,他们分别住在网格化的图中1,2,3,4,5标识的五处。问题是这个摊摆在那个网格点除,才能距离所有人的住处最近?距离的计算是指住处与摊点之间的纵横街区综述。
正数变号 给定一个m×n的实数表格,能否找出一个算法,能够在仅允许将一整行或一整列的数字改变符号的前提下,使得所有的行和列之和(m×n个数的和)非负?
(8)动态规划(dynamic programming)
说明
该策略不对彼此之间有重叠的子问题求解,只求解较小规模的子问题一次、并记录该结果, 最终问题的解从小规模子问题的记录中获得。问题必须具备最优子结构(optional substructure),才能从子问题的最优解有效的构建出全局最优解。
最优子结构性质:如果问题的最优解可以从其子问题的最优解有效的构造出,称该问题具备最优子结构性质。(In computer science, a problem is said to have optimal substructure if an optimal solution can be constructed efficiently from optimal solutions of its subproblems. 见wiki http://en.wikipedia.org/wiki/Dynamic_programming http://en.wikipedia.org/wiki/Optimal_substructure)
优势和局限性
不对存在重叠的子问题重复求解。
需记录小规模子问题结果,如何从该结果记录中生成最终的解需要特别考虑。
示例
最短路径计数 假设某城市中全是完全纵横方向的街道,计算十字路有A和B之间的最短路径条数。