剑指offer刷题详细分析:part2:6题——10题

  • 剑指offer所有题目详解,可访问我的github项目:KongJetLin-offer

  • 目录

  1. Number6:旋转数组的最小数字
  2. Number7:裴波那契数列
  3. Number8:跳台阶
  4. Number9:变态跳台阶
  5. Number10:矩形覆盖

题目6 旋转数组的最小数字

  题目描述:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

  可以有3种方法
1)直接暴力遍历,找到最小值。时间复杂度是O(n)

2)由于旋转后,前面是一个递增子序列,后面是一个递增子序列,找出不是递增的那个元素,这种情况下耗费的时间较少,因为不需要遍历整个数组。当然也有可能整个数组元素全部相等,这种情况下需要遍历整个数组。

3)利用二分查找。如果中间元素值大于最后一个元素值,说明最小值在右半区间,如果中间元素<最后一个元素区间,说明最小值在左半区间,如果相等说明有相同元素,需要将判断区间往前缩一下,继续判断,不断循环,当二分查找的的左右区间相等了,就说明找到最小值了。
  对于长度较长的数组来说,二分法明显比上面2种方法效率高。
  下面我们演示第二种方法与第三种方法,方法1没有技术含量就不演示。

方法2:

//方法2
    public int minNumberInRotateArray(int [] array) {
        if(array == null || array.length == 0)
            return 0;

        /*
        注意:
        1)由于有i+1,因此i最大只能到array.length-2,否则可能数组越界;
        2)由于原来的数组是非递减排序数组,要么数组全部元素相同,要么数组递增。
            当下面循环没有找到旋转数组 中 array[i] > array[i+1] 的情况,说明整个数组所有元素相同,最后return0即可;
            如果找到,说明array[i+1]就是最小数字
         */
        for (int i = 0; i < array.length-1 ; i++)
        {
//            if(array[i] <= array[i+1])
//            {
//                continue;
//            }
//            else
//            {
//                return array[i+1];
//            }
            //其实可以写为
            if(array[i+1]<array[i])
                return array[i+1];
        }
        return array[0];
        //运行时间:248ms,占用内存:28400k
    }

方法3
  二分查找需要注意几点:

1、循环内不判断哪一个值是数组最小值,循环到begin=end就会自动结束循环,此时array[begin]=array[end]就是最小值;
2、循环条件不可是 begin<=end,而必须是 begin<end,加上“=”可能使得程序陷入死循环;
3、array[mid]>array[end] 则 begin = mid+1 ;其他情况 end = mid。这样便不会错过最小值的点,循环到begin=end就会找到最小值点。
//方法3:二分查找
    public int minNumberInRotateArray1(int [] array)
    {
        if (array == null || array.length == 0)
            return 0;

        int begin = 0;
        int end = array.length-1;

        /*
        说明:我们不在循环内判断哪一个数是最小值,而是一直执行循环,直到 begin=end(不管数组元素个数是奇数还是偶数,循环到最后都有begin=end,不会直接就begin>end),
        此时begin位置(或者说end位置)的值就是最小值。
        当还没有查找到 begin=end的时候,我们不对哪一个值是最小值进行判断(其实此时有一些情况可以判断)。
        如果在循环内讨论某一个值是最小值,会有很多情况,讨论起来很麻烦。

        1)当 array[mid]>array[end],此时最小值一定在右边区间,begin=mid+1;

        2)当 array[mid]<array[end],此时有可能array[mid]是最小值,也有可能最小值在左边,为了避免错过array[mid]是最小值的情况,
        我们我们直接将end=mid,而不是end=mid-1,这样便不会错过array[mid]是最小值的情况(因为mid被考虑进来)。

        3)当 array[mid]=array[end],此时有可能array[mid]是最小值,也有可能最小值在左边。同样,为了避免错过mid位置的值,
        我们设置 end=mid;

        按上面这三种情况进行二分查找区间的缩小,不会错过最小值的点,缩小到begin=end的时候,他们所代表的的位置就是最小值所在位置!

        注意这种思想!!!不在循环内讨论,而是一直遍历,知道begin=end,在循环内讨论的话情况太多且复杂!!(掌握这种技巧)
        另外,循环不能加=,既不能是 begin<=end,否则有可能 begin=end=mid,使得循环陷入死循环。
         */
        int mid = 0;
        while (begin<end)
        {
            mid = (begin+end)/2;
            if(array[mid]>array[end])
                begin = mid+1;
            else
                end = mid;
        }
        return begin;
    }

题目7 裴波那契数列

  题目描述:大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39

  分析:裴波那契数列,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N),所以要得到第n个的斐波那契数列就要从前面去推出来,保留前面两个的值。

方法1
  使用循环实现
  时间复杂度: O(n) 单循环到 n;空间复杂度:O(1)

/**
     * 注意,这个数列从第0项开始,因此数列实际上为:0,1,1,2,3,5,8,13....
     */
    public static int Fibonacci(int n) {
        //当取第0,1项的时候,直接返回n(注意第0项不可忽略)
        if(n<=1)
            return n;

        int res = 0;//存储第n个数的值,初始化为0
        int n1 = 1;//F(n-1),初始化设置为 F(1)的值
        int n2 = 0;//F(n-2),初始化设置为 F(0)的值

        //由于0,1项已经确定,从第二项开始计算
        for (int i = 2; i <= n ; i++)
        {
            res = n1+n2;
            //更新,获取下一个循环的n1与n2的值(既计算下一个n的F(n-1)与F(n-2))
            n2 = n1;
            n1 = res;
        }
        return res;
        //运行时间:13ms,占用内存:9332k
    }

  此处也可以使用动态规划的方法,参考第八题的解法,解法类似,这里不赘述。

方法2
  当然,上面的公式也可以使用递推法实现,但是这样消耗的时间较长。
  时间复杂度: O(2ⁿ) - 递归树的所有节点数;空间复杂度:O(n) - 递归树可达深度。

public static int Fibonacci1(int n)
    {
        //1、解决规模最小的问题:n =0,1的情况,其他情况都可以由这些情况组合。
        if(n<=1)
            return n;

        //2/3、解决规模较小问题:求解F(n-1)与F(n-2);将较小问题整合成为较大问题的解:F(n)=F(n-1)+F(n-2)
        return Fibonacci1(n-1)+Fibonacci1(n-2);
        //运行时间:1089ms,占用内存:9408k
    }

题目8 跳台阶

  题目描述:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

  分析:假设现在n个台阶,我们可以从第n-1跳一步到 n,也可以从第 n-2 跳两步到 n 。(注意!从n-2跳一步到n-1,又从n-1跳一步到 n,这种情况其实包含在从第n-1跳一步到 n的所有情况**F(n-1)**里面!!,因此不需要考虑这种情况)

  假设有 F(n-1) 种方案跳到 n-1 ,有 F(n-2) 种方案跳到 n-2,而从 n-1 或者 n-2 跳到 n 只有一种方案,而无法从 n-2 以下的台阶跳到n。那么可以算得,跳到 n 的方案数 F(n) = F(n-1)+F(n-2).

  不难看出这实际是斐波拉契数列的变形应用,把斐波拉契数列的每一项向前移动了1位。此时从第1到n级台阶,分别有:1,2,3,5,8…F(n-2),F(n-1),F(n-2)+F(n-1)种。有几种解答方法,如下:

方法1 裴波那契数列的规律
  按照计算裴波那契数列的规律:F(n) = F(n-1)+F(n-2) 进行计算。
  时间复杂度: O(n) 单循环到 n;空间复杂度:O(1)

//1、裴波那契数列法
    public static int JumpFloor1(int target) {
        if(target<=1)
            return target;

        int res = 0;//用于记录第target个台阶的方案数
        int n1 = 2;//用于记录F(n-1)级台阶的方案数,初始化为第2及台阶的方案数:2
        int n2 = 1;//用于记录F(n-2)级台阶的方案数,初始化为第2及台阶的方案数:1

        //从第3级台阶开始,使用 F(n) = F(n-1)+F(n-2) 递推法
        for (int i = 3; i <= target ; i++)
        {
            res = n1+n2;//F(n)
            //更新F(n-1)与F(n-2),使得F(n-2)=F(n-1),F(n-1)=F(n),用于下一轮循环计算F(n+1)
            n2 = n1;
            n1 = res;
        }

        return res;
        //运行时间:13ms,占用内存:9408k
    }

方法2 动态规划
  动态规划同样适用到:F(n) = F(n-1)+F(n-2)。动态规划类似于上面的裴波那契方法,只不过上面使用变量来存储第n个台阶的方案数,这里我们用数据来存储第n个台阶的方案数:dp[i] = dp[i - 1] + dp[i -2]
  时间复杂度: O(n) 单循环到 n;空间复杂度:O(n) ,dp 数组用了 n 空间。

 //2、动态规划
    public static int JumpFloor2(int target)
    {
        if(target<=2)
            return target;

        //定义动态规划数组:为了方便表示,我们抛弃数组0位置,使用数组的 1-target位置,那么数组长度为 target+1
        int[] dp = new int[target+1];
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= target ; i++)
        {
            dp[i] = dp[i-1]+dp[i-2];
        }
        return dp[target];
        //运行时间:13ms,占用内存:9320k
    }

方法3 暴力递归(递归树)
  同样使用 F(n) = F(n-1)+F(n-2) 的递推公式,但是这里使用递归法解。
  可以看出递推法所消耗的时间较长。
  时间复杂度: O(2ⁿ) - 递归树的所有节点数;空间复杂度:O(n) - 递归树可达深度

//2、递推法
    public static int JumpFloor2(int target)
    {
        //1、解决规模最小的问题:target =0,1,2 的情况,其他情况都可以由这些情况组合。
//        if(target==0)
//            return 0;//有可能出现 F(2) = F(1)+F(0),F(0)时方案数为0,返回0
//        if(target==1)
//            return 1;
//        if(target==2)
//            return 2;
        if(target<=2)
            return target;


        //2/3、解决规模较小问题:求解F(n-1)与F(n-2);将较小问题整合成为较大问题的解:F(n)=F(n-1)+F(n-2)
        return JumpFloor2(target-1)+JumpFloor2(target-2);
        //运行时间:504ms,占用内存:9440k
    }

  还有一种记忆化递归的方法,可以将递归树的时间复杂度降低到 O(n),参考文章:添加链接描述


题目9 变态跳台阶

  题目描述:一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

  分析:由第八题的分析可知,想要到达第n个台阶,我们可以从第n-1跳一步到 n,也可以从第 n-2 跳两步到 n,也可以从第 n-3 跳三步到 n…也可以从第 n-mm步到 n(从 n-m 开始,跳非m的其他步数到n,最后都会包含在其他的F(n-m)里面,因此,对于台阶n-m,只需要考虑跳m不到n的情况即可!) 。
  那么就有

1f(n) = f(n-1)+f(n-2)++f(1)+f(0)
	注意f(n-m)的涵义:表示跳到第n-m既台阶的方案数,然后再跳m步可以到n级台阶,既不管f(n-m)是多少,总有一个方案可以跳转到n级台阶。
	此处需要考虑f(0)f(0)表示跳到第0个位置的方案数(为0),再跳n既台阶可以到达n,既不管f(0)是多少,从0位置跳转到n位置也有一个方案。
	但是我们发现f(0)=0,加的时候会将从0跳到n的方案覆盖。因此我们在使用暴力加的方式的时候,需要把f(0)设置为1,这样便不会遗漏f(0)这一种情况。
-------------------------------------------------	
2f(n) = f(n-1)+f(n-2)++f(1)+f(0)
	f(n-1) = f(n-2)+ f(n-3)+f(1)+f(0)
	两式相减,得到f(n) = 2*f(n-1).
	在使用这种方法的时候,我们不需要考虑f(0)=0的影响,因为此处f(n)总是通过f(n-1)来计算的,而f(1)不需要通过f(0)来计算,因此不需要考虑f(0)。(其实这种方法常用)

  那么有下面的解法

方法1:基于公式的方法(推荐)
  可以使用递归/循环实现这个公式。这种方法的时间复杂度是O(n)。

//1、基于公式的递归法
    public static int JumpFloorII1(int target) {
    //不需要考虑f(0)=0的影响
        if(target<=0)
            return -1;
        if(target==1)
            return 1;

        return 2*JumpFloorII1(target-1);
        //运行时间:12ms,占用内存:9404k
    }
//2、基于公式的循环法
    public static int JumpFloorII2(int target) {
    //不需要考虑f(0)=0的影响
        if(target<=0)
            return -1;
        if(target==1)
            return 1;

        int res = 1;//用于保存第n级台阶的方案数,初始化为第一级台阶的方案数
        for (int i = 2; i <= target ; i++)
        {
            res = res*2;
        }
        return res;
        //运行时间:19ms,占用内存:9372k
    }

方法2 暴力加法
  我们使用递归的方法,直接使用:f(n) = f(n-1)+f(n-2)+…+f(1) 公式,暴力加到f(n)。
  暴力递归方法的时间复杂度是:O(f(n)) = O(S(n-1)) = O(2^n-2) .
  结果是: O(2^n),可以看出暴力递归的时间复杂度较大。当然,也可以使用2次暴力循环,时间复杂度也较大,使用方法1解即可!
  参考:添加链接描述

 //3、暴力递归
    public static int JumpFloorII3(int target) {
        /*
        根据公式:f(n) = f(n-1)+f(n-2)+…+f(1)+f(0)
        为了避免遗漏从0 位置跳转n步到n位置这种情况,设置f(0)=1
         */
        if(target==0)
            return 1;
        if(target==1)
            return 1;

        int sum = 0;
        while(target>=1)
        {
            sum += JumpFloorII3(target-1);
            target--;
        }
        return sum;
        //运行时间:19ms,占用内存:9284k
    }

题目10 矩形覆盖

  题目描述:我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2n的大矩形,总共有多少种方法?
  比如n=3时,2
3的矩形块有3种覆盖方法:
在这里插入图片描述

  分析:
1)当 n 为 1 时,只有一种覆盖方法:
在这里插入图片描述

2)当 n 为 2 时,有两种覆盖方法:
在这里插入图片描述
3)要覆盖 2n 的大矩形,可以先覆盖 21 的矩形,再覆盖 2*(n-1) 的矩形;或者先覆盖 22 的矩形,再覆盖 2(n-2)的矩形。
  那么,覆盖 2n 的矩形的方法数,等于 覆盖 2(n-1)的矩形的方法数 + 覆盖 2(n-2)的矩形的方法数 ,覆盖 2(n-1) 和 2*(n-2) 的矩形可以看成子问题。该问题的递推公式如下:
在这里插入图片描述

  递推法:

//递推法
    public int RectCover(int target)
    {
        if(target<=2)
            return target;

        int temp = 0;
        int pre1 = 2;//代表f(n-1),初始为3-1=2
        int pre2 = 1;//代表f(n-1),初始为3-2=1

        //从 target = 3 开始递推
        for (int i = 3; i <= target ; i++)
        {
            temp = pre1 + pre2;//这一轮的f(n),也是下一轮的f(n-1)

            //注意,设置的顺序很重要,必须先将这一轮的f(n-1)设置为下一轮的f(n-2);再讲这一轮的f(n)设置为下一轮的f(n-1)。否则会出现错误
            pre2 = pre1;//下一轮的 f(n-2) 等于这一轮的 f(n-1)
            pre1 = temp;//temp赋予下一轮的f(n-1)
        }
        return temp;
    }

  递归法,递归法与递推法的原理相同,实现方法不同而已。

public int RectCover1(int target)
    {
        if(target<=2)
            return target;

        return RectCover1(target-1)+RectCover1(target-2);
    }

  递推法时间复杂度是O(n),递归法是O(2^n)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值