一、赋值运算符函数
题目:如下为类型CMyString的声明,请为该类型添加赋值运算符函数。
class CMyString {
public:
CMyString(char *pData = nullptr);
CMyString(const CMyString &str);
~CMyString();
private:
char *m_pData;
};
测试用例:
- 把一个CMyString的实例赋值给另外一个实例。
- 把一个CMyString的实例赋值给它自己。
- 连续赋值。
没有考虑异常安全性的解法:
CMyString& CMyString::operator=(const CMyString &rhs)
{
if(this != &rhs)
{
delete []m_pData;
m_pData = nullptr;
m_pData = new char[strlen(rhs.m_pData) + 1];
strcpy(m_pData, rhs.m_pData);
}
return *this;
}
考虑异常安全性的解法:
CMyString& CMyString::operator=(const CMyString &rhs)
{
if(this != &rhs)
{
CMyString strTemp(rhs); // 创建一个临时实例strTemp
/* 交换strTemp.m_pData和实例自身的m_pData */
char *pTemp = m_pData;
m_pData = strTemp.m_pData;
strTemp.m_pData = pTemp;
} /* 自动调用strTemp的析构函数,释放strTemp.m_pData指向的新内存 */
return *this;
}
补:在新的代码中,我们在CMyString的构造函数里用new分配内存,故如果内存不足将抛出诸如bad_alloc等异常,但由于我们还没有修改实例自身的状态,故实例的状态还是有效的,这也就保证了异常安全性。
考点:
- C++基础语法,如运算符函数、常量引用等。
- 内存泄露。
- 代码异常安全性。
二、实现Singleton(单例)模式
题目:设计一个类,我们只能生成该类的一个实例。
解读题意:只能生成一个实例的类是实现了Singleton模式的类型。
考点:
- 单例模式
- C#基础语法,如静态构造函数等。
- 多线程编程。
三、数组中重复的数字
题目一:找出数组中重复的数字。在一个长度为n的数组里的所有数字都在0~n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是重复的数字2或者3。
测试用例:
- 长度为n的数组里包含一个或多个重复的数字。
- 数组中不包含重复的数字。
- 无效输入测试用例,如输入空指针、长度为n的数组中包含0~n-1之外的数字。
时间复杂度和空间复杂度均为O(n)的解法:
int solve(int arr[], int len)
{
int ans = -1; // 没有重复的数字则返回-1
int *pArr = new int[len](); // 对动态数组进行值初始化
for(int i = 0; i != len; ++i)
{
++pArr[arr[i]];
}
for(int i = 0; i != len; ++i) {
if(pArr[i] > 1) {
ans = i;
break;
}
}
delete []pArr;
return ans;
}
补:书中的解法是时间复杂度为O(n),而空间复杂度为O(1)。
题目二:不修改数组找出重复的数字。在一个长度为n+1的数组里的所有数字都在1~n的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为8的数组{2,3,5,4,3,2,6,7},那么对应的输出是重复的数字2或者3。
分析:这一题和上面的面试题类似,但题目明确要求不能修改输入的数组。我们可以像上题一样,创建一个长度为n+1的辅助数组,但该方案需要O(n)的辅助空间。我们要设法尝试避免使用O(n)的辅助空间。但正如下面这个算法所示,即使它的空间复杂度为O(1),但其时间复杂度却为O(nlogn),而且它不能保证找出所有重复的数字。故我们选取什么算法应取决于面试官提出的功能要求或性能要求。
思路:由题意可以确定数组中一定有重复的数字,故可以把从1~n的数字从中间的数字m分为两部分,前面一半为1~m,后面一半为m+1~n。如果1~m的数字的数目超过m,那么这一半的区间里一定包含重复的数字;否则,另一半m+1~n的区间里一定包含重复的数字。然后,继续把包含重复数字的区间一分为二,直到找到一个重复的数字。
四、在二维数组中的查找
题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
测试用例:
- 二维数组中包含查找的数字。
- 二维数组中没有查找的数字。
- 特殊输入测试。
解法:
bool solve(int *matrix, int rows, int cols, int x)
{
bool found = false;
if(rows > 0 && cols > 0)
{
int row = 0, col = cols - 1;
while(row < rows && col >= 0)
{
if(matrix[row * cols + col] == x)
{
found = true;
break;
}
else if(matrix[row * cols + col] > x)
--col;
else
++row;
}
}
return found;
}
分析:若从二维数组(矩形)的中间选取一个数字来比较,那么如果该数字与目标值不等的话,接下来的查找将没有固定的范围。而如果从右上角或左下角开始进行比较,则会逐渐缩小查找范围,最终得到结果。所以,该题考察的是应聘者能否通过具体的例子来找出其中的规律。
五、替换空格
题目:请实现一个函数,把字符串中的每个空格替换成“%20”。例如,输入“We are happy!”,则输出“We%20are%20happy!”。
题意:假设面试官是让我们在原来的字符串上进行替换,并且保证输入的字符串后面有足够多的空余内存。
测试用例:
- 输入的字符串中包含空格,空格可位于字符串的最前面、最后面或中间,甚至可以有连续多个空格。
- 输入的字符串中没有空格。
- 特殊输入测试,如字符串是一个空字符串,或字符串只有一个空格字符。
时间复杂度为O(n)的解法:
从头到尾扫描字符串,每次碰到空格字符的时候进行替换。而由于是把1个字符替换成3个字符,所以我们必须要把空格后面所有的字符都后移2字节,否则就有两个字符被覆盖了。
假设字符串的长度为n,对每个空格字符,需要移动后面O(n)个字符,因此对于含有O(n)个空格字符的字符串而言,总的时间效率是O(n)。
时间复杂度为O(n)的解法:
先遍历一次字符串,这样就能统计出字符串中空格的总数,并可以由此计算出替换之后的字符串的总长度。我们从字符串的后面开始复制和替换。
六、从尾到头打印链表
题目:输入一个链表的头节点,从尾到头反过来打印出每个节点的值。
测试用例:
- 功能测试,如输入的链表有多个节点,或输入的链表只有一个节点。
- 特殊输入测试,如输入的链表头节点指针为nullptr。
栈+循环的解法:
struct ListNode {
int value;
ListNode *next;
}; void print_list(ListNode *pHead)
{
stack<int> sck;
ListNode *pNode = pHead;
while(pNode) {
sck.push(pNode->value);
pNode = pNode->next;
}
while(!sck.empty()) {
cout << sck.top() << endl;
sck.pop();
}
}
栈+递归的解法:
struct ListNode {
int value;
ListNode *next;
}; void print_list_recursively(ListNode *pHead)
{
if(pHead != nullptr) {
if(pHead->next != nullptr) {
print_list_recursively(pHead->next);
}
cout << pHead->value << cout;
}
}
补充:既然想到了用栈来实现该函数,并且递归在本质上就是一个栈结构,于是很自然地又想到了用递归来实现。
注意:当链表非常长的时候,就会导致函数调用的层级很深,从而有可能导致函数调用栈溢出。
七、重建二叉树
题目:输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树并输出它的头节点。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如,输入前序遍历序列{1, 2, 4, 7, 3, 5, 6, 8}和中序遍历序列{4, 7, 2, 1, 5, 3, 8, 6},则重建如下图所示的二叉树并输出它的头节点。
测试用例:
- 普通二叉树。
- 特殊二叉树,如只有一个节点的二叉树、所有节点都没有右子节点的二叉树、所有节点都没有左子节点的二叉树。
- 特殊输入测试,如二叉树的根节点指针为nullptr、输入的前序遍历序列和中序遍历序列不匹配。
考点:
- 对二叉树的前序遍历和中序遍历的理解程度。
- 分析复杂问题的能力,把构建二叉树的大问题分解成构建左、右子树的两个小问题,而小问题和大问题在本质上是一致的,故可以用递归的方式解决。
struct BinaryTreeNode {
int m_nValue;
BinaryTreeNode *m_pLeft;
BinaryTreeNode *m_pRight;
}; BinaryTreeNode* Construct(int* preorder, int* inorder, int length)
{
if(preorder == nullptr || inorder == nullptr || length <= 0)
return nullptr; 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 = nullptr; 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;
}
小结:其实面试不会让写这么一道题,但容易考到的是先序遍历的递归/循环实现方法,对于中序遍历、后序遍历也是如此。
八、二叉树的下一个节点
题目:给定一棵二叉树和其中的一个节点,如何找出中序遍历序列的下一个节点?树中的节点除了有两个分别指向左、右子节点的指针,还有一个指向父节点的指针。
分析:此题需要应聘者在掌握中序遍历的基础上,通过具体的例子来找出其中的规律,并由此设计出可行的算法。
解法:
BinaryTreeNode* find_next(BinaryTreeNode *pNode)
{
if(pNode == nullptr)
return nullptr;
BinaryTreeNode *pNext = nullptr;
if(pNode->m_pRight != nullptr) {
BinaryTreeNode *pTemp = pNode->m_pRight;
while(pTemp->m_pLeft != nullptr)
pTemp = pTemp->m_pLeft;
pNext = pTemp;
}
else if(pNode->m_pParent != nullptr) {
BinaryTreeNode *pCurrent = pNode;
BinaryTreeNode *pParent = pNode->m_pParent;
while(pParent != nullptr && pCurrent == pParent->m_pRight) {
pCurrent = pParent;
pParent = pParent->m_pParent;
}
pNext = pParent;
}
return pNext;
}
九、用两个栈实现队列
题目:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数appendTail和deleteHead,分别完成在队列尾部插入节点和在队列头部删除节点的功能。
测试用例:
- 往空的队列里添加、删除元素。
- 往非空的队列里添加、删除元素。
- 连续删除元素直至队列为空。
解法:
stack<int> sck1;
stack<int> sck2; void appendTail(int val)
{
sck1.push(val);
} int deleteHead()
{
if(sck2.empty()) {
while(!sck1.empty()) {
int temp = sck1.top();
sck1.pop();
sck2.push(temp);
}
}
if(sck2.empty())
throw new exception("queue is empty");
int ret = sck2.top();
sck2.pop();
return ret;
}
分析:这道题的意图是要求我们操作两个“后进先出”的栈sck1和sck2以实现一个“先进先出”的队列。当添加元素时,我们总是向栈sck1内插入;而删除元素时,则总是从栈sck2内删除。
拓展:用两个队列实现一个栈。分析思路见书p71。
十、斐波拉契数列
题目一:求斐波那契数列的第n项。写一个函数,输入n,求斐波那契(Fibonacci)数列的第n项。
递归解法:
int fibonacci(int n)
{
if(n == 0) return 0;
if(n == 1) return 1;
return fibonacci(n-1) + fibonacci(n-2);
}
分析:许多教科书在讲解递归算法时,总是以此段代码作为示例,但该解法有很严重的效率问题。一句话阐明,该递归代码分解的两个子问题中存在大量重复的计算。
循环解法:
int fibonacci(int n)
{
if(n == 0) return 0;
if(n == 1) return 1;
int x1 = 0;
int x2 = 1;
int ans = 0;
for(int i = 2; i <= n; ++i) {
ans = x1 + x2;
x1 = x2;
x2 = ans;
}
return ans;
}
分析:上述递归代码之所以慢,是因为存在大量重复的计算,因此,我们只要想办法避免重复计算就行了。该解法是从下往上计算,首先根据f(0)和f(1)算出f(2),再根据f(1)和f(2)算出f(3)……以此类推就可以算出第n项了。
题目二:青蛙跳台阶问题。一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个n级的台阶总共有多少种跳法。
分析:把n级台阶时的跳法看成n的函数,记为f(n),这就不难看出该问题就是斐波那契数列了。