人生就像是一场长跑,有很多机会,但也得看我们是否能够及时抓牢,就像下面这样的代码:

while(isRunning)
{
if(...){...}
else if(...){...}
...
else{..}
}

存在着太多的if...else if...else...,很多都是一闪而过,就看我们是否将isRunning时刻设置为true,一直不断在跑,一直不断在检查条件是否满足。就算条件达到了,有些人会选择return或者将isRunning设置为false,主动退出循环,有些人选择继续跑下去,不断追寻更高的目标。
      所以,如果我们一时看不到未来,请不断跑下去,迟早会有某个条件满足的,只要设置的条件是合理可达的。

在实际编程中,树是经常遇到的数据结构,但可惜的是,我们经常不知道该用树了。实际情况就是,我们在避免过早使用数据结构,以防止引入不必要的复杂性。

树的逻辑非常简单:除了根结点外,其他每个结点都只有一个父结点,除了叶结点外,其他所有结点都有一个或多个子结点。父结点和子结点间用指针链接。树有很多种形式,最常见的是二叉树,每个结点最多只有两个子结点。

二叉树中最重要的操作就是遍历,通常有中序遍历,前序遍历和后序遍历,简单一点讲,这三种遍历的区别就是根结点的遍历顺序问题,像是中序遍历就是左,根,右,而前序遍历是根,左,右,后序遍历则是左,右,根。复杂一点的遍历就是宽度优先遍历:先访问树的第一层结点,再访问树的第二层结点...一直到最下面一层结点。在同一层结点中,从左到右的顺序依次访问。

常见的二叉树结点的定义如下:

struct BinaryTreeNode
{
int m_nValue;
BinaryTreeNode* m_pLeft;
BinaryTreeNode* m_pRight;
};

二叉树中还有许多形式,像是二叉搜索树,左子结点总是小于或等于根结点,而右子结点总是大于或等于根结点。另外两种常见的形式就是堆和红黑树。堆分为最大堆和最小堆,在最大堆中,根结点的值最大,最小堆则相反。堆非常适合用于快速查找最值,像是堆排序,就是利用了这点。红黑树是把树中的结点定义为红和黑两种颜色,并通过规则确保从根结点到叶结点的最长路径的长度不超过最短路径的两倍。很多C++的STL都是基于红黑树实现的,像是set,multiset,map,multimap等数据结构。

题目一:输入某二叉树的前序遍历和中序遍历的结果,重建该二叉树。

     在拿到这道题目的时候,我们首先明确一点,就是如何根据前序遍历和中序遍历来求出根结点。根结点对于二叉树来说,至关重要,只有先确定根结点,我们才能确定其他结点。
     我们还是从一个测试用例开始。
     假设某个二叉树的前序遍历结果为{1, 2, 4, 7, 3, 5, 6, 8}, 中序遍历的结果为{4, 7, 2, 1, 5, 3, 8, 6}。因为二叉树的前序遍历是根据:根,左,右的顺序来,所以前序遍历开头的元素就是根结点,也就是说,1就是二叉树的根结点。然后再看看中序遍历:左, 根,右,前面已经确定了1就是根结点,那么1的左边序列就是二叉树左边的元素,也就是说,{4, 7, 2}就是二叉树左边的元素,而{5, 3, 8, 6}就是二叉树右边的元素。
     二叉树的特点就是任何非叶结点的结点都可以成为根结点,所以我们可以从上面得到的两个序列中,按照之前的分析推敲出整个树的结构,也就是采用递归的方法。
BinaryTreeNode* Construct(int* preorder, int* inorder, int length)
{
if(preorder == NULL || inorder == NULL || length <= 0)
{
return NULL;
} return ConstructCore(preorder, preorder + length - 1, inorder, inorder + length - 1);
} BinaryTreeNode* ConstructCore(int* startPreorder, int* endPreorder, int* startInorder, int* endInorder)
{
int rootValue = startPreorder[0];
BinaryTreeNode* root = new BinaryTreeNode();
root->m_nValue = rootValue;
root->m_pLeft = root->m_pRight = NULL; if(startPreorder == endPreorder)
{
if(startInorder == endInorder && *startPreorder == *startInorder)
{
return root;
}
else
{
throw std :: exception("Invalid input.");
}
} int* rootInorder = startInorder;
while(rootInorder <= endInorder && *rootInorder != rootValue)
{
++rootInorder;
} if(rootInorder == endInorder && *rootInorder != rootValue)
{
throw std :: exception("Invalid input.");
} int leftLength = rootInorder - startInorder;
int* leftPreorderEnd = startPreorder + leftLength;
if(leftLength > 0)
{
root->m_pLeft = ConstructCore(startPreorder + 1, leftPreorderEnd, startInorder, rootInorder - 1);
}
if(leftLength < endPreorder - startPreorder)
{
root->m_pRight = ConstructCore(leftPreorderEnd + 1, endPreorder, rootInorder + 1, endInorder);
} return root;
}

题目二:输入两棵二叉树A和B,判断B是不是A的子结构。

要确定B是不是A的子结构,我们可以先在A中找到B的根结点,然后再看看这个根结点下面的左右结点是否和B相同。也就是说,我们首先要遍历二叉树A。

     二叉树特别适合使用递归的方式,这道题也不例外:
bool DoesTree1HaveTree2(BinaryTreeNode* pRoot1, BinaryTreeNode* pRoot2)
{
if(pRoot2 == NULL)
{
return true;
} if(pRoot1 == NULL)
{
return false;
} if(pRoot1->m_nVlaue != pRoot2->m_nValue)
{
return false;
} return DoesTree1HaveTree2(pRoot->m_pLeft, pRoot2->m_pLeft) && DoesTree1HaveTree2(pRoot1->m_pRight, pRoot2->m_pRight);
}

解决二叉树的编程问题,需要注意的有两方面:鲁棒性和简洁性。因为二叉树涉及到大量的指针操作,所以每次使用指针的时候我们都必须提醒自己:是否有空指针的危险。

     简洁性对于二叉树问题求解非常重要,因为大量的指针操作非常容易出现问题,就算我们已经足够小心了,但最好就是通过使用递归来让代码更加简洁,这样就可以少写几个指针,少犯点错误。
题目三:输入一个二叉树,输出它的镜像。
     所谓的二叉树镜像,其实也就是将一个结点的左右子结点交换,就像我们平时照镜子一样。
     根据这样的原理,我们知道,这是一个前序遍历二叉树,然后在找到左右子结点后将它们进行交换的过程。
void MirrorRecursively(BinaryTreeNode* pNode)
{
if(pNode == NULL) || (pNode->m_pLeft == NULL && pNode->m_pRight))
{
return;
} BinaryTreeNode* pTemp = pNode->m_pLeft;
pNode->m_pLeft = pNode->m_pRight;
pNode->m_pRight = pTemp; if(pNode->m_pLeft)
{
MirrorRecursively(pNode->m_pLeft);
} if(pNode->m_pRight)
{
MirrorRecursively(pNode->m_pRight);
}
}

题目四:从上到下打印二叉树的每个结点,同一层的结点按照从左到右的顺序。

     这就是宽度优先遍历,代码如下:
void PrintFromTopToBottom(BinaryTreeNode* pTreeRoot)
{
if(!pTreeRoot)
{
return;
} std :: deque<BinaryTreeNode*> dequeTreeNode; dequeTreeNode.push_back(pTreeRoot); while(dequeTreeNode.size())
{
BinaryTreeNode* pNode = dequeTreeNode.front();
dequeTreeNode.pop_front(); printf("%d ", pNode->m_nValue); if(pNode->m_pLeft)
{
dequeTreeNode.push_back(pNode->m_pLeft);
} if(pNode->m_pRight)
{
dequeTreeNode.push_back(pNode->m_pRight);
}
}
}
     这种题目是最常见的,就算是后序遍历这种常见的遍历算法,要想完全写好代码还是不容易的:

bool VerifySquenceOfBST(int sequence[], int length)
{
if(sequence == NULL || length <= 0)
{
return false;
} int root = sequence[length - 1]; int i = 0;
for(; i < length; ++i)
{
if(sequence[i] > root)
{
break;
}
} int j = i;
for(; j < length; ++j)
{
if(sequence[j] < root)
{
return false;
}
} bool left = true;
if(i > 0)
{
left = VerifySequenceOfBST(sequence, i);
} bool right = true;
if(i < length - 1)
{
right = VerifySequenceOfBST(sequence + i, length - i - 1);
} return left && right;
}

结合二叉搜索树的特点,再加上递归,这个代码不难实现。
     利用二叉树的遍历算法,我们能够做很多事情,像是这道题目:

void FindPath(BinaryTreeNode* pRoot, int expectedSum)
{
if(pRoot == NULL)
{
return;
} std :: vector<int> path;
int currentSum = 0;
FindPath(pRoot, expectedSum, path, currentSum);
} void FindPath(BinaryTreeNode* pRoot, int expectedSum., std :: vector<int>& path, int& currentSum)
{
currentSum += pRoot->m_nValue;
path.push_back(pRoot->m_nValue); bool isLeft = pRoot->m_pLeft == NULL && pRoot->m_pRight == NULL;
if(currentSum == expectedSum && isLeft)
{
printf("A path is found: ");
std :: Vector<int> :: iterator iter = path.begin();
for(; iter != path.end(); ++iter)
{
printf("%d\t", *iter);
}
printf("\n");
} if(pRoot->m_pLeft != NULL)
{
FindPath(pRoot->m_pLeft, expectedSum, path, currentSum);
}
if(pRoot->m_pRight != NULL)
{
FindPath(pRoot->m_pRight, expectedSum, path, currentSum);
} currentSum -= pRoot->m_nValue;
path.pop_back();
}

上面的代码只要仔细看一下,就会发现很严谨,像是我们在传递一个容器色时候,一般都是传递它的引用,这是为了防止传参的时候的副本复制,但是引用的作用并不仅仅如此,像是接下来的参数currentSum之所以是int&,是因为我们希望该值能够在函数递归调用的时候被改变,如果不是这样,离开该函数后,currentSum就会变为原值,因为它只是原本的currentSum的一个副本。
      这里我们并不使用STL中的stack,而是采用vector的原因就是stack只能取得栈顶的元素。

BinaryTreeNode* Convert(BinaryTreeNode* pRootOfTree)
{
BinaryTreeNode* pLastNodeInList = NULL;
ConvertNode(pRootOfTree, &pLastNodeInList); BinaryTreeNode* pHeadOfList = pLastNodeInList;
while(pHeadOfList != NULL && pHeadOfList->m_pLeft != NULL)
{
pHeadOfList = pHeadOfList->m_pLeft;
} return pHeadOfList;
} void ConvertNode(BinaryTreeNode* pNode, BinaryTreeNode** pLastNodeInList)
{
if(pNode == NULL)
{
return;
} BinaryTreeNode* pCurrent = pNode; if(pCurrent->m_pLeft != NULL)
{
ConvertNode(pCurrentNode->m_pLeft, pLastNodeInList);
} pCurrent->m_pLeft = *pLastNodeInList;
if(*pLastNodeInList != NULL)
{
(*pLastNodeInList)->m_pRight = pCurrent;
} *pLastNodeInList = pCurrent; if(pCurrent->m_pRight != NULL)
{
ConvertNode(pCurrent->m_pRight, pLastNodeInList);
}
}

二叉树不仅可以看成链表,也可以看成图。

struct Node
{
Node* pLeft;
Node* pRight;
int nMaxKLeft;
int nMaxRight;
char cValue;
}; int nMaxLen = 0; void FindMaxLen(Node* pRoot)
{
if(pRoot == NULL)
{
return;
} if(pRoot->pLeft == NULL)
{
pRoot->nMaxLeft = 0;
} if(pRoot->pRight == NULL)
{
pRoot->nMaxRight = 0;
} if(pRoot->pLeft != NULL)
{
FindMaxLen(pRoot->pLeft);
} if(pRoot->pRight != NULL)
{
FindMaxLen(pRoot->pRight);
} if(pRoot->pLeft != NULL)
{
int nTempMax = 0;
if(pRoot->pLeft->nMaxLeft > pRoot->pLeft->nMaxRight)
{
nTempMax = pRoot->pLeft->nMaxLeft;
}
else
{
nTempMax = pRoot->pLeft->nMaxRight;
} pRoot->nMaxLeft = nTempMax + 1;
} if(pRoot->pRight != NULL)
{
int nTempMax = 0;
if(pRoot->pRight->nMaxLeft > pRoot->pRight->nMaxRight)
{
nTempMax = pRoot->pRight->nMaxLeft;
}
else
{
nTempMax = pRoot->pRight->nMaxRight;
}
pRoot->nMaxRight = nTempMax + 1;
} if(pRoot->nMaxLeft + pRoot->nMaxRight > nMaxLeft)
{
nMaxLen = pRoot->nMaxLeft + pRoot->nMaxRight;
}
}
     二叉树的深度也是一个重要的考点。
题目九:输入一棵二叉树的根结点,求该树的深度。
     所谓的深度,指的就是从根结点到叶结点依次经过的结点(含根,叶结点)形成树的一条路径,最长路径的长度为树的深度。
     表面上好像需要遍历整棵树,以便知道那部分的结点最多,但实际上根本就不需要这样做。如果一棵树只有一个结点,它的深度为1,如果结点只有左子树而没有右子树,那么树的深度应该是其左子树的深度加1,同样如果结点只有右子树而没有左子树,那么树的深度应该是其右子树的深度加1.如果既有左子树又有右子树,那么该树的深度就是其左右子树深度的较大值加1。
     代码如下:
int TreeDepth(BinaryTreeNode* pRoot)
{
if(pRoot == NULL)
{
return 0;
} int nLeft = TreeDepth(pRoot->m_pLeft);
int nRight = TreeDepth(pRoot->m_pRight); return nLeft > nRight ? nLeft + 1 : nRight + 1;
}

在这道题的基础上,我们还可以增加难度:
题目十:输入一棵二叉树的根结点,判断该树是否是平衡二叉树。如果某二叉树中任意结点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

这不难,我们可以在前面代码的基础上,在每次得到左右子树的深度的时候进行一次判断就可以,但是时间效率不高,和数组遍历一样,从前面开始不行,那么就从后面开始。

后序遍历的好处就是在我们遍历到一个结点前就已经遍历了它的左右子树,只要在遍历每个结点的时候记录它的深度就可以了。

bool IsBalanced(BinaryTreeNode* pRoot)
{
int depth = 0;
return IsBalanced(pRoot, &depth);
} bool IsBalanced(BinaryTreeNode* pRoot, int* pDepth)
{
if(pRoot == NULL)
{
*pDepth = 0;
return true;
} int left, right;
if(IsBalanced(pRoot->m_pLeft, &left) && IsBalanced(pRoot->m_pRight, &right))
{
int diff = left - right;
if(diff <= 1 && diff >= -1)
{
*path = 1 + (left > right ? left : right);
return true;
}
} return false;
}

上面的题目都是显式的指定二叉树,但实际中的编程可不是这样,像是下面这道:

题目十一:输入n个整数,找出其中最小的k个数。

第一眼的想法肯定是利用数组来求解。

我们可以对这些数字进行排序,排序后位于最前面的k个数字就是最小的k个数,这种思路的时间复杂度是O(Nlog2N),前提就是使用快速排序。

在之前的数组总结中,我们曾经提及过Partition这个函数,这里同样也可以使用:

void GetLeastNumbers(int* input, int n, int* output, int k)
{
if(input == NULL || output == NULL || k > n || n <= 0 || k <= 0)
{
return;
} int start = 0;
int end = n - 1;
int index = Partition(input, n, start, end);
while(index != k - 1)
{
if(index > k - 1)
{
end = index - 1;
index = Partition(input, n, start, end);
}
else
{
start = index + 1;
index = Partition(input, n, start, end);
}
} for(int i = 0; i < k; ++i)
{
output[i] = input[i];
}
}

这种算法的局限就是我们需要修改输入的数组,因为函数Partition会调整数组中数字的顺序。
     我们可以先创建一个大小为k的数据容器来存储最小的k个数字,然后每次从输入的n个整数中读入一个数,如果容器中已有的数字少于k个,则直接把这次读入的整数放入容器中,如果容器中已有k个数字,也就是容器已满,此时我们不能再插入新的数字而只能替换已有的数字。找出已有的k个数中的最大值,然后拿这次待插入的整数和最大值进行比较。如果待插入的值比当前已有的最大值还要大,那么这个数字不可能是最小的k个整数之一,于是我们可以抛弃这个整数。

因此当容器满了之后,我们要做3件事情:一是在k个整数中找到最大数;二是有可能在这个容器中删除最大数;三是有可能要插入一个新的数字。如果用一个二叉树来实现这个容器,那么我们可能在O(log2K)时间内实现这些操作,所以对于n个输入数字而言,总的时间效率就是O(Nlog2K)。

因为都需要找到k个整数中的最大数字,我们很容易想到用最大堆。在最大堆中,根结点的值总是大于它的子树中的任意结点的值。于是我么每次都可以在O(1)得到已有的k个数字中的最大值,但需要O(log2K)时间完成删除和插入操作。

自己从头到尾实现一个红黑树是需要一定的代码,我们可以利用现成的基于红黑树的容器:

typedef multiset<int, greater<int>> intSet;
typedef multiset<int, greater<int>> :: iterator setIterator; void GetLeastNumbers(const vector<int>& data, intSet& leastNumbers, int k)
{
leastNumbers.clear(); if(k < 1 || data.size() < k)
{
return;
} vector<int> :: const_iterator iter = data.begin();
for(; iter != data.end(); ++iter)
{
if((leastNumbers.size()) > k)
{
leastNumbers.insert(*iter);
}
else
{
setIterator iterGreatest = leastNumbers.begin(); if(*iter < *(leastNumbers.begin())
{
leastNumbers.erase(iterGreastest);
leastNumbers.insert(*iter);
}
}
}
}

这种算法的时间复杂度是O(N),比起第一种是慢了,但是它不需要修改原有的数据,而且非常适合海量数据的输入,因为内存大小是有限的,我们根本不可能一次性存入数组中,所以我们只能从辅助空间中每次读入一个数字,再进行判断。

面试常备题---二叉树总结篇(zt)的更多相关文章

  1. 面试大总结之二:Java搞定面试中的二叉树题目

    package BinaryTreeSummary; import java.util.ArrayList; import java.util.Iterator; import java.util.L ...

  2. LeetCode刷题专栏第一篇--思维导图&时间安排

    昨天是元宵节,过完元宵节相当于这个年正式过完了.不知道大家有没有投入继续投入紧张的学习工作中.年前我想开一个Leetcode刷题专栏,于是发了一个投票想了解大家的需求征集意见.投票于2019年2月1日 ...

  3. Android开发如何准备技术面试(含Android面试押题)

    今年毋庸置疑是找工作的寒冬,每一个出来找工作的同学都是值得尊敬的.现在找工作,虽然略难,但是反过来看也会逼迫我们成为更加优秀的自己. 但是不管是旺季还是寒冬,有一些优秀的同学找工作还是挺顺利的.所以说 ...

  4. (转载)Autodesk面试技术题解答

    Autodesk面试技术题解答 By SmartPtr(http://www.cppblog.com/SmartPtr/)          近一年以来,AUTODESK的面试题在网上是闹的沸沸扬扬, ...

  5. 二叉树——遍历篇(递归/非递归,C++)

    二叉树--遍历篇 二叉树很多算法题都与其遍历相关,笔者经过大量学习.思考,整理总结写下二叉树的遍历篇,涵盖递归和非递归实现. 1.二叉树数据结构及访问函数 #include <stdio.h&g ...

  6. 数据结构+算法面试100题~~~摘自CSDN

    数据结构+算法面试100题~~~摘自CSDN,作者July 1.把二元查找树转变成排序的双向链表(树) 题目:输入一棵二元查找树,将该二元查找树转换成一个排序的双向链表.要求不能创建任何新的结点,只调 ...

  7. 剑指offer 面试7题

    面试7题: 题目:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字.例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{ ...

  8. 剑指offer 面试8题

    面试8题: 题目:二叉树的下一个节点 题目描述:给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回.注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针. 解题思路:详见剑 ...

  9. 剑指offer 面试26题

    面试26题: 题目:树的子结构 题:输入两棵二叉树A和B,判断B是不是A的子结构. 解题思路:递归,注意空指针的情况. 解题代码: # -*- coding:utf-8 -*- # class Tre ...

随机推荐

  1. 我是面试官--"自我介绍"

    工作10余年,经历过很多次面试,也面试了N多人.这些年来,已经有好些位朋友(或同事)与我聊起相关话题,涉及面试,更关乎职业生涯规划.感触颇多,就借助自媒体的浪潮,与更多的程序员一起共谈面试经历,希望可 ...

  2. 面向切面编程(AOP)及其作用

    在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用. 1.面向切面编程(AOP) 面向切面编程(AOP)就是对软件系统不同关注点的分离,开发者通过拦截方法调用并在方法调用前后添加辅助代码. ...

  3. JSON2 源代码

    /* json2.js 2014-02-04 Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. See ht ...

  4. VS2015创建的C++程序在Debug模式下不能调试

    如题,不能调试的问题,解决办法: 1.修改工程属性页 2.修改VS2015托管兼容模式 工具->选项->调试,勾选使用托管兼容

  5. html学习笔记之position

    今天主要一直看试验position的各种属性,现在记录下来以此备忘. position有四种常有属性,分别是static,fixed.absolute,relative fixed就是相对于窗口的位置 ...

  6. 【现代程序设计】【homework-07】

    C++11 中值得关注的几大变化 1.Lambda 表达式 Lambda表达式来源于函数式编程,说白就了就是在使用的地方定义函数,有的语言叫“闭包”,如果 lambda 函数没有传回值(例如 void ...

  7. 新手指南:详解Linux Top 命令

    Linux top命令简介 top 命令是最流行的性能监视工具之一,我们必需了解.它是一个优秀的交互式工具,用于监视性能.它提供系统整体性能,但报告进程信息才是 top 命令的长处.top 命令交互界 ...

  8. CSS 酷站

    http://mikkelbang.com/#!videos

  9. HL7及PIX相关的测试工具

    最近在开发PIX项目时需要一些工具, 比如PIX各个Actor的测试工具, HL7消息的验证工具等等. 下面列下我找见的几个 必备工具. 1. http://hit-testing.nist.gov: ...

  10. DotNetZip封装类

      DotnetZip是一个开源类库,支持.NET的任何语言,可很方便的创建,读取,和更新zip文件.而且还可以使用在.NETCompact Framework中. 下载地址在这里: http://d ...