第7章 两个面试案例
介绍项目经历的时候,参照star模型,着重介绍自己完成的工作(包括基于什么平台,用来哪些技术,实现了哪些算法等),以及最终对项目组的贡献。
技术面试环节,从编程语言,数据结构和算法等考察基础知识是否全面,很有可能要求面试者实现一两个函数。如果碰到简单的也不能掉以轻心,一定要从基本
功能,边界条件和错误处理等方面确保代码的鲁棒性和完整性。如果碰到难的题目,可以尝试画图让抽象的问题变得形象化,也可以尝试举例子去分析隐藏的规律,还可以
试图把大的问题分解成两个或者多个小问题再递归的解决。很多面试题不止一种解法,可以从时间复杂度和空间复杂度两个方面选择最优的解法。
除了技术能力,还会关注应聘者的沟通和学习能力,并有可能考察知识迁移能力,抽象建模能力和发散思维能力。
面试结束前几分钟,提问环节。可以从当前招聘的项目及其团队情况提几个问题。不建议问薪资,问结果。
面试题49:把字符串换成整数
思路分析:当扫描字符串的第一个字符“1“时,由于是第一位,故得到数字1;继续向后扫描到第二个字符”2“,之前已经得到数字1,在其后添加一个数字2,
得到数字12,相当于前面的数字扩大了10倍然后加上刚扫描到的数字2,即:1×10+2=12。同理,扫描到第三个字符”3“,即可得到最终整数123为所求。
故而,其基本思路就是:从左至右扫描字符串,把之前得到的数字乘以10,再加上当前字符表示的数字。
enum Status { kValid = 0, kInvalid };
int g_nStatus = kValid;
int StrToInt(const char* str)
{
g_nStatus = kInvalid;
long long num = 0;
if (str != null && *str != '\0') {
bool minus = false;
if (*str == '+') {
str++;
} else if (*str == '-') {
str++;
minus = true;
}
if (*str != '\0') {
num = StrToIntCore(str, minus);
}
}
return (int)num;
}
long long StrToIntCore(const char* digit, bool minus)
{
long long num = 0;
while (*digit != '\0') {
if (*digit >= '0' && *digit <= '9') {
int flag = minus ? -1 : 1;
num = num * 10 + flag * (*digit - '0');
//正数的最大值0x7FFFFFFF,负数的最大值0x80000000,判断整数是否上溢和下溢
if ((!minus && num > 0x7FFFFFFF) || (minus && num < (signed int)0x80000000)) {
num = 0;
break;
}
digit++;
} else {
num = 0;
break;
}
}
if (*digit == '\0') {
g_nStatus = kvalid;
}
return num;
}
面试题50:树中两个结点的最低公共祖先
题目一:如果这棵树是二叉搜索树
分析:
二叉搜索树是排序过的。如果当前结点的值比两个结点的值都大,那么最低的公共父结点一定是在当前结点的左子树中,于是下一步遍历当前结点的左子树;
如果当前结点的值比两个结点都小,那么最低公共结点一定在当前结点的右子树中,于是下一步遍历当前结点的右子结点。这样在树中从上到下找到的第一个在
两个输入结点的值之间的结点,就是最低的公共祖先。
题目二:如果这棵树不是二叉搜索树,甚至二叉树都不是,只是普通的树,那怎么办?
分析:
如果树中的每个结点(除了根结点以外)都有一个指向父结点的指针,这个问题可以转换为求两个链表的第一个公共结点。比如输入两个结点分为为F和H,
那么F在链表 F=>D=>B=>A上,而H在链表H=>E=>B=>A上,B刚好是它们最低的公共祖先。
题目三:现在这棵树是普通的树,甚至树中结点没有指向父结点的指针。
分析:
所谓两个结点的公共祖先,指的是这两个结点都出现在某个结点的子树中。我们可以从根结点开始遍历一棵树,每遍历到一个结点时,判断两个输入的结点
是不是在它的子树中。如果在子树中,则分别遍历它的所有子节点,并判断两个输入结点是不是在它们的子树中。这样从上到下找到的第一个结点,它自己的子树
中同时包含两个输入的结点而它的子节点却没有,那么该结点就是最低的公共祖先。
还是假设输入结点F和H。我们先判断A的子树中是否同时包含F和H,得到的结果是true。接着再先后判断A结点的两个子节点B和C的子树是不是同时包含F和
H,结果B的结果是true,而C的结果是false。接下来我们再判断B的两个节点D和E,发现这两个结点的结果都是false。于是B是最后一个公共祖先。
缺点:
这种思路会对同一个结点重复遍历很多次。
改进:
使用辅助空间。用两个链表分别保存从根结点到输入的两个结点的路径,把问题转换为两个链表的最后公共结点。
首先我们得到一条从根结点到树中某一结点的路径,这就要求在遍历的时候,有一个辅助的内存来保存路径。比如我们用前序遍历的方法来得到从根结点
到H的路径A=>B=>D。再从根结点到F的必经路径,A=>B=>D。最后求这两个路径的最后公共结点,也就是B。时间复杂度是O(n)。
//获取从根结点 pRoot 到 pNode 的路径,保存在 path中。
bool GetNodePath(TreeNode* pRoot, TreeNode* pNode, list<TreeNode*>&path)
{
if (pRoot == pNode)
return true;
path.push_back(pRoot);
bool found = false;
vector<TreeNode*>::iterator i = pRoot->m_vChildren.begin();
while (!found && i < pRoot->m_vChildren.end()) {
found = GetNodePath(*i, pNode, path);
i++;
}
if (!found)
path.pop_back();
return found;
}
//用来得到两个路径path1,path2 的最后一个公共节点
TreeNode* GetLastCommonNode(const list<TreeNode*>& path1, const list<TreeNode*>& path2)
{
list<TreeNode*>::const_iterator iterator1 = path1.begin();
list<TreeNode*>::const_iterator iterator2 = path2.begin();
TreeNode* pLast = null;
while (iterator1 != path1.end() && iterator2 != path2.end()) {
if (*iterator1 == *iterator2) {
pLast = *iterator1;
}
iterator1++;
iterator2++;
}
return pLast;
}
TreeNode* GetLastCommonParent(TreeNode* pRoot, TreeNode* pNode1, TreeNode* pNode2)
{
if (pRoot == null | pNode1 == null || pNode2 == null)
return null;
list<TreeNode*> path1;
GetNodePath(pRoot, pNode1, path1);
list<TreeNode*> path2;
GetNodePath(pRoot, pNode2, path2);
return GetLastCommonNode(path1, path2);
}