0508
见剑指offer读书笔记1
见剑指offer读书笔记2
见剑指offer读书笔记3
笨方法:遍历一遍,统计数字
因为数组有序,所以可以想到二分法。
二分法的原始思路是:
当nums[mid] <= target
,就往右找;nums[mid] > target
,就往左找;
(或者当nums[mid] < target
,就往右找;nums[mid] >= target
,就往左找;)
这道题的关键就在于当nums[mid] == target
时,该怎么找?
找左边界:等于target时,继续往左找,即j = mid - 1
,最后的左边界left = i;
找右边界:等于target时,继续往右找,即i = mid + 1
,最后的右边界right = j;
代码1:二分法
(注意:在求mid时,mid = i + (j - i) / 2;
//这样写可以降低溢出的风险; mid = (i + j) / 2;
//这样写容易溢出)
//(在LeetCode上写的:)
class Solution {
public:
int search(vector<int>& nums, int target) {
//寻找右边界:
int i = 0, j = nums.size() - 1;
while(i <= j){
int mid = i + (j - i) / 2;//这样写可以降低溢出的风险 mid = (i + j) / 2;//这样写容易溢出
if(nums[mid] <= target)//等于target时,继续往右找
i = mid + 1;
else//只有大于的时候才往左找,这样j就能指向最右边的一个target了
j = mid - 1;
}
int right = j;//右边界
//寻找左边界:
i = 0, j = nums.size() - 1;
while(i <= j){
int mid = i + (j - i) / 2;//这样写可以降低溢出的风险 mid = (i + j) / 2;//这样写容易溢出
if(nums[mid] < target)//只有小于的时候才往右找,这样i就能指向最左边的一个target了
i = mid + 1;
else//等于target时,继续往左边找
j = mid - 1;
}
int left = i;//左边界
//返回个数:
return right - left + 1;
}
};
//或者这样写更直观一些:(在acwing上写的)
class Solution {
public:
int getNumberOfK(vector<int>& nums , int k) {
int i = 0;
int j = nums.size() - 1;
//找左边界:
while(i <= j){
int mid = i + (j - i) / 2;
if(nums[mid] < k)
i = mid + 1;
else if(nums[mid] > k)
j = mid - 1;
else if(nums[mid] == k)//找左边界时nums[mid] == k,继续往左找,所以是j = mid - 1;
j = mid - 1;
}
int left = i;
//找右边界:
i = 0; j = nums.size() - 1;
while(i <= j){
int mid = i + (j - i) / 2;
if(nums[mid] < k)
i = mid + 1;
else if(nums[mid] > k)
j = mid - 1;
else if(nums[mid] == k)//找右边界时nums[mid] == k,继续往右找,所以是i = mid + 1;
i = mid + 1;
}
int right = j;
return right - left + 1;
}
};
自己第三遍写的:(看着少一点)
class Solution {
public:
int search(vector<int>& nums, int target) {
//二分法:找左右边界
int i = 0, j = nums.size() - 1;
//找左边界:
while(i <= j){
int mid = i + (j - i) / 2;
if(nums[mid] > target) j = mid - 1;
else if(nums[mid] < target) i = mid + 1;
else j = mid - 1;
}
int left = i;
//找右边界:
i = 0, j = nums.size() - 1;
while(i <= j){
int mid = i + (j - i) / 2;
if(nums[mid] > target) j = mid - 1;
else if(nums[mid] < target) i = mid + 1;
else i = mid + 1;
}
int right = j;
return right - left + 1;
}
};
代码2:笨方法
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = nums.size();
if(n == 0) return 0;
int count = 0;
for(int& x : nums){
if(x == target)
++count;
}
return count;
}
};
思路:数组有序,所以用二分法
数字和下标相等的是未缺失的值,第一个数字和下标不相等的就是那个缺失的数字。
代码:
(注意:如果要用位运算,还要用加法,就要把位运算的部分括起来,否则会出错!!!
int mid = i + (j - i) / 2;
// int mid = i + ((j - i) >> 1);
//位运算更快一些
//int mid = i + (j - i) >> 1;
//记得把位运算的部分括起来,否则会出错!!!)
class Solution {
public:
int missingNumber(vector<int>& nums) {
int n = nums.size();
int i = 0, j = n - 1;
while(i <= j){
int mid = i + (j - i) / 2;//int mid = i + ((j - i) >> 1);//位运算更快一些
//int mid = i + (j - i) >> 1;//记得把位运算的部分括起来,否则会出错!!!
if(nums[mid] == mid)//数字和下标相等就往右走
i = mid + 1;
else//不相等就往左走,即j指向最后一个数组和下标相等的数
j = mid - 1;
}
return i;//返回的是i,不是nums[i]
}
};
思路:数组有序,还是用二分法
如果一个数等于它的下标,那就返回这个数;
如果一个数大于它的下标,因为数字单调递增,所以它后面的数一定都大于它的下标,所以它右面的部分都不用看了;
同理,如果一个数小于它的下标,那它左边的数也一定小于它的下标,它左边的部分也不用看了。
代码:
class Solution {
public:
int getNumberSameAsIndex(vector<int>& nums) {
int n = nums.size();
int i = 0;
int j = n - 1;
while(i <= j){
int mid = i + (j - i) / 2;
if(nums[mid] == mid)
return mid;
else if(nums[mid] > mid)
j = mid - 1;
else
i = mid + 1;
}
return -1;
}
};
中序遍历二叉搜索树,结果是一个递增序列,所以v[k - 1]
是第k
小的数,把v
反转一下,就是第k
大的数了;
或者直接逆中序遍历,然后直接输出v[k - 1]
,不需要反转链表的操作。
代码:
class Solution {
public:
int kthLargest(TreeNode* root, int k) {
//中序遍历:递增的序列
InOrder(root);
//for(int& num : v) cout << num << ", ";
//要反转一下,才能求出第k大的数,否则求出来的是第k小的数:
reverse(v.begin(), v.end());
return v[k - 1];
}
private:
vector<int> v;
void InOrder(TreeNode* root){
if(root == NULL) return;
InOrder(root->left);
v.push_back(root->val);
InOrder(root->right);
}
};
//或者直接逆中序遍历:
class Solution {
public:
int kthLargest(TreeNode* root, int k) {
//逆中序遍历:递减的序列
ReInOrder(root);
//无需翻转,直接输出v[k - 1]
return v[k - 1];
}
private:
vector<int> v;
void ReInOrder(TreeNode* root){
if(root == NULL) return;
ReInOrder(root->right);
v.push_back(root->val);
ReInOrder(root->left);
}
};
acwing上的题目是返回一个结点,并且是返回第k小的结点:
(直接用中序遍历,v中存储的是TreeNode*,不存val值了)
class Solution {
public:
TreeNode* kthNode(TreeNode* root, int k) {
//中序遍历:递增的序列
InOrder(root);
return v[k - 1];
}
private:
vector<TreeNode*> v;
void InOrder(TreeNode* root){
if(root == NULL) return;
InOrder(root->left);
v.push_back(root);
InOrder(root->right);
}
};
方法1:深度优先遍历(后序遍历,背下来,平衡二叉树可以直接调用)
方法2:广度优先遍历(层序遍历)
方法3:深度优先遍历(先序遍历)
方法1:
深度优先遍历(后序遍历,背下来,平衡二叉树可以直接调用)
(其实这种方法是一种后序遍历:先遍历root的左右子树求出深度,然后取左右子树深度的max值,再加上1就是root的深度)
class Solution {
public:
int maxDepth(TreeNode* root) {
//root为空,表示第0层:
if(root == NULL) return 0;
//root非空,就进入递归,找它的左右子树的深度:
int left = maxDepth(root->left);
int right = maxDepth(root->right);
//求出左右子树的深度的max值,并且加上1,这个1表示root所在的第1层
return 1 + max(left, right);//这样写也可以: return (left > right) ? (1 + left) : (1 + right);
//或者上面三行直接写成下面一行:
//return 1 + max(maxDepth(root->left), maxDepth(root->right));
}
};
方法2:广度优先遍历(层次遍历)
层次遍历,然后返回二维数组res的行数,即为二叉树的深度。
class Solution {
public:
int treeDepth(TreeNode* root) {
if(root == NULL) return 0;
//广度优先遍历:
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
int n = q.size();
vector<int> v;
for(int i = 0; i < n; ++i){
TreeNode* node = q.front();
v.push_back(node->val);
q.pop();
if(node->left) q.push(node->left);
if(node->right) q.push(node->right);
}
res.push_back(v);
}
return res.size();
}
private:
vector<vector<int>> res;
};
或者不用这么复杂,不用创建二维数组,直接在每次for循环之前记录一次。
class Solution {
public:
int treeDepth(TreeNode* root) {
if(root == NULL) return 0;
//广度优先遍历:
queue<TreeNode*> q;
q.push(root);
int res = 0;
while(!q.empty()){
int n = q.size();
++res;//每次for循环都表示一层
for(int i = 0; i < n; ++i){
TreeNode* node = q.front();
q.pop();
if(node->left) q.push(node->left);
if(node->right) q.push(node->right);
}
}
return res;
}
};
方法3:深度优先遍历(只能用前序遍历,不能用中序、后序)
记得每走完一条路更新一下res,并且记得把depth减一。
class Solution {
public:
int maxDepth(TreeNode* root) {
if(root == NULL) return 0;
dfs(root);
return res;
}
private:
int depth = 0;
int res = 0;
void dfs(TreeNode* root){
if(root == NULL){
if(depth > res)
res = depth;
return;
}
++depth;
dfs(root->left);
dfs(root->right);
--depth;
}
};
方法1:先序遍历 + 判断深度 (从顶至底)(推荐这个方法,第二种方法不好想到)
方法2:后序遍历 + 剪枝 (从底至顶)
方法1:先序遍历 + 判断深度 (从顶至底)
思路:(根->左->右)
1.root为空时,也是一棵平衡二叉树;
2.root非空,就计算它的左右子树的深度差的绝对值:
3.然后接着遍历它的左右子树,只有它的左右子树同时满足平衡的条件才算平衡二叉树,所以是与&&
的关系。
求左右子树的深度差绝对值:
int tmp = abs(treeDepth(root->left) - treeDepth(root->right)); if(tmp > 1) return false;
//用abs求绝对值
或者 int tmp = treeDepth(root->left) - treeDepth(root->right); if(tmp > 1 || tmp < -1) return false;
//直接求差值,然后大于1或者小于-1
代码:
(要写个函数用来计算以某个结点为根节点的子树的深度,就是上面的55题。)
class Solution {
public:
bool isBalanced(TreeNode* root) {
//root为空时,也是一棵平衡二叉树:
if(root == NULL) return true;
//root非空,就计算它的左右子树的深度差绝对值
int tmp = treeDepth(root->left) - treeDepth(root->right);
//如果绝对值大于1,就返回false
if(tmp > 1 || tmp < -1)
return false;
//上面三行可以合并成下面两行:
//int tmp = abs(treeDepth(root->left) - treeDepth(root->right));
//if(tmp > 1) return false;
//如果左右子树的深度差绝对值小于1,就说明这个节点满足平衡二叉树的规则,然后接着遍历它的左右子树,只有左右子树同时满足平衡才算平衡二叉树,所以是与&&的关系:
return isBalanced(root->left) && isBalanced(root->right);
}
private:
//求以root结点为根节点的子树的深度:
int treeDepth(TreeNode* root){
if(root == NULL) return 0;
//求出左右子树的深度:
int left = maxDepth(root->left);
int right = maxDepth(root->right);
//求出左右子树的深度的max值,并且加上1,这个1表示root所在的第1层
return 1 + max(left, right);
}
};
//第二遍:
class Solution {
public:
bool isBalanced(TreeNode* root) {
if(root == NULL) return true;
int left = maxDepth(root->left);
int right = maxDepth(root->right);
int tmp = abs(left - right);
if(tmp > 1) return false;
return isBalanced(root->left) && isBalanced(root->right);
}
private:
int maxDepth(TreeNode* root) {
if(root == NULL) return 0;
int left = maxDepth(root->left);
int right = maxDepth(root->right);
return 1 + max(left, right);
}
};
上面的代码会对某一个结点重复遍历多次,时间效率不高。
方法2:后序遍历 + 剪枝 (从底至顶)
思路是对二叉树做后序遍历,从底至顶返回子树深度,若判定某子树不是平衡树则 “剪枝” ,直接向上返回。
代码:
class Solution {
public:
bool isBalanced(TreeNode* root) {
//-1表示不平衡,就返回false
if(recur(root) != -1)
return true;
return false;
}
private:
int recur(TreeNode* root){
//递归停止的条件:
if(root == NULL) return 0;
//左子树:
int left = recur(root->left);
if(left == -1) return -1;//剪枝
//右子树:
int right = recur(root->right);
if(right == -1) return -1;//剪枝
//左右子树深度差的绝对值:
int tmp = abs(left - right);
//满足平衡二叉树的条件就返回1 + max(left, right),否则返回-1,表示不平衡
if(tmp > 1)
return -1;
return 1 + max(left, right);
}
};
一个整型数组里除两个数字之外,其他数字都出现了两次,找出这两个只出现一次的数字。
先做一道题:
一个整型数组里只有1个数字出现了一次(奇数次),其他数字都出现了两次(偶数次),找出这个只出现一次的数字。
笨方法:哈希表
class Solution {
public:
vector<int> singleNumbers(vector<int>& nums) {
vector<int> res;//这里不要写成vector res(2);
//哈希表法:
unordered_map<int, int> hashMap;
for(int& x : nums) ++hashMap[x];
for(auto it = hashMap.begin(); it != hashMap.end(); ++it){
if(it->second == 1) res.push_back(it->first);
}
return res;
}
};
更优解:
上面这两道题属于异或运算的题目,可以看看之前写的代码随想录里的异或运算部分。
异或运算的性质:
相同为0,不同为1;(辅助理解:不进位相加
)
任何一个数字和它自己异或,结果都等于0;例如:1001 ^ 1001
等于 0
0和任意数异或,结果还是这个数;例如:0 ^ 1010
等于 1010
异或满足交换律和结合律;
异或运算的应用:
1.不申请临时变量交换两个数;
2.数组中有一种数出现了奇数次,其他的数都出现了偶数次,怎么找到这个数?
常规的解法是把每种数的个数进行统计,即可找到出现次数为奇数的那种数,但需要的额外空间很多;
也可以用异或来解决,只申请一个变量,赋0
,然后用这个变量分别和数组中的每个数进行异或^
操作,最后得出的结果即为想要的答案。
证明:依然是利用了 交换律 和 0和谁异或结果还是谁 和 任何一个数字和它自己异或,结果都等于0 的性质,最后所有出现偶数次的数都抵消为0了,就剩下那个只出现了一次的数字,它和0异或还是它自己,所以用0和所有元素挨个异或之后的结果就是我们要找的那个数。
int eor = 0;
for(int& x : nums) eor ^= x;
return eor;
3.数组中有两种数出现了奇数次,其他的数都出现了偶数次,怎么找到这两个数?(本题)
a
和b
, 先申请一个变量eor
,赋0
,让它和数组中的每个数进行异或操作,最后得出的结果eor
等于a^b
,因为其他出现偶数次的数都被消掉了;a
和b
肯定不一样,所以异或的结果不是0
,即此时的eor
的二进制表示中至少有一位为1
,我们可以找到最右边的那个1
的位置,记为第rightOne
位;(eor
取反加一 再和 它自己eor
进行 与&
操作);rightOne
位是否为1
)为标准把原来的数组分成两个子数组,第一个子数组中每个数字的第rightOne
位为1
,第二个子数组中每个数字的第rightOne
位为0
;1
还是0
,那么出现偶数次的数字肯定被分在了同一个子数组中,因为两个相同的数字的每一位都是相同的,我们不可能把两个相同的数字分配到两个子数组中去,于是我们已经把原数组分成了两个子数组,每个子数组都包含一个出现奇数次的数字,而其他数字都出现了偶数次。rightOne
和每个元素进行相与操作,结果是0的单独处理,结果是1的单独处理,最终就能分别求出a和b。代码:
(注意:异或运算^
如果和 关系运算符==
一起用,异或运算^
要用括号括起来!!!)
class Solution {
public:
vector<int> singleNumbers(vector<int>& nums) {
vector<int> res(2);
int eor = 0;
for(int& x : nums) eor = eor ^ x;
//找eor最右边的一个1:
//int rightOne = eor & (~eor + 1);
int rightOne = 1;
while ((rightOne & eor) == 0)//记得加括号!!!
rightOne = rightOne << 1;
int a = 0, b = 0;
for(int& x : nums){
if((rightOne & x) == 0)//记得加括号!!!
a = a ^ x;
else//(rightOne & x) == 1
b = b ^ x;
}
res[0] = a;
res[1] = b;
return res;
}
};
eor
最右边的一个1
的两种方法 && 运算符优先级1.以下为运算符优先级,忘了的人赶紧看一下,((rightOne & eor) == 0)
,这里的括号不能少,因为==
的优先级大于&
。
对C++而言:
==
等于 、 !=
不等于&
按位与^
按位异或|
按位或2.找eor
最右边的一个1
的两种方法:
//方法1:eor取反加一 再和 它自己eor 进行 与& 操作
int rightOne = eor & (~eor + 1); //
//方法2:1一直往左移,直到遇到eor最右边的1
int rightOne = 1;
while ((rightOne & eor) == 0)//记得加括号!!!
rightOne = rightOne << 1;
在一个数组中除一个数字只出现一次之外,其他数字都出现了三次,找出这个只出现一次的数字。
笨方法:哈希表
class Solution {
public:
int singleNumber(vector<int>& nums) {
//哈希表法:
unordered_map<int, int> hashMap;
for(int& x : nums) ++hashMap[x];
for(int& x : nums){
if(hashMap[x] == 1)
return x;
}
return -1;
}
};
更高效的方法:
把数组中所有数字的二进制表示的每一位加起来,如果某一位的和可以被 3 整除,那么那个只出现一次的数字二进制表示中对应的那一位是 0
,否则是 1
。
class Solution {
public:
int singleNumber(vector<int>& nums) {
//位运算:
vector<int> vec(32);//1 <= nums[i] < 2^31 所以是32位的数组
//把数组中所有数字的二进制表示的每一位都加起来:
for(int i = 0; i < nums.size(); ++i){
//一个数的二进制表示的每一位:两种方法都可以
//方法1:位运算,所以效率更高
for(int j = 0; j < 32; ++j){
//每个nums[i]和数字1相与,然后nums[i]往右移一位:
vec[j] += (nums[i] & 1);//这里是+=
nums[i] = nums[i] >> 1;
}
//方法2:除法和取余运算,效率没有方法1高
int j = 0;
while(nums[i]){
vec[j++] += nums[i] % 2;//这里是+=
nums[i] /= 2;
}
}
//每一位的和 对3取余,v中就是要找的那个数的二进制表示:
for(int& x : vec) x = x % 3;
//把二进制表示转换成十进制:
int res = 0;
for(int i = 0; i < 32; ++i){
res = res + vec[i] * pow(2, i);
}
return res;
}
};
上面代码中求每个数的二进制表示的每一位时,是让每个nums[i]
和数字1
相与,然后nums[i]
往右移一位;
我用下面的方法来求,即tmp
初始为1
,每次向左移一位,想着这样也能求出nums[i]
的二进制数,但结果是不对的;
假如nums[i]
是32,用上面的方法求出来就是0 0 0 0 0 1 0 0 ...
;用下面的方法求出来就是0 0 0 0 0 32 0 0 ...
;
因为tmp
一直在翻倍,所以和tmp
相与之后的结果就不再是0
或者1
了,而是0
或者2的n次方
了,所以说下面这种方法不对。
//错误的求每个数的二进制表示的每一位:
int tmp = 1;
for(int j = 0; j < 32; ++j){
vec[j] += (nums[i] & tmp);//让nums[i]每次和tmp相与
if(tmp != 31)
tmp = tmp << 1;//tmp向左移一位
}
把一个整数减去1,再和原来的数做位与运算,得到的结果相当于把整数的二进制表示中最右边的1变为0;
nums[i] = nums[i] & (nums[i] - 1)
//示例:
nums[i]开始为 11010010
经过上面的操作之后就成了 11010000,相当于消去最右边的那个1
很多二进制的问题都可以用这种思路解决:
两种方法:
①nums[i]
取反加1,再和它自己做位与运算;
②用一个初始化为1
的 rightOne
和nums[i]
做位与运算,如果结果是0,就让rightOne
左移一位,再和nums[i]
做位与运算,直到结果非零;
上面两种方法得到的结果就是nums[i]
的二进制表示中只剩最右边的一个1,其他位全为0;
//方法1:
int rightOne = nums[i] & (~nums[i] + 1);
//方法2:
int rightOne = 1;
while((rightOne & nums[i]) == 0) rightOne = rightOne << 1;
//示例:
nums[i] 开始为 11010010
经过上面的操作之后就成了 00000010
初始化一个vector
,用来存放nums[i]
的每一位二进制,下标i
从小到大,对应的数也是nums[i]
的二进制表示的从低位到高位;
方法1:让nums[i]
每次和1
相与&
,然后nums[i]
右移一位;
方法2:让nums[i]
每次对2
取余%
,然后nums[i]
除以2;
比较:两个方法其实是一个思路,右移一位就相当于是除以2
;和1
相与的结果不是0
就是1
,对2
取余的结果不是0
就是1
;
错误的方法:
初始化一个tmp
为1
,然后让nums[i]
每次和tmp
相与,然后tmp
左移一位,这样求出来的并不是nums[i]
的二进制表示的每一位,看下面的示例。
vector<int> vec(32);
//方法1:
for(int j = 0; j < 32; ++j){
//每个nums[i]和数字1相与,然后nums[i]往右移一位:
vec[j] = (nums[i] & 1);
nums[i] = nums[i] >> 1;
}
//方法2:
int j = 0;
while(nums[i]){
vec[j++] = nums[i] % 2;
nums[i] /= 2;
}
//错误的求每个数的二进制表示的每一位:
int tmp = 1;
for(int j = 0; j < 32; ++j){
vec[j] = (nums[i] & tmp);//让nums[i]每次和tmp相与
if(tmp != 31)
tmp = tmp << 1;//tmp向左移一位
}
//示例:
假如`nums[i]`是32,
用错误的方法求出来就是 `0 0 0 0 0 1 0 0 ...`;
用方法1和方法2求出来就是`0 0 0 0 0 32 0 0 ...`;
原因:
因为`tmp`一直在翻倍,所以和`tmp`相与之后的结果就不再是`0`或者`1`了,
而是`0`或者`2的n次方`了,所以说这种方法不对。
题目:输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s,如果有多对数字的和等于s,则输出任意一对即可。
方法1:
想到一个哈希表法:
(效率很低,但如果数组是无序的,那么就只能用这个方法了,下面的双指针法就会失效,acwing上的设定就是数组无序)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//哈希表:
unordered_map<int, int> hashMap;
for(int& x : nums){
if(target - x > 0)
hashMap[x] = target - x;
else
break;
}
//这个写法有问题,下面会说到:
vector<int> res(2);
for(auto it = hashMap.begin(); it != hashMap.end(); ++it){
///查找是否存在等于it->second的关键字:
if(hashMap.find(it->second) != hashMap.end()){//这样写也可以hashMap.count(it->second) != 0
res[0] = it->first;
res[1] = it->second;
break;
}
}
return res;
}
};
注意:由于题目是数组有序,而且默认最少有一组符合条件的答案,所以上面的程序通过了案例测试,其实上面的写法有问题:
如果数组长度是1,那么肯定找不到一组符合条件的解,因为题目说必须是两个数之和;
如果数组长度大于1,但是确实没找到一组符合条件的解,比如输入:nums = [2,3, 4], target = 15;
上面两种情况应该返回一个空数组[],而因为我提前初始化res是一个包含两个元素的数组,所以输出的是[0,0]
,
测试案例:
输入1:vector |
输入2:int target |
输出:vector |
应该输出:vector |
---|---|---|---|
[2,3] | 9 | [0,0] | [] |
[2,3,4] | 15 | [0,0] | [] |
[5] | 15 | [0,0] | [] |
[5] | 5 | [0,0] | [] |
[2,5,6] | 5 | [0,0] | [] |
应该把程序改成下面的形式:
(包括下面的双指针法也要改一下,
但上面的面试题56可以这么写:因为题目确定是要找出那两个只出现了一次的数字,就说明一定存在这么两个数,所以就可以提前把res
初始化为vector
)
//哈希表法:
int n = nums.size();
unordered_map<int, int> hashMap;
for(int i = 0; i < n; ++i){
hashMap[nums[i]] = target - nums[i];
}
vector<int> res;//(2)
for(auto it = hashMap.begin(); it != hashMap.end(); ++it){
if(hashMap.count(it->second) != 0){//hashMap.find(it->second) != hashMap.end()
res[0] = it->first; res[1] = it->second;
//res.push_back(it->first); res.push_back(it->second);
break;
}
}
return res;
方法2:
更高效的算法:双指针法(前提是数组有序)
左右夹击,
如果两数之和大于target,就让右边的指针左移一位;
如果两数之和小于target,就让左边的指针右移一位;
如果两数之和等于target,就返回左右指针指向的数字。
//LeetCode上的写法,返回值是一个数组,即默认至少能找到一对答案:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//双指针法:
int left = 0;
int right = nums.size() - 1;
//vector res(2);修改一下
vector<int> res;
while(left < right){
if(nums[left] + nums[right] > target)
--right;
else if(nums[left] + nums[right] < target)
++left;
else{
//res[0] = nums[left]; res[1] = nums[right];修改一下
res[0] = it->first; res[1] = it->second;
break;
}
}
return res;
}
};
//如果是返回bool类型:这种就是有可能找不到,上面的就是默认肯定至少能找到一对答案
class Solution {
public:
bool twoSum(vector<int>& nums, int target) {
//双指针法:
int left = 0;
int right = nums.size() - 1;
//vector res(2);修改一下
vector<int> res;
bool flag = false;
while(left < right){
if(nums[left] + nums[right] > target)
--right;
else if(nums[left] + nums[right] < target)
++left;
else{
//res[0] = nums[left]; res[1] = nums[right];修改一下
res[0] = it->first; res[1] = it->second;
flag = true;
break;
}
}
return flag;
}
};
输入一个正数s,打印出所有和为s的连续正数序列(至少含有两个数)。
还是双指针法:
首先把small
初始化为1
,big
初始化为2
,sum = small + big
;
然后进入循环,循环条件是(small <= (target - 1) / 2
),由于序列最少要有两个数,所以small的最大值就是当small + small + 1 == target
时:
sum > target;
就从序列中去掉较小的值(即sum
减去small
),然后让small
自加一;sum < target;
就让big
自加一,然后sum
再加上big
;sum == target;
就用一个数组存储区间[small, big]
的所有数,存入二维数组res
中big
值和sum
值;//这个别忘了!!!,否则滑动窗口就停滞不前了;最后返回二维数组res
。
代码:
class Solution {
public:
vector<vector<int>> findContinuousSequence(int target) {
vector<vector<int>> res;
if(target < 3) return res;
//双指针:
int small = 1;
int big = 2;
int sum = small + big;
//序列最少要有两个数,所以small的最大值就是当small + small + 1 == target时:
while(small <= (target - 1) / 2 && small < big){
if(sum > target){
sum -= small;
++small;
}
else if(sum < target){
++big;
sum += big;
}
else{
vector<int> tmp;
for(int i = small; i <= big; ++i)
tmp.push_back(i);
res.push_back(tmp);
//更新右指针和sum:
++big;
sum += big;//这个别忘了
}
}
return res;
}
};
上面的写法更清晰一些,但容易忘了当sum == target
时存储完区间[small, big]
的所有数之后也要更新big
值和sum
值,所以写成下面的代码也可以:
class Solution {
public:
vector<vector<int>> findContinuousSequence(int target) {
vector<vector<int>> res;
if(target < 3) return res;
//双指针:
int small = 1;
int big = 2;
int sum = small + big;
//序列最少要有两个数,所以small的最大值就是当small + small + 1 == target时:
while(small <= (target - 1) / 2){
//sum比target大,就要减去small值,然后更新small值:
if(sum > target){
sum -= small;
++small;
}
//如果sum小于等于target,都要更新big值和sum值:
else{
//等于target就存储区间[small, big]的所有数:
if(sum == target){
vector<int> tmp;
for(int i = small; i <= big; ++i)
tmp.push_back(i);
res.push_back(tmp);
}
//更新右指针和sum:
++big;
sum += big;//这个别忘了
}
}
return res;
}
};
题目:
首先想到的思路是:
遍历整个字符串,避开空格,把每个单词分离出来,存到vector中,然后再把vector逆序输出,拼成新的字符串。
自己写的在acwing上能过,在LeetCode上过不了,应该是有什么测试示例导致程序编译出了问题:
class Solution {
public:
string reverseWords(string s) {
//如果是空串,就直接返回一个空串:
if(s.size() == 0) return "";
vector<string> v;
string str = "";
for(int i = 0; i < s.size(); ++i){
if(s[i] != ' '){
str += s[i];
}
else{
if(str.size() != 0) //str.compare("") != 0
v.push_back(str);
str = "";
}
}
//字符串结束后把最后的一个str也保存下来,依然要判断str是否是空字符串,因为如果s最后就是以空格为结尾,例如:" hello world! "
if(str.size() != 0) //str.compare("") != 0
v.push_back(str);
//cout << "v.size() = " << v.size() << endl;
//vector内容翻转,然后再从头到尾拼接:
reverse(v.begin(), v.end());
string res = "";
int i = 0;
for(; i < v.size() - 1; ++i)
res = res + v[i] + ' ';
//最后一个v[i]要单独拼接到res上,后面不能加空格:
res += v[i];
return res;
}
};
后来找到了那个让上面程序崩溃的示例:如果一个字符串非空,但内容全是空格;
就会导致后面的res += v[i];
出错,所以加了个判断:如果v的长度为0,就不进行翻转和拼接了,直接返回res。
修改后的代码如下:
class Solution {
public:
string reverseWords(string s) {
//如果是空串,就直接返回一个空串:
if(s.size() == 0) return "";
vector<string> v;
string str = "";
for(int i = 0; i < s.size(); ++i){
if(s[i] != ' '){
str += s[i];
}
else{
//s[i]是空格并且str不是空串才把str存进vec中:
if(str.size() != 0) //!str.empty() str非空,再保存下来
v.push_back(str);
str = "";//str置空
}
}
如果s最后一个的最后一个字符是空格,str就是空串;如果不是空格,str也要存到vec中:
if(str.size() != 0) //没有这行,示例" hello world! "过不了
v.push_back(str);//如果没有这行和上一行,示例"the sky is blue"过不了
string res = "";
//如果容器v是空的,就不进行翻转和拼接了,直接返回res
if(v.size() != 0){
//vector内容翻转,然后再从头到尾拼接:
reverse(v.begin(), v.end());
//把v中的内容拼接成一个新的字符串res:两种方法都可以
//方法1:先拼前面的并且加上空格,最后一个单独处理
int i = 0;
for(; i < v.size() - 1; ++i)
res = res + v[i] + ' ';
//最后一个v[i]要单独拼接到res上,后面不能加空格:
res += v[i];
//方法2:全部处理,并加空格,最后重置res的长度,或者pop_back()一个字符
for(int i = 0; i < v.size(); ++i)
res = res + v[i] + ' ';
//把最后面加的空格删掉:
res.resize(res.size() - 1);//res.pop_back();//
}
return res;
}
};
LeetCode上的题解中还说了一种双指针法。
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串做选择操作的功能。
比如:输入字符串"abcdefg"
和数字2
,该函数将返回"cdefgab"
。
思路:
当i + n < s.size()时,把s[i + n]给到res[i],相当于先把s的后半部分给到res;
当i + n >= s.size()时,把s[i + n - s.size()]给到res[i],相当于把s的前半部分给到res;
代码:
class Solution {
public:
string reverseLeftWords(string s, int n) {
string res;
res.resize(s.size());
for(int i = 0; i < s.size(); ++i){
if(i + n < s.size())
res[i] = s[i + n];
else
res[i] = s[i + n - s.size()];
}
return res;
}
};
方法1:
暴力法:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
//笨方法:
vector<int> res;//(numWindows)
int n = nums.size();
if(n == 0 || k > n || k <= 0) return res;//
int numWindows = n - k + 1;
for(int i = 0; i < numWindows; ++i){
int max = nums[i];
for(int j = i; j < i + k; ++j){//j <= i + k是错的!!!
if(nums[j] > max) max = nums[j];
}
res.push_back(max);//res[i] = max;//
}
return res;
}
};
方法2:
更好的解法:
用一个双端队列,存储滑动窗口内的值,滑动窗口每向前滑动一次,就更新一次max值,并把最大值存入res数组中;
每一次滑动都是新加进来一个数(新数),然后删掉最老的数(老数),先判断滑动窗口内的max值是否等于这个即将被删除的老数:
如果不等于,那就直接让max和新数比大小,更新max值并保存;
如果等于,那就遍历一次滑动窗口内的值,求出一个新的max值并保存;
最后返回res。
代码:
class Solution {
public:
vector<int> maxInWindows(vector<int>& nums, int k) {
vector<int> res;
int n = nums.size();
if(k > n || n == 0 || k < 0) return res;//特殊情况
deque<int> winD;
int max = nums[0];
//先把winD填满,并得出一个最大值:
for(int i = 0; i < k; ++i){
winD.push_back(nums[i]);
max = (max < nums[i] ? nums[i] : max);
}
res.push_back(max);//保存这个最大值
for(int i = k; i < n; ++i){
//max值等于即将要被删除的老数:
if(max == winD.front()){
int right = i;
int left = right - k + 1;
max = nums[right];
for(int j = left; j < right; ++j)
max = (max < nums[j] ? nums[j] : max);
}
else{//不等于
max = (max < nums[i] ? nums[i] : max);
}
res.push_back(max);//保存最大值
winD.push_back(nums[i]);//滑动窗口加入新数
winD.pop_front();//滑动窗口删除老数
}
return res;
}
};
方法3:(推荐用这个)
直接用三个指针试试:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
vector<int> res;
if(k > n || k <= 0 || n == 0) return res;
//三个指针:left、i、max
int left = 0;//滑动窗口的尾巴
int i = left;//滑动窗口的头
int max = nums[0];//最大值
for(; i < left + k; ++i){
max = (max < nums[i]) ? nums[i] : max;
}
res.push_back(max);
for(; i < n; ++i){
if(nums[left] == max){
int j = i - k + 1;//窗口的尾巴
max = nums[i];
for(; j < i; ++j)
max = (max > nums[j]) ? max : nums[j];
}
else{
max = (max > nums[i]) ? max : nums[i];
}
++left;//窗口的尾巴也向前移动
res.push_back(max);
}
return res;
}
};
推荐方法1 和 方法3。
方法1:
一个队列queue + 一个双端数组deque:
对于queue
,就正常入队出队;
对于deque
:
back
开始判断把deque中比value小的都踢掉pop_back()
,然后再尾插push_back()
进deque中,相当于deque里面存的是从头到尾依次是最大值、第二大值,…front()
元素),那就把deque的front()出队pop_front()
,否则就不出队;class MaxQueue {
queue<int> q;
deque<int> d;
public:
MaxQueue() {
}
int max_value() {
if (d.empty())
return -1;
return d.front();
}
void push_back(int value) {
while (!d.empty() && d.back() < value) {
d.pop_back();
}
d.push_back(value);
q.push(value);
}
int pop_front() {
if (q.empty())
return -1;
int res= q.front();
if (res == d.front()) {
d.pop_front();
}
q.pop();
return res;
}
};
方法2:
暴力法:(用一个数组来实现)
class MaxQueue {
int q[10000];
int begin = 0, end = 0;
public:
MaxQueue() {
}
int max_value() {
if(begin == end) return -1;
int res = -1;
for(int i = begin; i <= end; ++i)
res = (res > q[i] ? res : q[i]);
return res;
}
void push_back(int value) {
q[end++] = value;
}
int pop_front() {
if(begin == end) return -1;
return q[begin++];
}
};
方法3:
暴力法的优化:用一个数组 + 三个指针实现
class MaxQueue {
int q[10000];
int left = 0;
int right = 0;
int max = 0;
public:
MaxQueue() {
}
int max_value() {
if(left == right) return -1;
return max;
}
void push_back(int value) {
q[right++] = value;
//入队时更新最大值:
max = (max > value) ? max : value;
}
int pop_front() {
if(left == right) return -1;
int res = q[left];
if(max == res){
//max恰好是要出队的那个数,就要重新找max值
max = q[right];
for(int j = left + 1; j < right; ++j)
max = (max > q[j]) ? max : q[j];
}
else{
//max不是要出队的那个数,就不用管了
}
++left;//出队时,滑动窗口的尾巴要往前走一格
return res;
}
};
方法4:
两个小根堆+一个队列:
class MaxQueue {
queue<int> q;
priority_queue<int> maxQ, eraseQ;//最大值大根堆 & 删除队列大根堆
public:
MaxQueue() {
}
int max_value() {
if(q.empty()) return -1;
//如果有数据要删除:
if(!eraseQ.empty()){
//如果删除队列的最大值等于最值队列的最大值,表明最值队列的首元素要删除
while(maxQ.top() == eraseQ.top()){
maxQ.pop(); eraseQ.pop();
}
}
return maxQ.top();
}
void push_back(int value) {
q.push(value);
maxQ.push(value);//存储到最大值大根堆中
}
int pop_front() {
if(q.empty()) return -1;
int tmp = q.front();
q.pop();
eraseQ.push(tmp);//要删的数入队
return tmp;
}
};
动态规划:
通过题目我们知道一共投掷 n
枚骰子,那最后一个阶段很显然就是:当投掷完 n 枚骰子后,各个点数出现的次数。
注意,这里的点数指的是前 n 枚骰子的点数和,而不是第 n 枚骰子的点数,下文同理。
状态表示:
首先用数组的第一维来表示阶段,也就是投掷完了几枚骰子。
然后用第二维来表示投掷完这些骰子后,可能出现的点数。
数组的值就表示,该阶段各个点数出现的次数。
所以状态表示就是这样的:dp[i][j]
,表示投掷完 i
枚骰子后,点数 j
的出现次数。
找出状态转移方程
找状态转移方程也就是找各个阶段之间的转化关系,同样我们还是只需分析最后一个阶段,分析它的状态是如何得到的。
最后一个阶段也就是投掷完 n 枚骰子后的这个阶段,我们用 dp[n][j]
来表示最后一个阶段点数 j
出现的次数。
单单看第 n 枚骰子,它的点数可能为 1 , 2, 3, ... , 6
,因此投掷完 n
枚骰子后点数 j
出现的次数,可以由投掷完 n−1
枚骰子后,对应点数 j-1, j-2, j-3, ... , j-6
出现的次数之和转化过来。
第 n 枚骰子的点数 j 出现的次数等于投完第n-1枚骰子后点数 j-1, j-2, j-3, ... , j-6 出现的次数之和:
for (int k = 1; k <= 6; ++k) {
dp[n][j] += dp[n - 1][j - k];
}
n
表示阶段,j
表示投掷完 n
枚骰子后的点数和,k
表示第 n
枚骰子会出现的六个点数。
边界处理:
这里的边界处理很简单,只要我们把可以直接知道的状态初始化就好了。
我们可以直接知道的状态是啥,就是第一阶段的状态:投掷完 1
枚骰子后,它的可能点数分别为 1, 2, 3, ... , 6
,并且每个点数出现的次数都是 1
.
for (int i = 1; i <= 6; i ++) {
dp[1][i] = 1;
}
最后,n
个骰子一共会出现pow(6, n)
种情况,所以要挨个求点数为 1 * n
到 6 * n
的出现概率(出现次数/所有可能出现的情况数),存到vector中,并返回。
代码:
class Solution {
public:
vector<double> dicesProbability(int n) {
//动态规划: dp[i][j]表示投完i个骰子,点数j出现的次数
vector<vector<int>> dp(15, vector<int>(70));//最多11个骰子,最多66点 dp(12, vector(67))也可以
//初始化:投1枚骰子,点数1-6出现的次数都是1
for(int i = 1; i <= 6; ++i) dp[1][i] = 1;
for(int i = 2; i <= n; ++i){//投第i枚骰子
for(int j = i; j <= i * 6; ++j){//前i枚骰子可能出现的点数:i*1到i*6
//投掷完 i 枚骰子后点数 j 出现的次数,可以由投掷完 i−1 枚骰子后,对应点数 j-1, j-2, j-3, ... , j-6 出现的次数之和转化过来
for(int k = 1; k <= 6; ++k){//第i枚骰子会出现的点数:1-6
if(j - k <= 0) break;//点数最小是1
dp[i][j] += dp[i - 1][j - k];//这里是+=
}
}
}
vector<double> res;
int all = pow(6, n);//总共会出现all种情况
for(int i = n; i <= 6 * n; ++i){//n枚骰子的点数:n*1到n*6
res.push_back(dp[n][i] * 1.0 / all);//这里要*1.0,把dp[n][i]转换成double型,否则结果不对!!!
}
return res;
}
};
//第二遍:
class Solution {
public:
vector<double> dicesProbability(int n) {
//动态规划:
vector<vector<int>> dp(15, vector<int>(70));//
//初始化:
for(int i = 1; i <= 6; ++i) dp[1][i] = 1;
//状态转移:
for(int i = 2; i <= n; ++i){
for(int j = i * 1; j <= i * 6; ++j){
for(int k = 1; k <= 6; ++k){//第i个骰子的点数
if(j - k < 1) break;
dp[i][j] += dp[i - 1][j - k];
}
}
}
double all = pow(6, n);
vector<double> res;
for(int i = n * 1; i <= n * 6; ++i)
res.push_back(dp[n][i] * 1.0 / all);
return res;
}
};
acwing上把题目改了:返回每个点数有多少种掷法,也就是每个点数出现的次数。
代码:
class Solution {
public:
vector<int> numberOfDice(int n) {
//动态规划:
vector<vector<int>> dp(15, vector<int>(70));//最多11个骰子,最多66点
//初始化:投1枚骰子,点数1-6出现的次数都是1
for(int i = 1; i <= 6; ++i) dp[1][i] = 1;
for(int i = 2; i <= n; ++i){//投第i枚骰子
for(int j = i * 1; j <= i * 6; ++j){//前i枚骰子可能出现的点数:i*1到i*6
for(int k = 1; k <= 6; ++k){//第i枚骰子会出现的点数:1-6
if(j - k <= 0) break;//点数最小是1点
dp[i][j] += dp[i - 1][j - k];//这里是+=
}
}
}
//double all = pow(6, n);//总共会出现all种情况
//vector res;
vector<int> res;
for(int i = n * 1; i <= n * 6; ++i){//n枚骰子的点数:n*1到n*6
//res.push_back(dp[n][i] * 1 / all);
res.push_back(dp[n][i]);
}
return res;
}
};
0表示大小王,可以看成任意数字,最多出现两个0;
1表示Ace, 11 12 13分别表示J Q K;
2 3 4 5 6 7 8 9 10就是数字本身;
思路:
0
的个数;代码:
class Solution {
public:
bool isStraight(vector<int>& nums) {
sort(nums.begin(), nums.end());
int zeroNum = 0;//count(nums.begin(), nums.end(), 0);
int kongNum = 0;
for(int i = 0; i < nums.size() - 1; ++i){
if(nums[i] == 0)
++zeroNum;//统计0的个数
else{ //if(nums[i] > 0){
//前后两个数之间的空缺数
int cha = nums[i + 1] - nums[i] - 1;
if(cha < 0)//空缺数小于0表示两个数相等,一定构不成顺子
return false;
//else//空缺数大于等于0,累加起来
kongNum += cha;
}
}
//空缺数为0或者空缺数小于等于0的个数,就能构成顺子
if(kongNum == 0 || kongNum <= zeroNum)
return true;
//否则就构不成顺子:
return false;
}
};
统计vector中0的个数:
int zeroNum = count(nums.begin(), nums.end(), 0);
//注意:count不是成员函数
代码:
class Solution {
public:
bool isStraight(vector<int>& nums) {
sort(nums.begin(), nums.end());
int zeroNum = count(nums.begin(), nums.end(), 0);
int kongNum = 0;
for(int i = 0; i < nums.size() - 1; ++i){
if(nums[i] > 0){
int tmp = nums[i + 1] - nums[i] - 1;
if(tmp < 0)
return false;
kongNum += tmp;
}
}
//空缺数为0或者空缺数小于等于0的个数,就能构成顺子
if(kongNum == 0 || kongNum <= zeroNum)
return true;
//否则就构不成顺子:
return false;
}
};
acwing中还要加个条件:if(nums.size() < 5) return false;
方法1会超时,推荐方法2。
方法1:用一个循环链表list
来实现,超出时间限制
(注意:
1.list
不是真的循环,遍历到链表的尾部时要手动转到链表头:if(it == L.end()) it = L.begin();
2.it = L.erase(it);
//删除it位置的数据,返回下一个数据的位置)
class Solution {
public:
int lastRemaining(int n, int m) {
if(n == 1) return 0;
list<int> L;
for(int i = 0; i < n; ++i) L.push_back(i);
auto it = L.begin();
while(L.size() > 1){
//走m-1步,找到第m个数:
for(int i = 1; i < m; i++){//走m - 1 步
++it;
if(it == L.end()) it = L.begin();
}
//删除第m个数:
//auto tmp = ++it;//tmp指向it的下一个位置,这里不能写it + 1,因为it是指针
//--it;//it再后退一步
//L.erase(it);//删除it处的数字
//it = tmp;//it再指向tmp
it = L.erase(it);删除it位置的数据,返回下一个数据的位置
if(it == L.end()) it = L.begin();//这个别忘了
}
return *it;
}
};
用数组去实现一个循环链表,怎么实现?
(只能用链表,用数组的话也是上面的思路,不太好实现,我太菜了)
方法2:用一个递归或者循环实现:
思路:
(可以看acwing的视频讲解:剑指Offer-Week7 53:42)
代码:
//递归1:
class Solution {
public:
int lastRemaining(int n, int m) {
return f(n, m);
}
private:
int f(int n, int m){
if(n == 1) return 0;
return (f(n - 1, m) + m) % n;
}
};
//递归2:
class Solution {
public:
int lastRemaining(int n, int m) {
if(n == 1) return 0;
return (lastRemaining(n - 1, m) + m) % n;
}
};
//循环:
class Solution {
public:
int lastRemaining(int n, int m) {
int res = 0;
for(int i = 2; i <= n; ++i)
res = (res + m) % i;//这里是i,不是n
return res;
}
};
题目:
常规思路:
从头到尾遍历,每次求tmp = nums[i] - min;
更新max
值(max = (tmp > max) ? tmp : max;
);
然后更新min
值;
最后返回max
值。
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if(n < 2) return 0;
int max = 0;//最小是0,如果比0小就是交易没有完成,也返回0
int min = prices[0];
for(int i = 1;i < n; ++i){
int tmp = prices[i] - min;
max = (tmp > max) ? tmp : max;
min = (prices[i] < min) ? prices[i] : min;
}
return max;
}
};
动态规划:
(和上面的方法差不多,只不过用dp[i]
代表到元素nums[i]
为止最大的利润值)
class Solution {
public:
int maxProfit(vector<int>& prices) {
//动态规划:
int n = prices.size();
if(n < 2) return 0;
vector<int> dp(n);
dp[0] = 0;
int min = prices[0];
for(int i = 1; i < n; ++i){
int tmp = prices[i] - min;
if(tmp > dp[i - 1])
dp[i] = tmp;
else
dp[i] = dp[i - 1];
min = (min < prices[i]) ? min : prices[i];
}
return dp[n - 1];
}
};
不能用乘除法、for、while、if、else、switch、case
等关键字即条件判断语句(A ? B : C
)
递归 + 加法;
天秀:创建一个bool
类型的二维数组,行列分别是n
和n + 1
,求出二维数组的大小再右移一位,相当于除以2
,是真的秀;我是不是也可以直接return n * (n + 1) / 2;
不行,因为不让用乘除法,天秀的做法是用求二维数组大小的方式代替求n * (n + 1)
,然后用位运算代替 / 2
;用vector创建二维数组也可以,时间效率低。
逻辑运算符的短路性质(这个跟递归一样)。
代码:
class Solution {
public:
int getSum(int n) {
//递归:
if(n == 0) return 0;
//if(n == 1) return 1;
return getSum(n - 1) + n;
//逻辑运算符的短路性质:
n && (n = n + getSum(n - 1));
return n;
//天秀:
bool a[n][n + 1];
return sizeof(a) >> 1;// sizeof(a) / 2;这样写不行,因为不能用除法 //return n * (n + 1) / 2;也不行,不让用乘除法
//ans=1+2+3+...+n
// =(1+n)*n/2
// =sizeof(bool a[n][n+1])/2
// =sizeof(a)>>1
}
};
题目:求两个整数之和,不能用+ - * /
,注意:两个数均可能是 负数
或 0
,结果不会溢出 32 位整数。
思路:
不能用四则运算符,只能考虑位运算了:
1.各位进行不进位相加;
2.做进位;
3.把前面的两个结果加起来。
具体实现:
1.之前说的异或运算就是不进位相加(见上面 面试题56下面的异或运算);
2.只有当两个1相加时会向前产生一个进位,可以让两个数做位与运算,然后左移一位;
3.前面两个步骤的结果相加,因为不允许用+号,所以相加的过程依然是重复上面两个步骤,直到不产生进位为止。
代码:
class Solution {
public:
int add(int a, int b) {
int t1, t2;
do{
t1 = a ^ b;//第一步:不进位相加
t2 = (unsigned int)(a & b) << 1;//第二步:做进位(有可能有负数,所以与的结果强制转换为无符号数)
//第三步:上面两步的结果相加,因为不允许用+号,所以只能让t1和t2转换成新的a和b,重复上面两个步骤,直到不产生进位为止,不产生进位的标志是b为0
a = t1;//
b = t2;//
}while(b != 0);
return a;//最后返回a
}
};
注意:计算t2
时,(a & b)
的结果要转换成unsigned int
型,然后再左移一位,这是为了应对出现负数相加的情况。
由于这里涉及到负数相加,所以补充下面的内容:
负数怎么用二进制表示
为什么计算机内存数值存储方式是补码?
在计算机系统中,数值一律用补码来存储,主要原因是:
看个例子:(来自链接:十进制转换二进制)
问:int i = 3;int j = 6;计算i + ~j
的值?
答:
思路:遍历两次,第一次计算下三角,第二次计算上三角。
注意边界,因为要乘以a[i - 1]
和 a[i + 1]
,所以是从第二个元素和倒数第二个元素进行遍历的,即下标为1
和i- 2
开始遍历。
代码:
class Solution {
public:
vector<int> constructArr(vector<int>& a) {
int n = a.size();
if(n == 0) return {};
初始化数组全为1
vector<int> res(n, 1);
//计算下三角的值:
int tmp = 1;
for(int i = 1; i < n; ++i){
tmp *= a[i - 1];
res[i] *= tmp;
//res[i] = res[i - 1] * a[i - 1];//不用tmp也行
}
//计算上三角的值:
tmp = 1;
for(int i = n - 2; i >= 0; --i){
tmp *= a[i + 1];
res[i] *= tmp;
}
return res;
}
};
思路:具体见链接
根据题意,有以下四种字符需要考虑:
越界情况:
评论里有个更好的方法,直接用long
型,然后最后输出时转换成int
型,这样就不用上面那么麻烦了。
如果mark
是负号,就用-num
去和INT_MIN
比较,否则就用num
和INT_MAX
比较。
//数字:
long num = 0;
for(; index < n && str[index] >= '0' && str[index] <= '9'; ++index){
int tmp = str[index] - '0';
num = num * 10 + tmp;
if(mark == '-'){//如果前面有负号:
if(-num < INT_MIN)
return INT_MIN;
}
else{//只要不是负号
if(num > INT_MAX)
return INT_MAX;
}
}
代码:
class Solution {
public:
int strToInt(string str) {
int n = str.size();
//空串:
if(n == 0) return 0;
int index = 0;
//空格:
while(index < n && str[index] == ' ') ++index;
//正负号:
char mark;//
if(str[index] == '+' || str[index] == '-'){
sign = str[index];
++index;//记得index自加一
}
//数字:
long num = 0;
for(; index < n && str[index] >= '0' && str[index] <= '9'; ++index){
int tmp = str[index] - '0';
num = num * 10 + tmp;
if(mark == '-'){//如果前面有负号:
if(-num < INT_MIN)
return INT_MIN;
}
else{//只要不是负号
if(num > INT_MAX)
return INT_MAX;
}
}
//返回:
if(mark == '-') num = -num;
return (int)num;//最后返回int型
}
};
第二遍:
class Solution {
public:
int myAtoi(string s) {
int n = s.size();
if(n == 0) return 0;
int i = 0;
//空格
while(i < n && s[i] == ' ') ++i;
//符号位
char mark;
if(s[i] == '+' || s[i] == '-') mark = s[i++];
//数字
long num = 0;
while(i < n && s[i] >= '0' && s[i] <= '9'){//
num = num * 10 + (s[i] - '0');
++i;
if(mark == '-'){
if(-num < INT_MIN) return INT_MIN;
}
else{//符号位为+或者为空
if(num > INT_MAX) return INT_MAX;
}
}
//返回
if(mark == '-') num = -num;
return num;//(int)num
}
};
前言:
方法1是类似于二分查找,直接用while循环,比较好理解,利用了二叉搜索树的特点;
方法2用递归不太好理解,要用递归就直接用下一题面试题68’:二叉树的最近公共祖先中的递归方法,考虑了所有情况,包括p、q都不存在或者有一个不存在的情况,力扣上是默认p、q一定存在,相当于只考虑了一种情况,不够全面。
题目:
1.二叉搜索树; 2.一个节点也可以是自己的祖先(示例2)
这个题的前提是:
所以说一定会找到一个最近公共组先结点。
思路1:
因为是二叉搜索树,所以找最近公共祖先相当于是找一个介于p和q之间的并且离p和q最近的结点,例如上面的示例,如果p是0,q是7,结果也是6。
代码:(循环)
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
//循环:
while(root != NULL){
if(p->val > root->val && q->val > root->val)// p,q 都在 root 的右子树中
root = root->right;//遍历至右子节点
else if(p->val < root->val && q->val < root->val)//p,q 都在 root 的左子树中
root = root->left;//遍历至左子节点
else //p,q 在 root 的两侧,有可能p在左q在右,也可能p在右q在左
break;
}
return root;
}
};
注意:while循环中的三个操作实际上是三选一,类似二分查找。
优化:
因为二叉搜索树是有序的,若可保证 p.val < q.val
,则在循环中可减少判断条件。
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
//先让p小于q:
if(p->val > q->val){
TreeNode* tmp = p;
p = q;
q = tmp;
}
//循环:
while(true){//root != NULL
if(root->val < p->val)// p,q 都在 root 的右子树中
root = root->right;// 遍历至右子节点
else if(root->val > q->val)// p,q 都在 root 的左子树中
root = root->left;// 遍历至左子节点
else// if(root->val >= p->val && root->val <= q->val)//p,q 在 root 的两侧,并且p在左q在右
break;
}
return root;
}
};
代码:(递归)
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
//递归:
//终止条件:不需要,因为官方给了均存在与树中的条件
// 即老子现在肯定是这两个乖孙子的公共祖先
//递推操作:
//第一种情况:
if(p->val < root->val && q->val < root->val)//看看是不是都是左儿子的后代
return lowestCommonAncestor(root->left, p, q);//是的话就丢给左儿子去认后代(继续递归)
if(p->val > root->val && q->val > root->val)//不是左儿子的后代,看看是不是都是右儿子的后代
return lowestCommonAncestor(root->right, p, q);//是的话就丢给右儿子去认后代(继续递归)
//第三种情况:
//左儿子和右儿子都说我只认识一个,唉呀妈呀,那就是老子是你们最近的祖先,因为老子本来就是你们的公共的祖先
//现在都只认一个,那就是老子是最近的。
//其实第三种才是题目需要找到的解,所以返回,拜托我的祖先们也传一下(递归回溯返回结果),我才是他们最近的公共曾爷爷
return root;
}
};
注意:因为是递归,所以第二个if
前不能写else
,和循环的做法不一样,循环中是三个操作三选一。
前言:
本题的递归写法考虑了所有可能出现的情况:p和q都能找到;p和q都不存在;p和q只有一个存在。力扣中只考虑了第一种情况,不够全面,下面的代码包含了所有情况,所以也适用于上面一题。
(规定:root中若只存在p与q其中一个节点时,返回存在的那个节点;若p与q都不在root中返回null;若两个节点p与q均存在就返回最近公共祖先结点)
题目:
1.普通的二叉树; 2.一个节点也可以是自己的祖先(示例2)
祖先的定义:
最近公共祖先的定义:
考虑通过递归对二叉树进行先序遍历,当遇到节点 p 或 q 时返回。
从底至顶回溯,当节点 p,q 在节点 root 的异侧时,节点 root 即为最近公共祖先,则向上返回 root 。
(这个过程可以看剑指 Offer 68 - II. 二叉树的最近公共祖先(DFS ,清晰图解)最后面的ppt演示)
思考:
这个函数题目给出的的定义为以root为根节点的的二叉树中p与q的公共祖先节点
题目中给出的案例中root树中都会有p、q节点,即一定能找到p和q,但是递归过程中子树可能不存在某个节点或者两个节点都不存在的情况:规定
root中若只存在p与q其中一个节点时,返回存在的那个节点;
若p与q都不在root中返回null;
若两个节点p与q均存在就遵循题目的定义。
下面的代码就包含了所有的情况。
代码:
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
//递归停止条件:
if(root == NULL) return NULL;//越过叶结点,直接返回null
if(root == p || root == q) return root;//root等于p或q,直接返回root,它本身就是自己的祖先
//递推工作:求出root左右子树中p与q的最近公共祖先
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
//返回值:
if(left != NULL && right != NULL) return root;//p和q分列root的两侧
if(left == NULL && right == NULL) return NULL;//root的左右子树都不包含p和q,root本身也不是p或q,返回null
if(left != NULL && right == NULL) return left;//p和q都不在root的左子树中,(p或q在root的右子树)(p和q都在root的右子树)
if(left == NULL && right != NULL) return right;//p和q都不在root的右子树中,(p或q在root的左子树)(p和q都在root的左子树)
return NULL;//这个单纯的就是需要有个返回值,不然编译不过去
}
};
//简洁版:
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == nullptr || root == p || root == q) return root;
TreeNode *left = lowestCommonAncestor(root->left, p, q);
TreeNode *right = lowestCommonAncestor(root->right, p, q);
if(left == nullptr && right == nullptr) return nullptr; // 1.
if(left == nullptr) return right; // 3.
if(right == nullptr) return left; // 4.
return root; // 2. if(left != null and right != null)
}
};
这道题就转变成了求两个链表的第一个公共结点,这是面试题52:两个链表的第一个公共结点。
ok,剑指offer第一遍刷完了,除了几个困难题,51题、43题、41题、37题。
(20220516 18:44)