面试题集-树和图

来源:互联网 发布:qt 软件架构设计 编辑:程序博客网 时间:2024/04/29 18:47

如何着手

这类问题主要有以下两种形式:

1、 完成创建一个树/查找一个结点/删除一个结点/其他常见的算法

2、 完成对一个已知算法的修改

不管怎样,我们强烈推荐你在面试之前搞懂与树有关的重要算法。如果你对树非常熟悉,那么就可以轻松应对一些棘手的问题了。我们给出一些非常重要的几点: 

重要提示:并非所有的二叉树都是二叉查找树当被问到有关二叉树的时候,许多面试者自以为面试官是指二叉查找树,而面试官实际上是说二叉树。所以,认真听是否有“查找”那个词。如果你没有听到,那么面试官可能就是指那些没有经过特定排序的结点的二叉树。如果你不确定,那就问下吧。

有关二叉树的最基础算法

在你面试之前你应该熟知以下算法:

1、中序遍历:首先访问左结点,接着访问根结点,然后访问右结点(通常在二叉查找树中使用)。

2、先序遍历:首先访问根节点,接着访问根节点的左子节点下的子树内的节点,然后访问根节点的右子节点下的子树内的节点。

3、后序遍历:首先访问根节点,接着访问根节点的右子节点下的子树内的节点,然后访问根节点的左子节点下的子树内的节点。

4、插入结点:在二叉查找树中,我们插入一个变量v的方法是这样的。跟根节点比较,如果v大于root,那么去右边,否则去左边。我们重复这样的比较直到有一个空结点。

注意:调整或者删除查找树的问题很少会问到,但你应该知道他们是如何工作的。这有助于你从其他面试者区分出来。  

有关图遍历的最基础算法

在你面试之前你应该熟知以下算法:

1、深度优先搜索DFS(Depth First Search):DFS算法涉及在访问兄弟结点前先访问自身以及子结点。

2、广度优先搜索BFS(Broad First Search): BFS算法涉及在访问子结点前先访问自身以及兄弟结点。 

例题

4.1完成一个用来判断一颗树是否是平衡树的函数。这个问题的目的是,平衡树是指两个叶结点在距离根结点的距离上不会超过1。pg123

解答

思路很简单:最小深度与最大深度的差别是不超过1.因此最大与最小深度的差值就是树中最大的深度差值。


4.2对于一个有方向的图,设计一种算法用来判断在两个顶点之间是否有路径。pg124思路很简单:最小深度与最大深度的差别是不超过1.因此最大与最小深度的差值就是树中最大的深度差值。

解答

这个问题通过对图的遍历就可以轻松解决,比如深度优先搜索或者广度优先搜索。我们从其中一个结点开始遍历,遍历过程中检查另外一个结点是否被找到。我们应该对算法中遍历过的结点进行标记,以避免对结点的重复访问以及循环访问。


  

4.3对于一个升序排列的数组,给出创建最小高度二叉树的算法。pg125

解答

如果可能,我们尝试建立这样一个二叉树,对于每一个结点,他左分支的结点总数与右分支的结点总数一致。

算法如下:

1、插入数组中的中间数

2、将中间数左边的元素插入到左分支

3、将中间数右边的元素插入到右分支

4、递归


  

4.4对于一个二叉查找树,设计可以给出在任一深度所有结点的链表的算法(比如树的深度是D,就有D个链接。)pg126

解答

我们可以做对树一层层的遍历,对树的初次遍历可以做轻微的修改。

在常见的初次遍历中,我们在遍历结点的时候不关心现在所在的层。在这个问题中,就有必要知道当前的层信息。因此我们使用一个虚拟结点来预测我们是否完成了本层并进行下一层访问。


4.5对于一个每一个结点都有一个指向其父结点的二叉查找树,设计一个寻找给定结点下一个结点的算法。pg127

解答

解决这个问题,我们需要仔细思考中序遍历中到底发生了什么。在先序遍历中,我们访问X.left,X,然后是X.right.

因此,如果我们想找到X.successor(),我们这样解决:

1、如果X有右分支,那么所谓的继承者肯定在X的右侧。很明确,在树的那个分支上,需要首先访问最左边的子结点。

2、否则我们访问X的父结点(记做P)

a) 如果X有一个左子结点(P.left=X),那么P就是X的继承者。

b) 如果X有一个右子结点(P.right=X),因此我们就调用successor(P)。


4.6设计一个二叉树中两个结点的第一个共同祖先结点的算法,并给出代码。避免在数据结构中存储额外的结点。注意:这不一定需要是二叉查找树。pg128

解答

假设这是二叉查找树,我们可以在这两个结点上做一个改进的搜索来看这两路径在哪里分叉。可惜的是,这不是二叉查找树,因此我们得试其他思路。

思路1

如果每个结点都连接到他的父结点,那么我们就可以跟踪p跟q的路径直到他们交叉。

思路2

另外,你可以看一个p跟q都在同一侧的一个分割线。也就是说,如果p跟q都在结点的左侧,那就在左侧分支中去寻找共同的祖先。当p跟q不在同一侧的时候,你不得另想其他办法。


思路3

对于任意一个结点r,我们有以下:

1、如果p在一侧,q在另外一侧,那么r就是最近的共同的祖先。

2、否则的话,最近的共同的祖先就在左侧或者右侧。

因此,我们可以给出一个叫左搜索-右搜索的递归算法来计算当前结点的左侧跟右侧分别有多少个结点(p或者q)。如果在某一侧只有2个结点,那么我们就需要判断这个子结点是不是p或者q(因为这种情况下,当前结点就是最近的共同的祖先)。如果不是p或者q,那我们就得从子结点开始继续搜索。

如果需要寻找的结点(p或者q)在当前结点的右侧,此外另一个结点在另一侧。那么当前及诶点就是最近的共同的祖先。



4.7如果有两个巨大的树,T1、T2,其中前者有百万个结点,后者有几百个结点。设计一种算法判断T2是否是T1的一个分支。pg130

解答

注意问题的特别指出T1有百万个结点,也就是说我们应该特别注意空间的开支。举个例子,如果T1有1000万个结点,那么单单数据量就是大约40mb。我们可以创建一个字符串来表示中序跟先序的遍历。如果T2的先序遍历是T1先序遍历的一个分支,T2的中序遍历是T1中序遍历的一个分支,那么T2就是T1的第一分支。我们可以通过使用一个后缀树来确认。然而,我们可能会受到内存的限制,因为后缀树对于内存是极端敏感的。如果这个成了一个问题,那么我们就得试下另外一个思路。

其他思路:treeMatch程序访问T2的每个结点最多一次,对于T1的每个结点的访问次数不超过1。最极端的情况是O(m*n),n跟m分别表示T1跟T2的大小。如果k表示T2的根节点在T1中出现的次数,那么极端的时间复杂度是O(k*m+n)。


4.8对于一个任何结点都包含值的二叉树,设计一种算法实现打印出和是那个值的所有路径。注意可以是任何路径(不需要一定从根结点开始)。pg131

解答undefined

undefined

我们通过简化这个问题来求解。假设这个路径从根结点开始会怎样?这样的话,我们就得到一个更简单的问题了:undefined

从根节点开始,然后左分支跟右分支,计算每个路径的总数。当我们找到这个总和的时候,我们就打印出当前路径。注意我们直到找到这个总和才停止计算。为什么?因为我们可以遇到下面这种情况(假设我们找总和是5的路径):2 + 3 + –4 + 3 + 1 + 2。如果我们遇到2+3就停止计算,我们就可以错过其他一些路径(2 + 3 + -4 + 3 + 1 和3 + -4 + 3 + 1 + 2)。因此我们对于每个可能的路径都要试一次。undefined

好,现在假设这个路径可以从任何一个结点开始? 在这种情况下,我们做个小修改。在每个结点,我们核对下是否找到总和。这并不是指我们是否从一个有这个总和的路径开始,而是这个路径达到总和了吗?undefined


这个算法的时间复杂度如何呢?假设一个在第r层的结点,我们做r数量级的计算。我们猜测是O(nlgn),或者我们仔细算下:第r层有2^r个结点
类似的逻辑,空间复杂度是O(nlgn)。