剑指offer题解
开始时间:2017.7.17
结束时间:2017.8.15
- 1. 赋值运算符函数
- 2. 实现Singleton模式
- 3. 二维数组中的查找
- 4. 替换空格
- 5. 从尾到头打印链表
- 6. 重建二叉树
- 7. 用两个栈实现队列
- 8. 旋转数组的最小数字
- 9. 斐波那契数列
- 10. 二进制中1的个数
- 11. 数值的整数次方
- 12. 打印1到最大的n位数
- 13. 在O(1)的时间删除链表节点
- 14. 调整数组顺序使奇数位于偶数前面
- 15. 链表中倒数第k个节点
- 16. 反转链表
- 17. 合并两个排序的链表
- 18. 树的子结构
- 19. 二叉树的镜像
- 20. 顺时针打印矩阵
- 21. 包含min函数的栈
- 22. 栈的弹出序列
- 23. 从上往下打印二叉树
- 24. 二叉搜索树的后序遍历
- 25. 二叉树中和为某一值的路径
- 26. 复杂链表的复制
- 27. 二叉搜索树与双向链表
- 28. 字符串的排列
- 29. 数组中出现次数超过一半的数字
- 30. 最小的k个数
- 31. 连续子数组的最大和
- 32. 从1到n整数中1出现的次数
- 33. 把数组排成最小的数
- 34. 丑数
- 35. 第一个只出现一次的字符
- 36. 数组中的逆序对
- 37. 两个链表的第一个公共节点
- 38. 数字在排序数组中出现的次数
- 39. 二叉树的深度
- 40. 数组中只出现一次的数字
- 41. 和为S的两个数字 VS 和为S的连续正数序列
- 42. 翻转单词顺序 VS 左旋转字符串
- 43. n个筛子的点数
- 44. 扑克牌顺子
- 45. 圆圈中最后剩下的数
- 46. 求1+2+...+n
- 47. 不用加减乘除做加法
- 48. 不能被继承的类
- 49. 把字符串转换成整数
- 50. 树中两个节点的最低公共祖先
- 52. 构建乘积数组
1. 赋值运算符函数
问题描述:如下为类型CMyString
的声明,请为该类型添加赋值运算符函数。
class CMyString
{
public:
CMyString(char* pData = NULL);
CMyString(const CMyString& str);
~CMyString(void);
private:
char* m_pData;
};
题目链接: lintcode
分析: Big three:拷贝构造、拷贝赋值、析构函数。对于拷贝赋值,基本步骤是:
- 释放左边内存
- 创建出与右边一样大的空间
- 把右边的复制到左边
当然,除了以上的主要步骤之外,还要进行corner case的判断:
- 传入的引用(拷贝赋值传入的参数都是对常量对象的左值引用)是否是自己?如果是,直接返回自己(
*this
)
代码:
CMyString& CMyString::operator=(const CMyString& str) {
if (this == &str) {
return *this;
}
delete[] m_pData;
m_pData = new char[ strlen(str.m_pData) + 1 ];
strcpy(m_pData, str.m_pData);
return *this;
}
在《剑指offer》中,提到了考虑异常安全性的做法,即开辟新空间时可能会出现内存不足,这个是我没有想到的方面。一个比较好的方法是先创建一个临时实例,再交换临时实例和原来的实例。这样由于内存不足抛出异常时,实例的状态还没有被改变。而且交换临时实例后,由于退出作用域的局部对象会被自动析构,因而原对象的内存也被自动释放。下面的代码是书上的例子:
CMyString& CMyString::operator=(const CMyString& str) {
if (this != &str) {
CMyString strTemp(str);
char* pTemp = strTemp.m_pData;
strTemp.m_pData = m_pData;
m_pData = pTemp;
}
return *this;
}
2. 实现Singleton模式
问题描述:设计一个类,我们只能生成该类的一个实例。
分析:没详细看书上的前面几个解法,实现单例模式比较好的做法就是把构造函数放在private
区避免用户创造对象,并在private
区声明一个类的静态实例,并通过静态函数控制对单例的访问:
class A {
public:
static A& getInstances() {
return a;
}
setup() {
...
}
private:
A();
A(const A& rhs);
static A a;
...
}
更好的做法是当需要访问对象时,才创建(Meyers Singleton):
class A {
public:
static A& getInstance();
private:
A();
A(const A& rhs);
...
}
A& A::getInstance() {
static A a;
return a;
}
3. 二维数组中的查找
问题描述:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样一个二维数组和一个整数,判断数组中是否含有该整数。
分析:最笨的方法是扫描一遍整个数组,没有利用数组有序这一信息。一个稍微好一点的方法是考虑到数组每一行都有序,遍历所有行,然后在每一行内进行二分查找,如果设矩阵是m行n列的,那么复杂度就是O(mlogn)
。
更好的做法是从矩阵的右上角开始(从左下角开始也行):
- 判断元素是否与目标相等,是则返回;
- 如果元素比目标小,那么当前元素所在行都不用再考察了,往下走一步;
- 如果元素比目标大,那么当前元素所在列就不用再考察了,往左走一步;
- 重复1~3,直到到达矩阵的边界。
复杂度O(m+n)
#include <iostream>
#include <vector>
#include <cstdio>
using namespace std;
int m, n, t;
int matrix[1001][1001];
void solve() {
int row = 0, col = n - 1;
while (row < m && col >= 0) {
if (t == matrix[row][col]) {
cout << "Yes" << endl;
return;
} else if (t < matrix[row][col]) {
col--;
} else {
row++;
}
}
cout << "No" << endl;
}
int main() {
while (cin >> m >> n) {
cin >> t;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
scanf("%d", &matrix[i][j]);
}
}
solve();
}
return 0;
}
如果问题不是判断是否存在目标值,而是找目标值出现的次数(假设每一行、每一列中没有重复的元素)?基本思想不变,只是在找到目标值后,我们不立即返回,而是把当前行、列都删除,继续往里走。
如果矩阵是整体有序的,即每行的第一个数大于上一行的最后一个数,怎么做?链接: search a 2d matrix
基本方法就是二分了。我们可以把这个矩阵看成一维数组,那么问题就是给定第i个元素,如何定位其在矩阵中的位置?记住如下结论(其中n表示矩阵的列数):
行id = i / n
列id = i % n
4. 替换空格
问题描述:请实现一个函数,把字符串中的每个空格替换成"%20"
。例如输入"We are happy."
,则输出"We%20are%20happy."
。假设该字符串有足够的空间来加入新的字符,且你得到的是“真实的”字符长度。
思路:先扫描一遍所有字符,记下空格数量,好决定新字符串的长度。然后从后往前填新字符串。
class Solution {
public:
/**
* @param string: An array of Char
* @param length: The true length of the string
* @return: The true length of new string
*/
void replaceBlank(char string[], int length) {
if (string == NULL || length <= 0) {
return;
}
int num = 0;
for (int i = 0; i < length; ++i) {
if (string[i] == ' ') {
num++;
}
}
int new_len = length + num * 2;
string[new_len] = '\0';
int p = length - 1, q = new_len - 1;
while (p >= 0) {
if (string[p] != ' ') {
string[q--] = string[p--];
} else {
string[q--] = '0';
string[q--] = '2';
string[q--] = '%';
p--;
}
}
}
};
5. 从尾到头打印链表
问题描述:输入一个链表的头结点,从尾到头反过来打印出每个节点的值。
链表节点定义如下:
struct ListNode
{
int val;
ListNode* next;
};
题目链接:牛客
扫描一遍链表,把每个节点的值压入一个栈中,之后元素挨个出栈即可。
vector<int> printListFromTailToHead(ListNode* head) {
if (head == NULL) {
return vector<int>();
}
stack<int> stk;
while (head != NULL) {
stk.push(head->val);
head = head->next;
}
vector<int> ret;
while (!stk.empty()) {
ret.push_back(stk.top());
stk.pop();
}
return ret;
}
6. 重建二叉树
问题描述:根据前序遍历和中序遍历构造二叉树。(可以假设树中不存在相同数值的节点)
问题链接:lintcode
一年之后再做。前序遍历的第一个元素是根节点,中序遍历根节点位于中间某个位置,思路就是想办法划分出左、右子树的前序遍历和中序遍历,然后递归构造二叉树。由于元素互不相同,可以确定根节点在中序遍历中的位置,同时也就知道了左子树有多少元素,于是也就能知道左子树的前序遍历了。
/**
* Definition of TreeNode:
* class TreeNode {
* public:
* int val;
* TreeNode *left, *right;
* TreeNode(int val) {
* this->val = val;
* this->left = this->right = NULL;
* }
* }
*/
class Solution {
/**
*@param preorder : A list of integers that preorder traversal of a tree
*@param inorder : A list of integers that inorder traversal of a tree
*@return : Root of a tree
*/
public:
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
if (preorder.empty()) return NULL;
return build(preorder, inorder, 0, preorder.size()-1, 0, inorder.size()-1);
}
TreeNode* build(vector<int>& preorder, vector<int>& inorder, int ps, int pe, int is, int ie {
if (ps > pe) return NULL;
int val = preorder[ps];
TreeNode* root = new TreeNode(val);
int left_num = 0, i;
for (i = is; i <= ie; ++i) {
if (inorder[i] == val) {
break;
}
left_num++;
}
root->left = build(preorder, inorder, ps+1, ps+left_num, is, i-1);
root->right = build(preorder, inorder, ps+left_num+1, pe, i+1, ie);
return root;
}
};
7. 用两个栈实现队列
题目链接:牛客
第一个栈只用来push元素,第二个栈只用来pop元素,若第二个栈为空,就把第一个栈的元素全部转移到第二个栈(转移的方式就是先pop出栈一再push入栈二)。上面的过程模拟的就是队列的行为。
class Solution
{
public:
void push(int node) {
stack1.push(node);
}
int pop() {
if (!stack2.empty()) {
return pop_stack2();
}
while (!stack1.empty()) {
stack2.push(stack1.top());
stack1.pop();
}
return pop_stack2();
}
int pop_stack2() {
int top = stack2.top();
stack2.pop();
return top;
}
private:
stack<int> stack1;
stack<int> stack2;
};
8. 旋转数组的最小数字
顺序扫描就不说了,这里更好的方法是二分查找。注意这道题须要留意有无重复元素,因为这影响到分支条件的设计,这里认为有重复元素。
- 首先,搜索范围
[start, end]
要始终包含最小值(初始化start = 0, end = nums.size() - 1
) - 当
mid
元素、两个端点的元素值都相等时,比如1,0,1,1,1
,1,1,1,0,1
,无法判断最小元素是左边递增部分还是右边递增部分,只能在搜索范围内顺序扫描或者分治了 - 否则,当
mid
元素比右端点的元素值大,说明mid
对应的元素位于旋转数组的左边的递增部分,最小值在mid
右边,令start = mid
- 其他情况,最小值在
mid
左边,令end = mid
int search(vector<int>& nums, int start, int end) {
if (start > end) return 0x3f3f3f3f;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (nums[mid] == nums[start] && nums[mid] == nums[end]) {
return min(search(nums, start, mid), search(nums, mid+1, end));
} else if (nums[mid] > nums[end]) {
start = mid;
} else {
end = mid;
}
}
if (nums[start] < nums[end]) return nums[start];
return nums[end];
}
int minNumberInRotateArray(vector<int>& nums) {
if (nums.empty()) {
return 0;
}
return search(nums, 0, nums.size() - 1);
}
9. 斐波那契数列
题目链接:牛客
递推,注意可以优化空间复杂度。
long long fibonacci(int n) {
if (n <= 0) return 0;
if (n == 1) return 1;
long long f1 = 1, f2 = 0, f;
for (int i = 2; i <= n; ++i) {
f = f1 + f2;
f2 = f1;
f1 = f;
}
return f;
}
ps. 这个问题有O(logn)
的矩阵快速幂算法。
跳台阶问题的递推式和斐波那契数列的一样,不过须要注意的是初始条件是n = 0, f(n) = 1; n = 1, f(n) = 1
,n = 0
时也有一种跳法的解释是“没有台阶就不需要跳了,但是不跳也是一种方法呀...”
class Solution {
public:
int jumpFloor(int number) {
return fibonacci(number);
}
int fibonacci(int n) {
if (n <= 0) return 1;
if (n == 1) return 1;
int f1 = 1, f2 = 1, f;
for (int i = 2; i <= n; ++i) {
f = f1 + f2;
f2 = f1;
f1 = f;
}
return f;
}
};
10. 二进制中1的个数
题目链接:牛客
如果是32位整数,就将其依次与1, 2, 4, ..., 2^31做与运算,判断每一位是否为1(结果不为0就是1)。
直接对所给整数移位然后与1与判断最后一位是否为1也是一个做法,不过问题是如果整数是负数,右移之后最高位是补1的。
int NumberOf1(int n) {
int ret = 0;
int x = 1;
for (int i = 0; i < 32; ++i) {
if ((n & x) != 0) {
ret++;
}
x = x << 1;
}
return ret;
}
另一种方法比较巧妙,根据算式n = (n - 1) & n
的功能是把整数n
的二进制表示中最后一个1去掉而启发。这个算法的迭代次数只等于整数中1的数量。
int NumberOf1(int n) {
int ret = 0;
while (n) {
n = ((n-1) & n);
ret++;
}
return ret;
}
11. 数值的整数次方
这题比较好的做法是用分治的结果复用加速幂的计算。但是更需要注意的是边界条件的判断。
- 底数是0?(不能用
==
判断double
型是否相等) - 指数为负?
#include <iostream>
#include <vector>
#include <cstdio>
#include <string>
#include <stack>
#include <set>
#include <cmath>
#include <cfloat>
using namespace std;
bool g_invalid_input = false;
double PowerUnsigned(double base, int exponent) {
if (exponent == 1) {
return base;
}
double tmp = PowerUnsigned(base, exponent / 2);
if (exponent % 2 == 0) {
return (tmp * tmp);
} else {
return (tmp * tmp * base);
}
}
double Power(double base, int exponent) {
g_invalid_input = false;
if (fabs(base - 0.0) < DBL_EPSILON) { // 注意浮点型不能用'=='作比较!
if (exponent == 0) return 1.0;
if (exponent < 0) {
g_invalid_input = true;
return DBL_MAX;
}
}
if (exponent == 0) return 1.0;
if (exponent < 0) return 1.0 / PowerUnsigned(base, -exponent);
return PowerUnsigned(base, exponent);
}
int main() {
int T;
while (cin >> T) {
double base;
int exp;
for (int i = 0; i < T; ++i) {
scanf("%lf%d", &base, &exp);
double ret = Power(base, exp);
if (g_invalid_input) {
printf("INF\n");
} else {
printf("%.2lef\n", ret);
}
}
}
return 0;
}
12. 打印1到最大的n位数
题目链接:九度
这题关键要清楚数据范围,主要是看最大的n位数能否用编译器定义的整数表示,能的话就很简单了,直接编译一下即可;否则就需要用数组或者字符串辅助。
九度OJ数据范围比较小,直接编译就能通过了:
#include <iostream>
#include <cstdio>
using namespace std;
int main() {
int n;
while (cin >> n) {
int N = 10;
for (int i = 1; i < n; ++i) {
N *= 10;
}
for (int i = 1; i < N; ++i) {
printf("%d\n", i);
}
}
return 0;
}
如果是大数,问题可以转化为输出0~9的全排列,遍历全排列的递归树即可(是一个深度为n的10叉树):
class Solution {
public:
void numbersByRecursion(int n) {
if (n <= 0) {
return;
}
dfs(n, "");
}
private:
/**
* @param n 还剩下几位
* @param cur 当前数值
*/
void dfs(int n, string cur) {
if (n == 0) {
if (!cur.empty()) {
cout << cur << endl;
}
return;
}
for (int i = 0; i < 10; ++i) {
if (cur.empty() && i == 0) {
dfs(n - 1, cur);
} else {
stringstream ss;
ss << i;
dfs(n - 1, cur + ss.str()); // C++11可以直接使用std::to_string(i)
}
}
}
};
13. 在O(1)的时间删除链表节点
题目链接:lintcode
准确地说,删除链表节点是无法保证O(1)的,因为正常的做法是首先得找到目标节点,用它前一个节点直接指向目标节点的后一个节点,然后释放目标节点的内存并将目标节点从链表中删除。如果可以O(1)删除链表节点的话,一定是有特殊条件:链表节点的成员构成是简单的,且链表节点是可以被修改的。
具体做法是,将后一节点的内容复制到当前节点,然后删除后一节点...其实原来想删除的节点并没有真正被删除,只是来了一招“狸猫换太子”...所以,O(1)的做法只能说是奇技淫巧。
下面的Java代码假设所要删除的节点是某个单链表的节点,并且不是尾节点。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
public class Solution {
public void deleteNode(ListNode node) {
if (node == null || node.next == null) return;
node.val = node.next.val;
node.next = node.next.next;
return;
}
}
注意,如果所删除的节点是尾节点,只能扫描到尾节点的前一个节点,再执行正常的删除操作。
14. 调整数组顺序使奇数位于偶数前面
一个简单的做法是开两个动态数组,一个存奇数,另一个存偶数。然后扫描一遍原数组,遇到奇数就放到奇数数组中,遇到偶数就放到偶数数组中。最后依次用奇数数组、偶数数组里的元素覆盖原数组。时间和空间复杂度都是O(n)。
void partitionArray(vector<int> &nums) {
if (nums.empty() || nums.size() == 1) return;
vector<int> odd, even;
for (int i = 0; i < nums.size(); ++i) {
if (is_odd(nums[i])) {
odd.push_back(nums[i]);
} else {
even.push_back(nums[i]);
}
}
int k = 0;
for (int i = 0; i < odd.size(); ++i) {
nums[k++] = odd[i];
}
for (int i = 0; i < even.size(); ++i) {
nums[k++] = even[i];
}
}
当然,也可以用类似插入排序的划分方式,用一个指针表示左边是目前为止的奇数,往右扫描的过程中遇到奇数就把它取出(同时要把指针到该奇数位置之间的所有元素整体右移)插入到指针的位置,然后指针右移。时间复杂度O(n^2)
,空间复杂度O(1)
。
如果不用额外空间呢?
注意可能要求奇数和奇数之间、偶数和偶数之间相对位置不变。如果有这个要求,就使用上面的算法,否则就可以用类似快排的partition算法:
class Solution {
public:
/**
* @param nums: a vector of integers
* @return: nothing
*/
void partitionArray(vector<int> &nums) {
if (nums.empty()) return;
int fst_even = 0;
for (int i = 0; i < nums.size(); ++i) {
if (is_odd(nums[i])) {
swap(nums[fst_even++], nums[i]);
}
}
}
private:
bool is_odd(int x) {
return (x % 2 != 0);
}
};
15. 链表中倒数第k个节点
题目链接:牛客
思路1:设链表长度为n
,链表倒数第k (k <= n)
个节点,就是正数第n-k+1
个节点。需要扫描两遍链表。
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
int len = 0;
ListNode* head = pListHead;
while (head != NULL) {
++len;
head = head->next;
}
if (k > len) {
return NULL;
}
unsigned n = len - k + 1, i = 1;
head = pListHead;
while (i < n) {
head = head->next;
++i;
}
return head;
}
思路2:再做这道题时,还是没有立即反应出扫描一遍的做法。方法是双指针:取两个指针放在链表头部,先用一个指针走到正数第k个节点,然后两个指针同时往前走,当前面的指针走到链表的尾节点时,后面的指针所处的位置就是倒数第k个节点。
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if (pListHead == NULL || k == 0) {
return NULL;
}
ListNode *p_behind = pListHead, *p_ahead = pListHead;
for (int i = 1; i < k; ++i) {
p_ahead = p_ahead->next;
if (p_ahead == NULL) { // 当k比链表长度大时会出现这种情况
return NULL;
}
}
while (p_ahead->next != NULL) {
p_ahead = p_ahead->next;
p_behind = p_behind->next;
}
return p_behind;
}
16. 反转链表
题目链接:牛客
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
if (pHead == NULL) {
return NULL;
}
ListNode *prev = NULL, *next;
while (pHead != NULL) {
next = pHead->next;
pHead->next = prev;
prev = pHead;
pHead = next;
}
return prev;
}
};
17. 合并两个排序的链表
题目链接:牛客
排序链表的合并比数组容易,因为不需要额外的空间,直接重组链表节点就好。注意接上最后剩下的链表部分。
ListNode *mergeTwoLists(ListNode *l1, ListNode *l2) {
ListNode dummy(0), *p = &dummy;
while (l1 && l2) {
if (l1->val <= l2->val) {
p->next = l1;
l1 = l1->next;
} else {
p->next = l2;
l2 = l2->next;
}
p = p->next;
}
p->next = l1? l1 : l2; // 这句不能漏
return dummy.next;
}
18. 树的子结构
设两个树A, B,要判断B是不是A的子结构。意思就是判断A是不是包含B(A中有与B一样的结构)。注意子结构和子树的区别:是子结构不一定是子树,是子树一定是子结构。思路就是遍历A的节点,如果A某个节点的值与B根节点的值相等,就接着判断从该节点开始是否包含B(条件是该节点的左子树包含B的左子树,且该节点的右子树也包含B的右子树,递归判断即可)。
class Solution {
public:
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) {
if (pRoot1 == NULL) return false;
if (pRoot2 == NULL) return false;
bool ret = false;
if (pRoot1->val == pRoot2->val) ret = IsContain(pRoot1, pRoot2);
if (!ret) ret = HasSubtree(pRoot1->left, pRoot2);
if (!ret) ret = HasSubtree(pRoot1->right, pRoot2);
return ret;
}
private:
bool IsContain(TreeNode* pRoot1, TreeNode* pRoot2) {
if (pRoot2 == NULL) return true;
if (pRoot1 == NULL) return false;
if (pRoot1->val != pRoot2->val) return false;
return IsContain(pRoot1->left, pRoot2->left) && IsContain(pRoot1->right, pRoot2->right);
}
};
九度OJ上的这题麻烦点的是输入,需要动态分配内存构造二叉树,在这里也贴上AC的代码,其中Solution
部分与上面完全一样。Note: 下面的代码最后并没有释放内存,偷懒了~
#include <iostream>
#include <vector>
#include <cstdio>
#include <string>
#include <stack>
#include <set>
#include <cmath>
#include <cfloat>
#include <algorithm>
#include <sstream>
using namespace std;
class TreeNode {
public:
int val;
TreeNode *left, *right;
TreeNode (int val = 0) : val(val), left(NULL), right(NULL) {}
};
TreeNode* make_tree(unsigned n) {
vector<TreeNode*> nodes(n+1);
vector<bool> is_child(n+1, false);
int tmp;
for (int i = 1; i <= n; ++i) {
cin >> tmp;
nodes[i] = new TreeNode(tmp);
}
int k, lid, rid;
for (int i = 1; i <= n; ++i) {
cin >> k;
if (k == 2) {
cin >> lid >> rid;
nodes[i]->left = nodes[lid];
nodes[i]->right = nodes[rid];
is_child[lid] = true;
is_child[rid] = true;
} else if (k == 1) {
cin >> lid;
nodes[i]->left = nodes[lid];
is_child[lid] = true;
}
}
for (int i = 1; i <= n; ++i) {
if (!is_child[i]) {
return nodes[i];
}
}
return NULL;
}
class Solution {
public:
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) {
if (pRoot1 == NULL) return false;
if (pRoot2 == NULL) return false;
bool ret = false;
if (pRoot1->val == pRoot2->val) ret = IsContain(pRoot1, pRoot2);
if (!ret) ret = HasSubtree(pRoot1->left, pRoot2);
if (!ret) ret = HasSubtree(pRoot1->right, pRoot2);
return ret;
}
private:
bool IsContain(TreeNode* pRoot1, TreeNode* pRoot2) {
if (pRoot2 == NULL) return true;
if (pRoot1 == NULL) return false;
if (pRoot1->val != pRoot2->val) return false;
return IsContain(pRoot1->left, pRoot2->left) && IsContain(pRoot1->right, pRoot2->right);
}
};
int main() {
unsigned int n, m;
while (cin >> n >> m) {
TreeNode* t1 = make_tree(n);
TreeNode* t2 = make_tree(m);
Solution sol;
bool target = sol.HasSubtree(t1, t2);
if (target) cout << "YES" << endl;
else cout << "NO" << endl;
}
return 0;
}
19. 二叉树的镜像
题目链接:牛客
先交换左、右子树,然后分别将左右子树变换为二叉树的镜像。
class Solution {
public:
void Mirror(TreeNode *pRoot) {
if (pRoot == NULL) return;
TreeNode* tmp = pRoot->left;
pRoot->left = pRoot->right;
pRoot->right = tmp;
Mirror(pRoot->left);
Mirror(pRoot->right);
}
};
20. 顺时针打印矩阵
题目链接:lintcode
上下左右bound,一圈一圈打印。为了防止出错,while循环里条件都是严格小于,这样最后出了while循环就多加几行代码。不过也易于分析,出while循环后无非两种情况:left=right, top=bottom (或者同时发生,不过针对一种处理也能cover这种case)。left=right的话,最后就剩一列了,容易分析,最终一定是从上往下走的;top=bottom的情况同理,只是从左往右走。
class Solution {
public:
/**
* @param matrix a matrix of m x n elements
* @return an integer array
*/
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if (matrix.empty()) return vector<int>();
if (matrix[0].empty()) return vector<int>();
int m = matrix.size(), n = matrix[0].size();
int N = m * n, k = 0;
vector<int> ret(N);
int top = 0, bottom = m - 1, left = 0, right = n - 1;
while (left < right && top < bottom) {
for (int j = left; j < right; ++j) {
ret[k++] = matrix[top][j];
}
for (int i = top; i < bottom; ++i) {
ret[k++] = matrix[i][right];
}
for (int j = right; j > left; --j) {
ret[k++] = matrix[bottom][j];
}
for (int i = bottom; i > top; --i) {
ret[k++] = matrix[i][left];
}
left++; right--; top++; bottom--;
}
if (top == bottom) {
for (int j = left; j <= right; ++j) {
ret[k++] = matrix[top][j];
}
} else if (left == right) {
for (int i = top; i <= bottom; ++i) {
ret[k++] = matrix[i][left];
}
}
return ret;
}
};
21. 包含min函数的栈
双栈法。下面的代码不考虑栈空的情况~
class Solution {
public:
void push(int value) {
stk1.push(value);
if (stk2.empty() || value <= stk2.top()) {
stk2.push(value);
}
}
void pop() {
int top = stk1.top();
stk1.pop();
if (top == stk2.top()) {
stk2.pop();
}
}
int top() {
return stk1.top();
}
int min() {
return stk2.top();
}
stack<int> stk1;
stack<int> stk2;
};
22. 栈的弹出序列
题目链接:牛客
用一个辅助栈去匹配输入序列和弹出序列,由于一个入栈序列和出栈序列只对应一种连续的栈操作,因而按顺序依次模拟相应的操作即可(注意压入栈的所有数字均不同这个假设很重要)
用两个指针维护下一个出栈元素和入栈元素。遍历出栈序列,对每个出栈元素,判断其与栈顶是不是刚好相等,是则直接弹出;否则继续从入栈元素中把数字压入辅助栈,直到遇到等于该出栈元素的数字。如果所有元素都入栈还没有遇到这样的数字,就说明这是一个不可能的case。
class Solution {
public:
bool IsPopOrder(vector<int> pushV, vector<int> popV) {
if (pushV.empty() && popV.empty()) return true;
if (pushV.empty() || popV.empty()) return false;
stack<int> stk;
int p_next_pop = 0, p_next_push = 0;
while (p_next_pop < popV.size()) {
int pop_ele = popV[p_next_pop];
while (stk.empty() || stk.top() != pop_ele) {
if (p_next_push == pushV.size()) break;
stk.push(pushV[p_next_push]);
p_next_push++;
}
if (stk.top() != pop_ele) return false;
stk.pop();
p_next_pop++;
}
return true;
}
};
23. 从上往下打印二叉树
二叉树的层次遍历,其实就是简化的宽度优先搜索。
vector<int> PrintFromTopToBottom(TreeNode* root) {
if (root == NULL) return vector<int>();
vector<int> ret;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()) {
TreeNode* node = q.front();
q.pop();
ret.push_back(node->val);
if (node->left != NULL) q.push(node->left);
if (node->right != NULL) q.push(node->right);
}
return ret;
}
如果要求输出每一层的元素,我们须要用一个变量维护每一层的元素数量,第一层有一个元素,每处理完一层,就取一下队列的大小得到下一层的元素数目。
vector<vector<int>> levelOrder(TreeNode *root) {
vector<vector<int>> ret;
if (root == NULL) return ret;
queue<TreeNode*> q;
q.push(root);
int level_num = q.size();
while (!q.empty()) {
vector<int> tmp;
for (int i = 0; i < level_num; ++i) {
TreeNode* node = q.front();
q.pop();
tmp.push_back(node->val);
if (node->left != NULL) q.push(node->left);
if (node->right != NULL) q.push(node->right);
}
ret.push_back(tmp);
level_num = q.size();
}
return ret;
}
24. 二叉搜索树的后序遍历
题目链接:牛客
根据BST的性质对序列分组,最后一个一定是根节点,然后按左子树都比根节点小,右子树都比根节点大的性质划出左、右子树的范围,接着递归判断左右子树是否为BST即可。
class Solution {
public:
bool VerifySquenceOfBST(vector<int> sequence) {
if (sequence.empty()) return false;
return helper(sequence, 0, sequence.size() - 1);
}
bool helper(vector<int>& seq, int start, int end) {
if (start >= end) return true;
int root = seq[end];
int mid = start;
while (seq[mid] < root) mid++;
int tmp = mid;
while (tmp < end) {
if (seq[tmp] <= root) return false;
tmp++;
}
return helper(seq, start, mid-1) && helper(seq, mid, end-1);
}
};
25. 二叉树中和为某一值的路径
题目链接:牛客
这题主要是深度优先搜索的设计。思路其实很简单(但是要一次写对不容易),就是按DFS遍历二叉树,并维护一个状态变量记录当前路径的和,当走到叶子节点时,通过判断和是否等于目标值来决定是否要保存路径。
class Solution {
public:
vector<vector<int> > FindPath(TreeNode* root, int expectNumber) {
vector<vector<int> > ret;
if (root == NULL) return ret;
vector<int> path;
helper(ret, path, 0, root, expectNumber);
return ret;
}
void helper(vector<vector<int> >& ret, vector<int> path, int sum, TreeNode* root, int& expectNumber) {
if (root->left == NULL && root->right == NULL) {
if (sum + root->val == expectNumber) {
path.push_back(root->val);
ret.push_back(path);
}
return;
}
path.push_back(root->val);
sum += root->val;
if (root->left != NULL) helper(ret, path, sum, root->left, expectNumber);
if (root->right != NULL) helper(ret, path, sum, root->right, expectNumber);
}
};
26. 复杂链表的复制
题目链接:牛客
链表的深拷贝,用哈希表建立原节点和复制节点的对应关系,第一遍扫描创造节点和对应关系,第二遍扫描实现指针域的对应。当然这题有不用额外空间的线性时间做法,不过那属于奇技淫巧了。
RandomListNode* Clone(RandomListNode* pHead) {
if (pHead == NULL) return NULL;
unordered_map<RandomListNode*, RandomListNode*> hash_map;
RandomListNode* p = pHead;
while (p != NULL) {
RandomListNode* node = new RandomListNode(p->label);
hash_map[p] = node;
p = p->next;
}
p = pHead;
RandomListNode* q = hash_map[p];
while (p != NULL) {
q->next = hash_map[p->next];
q->random = hash_map[p->random];
p = p->next;
q = q->next;
}
return hash_map[pHead];
}
27. 二叉搜索树与双向链表
题目链接:牛客
分别将左右子树转换成双向链表,然后连接起来。
TreeNode* Convert(TreeNode* pRootOfTree) {
if (pRootOfTree == NULL) return NULL;
TreeNode* left = Convert(pRootOfTree->left);
TreeNode* right = Convert(pRootOfTree->right);
// 找到左边最后一个节点,然后与根节点连接
TreeNode* head = left; // 保存头部
if (left != NULL) {
while (left->right != NULL) {
left = left->right;
}
left->right = pRootOfTree;
pRootOfTree->left = left;
}
// 根节点与右边连接
if (right != NULL) {
pRootOfTree->right = right;
right->left = pRootOfTree;
}
return (head == NULL? pRootOfTree : head);
}
28. 字符串的排列
题目链接:牛客
这题很考验对递归的理解,要写出简洁的递归代码有难度。对于牛客网上的题,难点在于:输入字符串的字符可能有重复;要求输出按字典序排列,且不能有重复字符串。
这个问题就是全排列,类似这种就须要遍历全排列的递归树,只不过这里是“无放回地”遍历。一个简单的思维方式是把字符串分成两部分:第一个字符、后面的所有字符。接下来分两步操作:
- 枚举第一个字符的所有可能情况,即从第一个字符往后的所有字符都可以成为首字符(其实这就相当于递归树的分支,第一个字符有多少种可能,递归树中当前节点就有多少个分支);
- 对1中的每一种可能(不重复),求后面所有字符的全排列(这一步就相当于对子树的递归遍历)。
class Solution {
public:
vector<string> Permutation(string& str) {
if (str.empty()) return vector<string>();
set<string> myset;
dfs(myset, str, 0);
return vector<string>(myset.begin(), myset.end());
}
private:
void dfs(set<string>& ret, string& str, int start) {
if (start >= str.size()) {
ret.insert(str);
return;
}
for (int i = start; i < str.size(); ++i) {
swap(str[start], str[i]);
dfs(ret, str, start + 1);
swap(str[start], str[i]);
}
}
};
实现上,用了STL的set
容器存储求得的排列结果,可以保证有序和唯一性(由于接口返回类型是vector
,最后将set
里的内容再推入vector
即可)。对于“第一个元素”所有可能的获得,用了交换操作。另外就是注意,dfs
函数的第二个参数,其实加不加引用都可以,这里可以加引用的原因是一个字符串无论初始什么顺序其全排列都是一样的结果。
29. 数组中出现次数超过一半的数字
题目链接:牛客
一个比较容易想到的方法是用哈希表统计一下所有元素的出现次数,返回大于数组长度一半的数字即可。时间和空间复杂度都是O(n)。
#include <unordered_map>
class Solution {
public:
int MoreThanHalfNum_Solution(vector<int> numbers) {
if (numbers.empty()) return 0;
int n = numbers.size();
unordered_map<int, int> hashmap;
for (int i = 0; i < numbers.size(); ++i) {
hashmap[numbers[i]]++;
}
for (auto& kv : hashmap) {
if (kv.second > n / 2) {
return kv.first;
}
}
return 0;
}
};
有没有不用额外空间的算法?上面的方法并没有利用起问题所描述的数组的特性。试想,如果一个数组中存在一个数字,它的出现次数超过数组长度的一半,那么容易发现,我们对数组排序后,位于中间的数字一定等于该数字。比如数组[1,2,3,2,2,2,5,4,2]
,排序之后是[1,2,2,2,2,3,4,5]
,位于中间的数字是2,它也就是我们所要找的数字。于是问题变成:如何找到排序后位于中间的数字?简单的做法是对整个数组排个序,取中间的;不过其实没有必要对整个数组排序,用快排的Partition操作,结合二分,可以平均复杂度O(n)找到这个数。另外须要注意的是,找到这个数后,必须要检查一遍它的出现次数,以确认它是否确实超过一半。
class Solution {
public:
int MoreThanHalfNum_Solution(vector<int>& numbers) {
if (numbers.empty()) return 0;
int n = numbers.size();
int mid = n / 2;
int start = 0, end = n - 1;
int pos = partition(numbers, start, end);
while (pos != mid) {
if (pos > mid) {
end = pos - 1;
} else {
start = pos + 1;
}
pos = partition(numbers, start, end);
}
return check(numbers, numbers[pos]) ? numbers[pos] : 0;
}
private:
int partition(vector<int>& nums, int start, int end) {
if (start >= end) return start;
int pivot = nums[end];
int fst_larger = start;
for (int i = start; i < end; ++i) {
if (nums[i] < pivot) {
std::swap(nums[i], nums[fst_larger++]);
}
}
std::swap(nums[end], nums[fst_larger]);
return fst_larger;
}
bool check(vector<int>& nums, int target) {
int times = 0;
for (auto x : nums) {
if (x == target) times++;
}
return times > nums.size()/2;
}
};
再进一步,如果不允许修改数组,或者是数据流的形式,应该如何做?idea是,注意到对于目标数字,其出现次数一定比其他所有不同的数的出现次数之和还多。我们维护一个计数器和其对应的数字,扫描数组,每遇到一个数字,如果它与我们维护的数字相等,就将计数器加1,否则计数器减1,如果计数器减为0,就把保存的数字替换为新遇到的数字,同时把计数器重新设置为1。根据目标数字出现次数的性质,当扫描结束后,所维护的数字一定等于目标数字(如果存在的话)。
class Solution {
public:
int MoreThanHalfNum_Solution(vector<int>& numbers) {
if (numbers.empty()) return 0;
int n = numbers.size();
int target = numbers[0], count = 1;
for (int i = 1; i < n; ++i) {
if (numbers[i] == target) {
++count;
} else {
--count;
if (count == 0) {
target = numbers[i];
count = 1;
}
}
}
return check(numbers, target) ? target : 0;
}
private:
bool check(vector<int>& nums, int target) {
int times = 0;
for (auto x : nums) {
if (x == target) times++;
}
return times > nums.size()/2;
}
};
30. 最小的k个数
题目链接:牛客
最简单的做法是对数组升序排序,返回前k个数。
vector<int> GetLeastNumbers_Solution(vector<int>& input, int k) {
if (input.empty()) return vector<int>();
if (k <= 0 || k > input.size()) return vector<int>();
vector<int> ret((unsigned)k);
std::sort(input.begin(), input.end());
for (int i = 0; i < k; ++i) {
ret[i] = input[i];
}
return ret;
}
时间O(n),不用额外空间的做法是类似上一题,基于快排的Partition结合二分确定第k小的数字,同时也就得到了前k小的数(只是不一定按序排好)。
class Solution {
public:
vector<int> GetLeastNumbers_Solution(vector<int>& input, int k) {
if (input.empty()) return vector<int>();
if (k <= 0 || k > input.size()) return vector<int>();
int start = 0, end = input.size() - 1;
int pos = partition(input, start, end);
while (pos != k - 1) {
if (pos > k - 1) {
end = pos - 1;
} else {
start = pos + 1;
}
pos = partition(input, start, end);
}
vector<int> ret((unsigned)k);
for (int i = 0; i < k; ++i) {
ret[i] = input[i];
}
return ret;
}
private:
int partition(vector<int>& nums, int start, int end) {
if (start >= end) return start;
int pivot = nums[end], fst_larger = start;
for (int i = start; i < end; ++i) {
if (nums[i] < pivot) {
std::swap(nums[i], nums[fst_larger++]);
}
}
swap(nums[end], nums[fst_larger]);
return fst_larger;
}
};
类似上一题,如果不允许修改传入的数组或者数字是以数据流的形式到达,如何做?答案是维护一个最大堆,或者长度不超过k的优先队列。当优先队列的长度小于k时,每遇到一个数字,就推入队列;当队列长度等于k时,将新来的数字与优先队列中最大的元素/堆顶做比较,如果它比堆顶元素小,就用它替换堆顶元素。当访问过所有元素后,优先队列中的k个数就是最小的k个数。这种做法的空间复杂度是O(k),时间复杂度是O(n logk),另外值得一提的就是它非常适合海量数据的处理。
class Solution {
public:
vector<int> GetLeastNumbers_Solution(vector<int>& input, int k) {
if (input.empty()) return vector<int>();
if (k <= 0 || k > input.size()) return vector<int>();
priority_queue<int> max_heap;
for (auto x : input) {
if (max_heap.size() < k) {
max_heap.push(x);
} else if (x < max_heap.top()) {
max_heap.pop();
max_heap.push(x);
}
}
vector<int> ret((unsigned)k);
int i = 0;
while (!max_heap.empty()) {
ret[i++] = max_heap.top();
max_heap.pop();
}
return ret;
}
private:
int partition(vector<int>& nums, int start, int end) {
if (start >= end) return start;
int pivot = nums[end], fst_larger = start;
for (int i = start; i < end; ++i) {
if (nums[i] < pivot) {
std::swap(nums[i], nums[fst_larger++]);
}
}
swap(nums[end], nums[fst_larger]);
return fst_larger;
}
};
31. 连续子数组的最大和
题目链接:牛客网
思路1:动态规划
定义f(i)
表示以a(i)
结尾的连续子数组的最大和,则递推公式:
f(i) = f(i-1) + a(i), f(i-1) > 0
f(i) = a(i), otherwise
边界条件:f(0) = a[0]
最大子数组和就是max{f(i)}
。
另外须要特别注意对数组为空的处理,下面的代码是返回0,并用一个(全局)变量标记输入的合法性。
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int>& array) {
invalid_input = false;
if (array.empty()) {
invalid_input = true;
return 0;
}
int ret = array[0];
vector<int> f(array.size());
f[0] = array[0];
for (int i = 1; i < array.size(); ++i) {
f[i] = (f[i-1] > 0 ? f[i-1] + array[i] : array[i]);
ret = max(ret, f[i]);
}
return ret;
}
bool invalid_input = false;
};
32. 从1到n整数中1出现的次数
《剑指offer》上对这道题的分析是找数字规律,不容易理解也不具有扩展性(如果不是1出现的次数而是其他的某个数字呢?),而且我想在面试的环境下,有限时间内清晰地分析出规律并写出代码做到bug free,大概还是很难的吧。
浏览了原书作者的博客,发现该问题的评论下有一些相当精彩的解法,下面给出我认为最容易理解的,更可贵的是,下面的方法不仅仅可以统计1出现的次数,它适用于对0~9中任何一个数字的统计(@Van_Nity)。
idea是按数位统计(注意不是比特位),某一数位的左侧代表右侧部分出现了几次,我们须要知道右侧部分中1出现了多少次。以30143为例:
- 个位:由于
3>1
,左边可以在0~3014变化,则个位上1出现的次数为(3014+1)*1
- 十位:由于
4>1
,左边可以在0301变化,右边可以在09变化,则十位上1出现的次数为(301+1)*10
- 百位:由于
1=1
,左边先在029变化,此时右边可以在099变化;左边为30时,右边可以在0~43变化,则百位上1出现的次数为30*100+(43+1)
- 千位:由于
0<1
,左边先在02变化,此时右边可以在0999变化;左边为3时,千位上不可能为1,则千位上1出现的次数为3*1000
- 万位:由于
3>1
,右边可以在0~9999变化,则万位上1出现的次数为10000
int NumberOf1Between1AndN_Solution(int n) {
if (n < 0) return 0;
int base_scale = 1;
int passed = 0;
int ret = 0;
while (n) {
int cur_dgt = n % 10;
n /= 10;
if (cur_dgt > 1) {
ret += (n + 1) * base_scale;
} else if (cur_dgt == 1) {
ret += n * base_scale + passed + 1;
} else {
ret += n * base_scale;
}
passed += cur_dgt * base_scale;
base_scale *= 10;
}
return ret;
}
上面的代码是针对统计1的个数的,下面的代码是统计0~9中任何一个数字,注意0和其他数字要分开统计:
class Solution {
public:
/*
* param k : As description.
* param n : As description.
* return: How many k's between 0 and n.
*/
int digitCounts(int k, int n) {
if (n < 0 || k < 0 || k > 9) return 0;
int base_scale = 1;
int passed = 0;
int ret = 0;
while (n) {
int dgt = n % 10;
n /= 10;
if (dgt > k) {
if (k > 0) ret += (n + 1) * base_scale;
else ret += n * base_scale;
} else if (dgt == k) {
if (k > 0) ret += (n * base_scale + passed + 1);
else ret += (n-1)*base_scale + passed + 1;
} else {
ret += n * base_scale;
}
passed += dgt * base_scale;
base_scale *= 10;
}
return k == 0? ret+1 : ret;
}
};
33. 把数组排成最小的数
题目链接:牛客
暴力枚举是肯定不可行的,那样是全排列的复杂度(阶乘级)。仔细想想,最优解中两个数字m和n是什么样?一定是mn拼接和nm拼接较小的那个所对应的顺序(如果不是这样的话,我们总能将m和n交换从而得到更好的组合),其他两两组合同理,于是解决这个问题只要定义排序规则就可以了,只不过这里是对字符串的排序。
注意容易出错的case:
case 1: 0 (输出0)
case 2: 0 2 (输出2)
因此最后要注意前导0的去除。
另外吐槽一下,剑指offer上对这道题的代码写得我是真心不欣赏,把C和C++的代码混着写,让人阅读起来太难受了...
string PrintMinNumber(vector<int>& numbers) {
if (numbers.empty()) {
return "";
}
vector<string> nums_str(numbers.size());
for (int i = 0; i < numbers.size(); ++i) {
nums_str[i] = std::to_string(numbers[i]);
}
sort(nums_str.begin(), nums_str.end(), [](const string& x, const string& y){return x + y < y + x;});
string ret;
int i = 0;
while (i < nums_str.size() && nums_str[i] == "0") ++i;
while (i < nums_str.size()) {
ret.append(nums_str[i++]);
}
return ret.empty() ? "0" : ret;
}
34. 丑数
题目链接:牛客
int GetUglyNumber_Solution(int index) {
if (index <= 0) {
return 0;
}
vector<int> nums((unsigned)index + 1);
nums[1] = 1;
int p2 = 1, p3 = 1, p5 = 1;
for (int i = 2; i <= index; ++i) {
nums[i] = min(nums[p2]*2, min(nums[p3]*3, nums[p5]*5));
while (nums[p2]*2 <= nums[i]) p2++;
while (nums[p3]*3 <= nums[i]) p3++;
while (nums[p5]*5 <= nums[i]) p5++;
}
return nums[index];
}
35. 第一个只出现一次的字符
题目链接:牛客
开一个哈希表,存每个字符的出现次数。第一次扫描字符串,填充哈希表;第二次扫描,找第一个出现次数为1的字符。
int FirstNotRepeatingChar(string& str) {
if (str.empty()) return -1;
vector<pair<int, int>> count_map(256, make_pair(0,0));
for (int i = 0; i < str.size(); ++i) {
char c = str[i];
if (count_map[c-'A'].first == 0) {
count_map[c-'A'].first = 1;
count_map[c-'A'].second = i;
} else {
count_map[c-'A'].first++;
}
}
for (auto c : str) {
if (count_map[c-'A'].first == 1) {
return count_map[c-'A'].second;
}
}
return -1;
}
36. 数组中的逆序对
题目链接:牛客
比较经典的问题了。高效的做法是分治:
- 统计左半边的逆序对
- 统计右半边的逆序对
- 统计横跨左右的逆序对
如果左、右半边是有序的,那么对横跨左右的逆序对的统计就可以用合并排序数组的方法做到O(n)。于是我们在统计左右半边的同时对它们排序,可见整个过程就是类似于归并排序过程。
不少同学背下这道题的方法是“归并排序”,但是并不理解为什么归并排序是对的,知其然不知其所以然。那是因为搞反了因果关系——其实并不是“归并排序有统计的功能”,而是为了加速计数过程,最好能创造一些条件,比如数组有序。如果说解决这个问题的出发点是分治,只不过恰好实现分治的过程中需要用到有序的性质,而这样的实现碰巧就是归并排序而已。
class Solution {
public:
int InversePairs(vector<int>& data) {
if (data.empty()) return 0;
vector<int> helper(data.size());
return count(data, 0, data.size() - 1, helper);
}
private:
int count(vector<int>& nums, int start, int end, vector<int>& helper) {
if (start >= end) return 0;
int mid = start + (end - start) / 2;
int left = count(nums, start, mid, helper);
int right = count(nums, mid+1, end, helper);
int cross = merge(nums, start, mid, end, helper);
return ((left + right) % 1000000007 + cross) % 1000000007;
}
int merge(vector<int>& nums, int start, int mid, int end, vector<int>& helper) {
std::copy(nums.begin()+start, nums.begin()+end+1, helper.begin()+start);
int p = start, q = mid+1, k = start;
int cnt = 0;
while (p <= mid && q <= end) {
if (helper[p] > helper[q]) {
nums[k++] = helper[p++];
cnt += (end - q + 1);
cnt %= 1000000007;
} else {
nums[k++] = helper[q++];
}
}
while (p <= mid) nums[k++] = helper[p++];
while (q <= end) nums[k++] = helper[q++];
return cnt;
}
};
37. 两个链表的第一个公共节点
题目链接:牛客
非常高频的问题。复习:链表判环
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
if (pHead1 == NULL || pHead2 == NULL) {
return NULL;
}
ListNode* p = pHead2;
while (p->next != NULL) p = p->next;
p->next = pHead2;
ListNode* ret = DetectCycle(pHead1);
p->next = NULL;
return ret;
}
private:
ListNode* DetectCycle(ListNode* head) {
bool has_cycle = false;
ListNode *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
has_cycle = true;
break;
}
}
if (!has_cycle) return NULL;
ListNode *u = head, *v = slow;
while (u != v) {
u = u->next;
v = v->next;
}
return u;
}
};
38. 数字在排序数组中出现的次数
题目链接:牛客
高效的做法是用两次二分法分别找到目标值的最左边和最右边的位置,然后相减即可。
最简单的实现当然是用std::lower_bound
和std::upper_bound
了,算是偷懒的写法吧:
int GetNumberOfK(vector<int>& A, int target) {
if (A.empty()) return 0;
auto it_lb = std::lower_bound(A.begin(), A.end(), target);
auto it_ub = std::upper_bound(A.begin(), A.end(), target);
return (it_ub - it_lb);
}
当然练习的话还是要自己实现二分的:
int GetNumberOfK(vector<int>& data, int k) {
if (data.empty()) {
return 0;
}
int left_pos = 0, right_pos = -1;
// find left of k
int start = 0, end = data.size() - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (data[mid] >= k) {
end = mid;
} else {
start = mid;
}
}
if (data[start] == k) left_pos = start;
else if (data[end] == k) left_pos = end;
// find right of k
start = 0; end = data.size() - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (data[mid] <= k) {
start = mid;
} else {
end = mid;
}
}
if (data[end] == k) right_pos = end;
else if (data[start] == k) right_pos = start;
return (right_pos - left_pos + 1);
}
39. 二叉树的深度
问题一:二叉树的深度
class Solution {
public:
int TreeDepth(TreeNode* pRoot) {
if (pRoot == NULL) {
return 0;
}
return 1 + max(TreeDepth(pRoot->left), TreeDepth(pRoot->right));
}
};
问题二:平衡二叉树
比较高效的做法是只对每个平衡的节点标记高度,对于不平衡的子树根节点标记为-1即可,这样回溯时,父节点就既能知道孩子节点是否平衡,又能知道如果平衡的话其高度是多少了,避免了重复计算。
class Solution {
public:
bool IsBalanced_Solution(TreeNode* pRoot) {
return Balanced(pRoot) != -1;
}
private:
int Balanced(TreeNode* root) {
if (root == NULL) return 0;
int left = Balanced(root->left);
if (left == -1) return -1;
int right = Balanced(root->right);
if (right == -1) return -1;
return abs(left-right) <= 1 ? 1 + max(left, right) : -1;
}
};
40. 数组中只出现一次的数字
题目链接:牛客
最直观的方法是用哈希表统计数字的出现次数。
void FindNumsAppearOnce(vector<int>& data, int* num1, int* num2) {
if (data.empty()) return;
unordered_map<int, int> hash_map;
for (auto x : data) {
hash_map[x]++;
}
vector<int> tmp;
for (auto& x : data) {
if (hash_map[x] == 1) {
tmp.push_back(x);
}
}
*num1 = tmp[0];
*num2 = tmp[1];
}
第二种方法参考了《剑指offer》,是用位运算做的。首先,对数组中所有数字做异或,得到的是两个只出现一次的数字的异或结果。因为两个数组不一样,这个结果一定不为0,也就意味着其二进制位中至少有一位是1。我们找到第一个是1的位,设为第k位,然后以第k位是否为1将原数组中的数字分为两部分。我们可以断定:一个目标数字的第k位一定是1,另一个目标数字的第k位一定是0,于是划分后两个目标数字就一定分别在两个不同的部分(仔细想一想为什么)。之后就是分别对两个部分求连续异或得到目标数字了。
class Solution {
public:
void FindNumsAppearOnce(vector<int>& data, int* num1, int *num2) {
if (data.empty()) {
return;
}
int ret = 0;
for (auto& x : data) {
ret ^= x;
}
int pos_bit1 = FindFirstBit1(ret);
if (pos_bit1 == -1) return;
int ret1 = 0, ret2 = 0;
for (auto& x : data) {
if ((x & (1 << pos_bit1)) != 0) {
ret1 ^= x;
} else {
ret2 ^= x;
}
}
*num1 = ret1;
*num2 = ret2;
}
private:
int FindFirstBit1(int num) {
int bits = sizeof(int) * 8;
for (int i = 0; i < bits; ++i) {
if ((num & (1 << i)) != 0) {
return i;
}
}
return -1;
}
};
41. 和为S的两个数字 VS 和为S的连续正数序列
问题0. Two Sum
扫描数组的同时用哈希表存下某一个值x
的位置,同时在表里查找是否有target-x
。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
if (nums.empty()) return vector<int>();
unordered_map<int, int> hash_map;
for (int i = 0; i < nums.size(); ++i) {
int another = target - nums[i];
if (hash_map.count(another) != 0) {
return vector<int>{hash_map[another], i};
}
hash_map[nums[i]] = i;
}
return vector<int>();
}
};
问题1. Two Sum II - Input array is sorted
多了数组有序的条件。一种方法是固定其中一个数,用二分法找另一个,时间复杂度O(n logn)。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
if (nums.empty()) {
return vector<int>();
}
vector<int> ret;
for (int i = 0; i < nums.size(); ++i) {
auto begin = nums.begin() + i + 1;
auto it = lower_bound(begin, nums.end(), target - nums[i]);
if (it != nums.end() && *it == target - nums[i]) {
ret.push_back(i + 1);
ret.push_back(it - nums.begin() + 1);
break;
}
}
return ret;
}
};
有一种更好的方法是维护两个指针,初始位置在数组的两端。当指针所指向的二数和大于目标值时,将右边的指针往左移一步;如果小于目标值就将左边的指针往右移一步,直到等于目标值或者两个指针相遇为止。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
if (nums.empty()) return vector<int>();
int start = 0, end = nums.size() - 1;
while (start + 1 < end) {
long long sum = (long long)nums[start] + nums[end];
if (sum == (long long)target) return vector<int>{start+1, end+1};
if (sum > (long long)target) end--;
else if (sum < (long long)target) start++;
}
if (nums[start]+nums[end] == target) {
return vector<int>{start+1, end+1};
}
return vector<int>();
}
};
牛客网上这题描述是可能有多对数字的和等于目标值的,这时输出两个数的乘积最小的。思路和上面一样,关键是两个指针的初始位置(想想为什么两个指针相距越远得到的两个数的乘积最小?)。
class Solution {
public:
vector<int> FindNumbersWithSum(vector<int> array, int sum) {
if (array.empty()) {
return vector<int>();
}
long long target = (long long) sum;
int start = 0, end = array.size() - 1;
while (start + 1 < end) {
long long s = (long long)array[start] + array[end];
if (s == target) {
return vector<int>{array[start], array[end]};
} else if (s > target) {
end--;
} else {
start++;
}
}
if (array[start] + array[end] == target) {
return vector<int>{array[start], array[end]};
}
return vector<int>();
}
};
问题2. 和为S的连续正数序列
借鉴双指针的思想:
- 用两个指针开始分别指向1和2
- 增量式计算两个指针之间的连续序列和,如果等于目标值,记录结果;如果大于目标值,将左边指针向前移动一步;如果小于目标值,将右边指针向前移动一步
- 循环终止条件是左边指针超过目标值的一半
class Solution {
public:
vector<vector<int>> FindContinuousSequence(int sum) {
vector<vector<int>> ret;
if (sum <= 0) {
return ret;
}
int start = 1, end = 2;
int mid = (sum + 1) / 2, target = sum;
int s = start + end;
while (start < mid) {
if (s == target) {
vector<int> seq;
for (int i = start; i <= end; ++i) {
seq.push_back(i);
}
ret.push_back(seq);
end++;
s += end;
} else if (s > target) {
s -= start;
start++;
} else {
end++;
s += end;
}
}
return ret;
}
};
42. 翻转单词顺序 VS 左旋转字符串
问题1. 翻转单词顺序
这题看起来简单,但是做起来却不容易。要翻转的是整个句子,但是要保持单词的内部顺序。很考察字符串处理的基本功。
思路:先把整个字符串翻转。然后按单词分割字符串并构造句子。C++
没有直接的字符串分割的操作,但是可以用stringstream
实现同样的效果。要特别注意处理字符串全部是空字符的情形。
string ReverseSentence(string& str) {
if (str.empty()) {
return str;
}
reverse(str.begin(), str.end());
stringstream ss(str);
string ret, word;
while (ss >> word) {
reverse(word.begin(), word.end());
ret.append(word);
ret.push_back(' ');
}
if (ret.empty()) {
return str;
}
ret.pop_back();
return ret;
}
问题2. 左旋转字符串
string LeftRotateString(string& str, int n) {
if (str.empty()) {
return str;
}
n %= str.size();
if (n == 0) {
return str;
}
string ret;
ret.append(str.substr(n));
ret.append(str.substr(0, n));
return ret;
}
43. n个筛子的点数
44. 扑克牌顺子
题目链接:牛客
这题也是看起来简单做起来要考虑不少细节。主要是对大小王的处理,即如何用大小王去代表数字以尽量拼出顺子。由于一共就5个整数,所以时间复杂度不是问题。思路是确定有几张大小王(几个0),先对剩下的数字构造顺子,然后看剩余的大小王数量能否继续拼出完整的顺子。比如{0,0,0,4,6}
,有3个大小王,先用其中1个拼出4,5,6
,然后发现还剩下两个大小王,因此可以拼出完整的顺子。又如{0,0,3,4,8}
,就拼不出顺子。
实现上用了std::bitset
(即bitmap),便于处理。
bool IsContinuous( vector<int>& numbers ) {
if (numbers.size() != 5) return false;
bitset<14> bitmap;
int num_kings = 0, minv = 100, maxv = -1;
for (auto x : numbers) {
if (x == 0) {
num_kings++;
} else {
bitmap[x] = 1;
minv = min(minv, x);
maxv = max(maxv, x);
}
}
if (num_kings >= 4) return true;
if (maxv - minv > 5) return false;
for (int idx = minv+1; idx < maxv; idx++) {
if (bitmap[idx] == 0) {
bitmap[idx] = 1;
if (num_kings == 0) return false;
num_kings--;
}
}
int len = maxv - minv + 1 + num_kings;
if (len == 5) return true;
return false;
}
45. 圆圈中最后剩下的数
题目链接:牛客网
用链表模拟,走到尾巴后再回到头部。
int LastRemaining_Solution(int n, int m) {
if (n <= 1 || m < 0) return -1;
list<int> kids;
for (int i = 0; i < n; ++i) {
kids.push_back(i);
}
auto it = kids.begin();
while (kids.size() > 1) {
for (int i = 1; i < m; ++i) {
it++;
if (it == kids.end()) {
it = kids.begin();
}
}
auto tmp = it;
it++;
if (it == kids.end()) it = kids.begin();
kids.erase(tmp);
}
return kids.front();
}
这题可以推导递推公式达到更好的性能,暂时就不去看了。
46. 求1+2+...+n
题目链接:牛客
要求是不能使用乘除法、for, while, if, else, switch, case
等关键字及条件判断语句(A? B:C)
。
思路1. 既然不能用循环,那就是递归了,但是递归的终止条件需要判断,如果能够不用if
实现判断的语义就好了。幸运的是可以利用逻辑表达式的“短路原则”来实现递归的终止:
int Sum_Solution(int n) {
int ans = n;
ans && (ans += Sum_Solution(n-1));
return ans;
}
另一种实现这种思路的做法是利用虚函数的特性,用一个函数来做递归,另一个函数来做终止处理。然后利用虚函数的动态绑定特性进行函数的跳转:
class A {
public:
virtual unsigned Sum(unsigned n) {
return 0;
}
};
A* arr[2];
class B : public A {
public:
unsigned Sum(unsigned n) override {
return n + arr[(n!=0)]->Sum(n-1);
}
};
class Solution {
public:
int Sum_Solution(int n) {
if (n < 0) return -1;
A a;
B b;
arr[0] = &a;
arr[1] = &b;
return arr[1]->Sum((unsigned)n);
}
};
思路2. 1+2+...+n
的公式是n*(n+1)/2
,显然这里有乘除,如何不用乘除法实现这个表达式的计算?
int Sum_Solution(int n) {
char tmp[n][n+1];
return (sizeof(tmp) >> 1);
}
思路3. 利用构造函数和静态类成员的性质。通过静态成员变量来保存状态,调用n次构造函数:
class Helper {
public:
static unsigned i;
static unsigned sum;
public:
Helper() {
++i;
sum += i;
}
static void Reset() {
i = 0;
sum = 0;
}
static int GetRes() {return sum;}
};
unsigned Helper::i = 0;
unsigned Helper::sum = 0;
class Solution {
public:
int Sum_Solution(int n) {
Helper::Reset();
auto a = new Helper[n];
delete[] a;
a = nullptr;
return Helper::GetRes();
}
};
47. 不用加减乘除做加法
题目链接:牛客
在比特位上模拟加法运算。比如3+5
,是:
0011
+0101
-----
1000
关键在于进位怎么处理。我们这里用状态机的思路:用异或运算得到不考虑进位的结果A,同时用与运算(算完左移1位得到进位的对应位置)得到进位值B,然后继续将A和B当成两个新的加数重复上面的处理,直到进位值为0.
int Add(int num1, int num2) {
int tmp;
while (num2 != 0) {
tmp = num1 ^ num2;
num2 = (num1 & num2) << 1;
num1 = tmp;
}
return num1;
}
48. 不能被继承的类
利用C++11
的final
关键字,这个问题就不是问题了,如下:
class A final {};
class B : public A {};
当类B
试图继承A
时,由于A
被final
修饰而导致编译错误:
error: cannot derive from 'final' base 'A' in derived type 'B'
class B : public A {};
如果非要问不用C++11
的话怎么办,可以借鉴单例模式的思路,这里就不展开了。
49. 把字符串转换成整数
题目链接:lintcode
要求:
- 如果没有合法的整数,返回0;
- 如果整数超出了32位整数的范围,如果是正整数,返回
INT_MAX
,否则返回INT_MIN
;
要点:
- 先去除前导0和空格
- 记录正负
- 记录数字部分
- 合法性检验(是否溢出)
class Solution {
public:
/**
* @param str: A string
* @return An integer
*/
int atoi(string str) {
if (str.empty()) return 0;
// remove pre-0
unsigned i = 0;
while (str[i] == '0' || str[i] == ' ') i++;
str = str.substr(i);
bool sign = true;
if (str[0] == '+') {
sign = true;
str = str.substr(1);
} else if (str[0] == '-') {
sign = false;
str = str.substr(1);
}
// find first dot
for (i = 0; i < str.size(); ++i) {
if (str[i] == '.') break;
}
str = str.substr(0, i);
for (i = 0; i < str.size(); ++i) {
if (!(str[i] >= '0' && str[i] <= '9')) break;
}
str = str.substr(0, i);
string str_max = "2147483647";
string str_min = "2147483648";
if (sign && ((str.size() == str_max.size() && str > str_max) || str.size() > str_max.size())) {
return INT_MAX;
}
if (!sign && ((str.size() == str_min.size() && str > str_min) || str.size() > str_min.size())) {
return INT_MIN;
}
return helper(str, sign);
}
private:
int helper(string& str, bool sign) {
int ret = 0;
for (auto c : str) {
ret = ret * 10 + (c-'0');
}
return sign ? ret : -ret;
}
};
50. 树中两个节点的最低公共祖先
题目链接: leetcode
分治:
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* A, TreeNode* B) {
if (root == NULL) return NULL;
if (A == root || B == root) return root;
TreeNode* left = lowestCommonAncestor(root->left, A, B);
TreeNode* right = lowestCommonAncestor(root->right, A, B);
if (left != NULL && right != NULL) return root;
if (left != NULL) return left;
if (right != NULL) return right;
return NULL;
}
};
递归的做法虽然简单易写,但是复杂度较高,有不少重复计算。第二种思路是:先分别找到根节点到A,B的路径,然后求两个链表的最后一个公共节点。找路径用深度优先搜索,求两个链表的最后一个公共节点顺序扫描一次即可。
class Solution {
public:
/**
* @param root: The root of the binary search tree.
* @param A and B: two nodes in a Binary.
* @return: Return the least common ancestor(LCA) of the two nodes.
*/
TreeNode *lowestCommonAncestor(TreeNode *root, TreeNode *A, TreeNode *B) {
if (root == NULL || A == NULL || B == NULL) {
return NULL;
}
vector<TreeNode*> path1;
vector<TreeNode*> path2;
GetNodePath(root, A, path1);
GetNodePath(root, B, path2);
return GetLastCommonNode(path1, path2);
}
private:
void GetNodePath(TreeNode* root, TreeNode* target, vector<TreeNode*>& path) {
if (root == target) {
path.push_back(root);
return;
}
stack<TreeNode*> stk;
stk.push(root);
while (!stk.empty()) {
TreeNode* cur = stk.top(); stk.pop();
while (!path.empty() && path.back()->right != cur && path.back()->left != cur) {
path.pop_back();
}
path.push_back(cur);
if (cur == target) return;
if (cur->right != NULL) stk.push(cur->right);
if (cur->left != NULL) stk.push(cur->left);
}
}
TreeNode* GetLastCommonNode(const vector<TreeNode*>& path1, const vector<TreeNode*>& path2) {
TreeNode* ret = NULL;
auto it1 = path1.begin();
auto it2 = path2.begin();
while (it1 != path1.end() && it2 != path2.end()) {
if (*it1 == *it2) {
ret = *it1;
it1++;
it2++;
} else break;
}
return ret;
}
};
52. 构建乘积数组
题目链接: 牛客
class Solution {
public:
vector<int> multiply(const vector<int>& A) {
vector<int> ret;
if (A.empty()) {
return ret;
}
int n = A.size();
ret.resize(n);
// 预处理区间乘积
int p[n][n];
for (int i = 0; i < n; ++i) {
p[i][i] = A[i];
}
// 双重循环效率低
/*for (int i = 0; i < n - 1; ++i) {
for (int j = i + 1; j < n; ++j) {
p[i][j] = p[i][j-1] * A[j];
}
}*/
// 其实可以O(n)构建,不需要填充整个上三角
// 因为只需要p[0][j]以及p[i][n-1]即可
for (int j = 1; j < n; ++j) {
p[0][j] = p[0][j-1] * A[j];
}
for (int i = n - 2; i >= 0; --i) {
p[i][n-1] = A[i] * p[i+1][n-1];
}
// 填充结果
ret[0] = p[1][n-1];
ret[n-1] = p[0][n-2];
for (int i = 1; i < n - 1; ++i) {
ret[i] = p[0][i-1] * p[i+1][n-1];
}
return ret;
}
};