算法和数据操作Part
旋转数组的最小数字
题目描述
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
分析
该题可以用三种方法来解决:
- 从头到尾遍历数组,找到最小的元素。时间复杂度是O(n)。
- 由于旋转数组是有规律的,可以看作两个非减排序的子数组,且后一个子数组中的元素都小于等于前一个数组中的元素。这两个子数组的分界线就是最小的元素所在的位置,该元素一定是小于等于前一个元素。
- 利用二分查找的方法实现,复杂度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(n−1)
- 跳2个台阶:则 f ( n ) = f ( n − 2 ) f(n) = f(n-2) f(n)=f(n−2)
则一共有 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n-1) + f(n-2) f(n)=f(n−1)+f(n−2)种跳法。
上题说到从下往上求解斐波那契数列可以提高效率,本题同理。
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(2−1)+f(2−2)
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(3−1)+f(3−2)+f(3−3)
…
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(n−1)+f(n−2)+f(n−3)+...+f(n−(n−1))+f(n−n)
其中
f
(
n
−
a
)
f(n-a)
f(n−a)标识一共有
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(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
−
1
)
=
f
(
0
)
+
f
(
1
)
+
f
(
2
)
+
.
.
.
+
f
(
n
−
2
)
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
(
n
)
=
2
×
f
(
n
−
1
)
f(n) = 2 \times f(n-1)
f(n)=2×f(n−1)
结论为:
f
(
n
)
=
{
0
,
if
n
⩽
0
1
,
if
n
=
1
2
×
f
(
n
−
1
)
,
if
n
⩾
2
,
f(n)= \begin{cases} 0, &\text{if $n\leqslant 0$}\\ 1, &\text{if $n=1$}\\ 2\times f(n-1),&\text{if $n\geqslant 2$}, \end{cases}
f(n)=⎩⎪⎨⎪⎧0,1,2×f(n−1),if n⩽0if n=1if n⩾2,
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(n−1)+f(n−2)
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位没有变,Exp1和Exp2相与之后就消掉了最后一位的1。
- 第m位为1(m<n),后面的第m+1~ n位都为0时:减去1后得到新的表示Exp3,Exp3和Exp2相比,第m位变成了0,第m+1~n位都变成了1。Exp2和Exp3相与之后,就消掉了第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;
}
}