算法&数据结构学习(2) 剑指offer刷题笔记(2)

新的一天,新的开始,继续《剑指offer》!!!

面试题10- I. 斐波那契数列

在这里插入图片描述
思路1:在许多的算法与数据结构的书籍中,讲到递归的时候免不了肯定讲斐波那契数列,这是一个很典型的递归过程,写法很简单,但是很明显的缺点是递归中重复计算的部分很多,可以采用记忆化递归改进,如果没有优化,当n大于43时计算时间延迟明显能感受到。递归部分比较容易写出,直接省略。
思路2:当讲到递归特别是能使用记忆化递归的情况中,免不了出现动态规划的影子,动态规划可以减少很多重复的计算,在这个题目中,第n项之和第n-1与第n-2项有关系,因此在使用动态规划解题时只需要维护前两个变量即可,减少了递归中的重复计算以及额外的空间消耗。

class Solution {
public:
    int fib(int n) {
        //动态规划
        int dp_n,dp_0 = 0, dp_1 = 1;
        if(n<2) return n;
        for(int i=2; i<=n; ++i)
        {
            dp_n = (dp_0+dp_1)%1000000007;
            dp_0 = dp_1;//记录第n-2项信息
            dp_1 = dp_n;//记录第n-1项信息
        }
        return dp_n;
    }
};

面试题10- II. 青蛙跳台阶问题在这里插入图片描述

思路:本题思路与斐波那契数列实质上是一样的,只是初始情况不同。由于每次只能上一级或两级台阶,显而易见,到第n层台阶只能有两个途径:要么从第n-2跳两级上来,或者从第n-1跳一级上来。这样就得到和斐波那契数列一样的递推公式:f(n) = f(n-1)+f(n-2)。因此一样可以使用动态规划解决问题,只是这次初始状态不同,从1和1开始而不是从0和1开始。

class Solution {
public:
    int numWays(int n) {
        int dp_n, dp_0 =1, dp_1 = 1;
        if(n<2) return 1;
        for(int i=2; i<=n; ++i)
        {
            dp_n = (dp_0+dp_1)%1000000007;
            dp_0 = dp_1;
            dp_1 = dp_n;
        }
        return dp_n;
    }
};

面试题11. 旋转数组的最小数字

在这里插入图片描述
思路:题目给出的是一个排序数组以某个位置旋转以后的数组,要求找出其中最小的数字,当看到排序数组查找某个数首先想到的肯定是二分查找,但本题并不是单纯的排序数组,而是已经经过旋转后的,这个题目在leetcode主站的当中有两个类似的题目,第一个是旋转数组中不存在重复元素(题目和解答放在这题之后),第二个就是和本题一样的允许重复的数字。允许重复数字需要进行额外的操作,可以先做了没有重复数字的情况。本题有重复数字的情况在原来的基础之上需要进一步改进。
如果数组没有重复:
1、容易看出旋转后的数组,不是完全有序,而是分成了两部分,前半部有序和后半部有序;
2、采用二分查找时,我们需要关注一个target,用nums[mid]与之比较,在此题中,我们可以选择待查找区间的尾元素即nums[hi]作为target,很明显当选取了mid以后,一定会存在两种情况:A、nums[mid]<nums[hi],此时我们能肯定[mid,hi]这部分数组肯定有序,且最小值应该为nums[mid],因此我们令右端点hi收缩至mid;B、如果nums[mid]>nums[hi],此时,说明前半部分一定有序,最小值为nums[lo],但是因为旋转数组的缘故,nums[hi]在此时一定是小于nums[lo]的,不然不可能有nums[mid]>nums[hi]的情况,所以可以肯定最小值一定在[mid+1,hi]之间,此时我们应该收缩lo=mid+1;
3、二分查找最令人头疼的属于边界情况,思路很简单,边界往往很坑,因此由上述判断当nums[mid]>nums[hi]时我们能肯定nums[mid]一定不会是最小值,所以大胆的将lo=mid+1;而反过来时,我们不能确定nums[mid]在[lo,mid]之中的情形,只知道nums[mid]是后半部分的最小值,因此不能令hi=mid-1而只能领hi=mid,以确保如果nums[mid]正好是最小值的时候被跳过的情况。最后二分查找跳出循环时,返回前还要对nums[lo]与nums[hi]做一次判断,取二者最小值返回。

如果数组允许重复:
1、前两种情况还是和之前没有重复一样,即nums[mid]>nums[hi], lo = mid+1; nums[mid]<nums[hi], hi=mid;
2、但是允许重复元素,就存在第三种即nums[mid] == nums[hi],这时候该如何办,此时,存在两种情况:A、[mid,hi]之间的元素都相等;B、[lo,mid]之间的元素都相等,且都等于nums[hi];不论是遇到何种情形,只要我们将hi向左端缩进一个位置即可,此时如果在情况A,hi的缩进不影响最小值的取值,并且在每一步缩进的时候,都有可能将mid继续向前移动,由于[mid,hi]之间的元素是相等的,hi在缩进的时候值还能保持,直到越过mid位置,但在移动hi的同时,mid有可能也在移动,同时存在符合先前的两个条件之一,即可选择其他缩小区间的方式;如果是情况B,hi的缩进有可能就直接导致nums[hi]的改变,但时由于[lo,mid]之间的元素都一样,因此hi的改变也可能会改变原来的条件,而改变区间的取值;这两种情况都避免了大幅度移动区间端点而可能造成的取值问题。

代码如下,有重复元素:

class Solution {
public:
    int findMin(vector<int>& nums) {
        int lo = 0, hi = nums.size()-1;
        while(1<hi-lo)
        {
            int mid = lo+(hi-lo)/2;
            if(nums[mid]>nums[hi])
                lo = mid+1;
            else if(nums[mid]<nums[hi])
                hi = mid;
            else   
                hi-=1;
        }
        return nums[lo]<nums[hi]?nums[lo]:nums[hi];
    }
};

153. 寻找旋转排序数组中的最小值

在这里插入图片描述

class Solution {
public:
    int findMin(vector<int>& nums) {
        int lo=0, hi=nums.size()-1;
        while(1<hi-lo)
        {
            int mid = lo+(hi-lo)/2;
            if(nums[mid]>nums[hi])//如果中点值大于右端点,说明最小值在右半边
                lo = mid+1;
            else//否则,最小值在左半边,不排除恰好为中点值,故不能hi=mid-1;
                hi = mid;
        }
        return nums[lo]<nums[hi]?nums[lo]:nums[hi];
    }
};

面试题12. 矩阵中的路径

在这里插入图片描述
解题思路:从题目就能看到要查找的是路径,这类排列组合、子集、路径的题目很容易想到的就是回溯法,本题即使用经典回溯法,回溯法有一个经典的框架,比较容易理解。
这里推荐一个:labuladong老哥的链接,老哥总结得很清晰明了,有基本的框架如下:
在这里插入图片描述
代码如下(思路都在注释中):

class Solution {
public:
    bool exist(vector<vector<char>>& board, string word) {
        int m=board.size(),n=board[0].size();
        for(int i=0; i<m; ++i)
            for(int j=0; j<n; ++j)
                if(backtrack(board,word,i,j,0)) return true;
        return false;
    }
    bool backtrack(vector<vector<char>>& board, string& word, int i, int j, int k)
    {
        //判断是否匹配,越界或不匹配直接返回false
        if(i<0 || i>=board.size() || j<0 || j>=board[0].size() || board[i][j] != word[k]) return false;
        //如果k的值等于word的长度,即代表完全匹配,直接返回True
        if(k==word.size()-1) return true;
        //记录原来的字母,由于要进行递归查询且不能重复使用该字符
        char tmp = board[i][j];
        //所以将原字符改为不可能的标记*即代表以及选择过了
        board[i][j] = '*';
        //递归查询下一层,四个方向只要有个符合即可
        if(backtrack(board,word,i-1,j,k+1) || backtrack(board,word,i+1,j,k+1) || 
            backtrack(board,word,i,j-1,k+1) || backtrack(board,word,i,j+1,k+1))
                return true;
        //撤销选择
        board[i][j] = tmp;
        return false;
    }
};

面试题13. 机器人的运动范围

在这里插入图片描述
解法1:深度优先搜索(DFS)
以下的数学基础参考:机器人的运动范围
基础:我们需要在深度优先搜索中判断下标的数位和是否大于k以便判断能否遍历到该位置,最直接的想法肯定是对一组下标值[row,col]逐位求和来判断,但是这样每次都循环一遍进行求值,而且反复进行,浪费很多时间。那由于深度优先搜索的想法是往一个方向一直前进直至结束,那我们可以利用这个性质进行简便计算下标各位的和,我们发现,当向下即row+1等于10的倍数时,有一个规律如19->20,各位的和由1+9变为2+0,即10->2,此时sum_r的值应该为原来的值减8,如果不是10的倍数,则加1即可。因此在递归进行时对sum_r和sum_c进行判断取值,可以省去循环求下标和的过程。

class Solution {
public:
    int movingCount(int m, int n, int k) {
        //法1:DFS
        //将参数设置为全局变量使递归时能使用判断退出条件
        r = m;
        c = n; 
        t = k;
        //visit数组记录已访问过的元素
        vector<vector<bool>> visit(r,vector<bool>(c,false));
        return DFS(visit,0,0,0,0);
    }
private:
    int r,c,t;
    int DFS(vector<vector<bool>>& visit,int row, int col, int sum_r, int sum_c)
    {
        //递归退出条件
        if(row>=r || col>=c || sum_c+sum_r>t || visit[row][col]) return 0;
        //标记已访问元素
        visit[row][col] = true;
        //此处做两处递归,向下或向右
        return 1+DFS(visit,row+1,col,(row+1)%10==0?sum_r-8:sum_r+1,sum_c)
            +DFS(visit,row,col+1,sum_r,(col+1)%10==0?sum_c-8:sum_c+1);
    }
};

解法2:广度优先搜索(BFS)

class Solution {
public:
    int movingCount(int m, int n, int k) {
        //法2:BFS
        int res = 0;
        vector<vector<bool>> visit(m,vector<bool>(n,false));
        queue<vector<int>> q;
        q.push({0,0,0,0});
        while(!q.empty())
        {
            auto tmp = q.front();
            q.pop();
            int row = tmp[0],col = tmp[1], sum_r=tmp[2],sum_c=tmp[3];
            //如果不符合条件则跳过
            if(row>=m || col>=n || sum_c+sum_r>k || visit[row][col]) continue;
            ++res;
            visit[row][col] = true;
            q.push({row+1,col,(row+1)%10==0?sum_r-8:sum_r+1,sum_c});
            q.push({row,col+1,sum_r,(col+1)%10==0?sum_c-8:sum_c+1});
        }
        return res;
   }
};

面试题14- I. 剪绳子

在这里插入图片描述
解法1:贪心思想(数学规律)
此法比较难想到,需要了解一些数学规律,但也是效率最高的解法。数学规律分析参考:数学推导

class Solution {
public:
    int cuttingRope(int n) {
        //法1:数学规律(贪心思想)
        if(n<4) return n-1;
        int a = n/3, b = n%3;
        if(b==0) return pow(3,a);
        if(b==1) return pow(3,a-1)*4;
        return pow(3,a)*2;
    }
};

解法2:动态规划
思路:dp[i]表示长度为i的绳子剪成多段能获得的最大乘积,初始状态很明显为dp[1] = dp[2] =1;
状态转移方程:dp[i] = max(dp[i],max((i-j)j,jdp[i-j]))可以理解为:dp[i]即不需要裁剪保持,(i-j)*j表示,将长度为i的绳子,裁剪为两段j与(i-)的长度,或者是先裁剪一段j以后,再对余下的(i-j)进行相应的裁剪即dp[i-j]。

class Solution {
public:
    int cuttingRope(int n) {
        //动态规划
        vector<int> dp(n+1);
        dp[1] = dp[2] =1;
        for(int i=3; i<=n; ++i)
        {
            for(int j=0; j<i; ++j)
                dp[i] = max(dp[i],max((i-j)*j,j*dp[i-j]));
        }
        return dp[n];
    }
};

本题还有其他一些奇淫技巧可以优化动态规划的方法,将空间复杂度优化为O(1),时间复杂度优化为O(n),但是都不太容易想到。

面试题14- II. 剪绳子 II

在这里插入图片描述
思路:本题与上一题是一样的,只是n的取值更大了,也即是有可能乘积出来得到一个很大的数值,造成整形溢出,因此要进行取模1000000007的处理,思路与之前一样,只是在计算的时候需要循环计算而不能在使用Pow函数计算了,在计算过程中就对1000000007取模,防止溢出,因此选择一个long long 的变量ans存放中间变量结果,最后返回时再进行类型转换,不然中间值有可能整形溢出。

class Solution {
public:
    int cuttingRope(int n) {
        if(n<4) return n-1;
        int a=n/3,b=n%3;
        long long ans = 1;
        while(--a)
            ans=(ans*3)%1000000007;
        if(b==0)  ans=ans*3%1000000007;
        if(b==1)  ans=ans*4%1000000007;
        if(b==2)  ans=ans*6%1000000007;
        return (int)ans;
    }
};

今日刷题笔记顺利结束,明日继续努力!!!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值