【剑指Offer】算法和数据操作Part

旋转数组的最小数字

题目描述

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

分析

该题可以用三种方法来解决:

  1. 从头到尾遍历数组,找到最小的元素。时间复杂度是O(n)。
  2. 由于旋转数组是有规律的,可以看作两个非减排序的子数组,且后一个子数组中的元素都小于等于前一个数组中的元素。这两个子数组的分界线就是最小的元素所在的位置,该元素一定是小于等于前一个元素
  3. 利用二分查找的方法实现,复杂度O(logn)。

最一般的情况:
例如:
在这里插入图片描述
mid = (left+right) / 2
可知:

  • 当mid指向的元素大于或等于right的时候,最小的元素应该在mid后面,更新left=mid;
  • 当mid指向的元素小于或等于left的时候,最小的元素应该在mid前面,或者就是mid,更新right=mid;

更新mid指针,重复以上工作。
循环终止条件:当left和right是相邻元素的时候,第二个指针指的就是最小的元素。

特殊情况1:
如果把原始的非减数组的前面的0个元素搬到最后面,即排序数组本身。
当初始的left小于right时,说明该数组是非减的,直接返回left指向的数字。所以初始化left = 0。

特殊情况2:
在这里插入图片描述
上面两个Case演示了left = right = mid的情况,这时无法比较指针之乡的元素的大小,只能通过顺序查找的方法解决。

Java代码

import java.util.ArrayList;
public class Solution {
    public int minNumberInRotateArray(int [] array) {
        // 数组大小为0
        if(array.length == 0)
            return 0;
        
        // 数组大小不为0
        int left = 0;
        int right = array.length - 1;
        int mid = (left + right) / 2;
        // 普遍情况
        while(array[left] >= array[right]){
            if(right - left == 1){
                mid = right;
                break;
            }
            if(array[mid] > array[right])
                left = mid;
            if(array[mid] < array[left])
                right = mid;
            mid = (right + left) / 2;
        }
        // 特殊情况1
        if(array[left] < array[right]){
            mid = left;
        }
        // 特殊情况2
        if(array[left] == array[right] && array[left] == array[mid])
            mid = orderSearch(array);
        
        return array[mid];
    }
    // 顺序查找
    public int orderSearch(int[] array){
        for(int i=1; i<array.length; i++){
            if(array[i] < array[i-1])
                return i;
        }
        return 0;
    }
}

斐波那契数列

题目描述

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,n<=39)。
斐波那契数列定义如下:
在这里插入图片描述

分析

利用递归很容易求解,但是效率很低,代码如下:

public class Solution {
    public int Fibonacci(int n) {
        if(n==0 || n==1)
            return n;
        else
            return(Fibonacci(n-1)+Fibonacci(n-2));
    }
}

递归效率低的原因是因为中间有很多重复的计算,如下图:
在这里插入图片描述
上图用树的方式表现出计算过程,可以看出 f ( 7 ) f(7) f(7)重复计算了三次。由此提出一种改进算法,避免重复运算。
思路就是可以把计算得到的中间项先保存下来,如果下次需要计算的时候就直接拿出来,不必重复计算。这样从下往上计算即可。
例如根据 f ( 0 ) f(0) f(0) f ( 1 ) f(1) f(1)计算 f ( 2 ) f(2) f(2),再根据 f ( 1 ) f(1) f(1) f ( 2 ) f(2) f(2)计算 f ( 3 ) f(3) f(3),依次类推。

Java代码

public class Solution {
    public int Fibonacci(int n) {
        if(n<=1)
            return n;
        else{
        	//f(n)
            int present = 0;
            // f(n-2)
            int frontTwo = 0;
            // f(n-1)
            int frontOne = 1;
            for(int i = 2; i <= n; i++){
                present = frontTwo + frontOne;
                frontTwo = frontOne;
                frontOne = present;
            }
            return present;
        }
    }
}

斐波那契数列拓展——跳台阶

题目描述

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

分析

  • 有1个台阶,只有一种跳法: 1 = 1 1=1 1=1,所以 f ( 1 ) = 1 f(1) = 1 f(1)=1

  • 有2个台阶,有两种跳法: 2 = 2 2=2 2=2或者 2 = 1 + 1 2=1+1 2=1+1,所以 f ( 2 ) = 2 f(2) = 2 f(2)=2

  • 假设一共有n个台阶,最后一跳有两种情况:

    • 跳1个台阶:则 f ( n ) = f ( n − 1 ) f(n) = f(n-1) f(n)=f(n1)
    • 跳2个台阶:则 f ( n ) = f ( n − 2 ) f(n) = f(n-2) f(n)=f(n2)

    则一共有 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n-1) + f(n-2) f(n)=f(n1)+f(n2)种跳法。
    上题说到从下往上求解斐波那契数列可以提高效率,本题同理。

Java代码

public class Solution {
    public int JumpFloor(int target) {
        if(target <= 0)
            return 0;
        else if(target == 1 || target == 2)
            return target;
        else{
            // f(n)
            int present = 0;
            // f(n-2)
            int frontTwo = 1;
            // f(n-1)
            int frontOne = 2;
            for(int i=3; i<=target; i++){
                present = frontTwo + frontOne;
                frontTwo = frontOne;
                frontOne = present;
            }
            return present;
        }
    }
}

变态跳台阶

题目描述

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

分析

n个台阶会有一次n阶的跳法。分析如下:

f ( 1 ) = 1 f(1) = 1 f(1)=1
f ( 2 ) = f ( 2 − 1 ) + f ( 2 − 2 ) f(2) = f(2-1) + f(2-2) f(2)=f(21)+f(22)
f ( 3 ) = f ( 3 − 1 ) + f ( 3 − 2 ) + f ( 3 − 3 ) f(3) = f(3-1) + f(3-2) + f(3-3) f(3)=f(31)+f(32)+f(33)

f ( n ) = f ( n − 1 ) + f ( n − 2 ) + f ( n − 3 ) + . . . + f ( n − ( n − 1 ) ) + f ( n − n ) f(n) = f(n-1) + f(n-2) + f(n-3) + ... + f(n-(n-1)) + f(n-n) f(n)=f(n1)+f(n2)+f(n3)+...+f(n(n1))+f(nn)
其中 f ( n − a ) f(n-a) f(na)标识一共有 n n n个台阶,最后一跳为 a a a个台阶的跳法数。
可得到,
f ( n ) = f ( n − 1 ) + f ( n − 2 ) + f ( n − 3 ) + . . . + f ( n − ( n − 1 ) ) + f ( n − n ) = f ( 0 ) + f ( 1 ) + f ( 2 ) + . . . + f ( n − 2 ) + f ( n − 1 ) f(n) = f(n-1) + f(n-2) + f(n-3) + ... + f(n-(n-1)) + f(n-n) \\ = f(0)+f(1)+f(2)+...+f(n-2)+f(n-1) f(n)=f(n1)+f(n2)+f(n3)+...+f(n(n1))+f(nn)=f(0)+f(1)+f(2)+...+f(n2)+f(n1)
因为,
f ( n − 1 ) = f ( 0 ) + f ( 1 ) + f ( 2 ) + . . . + f ( n − 2 ) f(n-1) = f(0)+f(1)+f(2)+...+f(n-2) f(n1)=f(0)+f(1)+f(2)+...+f(n2)
所以,
f ( n ) = 2 × f ( n − 1 ) f(n) = 2 \times f(n-1) f(n)=2×f(n1)

结论为:
f ( n ) = { 0 , if  n ⩽ 0 1 , if  n = 1 2 × f ( n − 1 ) , if  n ⩾ 2 , f(n)= \begin{cases} 0, &amp;\text{if $n\leqslant 0$}\\ 1, &amp;\text{if $n=1$}\\ 2\times f(n-1),&amp;\text{if $n\geqslant 2$}, \end{cases} f(n)=0,1,2×f(n1),if n0if n=1if n2,

Java代码

递推

public class Solution {
    public int JumpFloorII(int target) {
        if(target<=0)
            return 0;
        else if(target == 1)
            return target;
        else
            return JumpFloorII(target-1)*2;
    }
}

效率更高的方法:

public class Solution {
    public int JumpFloorII(int target) {
        if(target<=0)
            return 0;
        else if(target == 1)
            return target;
        else{
            int present = 0;
            int frontOne = 1;
            for(int i=2; i<=target; i++){
                present = 2 * frontOne;
                frontOne = present;
            }
            return present;
        }
    }
}

斐波那契数列拓展——矩形覆盖

题目描述

我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

分析

在这里插入图片描述
可推出: f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n-1)+f(n-2) f(n)=f(n1)+f(n2)

Java代码

public class Solution {
    public int RectCover(int target) {
        if(target<=0)
            return 0;
        else if(target == 1)
            return target;
        else{
            int present = 0;
            int frontTwo = 1;
            int frontOne = 1;
            for(int i=2; i<=target; i++){
                present = frontTwo + frontOne;
                frontTwo = frontOne;
                frontOne = present;
            }
            return present;
        }
    }
}

二进制中1的个数

题目描述

输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

分析

要数清二进制表示中1的个数,最直接的方法就是遍历,但是这样做的效率太低。有一种更巧妙的方法。
例如11的二进制表达是1011,将1011减1得到1010,1011&1010=1010,就消除了最右边的1。同理如下图所示,每这样操作一次,就能消掉最右边的一个1。操作的次数即为二进制表示中1的个数。
在这里插入图片描述
更具体来说,如果二进制表示Exp1共有n位。

  • 第n位为1时:减去1后,新的表达式Exp2除了最后一位1变成了0,前面的n-1位没有变,Exp1Exp2相与之后就消掉了最后一位的1。
  • 第m位为1(m<n),后面的第m+1~ n位都为0时:减去1后得到新的表示Exp3,Exp3和Exp2相比,第m位变成了0,第m+1~n位都变成了1。Exp2Exp3相与之后,就消掉了第m位的1,也就是最右边的1。

总结来说就是,把一个整数减掉1,在和原整数作与运算,会把该整数最右边的一个1变成0。

Java代码

public class Solution {
    public int NumberOf1(int n) {
        int cnt = 0;
        while(n!=0){
            n = n & (n-1);
            cnt++;
        }
        return cnt;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值