最近,鉴于目前网上关于《剑指Offer》C++版的汇总资料特别少或者不规范,特整理了一版书中重要的考题,以备学习使用。
目录
一、位运算
1. 二进制中1的个数
题目:输入一个整数,输出该数二进制表示中1的个数,负数用补码表示。
思路:&运算通常用于二进制的取位操作,n&(n-1) 操作相当于把二进制表示中最右边的1变成0。
int NumberOf1(int n)
{
int count = 0;
while (n != 0)
{
n = n & (n-1);
++count;
}
return count;
}
2. 二进制中0的个数
题目:输入一个整数,输出该二进制表示中0的个数,负数用补码表示。
思路:n&1操作相当于判断二进制最后一位数是否位1,左移相当乘2,右移除2,左移补0,有符号右移,正数补0,负数补1,无符号右移,均补0。每次无符号右移一位,同1做&运算,可以判断最后一位是否为1。
int NumberOf0(int n)
{
int count = 0;
while (n != 0)
{
if ((n&1) != 1)
count++;
n>>=1;
}
return count;
}
3. 二进制高位连续0的个数
题目:输入一个整数,输出该二进制中高位连续0的个数。
思路:0x80000000的二进制是1000 0000 0000 0000 0000 0000 0000 0000,仅最高位为1,每次左移一位,与最高位为1的二进制进行&操作,可以判断高位连续为0的个数。
int numberOfLeading0(int n)
{
if (n == 0)
return 32;
int count = 0;
int mask = 0x80000000;
int m = n & mask;
while (m == 0)
{
++count;
n <<= 1;
m = n & mask;
}
return count;
}
二、二叉树
1. 二叉搜索树第k个结点
题目:输入一颗二叉搜索树,输出第k个节点。
思路:二叉搜索树是满足根节点值大于左节点,小于右节点的。中序遍历二叉搜索树可得到有序的,判断当前是否为k即可。
struct treeNode {
int val;
treeNode * left;
treeNode * right;
treeNode(int x) : val(x), left(NULL), right(NULL) {}
};
treeNode *inorder(treeNode *root, int &k)
{
if (root == NULL) return NULL;
treeNode * node = NULL;
node = inorder(root->left,k);
if (k-- == 1)
node = root;
if (k > 0)
node = inorder(root->right, k);
return node;
}
treeNode *KthNode(treeNode * pRoot, int k)
{
return inorder(pRoot, k);
}
2. 从上往下打印二叉树
题目:输入一颗二叉树,输出从上到下,从左到右节点的值。
思路:可借助队列先进先出的特性,先层次遍历二叉树,依次存入队列中,最后再逐个弹出。
struct treeNode {
int val;
treeNode * left;
treeNode * right;
treeNode(int x) : val(x), left(NULL), right(NULL) {}
};
vector<int> PrintFromTopToBottom(treeNode *root)
{
vector<int> vec;
queue<treeNode*> queue;
if (root == NULL) return vec;
queue.push(root);
while(!queue.empty())
{
vec.push_back(queue.front()->val);
if (queue.front()->left != NULL)
queue.push(queue.front()->left);
if (queue.front()->right != NULL)
queue.push(queue.front()->right);
queue.pop();
}
}
3. 二叉树打印成多行
题目:输入一颗二叉树,输出从上到下,从左到右节点的值,且每一层输出在一行。
思路:可借助队列先进先出的特性,先层次遍历二叉树,依次存入队列中,最后再逐个弹出。
vector<vector<int>> Print(treeNode *root)
{
vector<vector<int>> vec;
queue<treeNode*> queue;
if (root == NULL) return vec;
queue.push(root);
while(!queue.empty())
{
int n = queue.size(); //n用于保存每一次队列的元素个数,即二叉树每一层节点的个数。
vector<int> v;
while(v.size() < n)
{
v.push_back(queue.front()->val);
if(queue.front()->left)
queue.push(queue.front()->left);
if(queue.front()->right)
queue.push(queue.front()->right);
queue.pop();
}
vec.push_back(v);
}
}
4. 数据流中位数
题目:输入一个数据流,输出数据流中的中位数。
思路:最容易的办法是如果排序取中间值,显然这种不是面试官希望的答案。简单的办法是采取优先队列priority_queue,优先队列具有队列的所有特性,包括基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的。把列表分成两半,分别用两个优先级队列来保存,左侧为数字越大优先级越高,右侧为数字越小优先级越高。那么取出两个优先级的top值即可。
priority_queue<int, vector<int>, greater<int>> Rqueue; //最小堆
priority_queue<int, vector<int>, less<int>> Lqueue; //最大堆
void addNum(int num)
{
Lqueue.push(num);
if ((Lqueue.size() - Rqueue.size())>1)
{
Rqueue.push(Lqueue.top());
Lqueue.pop();
}
if(Rqueue.size() > 0 && Lqueue.top() > Rqueue.top())
{
int tmp = Lqueue.top();
Lqueue.pop();
Lqueue.push(Rqueue.top());
Rqueue.top();
Rqueue.push(tmp);
}
}
double findMedian()
{
if (Lqueue.size() == Rqueue.size()) return 1.0*(Lqueue.top()+Rqueue.top())/2;
else return Lqueue.top();
}
5. 二叉树中和为某一值的路径
题目:输入一颗二叉树的跟节点和一个整数,输出二叉树中结点值的和为输入整数的所有路径,返回的数组长度大的靠前。
思路:使用前序遍历的方式访问节点,使用二维向量vec存储全部路径,使用一维向量tmp存储当前路径。遍历二叉树的过程:按前序遍历顺序访问每一个节点。访问每个结点时,将结点添加到路径向量tmp中。如果当前结点是叶子结点,则判断当前路径是否是符合条件的路径,符合条件的路径存入到二维向量vec;如果当前结点不是叶子结点,则递归当前节点的左右子节点。
struct treeNode {
int val;
treeNode * left;
treeNode * right;
treeNode(int x) : val(x), left(NULL), right(NULL) {}
};
vector<vector<int>> vec;
vector<int> tmp;
void search(treeNode* root, int target)
{
tmp.push_back(root->val);
if (root->left == NULL && root->right == NULL) {
if (root->val == target) {
vec.push_back(tmp);
}
}
if (root->left != NULL)
search(root->left, target-root->val);
if (root->right != NULL)
search(root->right,target-root->val);
if (!tmp.empty())
tmp.pop_back();
}
void findTarget(treeNode* root, int target)
{
if (root == NULL) return;
search(root,target);
}
6. 重建二叉树
题目:输入某二叉树的前序遍历和中序遍历的结果,且前序遍历和中序遍历的结果中都不含重复的数字,输出重建二叉树。
思路:根据前序遍历找到根节点,再根据中序遍历找到左右子树,最后根据前序遍历得到左右子树的前序遍历和中序遍历,进行递归求解。
struct treeNode {
int val;
treeNode * left;
treeNode * right;
treeNode(int x) : val(x), left(NULL), right(NULL) {}
};
treeNode* ReConstructTree(vector<int>& pre, vector<int>& mid)
{
if (pre.empty() || mid.empty())
return NULL;
treeNode *head = new treeNode(pre[0]);
vector<int> leftpre, leftmid;
vector<int> rightpre, rightmid;
size_t root = 0;
// 寻找根节点
for (size_t i = 0;i < mid.size();++i)
{
if (mid[i] == pre[0])
{
root = i;
break;
}
}
// 左子树
for (size_t i = 0;i < root;++i)
{
leftpre.push_back(pre[i+1]);
leftmid.push_back(mid[i]);
}
// 右子树
for (size_t i = root+1;i < mid.size();++i)
{
rightpre.push_back(pre[i]);
rightmid.push_back(mid[i]);
}
head->left = ReConstructTree(leftpre,leftmid);
head->right = ReConstructTree(rightpre,rightmid);
return head;
}
7. 树的子结构
题目:输入两棵二叉树A,B,判断B是不是A的子结构。
思路:从树A的根节点开始寻找与B根节点相同的节点,如果相同,则继续比较其他节点,不相同,则继续。
struct treeNode {
int val;
treeNode * left;
treeNode * right;
treeNode(int x) : val(x), left(NULL), right(NULL) {}
};
bool IsEqualTree(treeNode* pRoot1, treeNode* pRoot2)
{
if (pRoot2 == NULL)
return true;
if (pRoot1 == NULL)
return false;
if (pRoot1->val != pRoot2->val)
return false;
return IsEqualTree(pRoot1->left,pRoot2->left) && IsEqualTree(pRoot1->right,pRoot2->right);
}
bool HasSubTree(treeNode* pRoot1, treeNode* pRoot2)
{
bool result = false;
if (pRoot2 != NULL && pRoot1 != NULL)
{
if (pRoot1->val == pRoot2->val) //头结点相同
{
result = IsEqualTree(pRoot1,pRoot2); //比较子节点
}
if (!result)
{
result = HasSubTree(pRoot1->left, pRoot2); //继续找头结点相同的节点
}
if (!result)
{
result = HasSubTree(pRoot1->right, pRoot2);
}
}
return result;
}
8. 二叉树的镜像
题目:输入一颗二叉树,输出源二叉树的镜像。
思路:递归转换二叉树即可。
struct treeNode {
int val;
treeNode * left;
treeNode * right;
treeNode(int x) : val(x), left(NULL), right(NULL) {}
};
treeNode* imageTree(treeNode* pRoot)
{
if (!pRoot) return NULL;
treeNode* iRoot = new treeNode(pRoot->val);
iRoot->left = imageTree(pRoot->right);
iRoot->right = imageTree(pRoot->left);
return iRoot;
}
9. 二叉树的下一个节点
题目:给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
思路:1.如果存在右子节点,则右子节点的最左节点为下一个节点; 2.如果不存在右子节点,且该节点是父节点的左子节点,则父节点为下一个节点; 3.如果不存在右子节点,且该节点是父节点的右子节点,则向上找到一个节点是该其父节点的左孩子,则该其父节点为下一个节点。
struct treeNode {
int val;
treeNode * left;
treeNode * right;
treeNode * next;
treeNode(int x) : val(x), left(NULL), right(NULL), next(NULL) {}
};
treeNode* GetNext(treeNode *pNode)
{
if (pNode == NULL)
return NULL;
// 1.如果存在右子节点,则右子节点的最左节点为下一个节点
if (pNode->right)
{
pNode = pNode->right;
while(pNode->left)
{
pNode = pNode->left;
}
return pNode;
}
// 2.如果不存在右子节点,且该节点是父节点的左子节点,则父节点为下一个节点
if (!pNode->right)
{
if(pNode->next && pNode->next->left == pNode)
{
return pNode->next;
}
}
// 3.如果不存在右子节点,且该节点是父节点的右子节点,则向上找到一个节点是该其父节点的左孩子,则该其父节点为下一个节点
if (!pNode->right)
{
if(pNode->next && pNode->next->right == pNode)
{
pNode = pNode->next;
while (pNode->next)
{
if (pNode->next->left == pNode)
return pNode->next;
pNode = pNode->next;
}
}
}
return NULL;
}
三、字符串
1. 正则表达式的匹配
题目:输入一个字符串和其模式字符串,判断两者是否正则匹配。模式中的字符'.'表示任意一个字符,而'*'表示前一个字符重复出现任意次(包含0次)。
思路:按照pattern下个字符为'*'进行递归判断。
bool match(const char* str, const char* pattern)
{
if (str == NULL || pattern == NULL)
return false;
// 字符串都匹配结束,返回true
if (*str == '\0' && *pattern == '\0')
return true;
// pattern匹配结束,str还没匹配结束,返回false
if (*str != '\0' && *pattern == '\0')
return false;
// pattern下个字符为'*'
if (*(pattern+1) == '*')
{
// 1. 当前字符相等,则str后移一位或者pattern后移二位
if (*str == *pattern || (*str != '\0' && *pattern == '.'))
{
return match(str+1, pattern) || match(str, pattern+2);
}
// 2. 当前字符不相等,则pattern后移二位
else
{
return match(str, pattern+2);
}
}
// pattern下个字符不是'*'
else
{
// 当前字符相等,则str和pattern均后移一位,继续比较下一位
if (*str == *pattern || (*str != '\0' && *pattern == '.'))
return match(str+1, pattern+1);
else
return false;
}
}
2. 表示数值的字符串
题目:输入一个字符串,判断该字符串是否表示数值(包括整数、小数、正负数、科学计数法等)
思路:按照当前字符为正负号、小数点、指数e进行循环判断。
bool IsNumeric(char * str)
{
if (str == NULL)
return false;
// 第一位为符号位
if (*str == '+' || *str == '-')
++str;
// 第一位不为数字,且不为'.'
else if ((*str < '0' || *str > '9') && *str != '.')
return false;
bool flag = false; //是否出现过'.'或者'e'
while (*str != '\0')
{
// 当前字符为符号,且前一个字符不为'e',不为最后一个数
if ((*str == '+' || *str == '-') && *(str-1) != 'e' && *(str-1) != 'E' && *(str+1) >= '0' && *(str+1) <= '9')
return false;
// 当前字符为'.'
if (*str == '.')
{
// 已经出现或者最后一位为'.'或者'e'或者'E'
if (flag || *(str+1) == '\0')
return false;
else
flag = true;
}
// 当前字符为'e'或者'E'
else if (*str == 'e' || *str == 'E')
{
// 已经出现或者最后一位为'.'或者'e'或者'E'
if (flag || *(str+1) == '\0')
return false;
else {
flag = true;
}
}
else if(*str < '0' || *str > '9')
{
return false;
}
++str;
}
return true;
}
3. 第一个只出现一次的字符
题目:在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)。
思路:利用map形成键值对,用于存储字符串中每个字符出现的次数,最后再遍历一遍字符串,找到第一个map值为1的索引即可。
int FirstNotRepeatingChar(string str)
{
std::map<char,int> map;
for (int i=0;i < str.size();++i)
{
map[str[i]]++;
}
for (int i=0;i < str.size();i++)
{
if (map[str[i]] == 1)
return i;
}
return -1;
}
4. 左旋转字符串
题目:对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。
思路:拼接两次字符序列,然后从第n位开始取字符序列长度的字符串即可。例如,SS=”abcXYZdefabcXYZdef”,左移3位的话,从SS的第3个字符开始取,则翻转后的字符串为“XYZdefabc”。
string reversal(string str, int k)
{
string doubleS = str + str;
string reverS;
for (int i=k;i < str.size()+k;i++)
{
reverS += doubleS[i];
}
return reverS;
}
四、数组
1. 构建乘积数组
题目:给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1],不能使用除法。
思路:用矩阵的方式,先计算左下三角,再计算右上三角,将B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]看成两部分来计算,即:A[0]*A[1]*...*A[i-1]和 *A[i+1]*...*A[n-1]两部分的乘积就是B[i]。
vector<int> multiply(vector<int> A) {
int num = A.size();
vector<int> B1(num,1);
vector<int> B2(num,1);
vector<int> B(num,1);
// 计算左下角B1
for (int i=1;i < num;++i)
{
B1[i] = B1[i-1]*A[i-1];
}
// 计算右上角B2
for (int i=num-2;i >= 0;--i)
{
B2[i] = B2[i+1]*A[i+1];
}
// 计算矩阵B
for (int i=0;i < num;i++)
{
B[i] = B1[i]*B2[i];
}
return B;
}
2. 二维数组的查找
题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
思路:从右上角开始,若小,向下走,删除一行,若大,向左走,删除一列。
bool FindElement(vector<vector<int>> vecs, int target)
{
if (vecs.empty())
return false;
int m = 0;
int n = vecs[0].size()-1;
while (m < vecs.size() && n >= 0)
{
if (vecs[m][n] == target)
return true;
else if (vecs[m][n] < target)
m++;
else
n--;
}
return false;
}
3. 连续子数组的最大和
题目:给一个数组,返回它的最大连续子序列的和。
思路:利用动态规划解决,dp[i] = max(vec[i],dp[i-1]+vec[i])
int MaxContinuesSubarraySum(vector<int> vec)
{
vector<int> dp(vec.size(),0);
for (int i = 1;i < vec.size();i++)
{
dp[i] = vec[i] > (dp[i-1]+vec[i])?vec[i]:(dp[i-1]+vec[i]);
}
int max = 0;
for (int i = 0;i < dp.size();++i)
{
if (max < dp[i])
max = dp[i];
}
return max;
}
参考文献