Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.
For example, given n = 3, a solution set is:
[ "((()))", "(()())", "(())()", "()(())", "()()()" ]
背景
随着公司业务的增长,开发组的压力越来越大。王小二的工作也越来越多,老马决定给王小二找一个实习生(老马真是一个体贴的技术leader)。正好收到了一份应聘实习生的简历。这个候选人叫做 猪宝 ,是一个非常优秀的应届毕业生。他是留学归来的旅日硕士,日本东京帝国大学的计算机系高材生 。老马决定出一道考察计算机基本功底的算法题来面试这位候选人。
正文
王小二对出笔试题还是很好奇的,因为他一直是被“鄙视(笔试)”的对象,从来没有出过题目。他在来公司之前还面过其他几家公司的职位,可以说一位身经百战的“面试者”了。在过往的面试中,经常会遇到一些算法题。这让他很郁闷,一直觉得出这些算法题是刁难人啊,因为实际工作中根本用不到。所以,他对老马这次出题很感兴趣,非常想了解出笔试题的原则有哪些。
“老马,你打算出道什么样的题目啊?”
老马回答道“出一道字符串的题目吧。因为我们的大部分工作还是跟字符串打交道的。我想考察一下候选人的编程基本功底和对字符串的处理能力。”
“编程的基本功底具体是指什么?”王小二比较好奇。
“递归。”
“递归?”王小二满脸的疑惑。
“对”,老马肯定的说:“递归是很多算法的基础,比如深度优先搜索,二分法、分治算法等。”
“你知道递归有什么特点吗?”老马喜欢用提问的方式引导对方思考。
“不清楚…” 王小二有点不好意思的说道。
毕竟,承认自己的无知也是需要勇气的。老马看着王小二露出了不好意思的神态,微微一笑道:"因为递归能缩小问题集,可以将一个复杂的问题缩小到非常的简单的问题,可以使在子问题解决的基础上自行解决其他的部分。 "
"用递归往往可以写出非常简短的代码。"
“那你具体打算考什么题目呢?” 王小二有点迫不及待的想知道了。
“考这道题吧”, 老马递过来手中一张打印了题目的A4纸给王小二说:“你先试着想想思路,面试的时候,别被猪宝同学给问懵了”。老马对这次面试还是很重视的。毕竟,这是公司第一次引入有留学经历和工作经验的高素质人才。
王小二麻利的接过老马递过来的笔试题,仔细的阅读起来:“给定一个数n ,求符合要求的字符串的组合,字符只能是( 和 )”。
“这题目好难啊”,王小二心想“幸亏我当时笔试的时候不是这道题”。王小二假装思考了一会,叹了一口气跟老马说:“老马,这道题好难啊,给点提示吧。”
“哈哈,你可以试一试暴力搜索啊!”老马拍拍这个小年轻的肩膀说“更具体的说,就是深度优先搜索”。
“啊?深度优先搜索 ?” 王小二直接懵了。他可不傻,这道题根本没有图, 怎么可能用深度优先搜索。
“你学过数据结构吗”,老马没有正面回应王小二的疑问,而是问了一个奇怪的问题。
“学过啊,期末考试还考了优呢”,这算是王小二在大学期间为数不多值得骄傲的事情吧。
“你认为数据结构是什么?”
“数据结构是计算机存储和组织数据的方式”,王小二像一个小学生回答老师上课的提问一样的答道。
“嗯,这是课本上标准的答案”老马点点头道:“但数据结构最重要的特点是抽象!在使用数据结构的时候,一定不要把它套在具体的事情上面。抽象也说明了一个很重要的特点:数据结构存储的数据跟实际存储也是可以不相关的。”
老马看着王小二仍然是一脸疑惑的样子,知道要让一个新手这么快掌握抽象的概念是不可能的。所以,他打算一步一步的分析这道题来引导王小二思考。
“这道笔试题其实是可以抽象成一个树的模型”
“树由四个组成部分:1,根节点,树的起始位置;2, 叶节点,树的末尾节点;3,中间状态,4,状态转移方法,也就是操作”。
“只有定义清楚了这些概念,才能构造一棵完整的树。”
老马清了清嗓子,接着说:“结合我出的这道题,我们可以给出节点的数学形式 (x,y)。其中 x 表示的是 左括号的数量,y 表示的右括号的数量。”。
“如果根节点是 (0, 0) , 叶节点是 (n, n),操作是什么?”老马还是没有忍住提了一个问题。
“是 x +1 或者 y + 1”,王小二飞快的答道。这次小二终于没有回答不知道了。
“对!也就是每次增加一次左括号或者右括号。”老马给了王小二一个肯定的眼神:“现在你能构造出这个树了吗?”。
“可以了!”王小二高兴的说道。
“抽象化的能力实在太重要了。很多问题,经过抽象后是可以转换成我熟悉的数据结构的 。”王小二想道。他终于开始重视起对抽象能力的培养了。
“对。这道题还没有结束呢。我们已经构造出了树,接下来就是遍历树的所有节点了”老马说道:“你还记得树有哪几种遍历算法吗?”
“两个! dfs 和 bfs” 王小二的基础功底还是很不错的,不然也不可能加入公司的。
“嗯,这颗树用 dfs 比较合适” 老马接着说:“遍历树的时候,你觉得应该注意什么?”
“应该注意剪枝吧,放弃不符合要求的支路。这样可以提高效率” 王小二回答道。
“嗯。dfs本质上是一种暴力搜索方法,如果不加条件的话,就会搜索所有的情况。所以一定要加上限制条件的。” 老马笑着说:“那么问题来了,这颗树的剪枝条件是什么呢?”
“我觉得应该是符合要求的字符串的定义吧:左边括号的数量一定大于或者等于右括号的数量” 王小二简直就是比尔盖茨附体了,居然可以一口气回答出了老马的提问。
老马点点头,觉得王小二应该已经掌握了 dfs 的解法,他打算继续引导王小二思考其他方案:“你觉得本题还有其他解法吗?”
王小二摇摇头。
“还可以用栈啊,栈可以模拟递归。当然除了栈,还有递归本身的思路也值得分析的。”
“你有本题的递归思路吗?” 老马又开始提问了。
“根据前面你对递归的解释,我觉得有一个不成熟的想法:求子集 n - 1 的结果,然后寻找 n 与 n - 1 结果之间的关系。不过具体分析我不清楚。”
“挺好的,你能想到这点,说明你已经开始真正开始理解递归了。” 老马欣慰的道:“ 递归的关键是我们需要求出 n 与 n - 1 的关系。发现关系的技巧:从最简单的情况分析起,从其中发现规律。”
“当 n = 1 的时候,只有一个值()”。
“当 n = 2 的时候,有两个值 (()),()()”。
“经过分析,我们可以发现(()) 其实是有 ( + () + ) ; ()() 是 () + ()。”
“分别在()字符串的 index = 0, 1 的地方插入一对括号。” 老马加重语气道。
“好绕啊,能更简单的举个例子吗?” 王小二有点晕乎了。
“好的”, 老马真的很有耐心,因为他坚信只有教会了别人,才说明自己真正的懂了:“ 用A表示左括号’(', 用B表示右括号’)’。那么 n = 1 的结果就是 AB, n = 2的结果是 A()B, AB()。”
老马见王小二没有继续提问了,接着说道:“我们来写写伪代码吧”。
老马从口袋里掏出自己的英雄牌钢笔,在王小二的工位上随手拿了一张A4纸,开始写起了伪代码。
generateParenthesis(n)
strs = generateParenthesis(n-1) 取得 n - 1 的所有结果
i = 0 <- n , 遍历strs
str <- strs[i]
将 () 插入到 str 的第j 个位置 j = 0 <- str.length
“老马,我觉得有可能重复啊。” 没等老马写完,小二迫不及待的说。
老马停下来,示意王小二继续。王小二能发现这段伪代码的问题,他也是很欣慰的。
“比如 n = 1 的结果 (),如果按照你的伪代码,()一次插在0的位置,一次插在最后的位置, 会出现两次 ()(), ()()。”
“对,你看的很仔细。那么你有办法解决这个问题吗?” 老马点点头道。
“我觉得可以用 Set 或者 Hash 来处理,因为Set 和 Hash 可以去重” 王小二回答道。
“那你来接着写吧”,老马把笔递给王小二,起身站了起来。毕竟,机会还是要留给年轻人的。
王小二也是急不可耐了,二话没说接过笔就开始写起来了。
generateParenthesis(n)
set = a new set
strs = generateParenthesis(n-1) 取得 n - 1 的所有结果
i = 0 <- n , 遍历strs
str <- strs[i]
将 () 插入到 str 的第j 个位置 j = 0 <- str.length
if set 中不存在该组合 then 将结果加入 set 中
return set中的所有的value
... ...
总结
下班回家的路上,王小二心里感觉很充实。他总结了一下今天跟老马说的重点:
-
暴力搜索的分析开始于构建一颗树
-
构件树的关键点是找到:根节点,叶节点和操作
-
递归是dfs的基础,但是递归跟dfs是有区别的
-
递归不需要构造树,但是需要找出出 n 与 n - 1的关系
-
递归的特点在于:缩小问题规模 和 解决子问题后,其余问题能自动解决
-
能用递归解决的问题,一定个可以用dfs 也可以用 栈,或许可以用DP
@TK 提交的 C++ 版本
@fzy 提交的 Java 版本
@Douglas 提交的 Java 版本
@小小鹅 提交的 Java 版本
C++ 版本
Java 版本
Haskell 版本
Python 版本