zoukankan      html  css  js  c++  java
  • 【数据结构与算法】《剑指offer》学习笔记----第六章 各项能力(含53-66题)

    第六章 各项能力

    总结:扎实的编程能力,沟通能力,学习能力,知识迁移能力,抽象建模能力,发散思维能力。

    面试题53 - I. 在排序数组中查找数字 I

    统计一个数字在排序数组中出现的次数。

    示例 1:
    输入: nums = [5,7,7,8,8,10], target = 8
    输出: 2
    
    
    示例 2:
    输入: nums = [5,7,7,8,8,10], target = 6
    输出: 0
     
    
    限制:
    0 <= 数组长度 <= 50000
    
    class Solution {
    public:
        int search(vector<int>& nums, int target) {
            int number = 0;
            int len = nums.size();
    
            if(len == 0){return 0;}
    
            int fistK = GetFirstK(nums,target,0,len-1);
            int lastK = GetLastK(nums,target,0,len-1);
    
            if(fistK>-1 && lastK>-1){
                number = lastK-fistK+1;
            }
            return number;
        }
    
        //得到第一个target位置
        int GetFirstK(vector<int>& nums,int target,int start,int end){
            if(start>end){
                return -1;
            }
            int mid = end + (start-end)/2;
            if(target == nums[mid]){//相等,判断此时mid对应的是不是第一个target
                if(mid > start && nums[mid-1]!=target || mid == start){//mid左边没到达数组最开头,并且左边的值不等于target时,此时mid对应的就是第一次出现的target;或者mid左边就是边界,那么这个target只能是数组中第一次出现的target
                    return mid;
                }else{
                    end = mid-1;//如果当前这个mid对应的元素不是第一个target,说明mid左侧还有target,去掉右侧的多余部分,让右边界变成重点左边的那个元素,新的元素范围为[start,mid-1]
                }
            }else if(target<nums[mid]){//target在mid左边
                end = mid-1;
            }else{//target>nums[mid],target在mid右边
                start = mid+1;
            }
            return GetFirstK(nums,target,start,end);
        }
    
        //得到最后一个target位置
        int GetLastK(vector<int>& nums,int target,int start,int end){
            if(start>end){
                return -1;
            }
            int mid = end + (start-end)/2;
            if(target == nums[mid]){//相等,判断此时mid对应的是不是最后一个target
                if(mid < end && nums[mid+1]!=target || mid == end){//mid右边没到达数组末尾,并且右边的值不等于target时,此时mid对应的就是最后一次出现的target;或者mid右边就是边界,那么这个target只能是数组中最后一次出现的target
                    return mid;
                }else{
                    start = mid+1;//如果当前这个mid对应的元素不是第一个target,说明mid右侧还有target,去掉左侧的多余部分,让左边界变成中点右边的那个元素,新的元素范围为[mid+1,end]
                }
            }else if(target<nums[mid]){//target在mid左边
                end = mid-1;
            }else{//target>nums[mid],target在mid右边
                start = mid+1;
            }
            return GetLastK(nums,target,start,end);
        }
    };
    

    面试题53 - II. 0~n-1中缺失的数字

    一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

    示例 1:
    输入: [0,1,3]
    输出: 2
    
    
    示例 2:
    输入: [0,1,2,3,4,5,6,7,9]
    输出: 8
     
    
    限制:
    1 <= 数组长度 <= 10000
    
    class Solution {
    public:
        int missingNumber(vector<int>& nums) {
            int len = nums.size();
            if(len<=0) return -1;
    
            int left = 0, right = len-1;
            while(left<=right){//这里是<=,而不仅仅是<,当left == right的时候,也要继续进行一波这个循环,为的就是当数组只有一个元素时,能够正常工作
                int mid = left + (right-left)/2;
                if(nums[mid] != mid){//当元素值 不等于 下标值时,说明缺失的那个元素就是这个mid或者在当前mid的前面
                    if(mid == 0 || nums[mid-1]==mid-1)//如果mid此时是0,说明此时数组只有一个元素,如果不是0就缺零,返回0;或者,还有一个情况需要返回mid本身那就是当前这个mid它就是缺失的值,即mid元素值不等于下标值,但是mid-1对应的值等于mid-1
                        return mid;
                    right = mid - 1;//如果当前mid不是要找的内容,把右边界缩小为mid-1
                }else{//缺失值左侧的数据都和下标相等,说明当前处在缺失值左侧,无意义,去mid的右侧找缺失值
                    left = mid + 1;
                }   
            }
            if(left == len){//当left>right,且left==len的时候,返回len,就是left跑到了数组的尾后位置
                return len;
            }
            return -1;
        }
    
    };
    

    面试题54. 二叉搜索树的第k大节点

    给定一棵二叉搜索树,请找出其中第k大的节点。

    示例 1:
    
    输入: root = [3,1,4,null,2], k = 1
       3
      / 
     1   4
      
       2
    输出: 4
    示例 2:
    
    输入: root = [5,3,6,2,4,null,null,1], k = 3
           5
          / 
         3   6
        / 
       2   4
      /
     1
    输出: 4
     
    
    限制:
    
    1 ≤ k ≤ 二叉搜索树元素个数
    
    /**
     * Definition for a binary tree node.
     * struct TreeNode {
     *     int val;
     *     TreeNode *left;
     *     TreeNode *right;
     *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
     * };
     */
    class Solution {
    public:
    
        vector<int> res;//全局数组
        void midThTravel(TreeNode * root){//中序遍历
            if(root==NULL)return;//递归终止条件,就是找到了叶子节点后,对叶子节点取左右孩子时自然是空
            midThTravel(root->left);//左
            res.push_back(root->val);//根,对应的值加入到全局数组中
            midThTravel(root->right);//右
        }
        int kthLargest(TreeNode* root, int k) {
            midThTravel(root);//调用这个递归函数,不会修改root的值,只是中序遍历它,按照从小到大的顺序把元素存入全局数组中
            return res[res.size()-k];//返回那个第K大的数
        }
    };
    

    面试题55 - I. 二叉树的深度

    输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

    例如:
    
    给定二叉树 [3,9,20,null,null,15,7]3
       / 
      9  20
        /  
       15   7
    返回它的最大深度 3 。
    
     
    
    提示:
    
    节点总数 <= 10000
    
    /**
     * Definition for a binary tree node.
     * struct TreeNode {
     *     int val;
     *     TreeNode *left;
     *     TreeNode *right;
     *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
     * };
     */
    class Solution {
    public:
        int maxDepth(TreeNode* root) {
            if(root==NULL){
                return 0;
            }
            int nLeft = maxDepth(root->left);
            int nRight = maxDepth(root->right);
            return (nLeft<nRight ? (nRight+1) : (nLeft+1));//树的深度 = 左子树或右子树中深度大的那个的深度+1,这个+1是加的那个根,根作为树的第一个深度
        }
    };
    

    面试题55 - II. 平衡二叉树

    输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

    示例 1:
    
    给定二叉树 [3,9,20,null,null,15,7]
    
        3
       / 
      9  20
        /  
       15   7
    返回 true 。
    
    示例 2:
    
    给定二叉树 [1,2,2,3,3,null,null,4,4]
    
           1
          / 
         2   2
        / 
       3   3
      / 
     4   4
    返回 false 。
    
     
    
    限制:
    
    1 <= 树的结点个数 <= 10000
    

    方法一:后续遍历,先遍历左右子树,比较左右子树的深度后,决定是否是AVL树

    /**
     * Definition for a binary tree node.
     * struct TreeNode {
     *     int val;
     *     TreeNode *left;
     *     TreeNode *right;
     *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
     * };
     */
    
    
    class Solution {
    public:
        bool isBalanced(TreeNode* root) {
            int depth = 0;
            return isBalancedCore(root,depth);
        }
    
    
        bool isBalancedCore(TreeNode* root,int& depth){
            if(root==NULL){
                depth = 0;
                return true;
            }
            int left=0,right=0;
            if(isBalancedCore(root->left,left)&& isBalancedCore(root->right,right)){
                int diff = left-right;
                if(diff<=1 && diff>= -1){// if(abs(diff)<=1)
                    depth = 1+(left>right ? left : right);
                    return true;
                }
            }
            return false;
        }
    };
    

    方法二:在55-1的基础上

    class Solution {
        int maxDepth(TreeNode* root) {
            if(root == NULL) return 0;
            return max(maxDepth(root->left), maxDepth(root->right)) + 1;
        }
    public:
        bool isBalanced(TreeNode* root) {
            if(root == NULL) return true;
            if(abs(maxDepth(root->left) - maxDepth(root->right)) > 1) return false;
            return isBalanced(root->left) && isBalanced(root->right);
        }
    };
    

    面试题56 - I. 数组中数字出现的次数

    一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

    示例 1:
    输入:nums = [4,1,4,6]
    输出:[1,6][6,1]
    
    
    示例 2:
    输入:nums = [1,2,10,4,1,4,3,3]
    输出:[2,10][10,2]
     
    
    限制:
    2 <= nums.length <= 10000
    

    知识点补充:

    异或的性质:任何一个数字异或它自己都等于0。也就是说,如果从头到尾依次异或数组中的每个数组,那么最终结果刚好是那个只出现一次的数字,因为那些成对出现两次的数组全部在异或中抵消了。

    高效写法:

    class Solution {
    public:
        vector<int> singleNumbers(vector<int>& nums) {
            int len = nums.size();
            if(len<2) return {};
            //获得整个数组异或后的数字n
            int n = 0;
            for(int i=0;i<nums.size();++i){
                n ^= nums[i];
            }
            unsigned int indexOf1 = findIndexOf1(n);//全部异或完整个数组后,得到结果中的最右端的1的位置indexOf1,根据nums数组中元素的二进制表达中第indexOf1位是1还是0将整个nums数组分成两部分,一部分是1,一部分是0。那一位之所以为1,是因为两个数不相等,异或出的结果必然有1,两人在那一位上不同,所以异或出来那一位就是1,这样,可以把两个单蹦出现的数,分到两个数组中去
            
            int res1 = 0, res2 = 0;
            for(int j = 0;j<nums.size();++j){
                if(isBit1(nums[j],indexOf1)){//数字nums[j]的indexOf1位是1
                    res1 ^= nums[j];
                }else{//数字nums[j]的indexOf1位是0
                    res2 ^= nums[j];
                }
            }
            return vector<int>{res1,res2};
        }
    
        unsigned int findIndexOf1(int n){//找到n的二进制表示中最右端的1所在的下标indexOf1
            unsigned int indexOf1 = 0;
            while(((n & 1) == 0) && (indexOf1 < 8*sizeof(int))){//n的末尾一位是1,或者已经遍历完了n的每一位,这两种情况下终止循环
                n >>= 1;
                ++indexOf1;
            }
            return indexOf1;
        }
    
        bool isBit1(int n, int indexOf1){//判断数字n的二进制表达式中的下标为indexOf1的元素是不是1,是1返回真
            bool res = false;
            if(indexOf1<32){
                n = n>>indexOf1;
                res = (n&1);
            }
            return res;
        }
    };
    

    简单写法,效率偏低:

    
    class Solution {
    public:
    vector<int> singleNumbers(vector<int>& nums) {
            int res = 0;
            for(auto n : nums){
                res ^= n;
            }
            //用个1不停地检查res的二进制表达中的右侧,从右往左检查,看哪一位是1
            int div=1;
            while((div & res) == 0){
                div <<= 1;  //不可写成div << 1
            }//此时div记录着res的最优端哪一位是1
    
            int res1=0,res2=0;
            for(auto n:nums){
                if(div & n){//该位为1,不代表整个数字就是1,因此不可写成:(div&n)==1
                    res1 ^= n;
                }else{//该位为0
                    res2 ^= n;
                }
            }
            return vector<int>{res1,res2};
        }
    };
    

    面试题56 - II. 数组中数字出现的次数 II

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

    示例 1:
    输入:nums = [3,4,3,3]
    输出:4
    
    
    示例 2:
    输入:nums = [9,1,7,9,7,9,7]
    输出:1
     
    
    限制:
    1 <= nums.length <= 10000
    1 <= nums[i] < 2^31
    
    class Solution {
    public:
        int singleNumber(vector<int>& nums) {
            int len = nums.size();
            if(len<=0) return -1;
    
            vector<int> bitSum(32,0);
            for(auto n : nums){
                unsigned int bitMask = 1;//对于数组中的每个数字,bitMask都把1从下标为0左移到下标为31
                for(int i=31; i>=0; --i){
                    unsigned int bit = n & bitMask;//bitMask 务必要定义成unsigned,此处bit定义成int也还好
                    if(bit!=0){//只统计1的个数,不统计0的个数
                        bitSum[i] += 1;
                    }
                    bitMask = bitMask << 1;//bitMask 务必要定义成unsigned
                }
            }
    
            int res = 0;
            for(int i=0; i<32; ++i){//这种赋值的方式很奇妙,可以多多学习
                res <<= 1;//先把所有已有内容左移一位,然后开始审判最后一位挪出来的空位,空位目前是零,但是,得根据审判依据来判断这一位到底应该是0还是1,审判依据就是下行注释。不管该是啥,直接加上就完事了
                res += bitSum[i] % 3;//如果某位上,1的个数能被3整除,说明那个唯一单蹦的数在这个位为是0
            }
            return res;
        }
    };
    

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

    输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

    示例 1:
    输入:nums = [2,7,11,15], target = 9
    输出:[2,7] 或者 [7,2]
    
    
    示例 2:
    输入:nums = [10,26,30,31,47,60], target = 40
    输出:[10,30] 或者 [30,10]
     
    
    限制:
    1 <= nums.length <= 10^5
    1 <= nums[i] <= 10^6
    
    class Solution {
    public:
        vector<int> twoSum(vector<int>& nums, int target) {
            int len = nums.size();
            if(len<1) return {};
            int left = 0,right = len-1;
    
            while(left <= right){
                if(left <= right  && nums[left] + nums[right] == target){//找到了
                    return vector<int>{nums[left], nums[right]};
                }else if(left <= right && nums[left] + nums[right] > target){//和过大了,把大的数调小
                    --right;
                }else if(left<=right &&  nums[left] + nums[right] < target){//和过小了,把小的数调大
                    ++left;
                }
            }
            return {};
        }
    };
    

    面试题57 - II. 和为s的连续正数序列

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

    序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

    示例 1:
    输入:target = 9
    输出:[[2,3,4],[4,5]]
    
    
    示例 2:
    输入:target = 15
    输出:[[1,2,3,4,5],[4,5,6],[7,8]]
     
    
    限制:
    1 <= target <= 10^5
    
    class Solution {
    public:
        vector<vector<int>> findContinuousSequence(int target) {
            if(target<3) return {};
            vector<vector<int>> res;
            vector<int> res_fb;
    
            int small = 1, big = 2;//正整数起始就是1,2
            int mid = (1+target)/2;//small结束的边界,如果target是3,那么mid就是2;如target是4,mid也是2
            int curSum = small + big;//当前总和
            while(small < mid){//small没追到mid时
                if(curSum == target){//找到了
                    res_fb.clear();//先将数组清空
                    for(int i = small;i<=big;++i){//将[small,big]添加到数组中
                        res_fb.push_back(i);
                    }
                    res.push_back(res_fb);//把数组添加到二维数组中
                }
                while(curSum > target && small < mid){//当前的和太大了,且small还没加到它的头mid
                    curSum -= small;//去掉这个最小值,维持当前数组中的和
                    ++small;//同时最小值加1
                    if(curSum == target){
                        res_fb.clear();
                        for(int i = small;i<=big;++i){
                            res_fb.push_back(i);
                        }
                        res.push_back(res_fb);
                    }
                }//到了此处curSum既不大于target,又不等与target,说明小于target,增大big看看
                ++big;
                curSum += big;
            }
            return res;
        }
    };
    

    面试题58 - I. 翻转单词顺序

    输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. “,则输出"student. a am I”。

    示例 1:
    输入: "the sky is blue"
    输出: "blue is sky the"
    
    
    示例 2:
    输入: "  hello world!  "
    输出: "world! hello"
    解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
    
    
    示例 3:
    输入: "a good   example"
    输出: "example good a"
    解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
     
    
    说明:
    无空格字符构成一个单词。
    输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
    如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
    
    class Solution {
    public:
        string reverseWords(string s) {
            //翻转每个单词
            int k = 0;
            for(int i = 0; i < s.size(); ++i){
                while (i < s.size() && s[i] == ' ') ++i;  //找到第一个非空格字符
                if (i == s.size()) break;//如果这个非空字符是尾后位置,中断当前循环
                int j = i;//从当前非空字符开始
                while (j < s.size() && s[j] != ' ') ++j;//从i开始找到第一个空格,遍历1个非空单词
                reverse(s.begin() + i, s.begin() + j); //反转1个单词,这里的reverse用的STL,[i,j)
    
                if(k) s[k++] = ' ';//当k不为0的时候,先给字符串s的k位置赋一个空格,再添加上翻转后的单词
                while (i < j) s[k++] = s[i++]; //反转后的1个单词赋给s[k]
            }
            s.erase(s.begin() + k, s.end());//删除k后面空格
    
            //翻转整个字符串
            reverse(s.begin(), s.end());
    
            //返回修改后的s
            return s;
        }
    };
    

    面试题58 - II. 左旋转字符串

    字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

    示例 1:
    输入: s = "abcdefg", k = 2
    输出: "cdefgab"
    
    
    示例 2:
    输入: s = "lrloseumgh", k = 6
    输出: "umghlrlose"
     
    
    限制:
    1 <= k < s.length <= 10000
    
    class Solution {
    public:
        string reverseLeftWords(string s, int n) {
            if(s.empty())return s;
            int len = s.size();
            if(len>0 && n>0 && n<len){//如果字符串长度len大于0,前n字符的n大于0,且要翻转的字符不能超过整个字符串的字符
                reverse(s.begin(),s.begin()+n);//翻转前半部分
                reverse(s.begin()+n,s.end());//翻转后半部分
                reverse(s.begin(),s.end());//翻转整个字符
            }
            return s;
        }
    };
    

    面试题59 - I. 滑动窗口的最大值

    给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

    示例:
    输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
    输出: [3,3,5,5,6,7] 
    解释: 
    
      滑动窗口的位置                最大值
    ---------------               -----
    [1  3  -1] -3  5  3  6  7       3
     1 [3  -1  -3] 5  3  6  7       3
     1  3 [-1  -3  5] 3  6  7       5
     1  3  -1 [-3  5  3] 6  7       5
     1  3  -1  -3 [5  3  6] 7       6
     1  3  -1  -3  5 [3  6  7]      7
     
    
    提示:
    你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。
    
    class Solution {
    public:
        vector<int> maxSlidingWindow(vector<int>& nums, int k) {
            vector<int> ans;
            deque<int> deq;
            int n = nums.size();
            for (int i = 0; i < n; ++i){
                //新元素入队时如果比队尾元素大的话就让队尾元素出队,后面用新元素替代队尾元素
                while(!deq.empty() && nums[i] > nums[deq.back()]){
                    deq.pop_back();
                }
                //检查队首的index是否在窗口内,不在的话需要出队
                if(!deq.empty() && deq.front() < i - k + 1) deq.pop_front();
                deq.push_back(i);//如果队列为空,直接加入队列;或者新元素小于队尾元素,也加入队列
                if(i >= k - 1) ans.push_back(nums[deq.front()]);//当前下标>=k-1时,说明已经经历了一轮k,说明第一个滑动窗口已经遍历一遍了,把当前元素压入数组。从k-1开始,窗口每滑动一个单位,处理一波,将队首元素加入数组
            }
            return ans;
        }
    };
    

    面试题59 - II. 队列的最大值

    请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

    若队列为空,pop_front 和 max_value 需要返回 -1

    示例 1:
    输入: 
    ["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
    [[],[1],[2],[],[],[]]
    输出: [null,null,null,2,1,2]
    
    
    示例 2:
    输入: 
    ["MaxQueue","pop_front","max_value"]
    [[],[],[]]
    输出: [null,-1,-1]
     
    
    限制:
    1 <= push_back,pop_front,max_value的总操作数 <= 10000
    1 <= value <= 10^5
    
    class MaxQueue {
        deque<int> dqMax;
        deque<int> dqData;
    
    public:
        MaxQueue() {
    
        }
        
        int max_value() {
            if(dqMax.empty()){
                return -1;
            }
            return dqMax.front();
        }
        
        void push_back(int value) {
            dqData.push_back(value);
            while(!dqMax.empty() && value>dqMax.back()){//当dqMax不为空,并且当前值大于最大值序列的尾部元素,弹出尾部元素,直到弹到dqMax为空或者遇到dqMax中一个比当前值value更大的元素
                dqMax.pop_back();
            }
            dqMax.push_back(value);
        }
        
        int pop_front() {
            if(dqData.empty()){
                return -1;
            }
            int res = dqData.front();
            dqData.pop_front();
            if(res == dqMax.front()){
                dqMax.pop_front();
            }
            return res;
        }
    };
    
    
    /**
     * Your MaxQueue object will be instantiated and called as such:
     * MaxQueue* obj = new MaxQueue();
     * int param_1 = obj->max_value();
     * obj->push_back(value);
     * int param_3 = obj->pop_front();
     */
    

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

    把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

    你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。

    示例 1:
    输入: 1
    输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
    
    
    示例 2:
    输入: 2
    输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
     
    
    限制:
    1 <= n <= 11
    

    分析:

    动态规划
    使用动态规划解决问题一般分为三步:

    1. 表示状态
    2. 找出状态转移方程
    3. 边界处理

    下面我们一步一步分析,相信你一定会有所收获!

    1. 表示状态

    分析问题的状态时,不要分析整体,只分析最后一个阶段即可!因为动态规划问题都是划分为多个阶段的,各个阶段的状态表示都是一样,而我们的最终答案在就是在最后一个阶段

    对于这道题,最后一个阶段是什么呢?

    通过题目我们知道一共投掷 n 枚骰子,那最后一个阶段很显然就是:当投掷完 n 枚骰子后,各个点数出现的次数。

    注意,这里的点数指的是前 n 枚骰子的点数和,而不是第 n 枚骰子的点数,下文同理。

    找出了最后一个阶段,那状态表示就简单了。

    首先用数组的第一维来表示阶段,也就是投掷完了几枚骰子
    然后用第二维来表示投掷完这些骰子后,可能出现的点数
    数组的值就表示,该阶段各个点数出现的次数

    所以状态表示就是这样的:dp[i][j] ,表示投掷完i枚骰子后,点数j的出现次数。

    2. 找出状态转移方程
    找状态转移方程也就是找各个阶段之间的转化关系,同样我们还是只需分析最后一个阶段,分析它的状态是如何得到的。

    最后一个阶段也就是投掷完 n 枚骰子后的这个阶段,我们用 dp[n][j] 来表示最后一个阶段点数 j 出现的次数。

    单单看第 n 枚骰子,它的点数可能为 1 , 2, 3, ... , 6,因此投掷完 n 枚骰子后点数 j 出现的次数,可以由投掷完 n-1枚骰子后,对应点数 j-1, j-2, j-3, ... , j-6 出现的次数之和转化过来。

    for (第n枚骰子的点数 i = 1; i <= 6; i ++) {
        dp[n][j] += dp[n-1][j - i]
    }
    

    对于状态转移方程的另一种解释:

    当我有c-1个骰子时,再增加一个骰子,这个骰子的点数只可能为1、2、3、4、5或6,则有:

    (c-1, k-1):第c个骰子投了点数1
    
    (c-1, k-2):第c个骰子投了点数2
    
    ……
    
    (c-1, k-6):第c个骰子投了点数6
    

    c-1个骰子的基础上,再增加一个骰子出现点数和为k的结果只有这6种情况,所以:

    dp(c, k) = dp(c-1, k-1) + dp(c-1, k-2) + dp(c-1, k-3) 
    		 + dp(c-1, k-4) + dp(c-1, k-5) + dp(c-1, k-6)
    		 (注意当k<6的处理越界问题)
    

    3. 边界处理

    这里的边界处理很简单,只要我们把可以直接知道的状态初始化就好了。

    我们可以直接知道的状态是啥,就是第一阶段的状态:投掷完 1 枚骰子后,它的可能点数分别为 1, 2, 3, ... , 6,并且每个点数出现的次数都是 1.

    for (int i = 1; i <= 6; i ++) {
        dp[1][i] = 1;
    }
    

    代码:

    class Solution {
    public:
        vector<double> twoSum(int n) {
            //int dp[15][70];
            //memset(dp, 0, sizeof(dp));
    
            vector<vector<int>> dp(15,vector<int>(70,0));
            for (int i = 1; i <= 6; i ++) {//边界条件:解决第一个骰子投掷的问题
                dp[1][i] = 1;//dp[i][j],表示投掷完`i`枚骰子后,点数`j`的出现次数。这行代码表示第1次投掷,累加和是i的可能次数是1。肯定的,只投一个,出现1-6的方法都只有1种。
            }
            for (int i = 2; i <= n; i ++) {//i表示的是共几个骰子一同投掷,[2,n],第一个骰子投掷已经被解决了
                for (int j = i; j <= 6*i; j ++) {//j表示的是点数和,[i,6*i],如果投掷2个,最多拿到的就是6*2=12种组合结果,因为求和相当于组合,而非排列,排列就多了,投一个有6种可能,同时投两个就有36种排列
                    for (int cur = 1; cur <= 6; cur ++) {//cur表示每次投掷可能出现的点数,[1,6]
                        if (j - cur <= 0) {//如果点数和j,减去当前投出的点数cur,竟然小于等于0,说明
                            break;
                        }
                        dp[i][j] += dp[i-1][j-cur];
                    }
                }
            }
            int all = pow(6, n);//all = 6的n次方
            vector<double> ret;
            for (int i = n; i <= 6 * n; i++) {//n是n个骰子同时投掷总共可能出现的和的个数最小值,就是最少也有n种结果,6*n是n个骰子同时投掷总共可能出现的和的个数最大值
                ret.push_back(dp[n][i] * 1.0 / all);//计算概率
            }
            return ret;
        }
    }; 
    

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

    从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

    示例 1:
    输入: [1,2,3,4,5]
    输出: True
     
    
    示例 2:
    输入: [0,0,1,2,5]
    输出: True
     
    
    限制:
    数组长度为 5 
    数组的数取值为 [0, 13] .
    
    class Solution {
    public:
        bool isStraight(vector<int>& nums) {
            //先给数组排序,从小到大
            sort(nums.begin(),nums.end());
    
            //统计数组中0的个数
            int countOf0 = 0;
            for(auto i:nums){
                if(i==0) ++countOf0;
            }
    
            //统计数组中非零数的间隔数目
            int small = countOf0, big = small+1, countOfGap = 0;//这里要让samll初始化为0的个数,因为刚才已经将整个数组进行递增排序,所有的0都集中到数组最前头,0和0相等并不应该作为对子,应该只看非零数中是否有对子,以及非零数中的间隔数目
            while(big<nums.size()){
                if(nums[small]==nums[big]) return false;//非零数中有对子,不顺子
                countOfGap += nums[big]-nums[small]-1;//理论上,如果是顺子,那么后面的数比相邻的前一个数大1,再减去1,正好就是0,相当于没有加任何数
                ++small;
                ++big;
            }
            return (countOf0>=countOfGap) ? true : false;
            
        }
    };
    

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

    0,1,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。

    例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

    示例 1:
    输入: n = 5, m = 3
    输出: 3
    
    
    示例 2:
    输入: n = 10, m = 17
    输出: 2
     
    
    限制:
    1 <= n <= 10^5
    1 <= m <= 10^6
    

    循环链表方法,超时:

    class Solution {
    public:
        int lastRemaining(int n, int m) {
            if(n<1 || m<1)return -1;
    
            //把0到n-1存入链表
            list<int> numbers;
            for(int i= 0;i<n;++i){
                numbers.push_back(i);
            }
            //
            auto current = numbers.begin();
            while(numbers.size()>1){
                
                for(int i=1;i<m;++i){//遍历m-1个数字:[1,m-1]
                    ++current;
                    if(current==numbers.end()){//实现循环链表,遍历过程中一旦检测到到达了末尾,即刻让他转链表头
                        current = numbers.begin();
                    }
                }//已经找到要删除的位置,就是当前current
    
                auto next = ++current;//记录当前current的下一个位置next
                if(next == numbers.end()){//如果next指向尾后位置了,那么转到链表头元素
                    next = numbers.begin();
                }
    
                --current;//刚才求next的时候前进了一位置,现在退回去
                numbers.erase(current);//删除该删除的数字
                current = next;//让当前current指向下一个位置
            }
            return *current;
    
        }
    };
    

    数学方法:

    class Solution {
    public:
        int lastRemaining(int n, int m) {
            if(n<1 || m<1)return -1;
            int last = 0;
            for(int i=2;i<=n;++i){
                last = (last + m) % i;
            }
            return last;
        }
    };
    

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

    假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

    示例 1:
    输入: [7,1,5,3,6,4]
    输出: 5
    解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
         注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
    
    
    示例 2:
    输入: [7,6,4,3,1]
    输出: 0
    解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
     
    
    限制:
    0 <= 数组长度 <= 10^5
    
    class Solution {
    public:
        int maxProfit(vector<int>& prices) {
            int len = prices.size();
            if(len<2) return 0;
    
            int min = prices[0];//在卖出价固定的时候,买入价月底,利润越大。在扫描到数组第i个数字的时候,只要能记住之前i-1个数字中的最小值,就能算出在当前价位卖出时可能得到的最高利润。之前i-1个数字中的最小值就保存在min中
            int maxDiff = prices[1]-min;//卖出价为数组中的第i个数字时,可能获得的最大利润保存在maxDiff中
    
            for(int i=2;i<len;++i){//从下标为2开始遍历到数组尾,范围为[2,len-1],共len-2个数字
                if(prices[i-1] < min){
                    min = prices[i-1];//min只负责保存前i-1个数字中的最小值,遍历过程中顺带着就更新了
                }
    
                int currentDiff = prices[i]-min;//当前最大利润currentDiff,是当前卖价 减去 之前买价中的最小值
                if(currentDiff>maxDiff){
                    maxDiff = currentDiff;//顺带更新可能的最大利润
                }
            }
            return maxDiff>0 ? maxDiff : 0;
        }
    };
    

    发散思维能力

    思维的多向性和变通性。
    在思考问题时,注重运用多思路、多方案、多途径来解决问题。
    对于同一个问题,可以从不同的方向、侧面、层次,采用探索、转换、迁移、组合、分解等方法,提出多种创新解法。

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

    求 1+2+…+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

    示例 1:
    输入: n = 3
    输出: 6
    
    
    示例 2:
    输入: n = 9
    输出: 45
     
    
    限制:
    1 <= n <= 10000
    

    利用&&短路效应,如果前为真,才执行后;如果前为假,不执行后:

    class Solution {
    public:    
        int res = 0;
        int sumNums(int n) {
            bool x = n>1 && sumNums(n-1);
            res += n;
            return res;
        }
    };
    

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

    写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

    示例:
    输入: a = 1, b = 1
    输出: 2
     
    
    提示:
    a, b 均可能是负数或 0
    结果不会溢出 32 位整数
    

    关键代码解释:

    carry = (unsigned int)(a & b) << 1;

    因为只有1+1才会产生进位,0+0、0+1、1+0都不产生进位,所以像极了与操作&,1+1产生的情况,就是当前位变成了0,前一位要+1。当前位置已经在上面的异或过程中变成了0,下面就是要产生这个前一位要加的1。先通过与操作,在当前位产生1,再把1左移一位,后面只需要一直循环将sum和carry相加直到没有进位为止。

    class Solution {
    public:
        int add(int a, int b) {
            int sum,carry;
            while(b!=0){//进位不为0,就一直加下去
                sum = a ^ b;//这一步是为了先加每一位的数,并不考虑进位;1+1=0,0+0=0,1+0=1,0+1=1其实表现就是异或
                carry = (unsigned int)(a & b) << 1;//这一步是为了计算进位,就是同时计算两个数每一位的进位情况。
                a = sum;
                b = carry;
            }
            return a;
        }
    };
    

    面试题66. 构建乘积数组

    给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B 中的元素 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

    示例:
    
    输入: [1,2,3,4,5]
    输出: [120,60,40,30,24]
    

    构建二维矩阵,分成下三角和上三角两部分,从上往下计算下三角的积存放在b[i]中,在此基础上,从下往上计算上三角的积更新b[i]。

    class Solution {
    public:
        vector<int> constructArr(vector<int>& a) {
            int len = a.size();
            if(len<2) return {};
            vector<int> b(len,-1);
            
            b[0] = 1;
            for(int i=1;i<len;++i){
                b[i] = b[i-1] * a[i-1];
            }
            double tmp = 1;
            for(int i=len-2;i>=0;--i){//len-1是倒数第一行,len-2是倒数第二行,从倒数第二行开始往上走,因为最后一行最后一个数字是1,就是对角线上的数,就算乘上这个数,乘积也是原数
                tmp = tmp * a[i+1];//bp[i]只需要和tmp乘,而tmp默默地把该行对角线右侧所有数都背负在了自己身上
                b[i] = b[i] * tmp;
            }
            return b;
        }
    };
    
  • 相关阅读:
    js进阶 12-7 pageY和screenY以及clientY的区别是什么
    printf交替使用
    【iOS发展-81】setNeedsDisplay刷新显卡,并CADisplayLink它用来模拟计时器效果
    android tv 全屏幕垂直画
    uva 10305
    (札记)Java应用架构设计-模块化模式与OSGi
    《学习opencv》笔记——矩阵和图像处理——cvAnd、cvAndS、cvAvg and cvAvgSdv
    一旦专利
    SharePoint 2010 升级到2013时间 为了确保用户可以连接,但无法改变升级数据
    [TroubleShooting] The remote copy of database xx has not been rolled forward to a point in time
  • 原文地址:https://www.cnblogs.com/dindin1995/p/13059066.html
Copyright © 2011-2022 走看看