剑指offer所有题目详解,可访问我的github项目:KongJetLin-offer
目录
题目描述:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{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]
int mid = 0;
while (begin<end)
{
mid = (begin+end)/2;
if(array[mid]>array[end])
begin = mid+1;
else
end = mid;
}
return begin;
}
题目描述:大家都知道斐波那契数列,现在要求输入一个整数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
}
题目描述:一只青蛙一次可以跳上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),参考文章:添加链接描述
题目描述:一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
分析:由第八题的分析可知,想要到达第n个台阶,我们可以从第n-1跳一步到 n,也可以从第 n-2 跳两步到 n,也可以从第 n-3 跳三步到 n…也可以从第 n-m 跳m步到 n(从 n-m 开始,跳非m的其他步数到n,最后都会包含在其他的F(n-m)里面,因此,对于台阶n-m,只需要考虑跳m不到n的情况即可!) 。
那么就有
1、 f(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)这一种情况。
-------------------------------------------------
2、 f(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
}
题目描述:我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2n的大矩形,总共有多少种方法?
比如n=3时,23的矩形块有3种覆盖方法:
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)。