目录
面试题 3: 数组中重复的数字
题目一: 找出数组中重复的数字
测试用例:
- 长度为 n n n 的数组里包含一个或多个重复的数字
- 数组中不包含重复的数字
- 无效输入测试用例 (输入空指针 (假设输入参数为数组);长度为 n n n 的数组中包含 0 ∼ n − 1 0\sim n-1 0∼n−1 之外的数字)
解法一
- 遍历整个数组,将遍历到的数字存进哈希表。如果发现哈希表内已有该数字,则说明该数字为重复数字。该算法的时间复杂度和空间复杂度均为 O ( n ) O(n) O(n)
- 能不能进一步将空间复杂度降为 O ( 1 ) O(1) O(1)?
解法二
- 我们注意到数组中的数字都在 0 ∼ n − 1 0\sim n-1 0∼n−1 的范围内。如果这个数组中没有重复的数字,那么当数组排序之后数字 i i i 将出现在下标为 i i i 的位置。因此可以从头到尾依次扫描这个数组中的每个数字,当扫描到下标为 i i i 的数字时,首先比较这个数字 (用 m m m 表示) 是不是等于 i i i,如果是,则接着扫描下一个数字;如果不是,则再拿它和第 m m m 个数字进行比较。如果它和第 m m m 个数字相等,就找到了一个重复的数字;如果它和第 m m m 个数字不相等, 就把第 i i i 个数字和第 m m m 个数字交换, 把 m m m 放到属于它的位置。接下来再重复第 i i i 个位置的比较、交换的过程。如果第 i i i 个位置的数字交换为了 i i i,就继续在第 i + 1 i+1 i+1 个位置进行比较、交换的过程
- 该算法的时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)
在面试中, 如果我们打算修改输入的数据, 则最好先问面试官是不是允许修改
题目二:不修改数组找出重复的数字
测试用例:
- 长度为 n n n 的数组里包含一个或多个重复的数字
- 数组中不包含重复的数字
- 无效输入测试用例 (输入空指针 (假设输入参数为数组))
解法一
- 遍历整个数组,将遍历到的数字存进哈希表。如果发现哈希表内已有该数字,则说明该数字为重复数字。该算法的时间复杂度和空间复杂度均为 O ( n ) O(n) O(n)
- 如果要求空间复杂度为 O ( 1 ) O(1) O(1) 呢?
解法二
- 考虑为什么数组中会有重复的数字?假如没有重复的数字, 那么在从 1 ∼ n 1\sim n 1∼n 的范围里只有 n n n 个数字。由于数组里包含超过 n n n 个数字, 所以一定包含了重复的数字。看起来在某范围里数字的个数对解决这个问题很重要
- 我们使用二分法的思想,把
1
∼
n
1\sim n
1∼n 的数字从中间的数字
m
m
m 分为两个区间。如果前一个区间
1
∼
m
1\sim m
1∼m 的数字在数组中的数目超过
m
m
m, 那么这一半的区间里一定包含重复的数字;否则, 另一半
m
+
1
∼
n
m+1\sim n
m+1∼n 的区间里一定包含重复的数字。我们可以继续把包含重复数字的区间一分为二, 直到找到一个重复的数字
- 以长度为 8 的数组 { 2 , 3 , 5 , 4 , 3 , 2 , 6 , 7 } \{2,3, 5,4,3,2,6,7\} {2,3,5,4,3,2,6,7} 为例分析查找的过程。中间的数字 4 把 1 ∼ 7 1\sim7 1∼7 的范围分为两段,一段是 1 ∼ 4 1\sim4 1∼4, 另一段是 5 ∼ 7 5\sim7 5∼7。接下来我们统计 1 ∼ 4 1\sim4 1∼4 这 4 个数字在数组中出现的次数,它们一共出现了 5 次, 因此这 4 个数字中一定有重复的数字
- 该算法的时间复杂度为 O ( n log n ) O(n\log n) O(nlogn),空间复杂度为 O ( 1 ) O(1) O(1),相当于以时间换空间
int getDuplication(const int* numbers, int length)
{
if(numbers == nullptr || length <= 0)
return -1;
int start = 1;
int end = length - 1;
while(end >= start)
{
int middle = ((end - start) >> 1) + start;
int count = countRange(numbers, length, start, middle);
if(end == start)
{
if(count > 1)
return start;
else
break;
}
if(count > (middle - start + 1))
end = middle;
else
start = middle + 1;
}
return -1;
}
int countRange(const int* numbers, int length, int start, int end)
{
if(numbers == nullptr)
return 0;
int count = 0;
for(int i = 0; i < length; i++)
if(numbers[i] >= start && numbers[i] <= end)
++count;
return count;
}
- 从上述分析中我们可以看出, 如果面试官提出不同的功能要求或者性能要求 (时间效率优先、空间效率优先),那么我们最终选取的算法也将不同。这也说明在面试中和面试官交流的重要性,我们一定要在动手写代码之前弄清楚面试官的需求
面试题 4: 二维数组中的查找
测试用例:
- 二维数组中包含查找的数字 (查找的数字是数组中的最大值和最小值;查找的数字介于数组中的最大值和最小值之间)
- 二维数组中没有查找的数字 (查找的数字大于数组中的最大值;查找的数字小于数组中的最小值;查找的数字在数组的最大值和最小值之间但数组中没有这个数字)
- 特殊输入测试 (输入空指针)
解法一
- 由题意可知,在数组中任选一个元素,如果该元素的值比要查找的数字大,那么要查找的数字一定不在右下方;如果该元素的值比要查找的数字小,那么要查找的数字一定不在左上方。因此可以利用分而治之的思想,每次选取二维数组中心的元素进行比较,每次比较都可以排除四分之一的元素
bool findNumberInBlock(vector<vector<int>>& matrix, int target, int rstart, int rend, int cstart, int cend)
{
if (rstart > rend || cstart > cend)
{
return false;
}
int rmiddle = ((rend - rstart) >> 1) + rstart;
int cmiddle = ((cend - cstart) >> 1) + cstart;
bool found = false;
if (matrix[rmiddle][cmiddle] > target)
{
found |= findNumberInBlock(matrix, target, rstart, rend, cstart, cmiddle - 1);
found |= findNumberInBlock(matrix, target, rstart, rmiddle - 1, cmiddle, cend);
}
else if (matrix[rmiddle][cmiddle] < target)
{
found |= findNumberInBlock(matrix, target, rstart, rend, cmiddle + 1, cend);
found |= findNumberInBlock(matrix, target, rstart + 1, rend, cstart, cmiddle);
}
else {
found = true;
}
return found;
}
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
if (matrix.size() == 0)
{
return false;
}
return findNumberInBlock(matrix, target, 0, matrix.size() - 1, 0, matrix[0].size() - 1);
}
解法二
- 更简单的方法是每次选取数组中右上角的数字进行比较 (同理也可以选取数组中左下角的数字)。如果该数字等于要查找的数字, 则查找过程结束;如果该数字大于要查找的数字,则剔除这个数字所在的列;如果该数字小于要查找的数字,则剔除这个数字所在的行。 这样每一步都可以缩小查找的范围,直到找到要查找的数字,或者查找范围为空
bool Find(int* matrix, int rows, int columns, int number)
{
bool found = false;
if(matrix != nullptr && rows > 0 && columns > 0)
{
int row = 0;
int column = columns - 1;
while(row < rows && column >=0)
{
if(matrix[row * columns + column] == number)
{
found = true;
break;
}
else if(matrix[row * columns + column] > number)
-- column;
else
++ row;
}
}
return found;
}
面试题 5: 替换空格
下面假设面试官让我们在原来的字符串上进行替换,并且保证输入的字符串后面有足够多的空余内存
测试用例
- 输入的字符串中包含空格 (空格位于字符串的最前面;空格位于字符串的最后面; 空格位于字符串的中间;字符串中有连续多个空格)
- 输入的字符串中没有空格
- 特殊输入测试 (字符串是一个 nullptr 指针;字符串是一个空字符串;字符串只有一个空格字符;字符串中有连续多个空格)
相关题目
- 有两个排序的数组 A 1 A_1 A1 和 A 2 A_2 A2, 内存在 A 1 A_1 A1 的末尾有足够多的空余空间容纳 A 2 A_2 A2。请实现一个函数, 把 A 2 A_2 A2 中的所有数字插入 A 1 A_1 A1 中, 并且所有的数字是排序的
解法一
- 从后向前替换: 我们可以先遍历一次字符串统计出空格的总数。每替换一个空格,长度增加 2, 因此替换以后字符串的长度等于原来的长度加上 2 乘以空格数目。我们从字符串的后面开始复制和替换。 首先准备两个指针:
P
1
P_1
P1 和
P
2
P_2
P2。
P
1
P_1
P1 指向原始字符串的末尾, 而
P
2
P_2
P2 指向替换之后的字符串的未尾
接下来我们向前移动指针 P 1 P_1 P1, 逐个把它指向的字符复制到 P 2 P_2 P2 指向的位置,直到碰到第一个空格为止
碰到第一个空格之后,把 P 1 P_1 P1 向前移动 1 格,在 P 2 P_2 P2 之前插入字符串 “%20”。由于 “%20” 的长度为 3, 同时也要把 P 2 P_2 P2 向前移动 3 格
按同样的方法处理剩余空格
当 P 1 P_1 P1 和 P 2 P_2 P2 指向同一位置时,表明所有空格都已经替换完毕
void ReplaceBlank(char str[], int length)
{
if(str == nullptr && length <= 0)
return;
/*originalLength 为字符串str的实际长度*/
int originalLength = 0;
int numberOfBlank = 0;
int i = 0;
while(str[i] != '\0')
{
++ originalLength;
if(str[i] == ' ')
++ numberOfBlank;
++ i;
}
/*newLength 为把空格替换成'%20'之后的长度*/
int newLength = originalLength + numberOfBlank * 2;
if(newLength > length)
return;
int indexOfOriginal = originalLength;
int indexOfNew = newLength;
while(indexOfOriginal >= 0 && indexOfNew > indexOfOriginal)
{
if(str[indexOfOriginal] == ' ')
{
str[indexOfNew --] = '0';
str[indexOfNew --] = '2';
str[indexOfNew --] = '%';
}
else
{
str[indexOfNew --] = str[indexOfOriginal];
}
-- indexOfOriginal;
}
}
面试题 6: 从尾到头打印链表
测试用例:
- 功能测试 (输入的链表有多个节点;输入的链表只有一个节点)
- 特殊输入测试 (输入的链表头节点指针为 nullptr)
- 可以很容易想到用栈去处理,不过栈中存链表节点的指针应该比直接存数据更好:
void PrintListReversingly_Iteratively(ListNode* pHead)
{
std::stack<ListNode*> nodes;
ListNode* pNode = pHead;
while(pNode != nullptr)
{
nodes.push(pNode);
pNode = pNode->m_pNext;
}
while(!nodes.empty())
{
pNode = nodes.top();
printf("%d\t", pNode->m_nValue);
nodes.pop();
}
}
- 相应地也可以用递归去实现,但当链表非常长的时候, 就会导致函数调用的层级很深, 从而有可能导致函数调用栈溢出。显然用栈基于循环实现的代码的鲁棒性要好一些
void PrintListReversingly_Recursively(ListNode* pHead)
{
if(pHead != nullptr)
{
if (pHead->m_pNext != nullptr)
{
PrintListReversingly_Recursively(pHead->m_pNext);
}
printf("%d\t", pHead->m_nValue);
}
}
面试题 7: 重建二叉树
测试用例:
- 普通二叉树 (完全二叉树;不完全二叉树)
- 特殊二叉树 (所有节点都没有右子节点的二叉树;所有节点都没有左子节点的二叉树;只有一个节点的二叉树)
- 特殊输入测试 (二叉树的根节点指针为 nullptr; 输入的前序遍历序列和中序遍历序列不匹配)
- 常规题,可用递归解决
#include "..\Utilities\BinaryTree.h"
#include <exception>
#include <cstdio>
BinaryTreeNode* ConstructCore(int* startPreorder, int* endPreorder, int* startInorder, int* endInorder);
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.");
}
// 在中序遍历中找到根结点的值
// 这里每次都要遍历中序序列去寻找根结点的位置,为了进一步节省时间,我们可以事先
// 用 unordered_map 去存储中序序列中每个值与其位置的映射关系
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;
}
面试题 8: 二叉树的下—个节点
测试用例:
- 普通二叉树 (完全二叉树;不完全二叉树)
- 特殊二叉树 (所有节点都没有右子节点的二叉树;所有节点都没有左子节点的二叉树;只有一个节点的二叉树;二叉树的根节点指针为 nullptr)
- 不同位置的节点的下一个节点 (下一个节点为当前节点的右子节点、右子树的最左子节点、父节点、跨层的父节点等当前节点没有下一个节点)
- 主要就是需要分类讨论,(1) 如果一个节点有右子树, 那么它的下一个节点就是它的右子树中的最左子节点;(2) 如果一个节点没有右子树且是它父节点的左子节点, 那么它的下一个节点就是它的父节点;(3) 如果一个节点既没有右子树,并且它还是它父节点的右子节点, 那么我们可以沿着指向父节点的指针一直向上遍历, 直到找到一个是它父节点的左子节点的节点,如果没有找到,则说明该节点为中序遍历的最后一个节点
BinaryTreeNode* GetNext(BinaryTreeNode* pNode)
{
if(pNode == nullptr)
return nullptr;
BinaryTreeNode* pNext = nullptr;
if(pNode->m_pRight != nullptr)
{
BinaryTreeNode* pRight = pNode->m_pRight;
while(pRight->m_pLeft != nullptr)
pRight = pRight->m_pLeft;
pNext = pRight;
}
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;
}
面试题 9: 用两个栈实现队列
测试用例:
- 往空的队列里添加、删除元素
- 往非空的队列里添加、删除元素
- 连续删除元素直至队列为空
相关题目
- 用两个队列实现一个栈
- 思路:元素入队时永远入栈 stack1;元素出队时,若 stack2 为空,则把 stack1 中的元素逐个弹出并压入 stack2,若 stack2 不为空,则说明在 stack2 中的栈顶元素是最先进入队列的元素,可以直接弹出
template<typename T> void CQueue<T>::appendTail(const T& element)
{
stack1.push(element);
}
template<typename T> T CQueue<T>::deleteHead()
{
int res = -1; // return -1 if queue is empty
if (stack2.empty())
{
while (!stack1.empty())
{
stack2.push(stack1.top());
stack1.pop();
}
}
if (!stack2.empty())
{
res = stack2.top();
stack2.pop();
}
return res;
}
面试题 10: 斐波那契数列
测试用例:
- 功能测试 (如输入 3、5、10 等)
- 边界值测试 (如输入 0、1、2)
- 性能测试 (输入较大的数字,如 40、50、100 等)
本题扩展:
- 在青蛙跳台阶的问题中,如果把条件改成: 一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级… …它也可以跳上 n n n 级,此时该青蛙跳上一个 n n n 级的台阶总共有多少种跳法?我们用数学归纳法可以证明 f ( n ) = 2 n − 1 f(n)=2^{n-1} f(n)=2n−1
相关题目:
- 我们可以用
2
×
1
2\times1
2×1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 8 个
2
×
1
2\times1
2×1 的小矩形无重叠地覆盖一个
2
×
8
2\times8
2×8 的大矩形,总共有多少种方法?
解法一: 递归
long long Fibonacci_Solution1(unsigned int n)
{
if(n <= 0)
return 0;
if(n == 1)
return 1;
return Fibonacci_Solution1(n - 1) + Fibonacci_Solution1(n - 2);
}
- 递归算法虽然简洁但效率极低,需要进行大量的重复计算,时间复杂度是指数级的:
解法二: 动态规划
long long Fibonacci_Solution2(unsigned n)
{
int result[2] = {0, 1};
if(n < 2)
return result[n];
long long fibNMinusOne = 1;
long long fibNMinusTwo = 0;
long long fibN = 0;
for(unsigned int i = 2; i <= n; ++ i)
{
fibN = fibNMinusOne + fibNMinusTwo;
fibNMinusTwo = fibNMinusOne;
fibNMinusOne = fibN;
}
return fibN;
}
- 动态规划通过自底向上的思想避免了大量重复计算,将时间复杂度优化到了 O ( n ) O(n) O(n)
解法三: 矩阵快速幂
- 使用数学归纳法可以证得
因此我们只需计算
上述矩阵为对称矩阵,可以对其进行谱分解,只需 O ( 1 ) O(1) O(1) 就可计算出结果;也可以考虑如下乘方的性质:
从上面的公式中我们可以看出,我们想求得 n n n 次方,就要先求得 n / 2 n/2 n/2 次方,再把 n / 2 n/2 n/2 次方的结果平方一下即可。这可以用递归的思路实现,只需 O ( log n ) O(\log n) O(logn) 的复杂度 (但由于隐含的时间常数较大,很少会有软件采用这种算法)
面试题 11: 旋转数组的最小数字
测试用例
- 功能测试 (输入的数组是升序排序数组的一个旋转,数组中有重复数字或者没有重复数字)
- 边界值测试 (输入的数组是一个升序排序的数组,只包含一个数字的数组)
- 特殊输入测试 (输入 nullptr 指针)
- 本题思路为二分查找,难点是要考虑一些特殊情况,思考不周全的话就很容易出错。我们用两个指针分别指向数组的第一个元素和最后一个元素。按照题目中旋转的规则, 第一个元素应该是大于或者等于最后一个元素的。接着我们可以找到数组中间的元素, 如果该中间元素大于等于第一个指针指向的元素,则说明该中间元素位于前面的递增子数组,此时数组中最小的元素应该位于该中间元素的后面,因此可以把第一个指针指向该中间元素, 这样可以缩小寻找的范围,移动之后的第一个指针仍然位于前面的递增子数组。同样, 如果该中间元素小于等于第二个指针指向的元素,则说明该中间元素位于后面的递增子数组,此时数组中最小的元素应该位于该中间元素的前面,因此可以把第二个指针指向该中间元素,这样可以缩小寻找的范围,移动之后的第二个指针仍然位于后面的递增子数组。按照上述思路, 第一个指针总是指向前面递增数组的元素, 而第二个指针总是指向后面递增数组的元素。最终第一个指针将指向前面子数组的最后一个元素, 而第二个指针会指向后面子数组的第一个元素。也就是它们最终会指向两个相邻的元素, 而第二个指针指向的刚好是最小的元素。这就是循环结束的条件
- 但注意,上面的讨论其实少考虑了两个特例:
- (1) 如果把排序数组的前面的 0 个元素搬到最后面,即排序数组本身,这仍然是数组的一个旋转,此时 数组中的第一个数字就是最小的数字,可以直接返回
- (2) 如果第一个指针、第二个指针和中间指针所指数均相同,则我们无法判断最小数字是在中间位置的左侧还是右侧,此时不得不进行顺序查找
#include <cstdio>
#include <exception>
int MinInOrder(int* numbers, int index1, int index2);
int Min(int* numbers, int length)
{
if(numbers == nullptr || length <= 0)
throw new std::exception("Invalid parameters");
int index1 = 0;
int index2 = length - 1;
int indexMid = index1;
while(numbers[index1] >= numbers[index2])
{
// 如果index1和index2指向相邻的两个数,
// 则index1指向第一个递增子数组的最后一个数字,
// index2指向第二个子数组的第一个数字,也就是数组中的最小数字
if(index2 - index1 == 1)
{
indexMid = index2;
break;
}
// 如果下标为index1、index2和indexMid指向的三个数字相等,
// 则只能顺序查找
indexMid = (index1 + index2) / 2;
if(numbers[index1] == numbers[index2] && numbers[indexMid] == numbers[index1])
return MinInOrder(numbers, index1, index2);
// 缩小查找范围
if(numbers[indexMid] >= numbers[index1])
index1 = indexMid;
else if(numbers[indexMid] <= numbers[index2])
index2 = indexMid;
}
return numbers[indexMid];
}
int MinInOrder(int* numbers, int index1, int index2)
{
int result = numbers[index1];
for(int i = index1 + 1; i <= index2; ++i)
{
if(result > numbers[i])
result = numbers[i];
}
return result;
}
面试题 12: 矩阵中的路径
测试用例
- 功能测试 (在多行多列的矩阵中存在或者不存在路径)
- 边界值测试 (矩阵只有一行或者只有一列:矩阵和路径中的所有字母都是相同的)
- 特殊输入测试 (输入 nullptr 指针)
- 典型的使用回溯法解决的题目
#include <cstdio>
#include <string>
#include <stack>
using namespace std;
bool hasPathCore(const char* matrix, int rows, int cols, int row, int col, const char* str, int& pathLength, bool* visited);
bool hasPath(const char* matrix, int rows, int cols, const char* str)
{
if(matrix == nullptr || rows < 1 || cols < 1 || str == nullptr)
return false;
bool *visited = new bool[rows * cols];
memset(visited, 0, rows * cols);
int pathLength = 0;
for(int row = 0; row < rows; ++row)
{
for(int col = 0; col < cols; ++col)
{
if(hasPathCore(matrix, rows, cols, row, col, str,
pathLength, visited))
{
return true;
}
}
}
delete[] visited;
return false;
}
bool hasPathCore(const char* matrix, int rows, int cols, int row,
int col, const char* str, int& pathLength, bool* visited)
{
if(str[pathLength] == '\0')
return true;
bool hasPath = false;
if(row >= 0 && row < rows && col >= 0 && col < cols
&& matrix[row * cols + col] == str[pathLength]
&& !visited[row * cols + col])
{
++pathLength;
visited[row * cols + col] = true;
hasPath = hasPathCore(matrix, rows, cols, row, col - 1,
str, pathLength, visited)
|| hasPathCore(matrix, rows, cols, row - 1, col,
str, pathLength, visited)
|| hasPathCore(matrix, rows, cols, row, col + 1,
str, pathLength, visited)
|| hasPathCore(matrix, rows, cols, row + 1, col,
str, pathLength, visited);
if(!hasPath)
{
--pathLength;
visited[row * cols + col] = false;
}
}
return hasPath;
}
面试题 13: 机器人的运动范围
测试用例
- 功能测试 (方格为多行多列; k k k 为正数)
- 边界值测试 (方格只有一行或者只有一列; k k k 等于 0)
- 特殊输入测试 ( k k k 为负数)
- 典型的回溯法解决的题目。要注意的是题意并不是求机器人能走的最长路径,而是求机器人能走的最大面积
#include <cstdio>
int movingCountCore(int threshold, int rows, int cols, int row, int col, bool* visited);
bool check(int threshold, int rows, int cols, int row, int col, bool* visited);
int getDigitSum(int number);
int movingCount(int threshold, int rows, int cols)
{
if(threshold < 0 || rows <= 0 || cols <= 0)
return 0;
bool *visited = new bool[rows * cols];
for(int i = 0; i < rows * cols; ++i)
visited[i] = false;
int count = movingCountCore(threshold, rows, cols,
0, 0, visited);
delete[] visited;
return count;
}
int movingCountCore(int threshold, int rows, int cols, int row,
int col, bool* visited)
{
int count = 0;
if(check(threshold, rows, cols, row, col, visited))
{
visited[row * cols + col] = true;
count = 1 + movingCountCore(threshold, rows, cols,
row - 1, col, visited)
+ movingCountCore(threshold, rows, cols,
row, col - 1, visited)
+ movingCountCore(threshold, rows, cols,
row + 1, col, visited)
+ movingCountCore(threshold, rows, cols,
row, col + 1, visited);
}
return count;
}
bool check(int threshold, int rows, int cols, int row, int col,
bool* visited)
{
if(row >= 0 && row < rows && col >= 0 && col < cols
&& getDigitSum(row) + getDigitSum(col) <= threshold
&& !visited[row* cols + col])
return true;
return false;
}
int getDigitSum(int number)
{
int sum = 0;
while(number > 0)
{
sum += number % 10;
number /= 10;
}
return sum;
}
面试题 14: 剪绳子
注: 绳子的长度也必须为整数
测试用例:
- 功能测试 (绳子的初始长度大于 5)
- 边界值测试 (绳子的初始长度分别为 0、1、2、3、4)
解法一: 动态规划 O ( n 2 ) O(n^2) O(n2)
- 首先定义函数
f
(
n
)
f(n)
f(n) 为把长度为
n
n
n 的绳子剪成若干段后各段长度乘积的最大值,递推公式为
f ( n ) = max { max { f ( i ) , i } × max { f ( n − i ) , n − i } } f(n)=\max\bigg\{\max\{f(i),i\}\times \max\{f(n-i),n-i\}\bigg\} f(n)=max{max{f(i),i}×max{f(n−i),n−i}}注意题意中划分时段数必须大于 1,也就是必须进行划分,因此在递推时需要考虑子段不进行划分的情况
int maxProductAfterCutting_solution1(int length)
{
vector<int> ans;
ans.resize(length + 1);
ans[1] = 1;
for (int i = 2; i <= length; ++i)
{
for (int k = 1; k <= i / 2; ++k)
{
int max_k = max(ans[k], k) * max(ans[i - k], i - k);
if (max_k > ans[i])
{
ans[i] = max_k;
}
}
}
return ans[length];
}
解法二: 贪心算法 O ( 1 ) O(1) O(1)
- 如果我们按照如下的策略来剪绳子,则得到的各段绳子的长度的乘积将最大:当 n ≥ 5 n\geq5 n≥5 时,尽可能多地剪长度为 3 的绳子;当剩下的绳子长度为 4 时,把绳子剪成两段长度为 2 的绳子;具体证明见 leetcode (神仙解法,反正我肯定想不出来)
int maxProductAfterCutting_solution2(int length)
{
if(length < 2)
return 0;
if(length == 2)
return 1;
if(length == 3)
return 2;
// 尽可能多地减去长度为 3 的绳子段
int timesOf3 = length / 3;
// 当绳子最后剩下的长度为 4 的时候,不能再剪去长度为 3 的绳子段。
// 此时更好的方法是把绳子剪成长度为 2 的两段,因为 2*2 > 3*1。
if(length - timesOf3 * 3 == 1)
timesOf3 -= 1;
int timesOf2 = (length - timesOf3 * 3) / 2;
return (int) (pow(3, timesOf3)) * (int) (pow(2, timesOf2));
}
面试题 15: 二进制中 1 的个数
测试用例
- 正数 (包括边界值 1、0x7FFFFFFF)
- 负数 (包括边界值 0x80000000、0xFFFFFFFF)
- 0
相关题目
- (1) 判断一个整数是不是 2 的整数次方
- (2) 输入两个整数 m m m 和 n n n, 计算需要改变 m m m 的二进制表示中的多少位才能得到 n n n。比如 10 的二进制表示为 1010, 13 的二进制表示为 1101, 需要改变 1010 中的 3 位才能得到 1101
可能引起死循环的解法
- 最基本的思路就是不断用整数最低位与 1 做位与运算,再把整数右移一位 (注意,右移不能换成除法,除法的效率比移位运算低得多)
int NumberOf1(int n)
{
int count = 0;
while (n)
{
count += n & 1;
n >>= 1;
}
return count;
}
- 如果 n n n 为负数,右移时就会在最高位不断补 1,使得上述算法陷入死循环
常规解法
- 为了避免死循环, 我们可以不右移输入的数字 n n n。首先把 n n n 和 1 做与运算,判断 n n n 的最低位是不是为 1。接着把 1 左移一位再和 n n n 做与运算,就能判断 n n n 的次低位是不是 1… 这样反复左移, 每次都能判断 n n n 的其中一位是不是 1
int NumberOf1_Solution1(int n)
{
int count = 0;
unsigned int flag = 1;
while (flag)
{
count += n & flag;
flag <<= flag;
}
return count;
}
更好的解法:整数中有几个 1 就只需要循环几次
- 在分析这种算法之前,我们先来分析把一个数减去 1 的情况。如果把一个整数减去 1, 都是把最右边的 1 变成 0。如果它的右边还有 0, 则所有的 0 都变成 1, 而它左边的所有位都保持不变。接下来我们把一个整数和它减去 1 的结果做位与运算,相当于把它最右边的 1 变成 0
- 总结一下可得如下算法:把一个整数减去 1, 再和原整数做与运算,会把该整数最右边的 1 变成 0。那么一个整数的二进制表示中有多少个 1, 就可以进行多少次这样的操作 (这是一个比较有用的结论,可以记一下)
int NumberOf1_Solution2(int n)
{
int count = 0;
while (n)
{
++count;
n = (n - 1) & n;
}
return count;
}
参考文献
- 《剑指 Offer》