剑指offer读书笔记4(面试题53-68)

0508

剑指offer读书笔记3

  • 面试题1-20
  • 面试题21-38
  • 面试题39-52
  • 第6章 面试中的各项能力
    • 6.3 知识迁移能力
      • 面试题53:数字在排序数组中出现的次数
      • 面试题53':0 ~ n-1中缺失的数字
      • 面试题53'':数组中数值和下标相等的元素
      • 面试题54:二叉搜索树的第k大结点
      • 面试题55:二叉树的深度(后序遍历)
      • 面试题55':平衡二叉树(先序遍历)
      • 面试题56:数组中只出现一次的两个数字
        • 异或运算
        • 补充:找`eor`最右边的一个`1`的两种方法 && 运算符优先级
      • 面试题56':数组中唯一只出现一次的数字
      • 位运算总结:
        • 面试题15:把nums[i]的二进制表示中最右边的1变为0
        • 面试题56:找到nums[i]的二进制表示中最右边的一个1
        • 面试题56':把nums[i]的二进制表示的每一位都保存下来
      • 面试题57:和为s的两个数字
      • 面试题57':和为s的连续正数序列
      • 面试题58:翻转单词顺序
      • 面试题58':左旋转字符串
      • 面试题59:滑动窗口的的最大值
      • 面试题59':队列的最大值--->面试题30:包含min函数的栈
    • 6.4 抽象建模能力
      • 面试题60:n个骰子的点数
      • 面试题61:扑克牌中的顺子
      • 面试题62:圆圈中最后剩下的数字
      • 面试题63:股票的最大利润
    • 6.5 发散思维能力
      • 面试题64:求1+2+...+n
      • 面试题65:不用加减乘除做加法
      • 面试题66:构建乘积数组
      • 面试题67:把字符串转换成整数
      • 面试题68:二叉搜索树的最近公共祖先
      • 面试题68':二叉树的最近公共祖先
      • 面试题68'':二叉树的最近公共祖先(每个结点有指向父节点的指针)

面试题1-20

见剑指offer读书笔记1

面试题21-38

见剑指offer读书笔记2

面试题39-52

见剑指offer读书笔记3

第6章 面试中的各项能力

6.3 知识迁移能力

面试题53:数字在排序数组中出现的次数

笨方法:遍历一遍,统计数字
因为数组有序,所以可以想到二分法。

二分法的原始思路是:
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;
    }
};

面试题53’:0 ~ n-1中缺失的数字

题目:
剑指offer读书笔记4(面试题53-68)_第1张图片

思路:数组有序,所以用二分法
数字和下标相等的是未缺失的值,第一个数字和下标不相等的就是那个缺失的数字

代码:
(注意:如果要用位运算,还要用加法,就要把位运算的部分括起来,否则会出错!!!
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]
    }
};

面试题53’':数组中数值和下标相等的元素

题目:
剑指offer读书笔记4(面试题53-68)_第2张图片

思路:数组有序,还是用二分法
如果一个数等于它的下标,那就返回这个数;
如果一个数大于它的下标,因为数字单调递增,所以它后面的数一定都大于它的下标,所以它右面的部分都不用看了;
同理,如果一个数小于它的下标,那它左边的数也一定小于它的下标,它左边的部分也不用看了。

代码:

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;
    }
};

面试题54:二叉搜索树的第k大结点

中序遍历二叉搜索树,结果是一个递增序列,所以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);
    }
};

面试题55:二叉树的深度(后序遍历)

方法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;
    }
};

面试题55’:平衡二叉树(先序遍历)

方法1:先序遍历 + 判断深度 (从顶至底)(推荐这个方法,第二种方法不好想到)
方法2:后序遍历 + 剪枝 (从底至顶)

方法1:先序遍历 + 判断深度 (从顶至底)

思路:(根->左->右)
1.root为空时,也是一棵平衡二叉树;
2.root非空,就计算它的左右子树的深度差的绝对值:

  • 如果绝对值大于1,就返回false;
  • 如果绝对值小于1,就说明这个节点满足平衡二叉树的规则;

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:后序遍历 + 剪枝 (从底至顶)
思路是对二叉树做后序遍历,从底至顶返回子树深度,若判定某子树不是平衡树则 “剪枝” ,直接向上返回。
剑指offer读书笔记4(面试题53-68)_第3张图片

代码:

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);
    }
};

面试题56:数组中只出现一次的两个数字

一个整型数组里除两个数字之外,其他数字都出现了两次,找出这两个只出现一次的数字。

先做一道题:
一个整型数组里只有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.不申请临时变量交换两个数;
剑指offer读书笔记4(面试题53-68)_第4张图片
2.数组中有一种数出现了奇数次,其他的数都出现了偶数次,怎么找到这个数?
常规的解法是把每种数的个数进行统计,即可找到出现次数为奇数的那种数,但需要的额外空间很多;
也可以用异或来解决,只申请一个变量,赋0,然后用这个变量分别和数组中的每个数进行异或^操作,最后得出的结果即为想要的答案。
证明:依然是利用了 交换律0和谁异或结果还是谁任何一个数字和它自己异或,结果都等于0 的性质,最后所有出现偶数次的数都抵消为0了,就剩下那个只出现了一次的数字,它和0异或还是它自己,所以用0和所有元素挨个异或之后的结果就是我们要找的那个数。

	int eor = 0;
    for(int& x : nums) eor ^= x;
    return eor;

3.数组中有两种数出现了奇数次,其他的数都出现了偶数次,怎么找到这两个数?(本题)

  • 假设这两个数是ab, 先申请一个变量eor,赋0,让它和数组中的每个数进行异或操作,最后得出的结果eor等于a^b,因为其他出现偶数次的数都被消掉了;
  • 因为ab肯定不一样,所以异或的结果不是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++而言:

  • 9: ==等于 、 !=不等于
  • 10: &按位与
  • 11: ^按位异或
  • 12: |按位或

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;

方法1的示例图:
剑指offer读书笔记4(面试题53-68)_第5张图片

方法2的示例图:
剑指offer读书笔记4(面试题53-68)_第6张图片

面试题56’:数组中唯一只出现一次的数字

在一个数组中除一个数字只出现一次之外,其他数字都出现了三次,找出这个只出现一次的数字。

笨方法:哈希表

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向左移一位
}

位运算总结:

面试题15:把nums[i]的二进制表示中最右边的1变为0

把一个整数减去1,再和原来的数做位与运算,得到的结果相当于把整数的二进制表示中最右边的1变为0

nums[i] = nums[i] & (nums[i] - 1)

//示例:
nums[i]开始为 11010010
经过上面的操作之后就成了 11010000,相当于消去最右边的那个1

很多二进制的问题都可以用这种思路解决:

  • 例如面试题15的这道题?
    问一个二进制数中有几个1,就做几次上面的操作(相当于一次消一个1),记录下操作的次数,就是二进制中1的个数;
  • 用一条语句判断一个整数是不是2的整数次方?
    如果是那么他的二进制表示中有且只有一个1,对这个数进行一次上面的操作,如果结果为0,就说明是2的整数次方,如果结果不是0,就不是2的整数次方;
  • 有两个整数m和n,计算把m变成n需要修改m的二进制表示中的几位?
    可以先求m和n的异或,因为是不同为1、相同为0,所以m和n的异或结果中1的个数即为答案,这样题目又变成了面试题15:求二进制中1的个数,有几个1,就做几次上面的操作(相当于一次消一个1),记录下操作的次数,就是二进制中1的个数,也即答案。

面试题56:找到nums[i]的二进制表示中最右边的一个1

两种方法:
nums[i]取反加1,再和它自己做位与运算
②用一个初始化为1rightOnenums[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

面试题56’:把nums[i]的二进制表示的每一位都保存下来

初始化一个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

错误的方法:
初始化一个tmp1,然后让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次方`了,所以说这种方法不对。

面试题57:和为s的两个数字

题目:输入一个递增排序的数组和一个数字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& nums 输入2:int target 输出:vector res(2) 应该输出:vector res
[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 res(2)

	//哈希表法:
	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;
    }
};

面试题57’:和为s的连续正数序列

输入一个正数s,打印出所有和为s的连续正数序列(至少含有两个数)。

还是双指针法
首先把small初始化为1big初始化为2sum = 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;
    }
};

面试题58:翻转单词顺序

题目:
剑指offer读书笔记4(面试题53-68)_第7张图片
首先想到的思路是:
遍历整个字符串,避开空格,把每个单词分离出来,存到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上的题解中还说了一种双指针法

面试题58’:左旋转字符串

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串做选择操作的功能。
比如:输入字符串"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;

画个图:
剑指offer读书笔记4(面试题53-68)_第8张图片

代码:

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;
    }
};

面试题59:滑动窗口的的最大值

题目:
剑指offer读书笔记4(面试题53-68)_第9张图片
推荐方法3,然后是方法2。

方法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;
    }
};

画个图:
剑指offer读书笔记4(面试题53-68)_第10张图片

方法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;
    }
};

面试题59’:队列的最大值—>面试题30:包含min函数的栈

推荐方法1 和 方法3。

方法1:
一个队列queue + 一个双端数组deque:

对于queue,就正常入队出队;
对于deque

  • 入队的时候从back开始判断把deque中比value小的都踢掉pop_back(),然后再尾插push_back()进deque中,相当于deque里面存的是从头到尾依次是最大值、第二大值,…
  • 出队的时候如果出的是deque中的最大值(即deque的front()元素),那就把deque的front()出队pop_front(),否则就不出队;
  • 求最大值的话就返回deque的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;
    }
};

6.4 抽象建模能力

面试题60:n个骰子的点数

动态规划:

通过题目我们知道一共投掷 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 * n6 * 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上把题目改了:返回每个点数有多少种掷法,也就是每个点数出现的次数
剑指offer读书笔记4(面试题53-68)_第11张图片
代码:

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;
    }
};

面试题61:扑克牌中的顺子

0表示大小王,可以看成任意数字,最多出现两个0;
1表示Ace, 11 12 13分别表示J Q K;
2 3 4 5 6 7 8 9 10就是数字本身;

思路:

  • 先排序;
  • 然后看0的个数;
  • 最后统计排序之后的数组中相邻数字之间的空缺总数
  • 如果出现两个相同的非0数字,直接返回false;
  • 如果空缺总数为0或者空缺总数小于等于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;

面试题62:圆圈中最后剩下的数字

方法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)
剑指offer读书笔记4(面试题53-68)_第12张图片

代码:

//递归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;
    }
};

面试题63:股票的最大利润

题目:
剑指offer读书笔记4(面试题53-68)_第13张图片
常规思路:
从头到尾遍历,每次求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];
    }
};

6.5 发散思维能力

面试题64:求1+2+…+n

不能用乘除法、for、while、if、else、switch、case等关键字即条件判断语句(A ? B : C

递归 + 加法;
天秀:创建一个bool类型的二维数组,行列分别是nn + 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
        

    }
};

面试题65:不用加减乘除做加法

题目:求两个整数之和,不能用+ - * /,注意:两个数均可能是 负数0,结果不会溢出 32 位整数。

思路:
不能用四则运算符,只能考虑位运算了:
1.各位进行不进位相加;
2.做进位;
3.把前面的两个结果加起来。

具体实现:
1.之前说的异或运算就是不进位相加(见上面 面试题56下面的异或运算);
2.只有当两个1相加时会向前产生一个进位,可以让两个数做位与运算,然后左移一位
3.前面两个步骤的结果相加,因为不允许用+号,所以相加的过程依然是重复上面两个步骤,直到不产生进位为止。

图解:
剑指offer读书笔记4(面试题53-68)_第14张图片

代码:

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的值?
答:
剑指offer读书笔记4(面试题53-68)_第15张图片

面试题66:构建乘积数组

思路:遍历两次,第一次计算下三角,第二次计算上三角。
注意边界,因为要乘以a[i - 1]a[i + 1],所以是从第二个元素倒数第二个元素进行遍历的,即下标为1i- 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;
    }
};

面试题67:把字符串转换成整数

思路:具体见链接

根据题意,有以下四种字符需要考虑:

  • 首部空格: 删除之即可;
  • 符号位: 三种情况,即 ‘’+‘’ , ‘‘−’’ , ''无符号" ;新建一个变量保存符号位,返回前判断正负即可。
  • 非数字字符: 遇到首个非数字的字符时,应立即返回。
  • 数字字符:
    字符转数字: “此数字的 ASCII 码” 与 “ 0 的 ASCII 码” 相减即可;
    数字拼接: 若从左向右遍历数字,设当前位字符为 c ,当前位数字为 x ,数字结果为 res,
    剑指offer读书笔记4(面试题53-68)_第16张图片

越界情况:
剑指offer读书笔记4(面试题53-68)_第17张图片
评论里有个更好的方法,直接用long型,然后最后输出时转换成int型,这样就不用上面那么麻烦了。
如果mark负号,就用-num去和INT_MIN比较,否则就用numINT_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
    }
};

面试题68:二叉搜索树的最近公共祖先

前言:
方法1是类似于二分查找,直接用while循环,比较好理解,利用了二叉搜索树的特点;
方法2用递归不太好理解,要用递归就直接用下一题面试题68’:二叉树的最近公共祖先中的递归方法,考虑了所有情况,包括p、q都不存在或者有一个不存在的情况,力扣上是默认p、q一定存在,相当于只考虑了一种情况,不够全面。

题目:
1.二叉搜索树; 2.一个节点也可以是自己的祖先(示例2)
剑指offer读书笔记4(面试题53-68)_第18张图片
这个题的前提是:

  • 所有节点的值都是唯一的;
  • p、q 为不同节点且均存在于给定的二叉搜索树中;

所以说一定会找到一个最近公共组先结点。

思路1:
因为是二叉搜索树,所以找最近公共祖先相当于是找一个介于p和q之间的并且离p和q最近的结点,例如上面的示例,如果p是0,q是7,结果也是6。
剑指offer读书笔记4(面试题53-68)_第19张图片

代码:(循环)

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;
    }
};

思路2:
剑指offer读书笔记4(面试题53-68)_第20张图片

代码:(递归)

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,和循环的做法不一样,循环中是三个操作三选一

面试题68’:二叉树的最近公共祖先

前言:
本题的递归写法考虑了所有可能出现的情况:p和q都能找到;p和q都不存在;p和q只有一个存在。力扣中只考虑了第一种情况,不够全面,下面的代码包含了所有情况,所以也适用于上面一题。
(规定:root中若只存在p与q其中一个节点时,返回存在的那个节点;若p与q都不在root中返回null;若两个节点p与q均存在就返回最近公共祖先结点

题目:
1.普通的二叉树; 2.一个节点也可以是自己的祖先(示例2)
剑指offer读书笔记4(面试题53-68)_第21张图片
祖先的定义:

  • 若节点 p 在节点 root 的左(右)子树中,或 p=root ,则称 root 是 p 的祖先。

剑指offer读书笔记4(面试题53-68)_第22张图片

最近公共祖先的定义:

  • 设节点 root 为节点 p,q 的某公共祖先,若其左子节点 root.left 和右子节点 root.right 都不是 p,q 的公共祖先,则称 root 是 “最近的公共祖先” 。
  • 根据以上定义,若 root 是 p,q 的 最近公共祖先 ,则只可能为以下情况之一:
    ①p 和 q 在 troot 的子树中,且分列 root 的 异侧(即分别在左、右子树中);
    ②p=root ,且 q 在 root 的左或右子树中;
    ③q=root ,且 p 在 root 的左或右子树中;

剑指offer读书笔记4(面试题53-68)_第23张图片
考虑通过递归对二叉树进行先序遍历,当遇到节点 p 或 q 时返回
从底至顶回溯,当节点 p,q 在节点 root 的异侧时,节点 root 即为最近公共祖先,则向上返回 root 。
(这个过程可以看剑指 Offer 68 - II. 二叉树的最近公共祖先(DFS ,清晰图解)最后面的ppt演示)

递归解析:
剑指offer读书笔记4(面试题53-68)_第24张图片

思考:
这个函数题目给出的的定义为以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)
    }
};

面试题68’':二叉树的最近公共祖先(每个结点有指向父节点的指针)

这道题就转变成了求两个链表的第一个公共结点,这是面试题52:两个链表的第一个公共结点。

ok,剑指offer第一遍刷完了,除了几个困难题,51题、43题、41题、37题。
(20220516 18:44)

你可能感兴趣的:(leetcode,算法,数据结构)