一.递归和循环:
基于递归实现的代码比基于循环实现的代码简洁,但由于每一次调用都需要在内存栈中分配空间以保存参数、返回地址及临时变量,实现效率不如循环,此外,递归可能会引起栈溢出。
10.斐波那契数列:
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项。n<=39
解题思路:直接采用递归的方式,由于n<=39,这种方法勉强还可以,但是存在重复计算节点的问题。
public class Solution {
public int Fibonacci(int n) {
if(n==0)
return 0;
if(n==1)
return 1;
return Fibonacci(n-1)+Fibonacci(n-2);
}
}
将递归算法用循环的方式实现,避免了重复计算节点,提高了效率。
public class Solution {
public int Fibonacci(int n) {
int fibOne = 0;
int fibTwo = 1;
int result =0;
if(n==0)
return fibOne;
if(n==1)
return fibTwo;
for(int i = 2; i<=n;i++){
result = fibOne + fibTwo;
fibOne = fibTwo;
fibTwo = result;
}
return result;
}
}
===>举一反三:青蛙跳台阶问题:
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
解题思路:实际上,这就是一个斐波那契数列问题,当N>2时,第一次跳就有两种选择,当第一次只跳1级时,跳法数目等于后面的N-1级台阶跳法的数目;当第一次只跳2级时,跳法数目等于后面的N-2级台阶跳法的数目。但起始值有所不同。
public class Solution {
public int JumpFloor(int target) {
int before1 = 1;
int before2 = 1;
int result = 0;
if(target==0)
return 0;
if(target==1)
return before1;
for(int i = 2; i<=target ; i++){
result = before1+ before2;
before1 = before2;
before2 = result;
}
return result;
}
}
===>举一反三:变态跳台阶问题:
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
解题思路:因为n级台阶,第一步有n种跳法:跳1级、跳2级、到跳n级
跳1级,剩下n-1级,则剩下跳法是f(n-1),跳2级,剩下n-2级,则剩下跳法是f(n-2)
所以f(n)=f(n-1)+f(n-2)+...+f(1),因为f(n-1)=f(n-2)+f(n-3)+...+f(1),所以f(n)=2*f(n-1)
public class Solution {
public int JumpFloorII(int target) {
if(target==0)
return 0;
return (int)Math.pow(2,target-1);
}
}
===>举一反三:矩形覆盖:
我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
解题思路:和青蛙跳台阶一样的思路,相当于一个菲波那切数列,初始值不同。
public class Solution {
public int RectCover(int target) {
int before1 = 1;
int before2 = 1;
int result = 0;
if(target==0)
return 0;
if(target==1)
return before1;
for(int i = 2; i<=target ; i++){
result = before1+ before2;
before1 = before2;
before2 = result;
}
return result;
}
}
二.回溯法:
在二维数组上搜索路径,可以利用回溯法,不能用递归实现时,可以考虑用栈来模拟。回溯法非常适合有多个步骤组成的问题,并且每步骤都有多个选项,适合用递归的方式实现。
12.矩阵中的路径:
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
解题思路:解题的关键在于利用栈的思想以及递归的思想,当字符与路径中字符相同时,设置为已访问,并在上下左右四个方向做递归寻找下一个点是否在数组中,如果不在,回溯时切记要将该点设置为未访问,路径长度也要减一。
public class Solution {
public boolean hasCore(char[] matrix, int rows, int cols, int row, int col,
char[] str,int pathLength,boolean [] visited){
boolean flag = false;
if( pathLength==str.length)
return true;
if(row>=0 && row<rows && col>=0 && col<cols && !visited[row*cols+col]&& str[pathLength]==matrix[row*cols+col]){
pathLength++;
visited[row*cols+col]= true;
flag = hasCore(matrix,rows,cols,row,col+1,str,pathLength,visited)
|| hasCore(matrix,rows,cols,row,col-1,str,pathLength,visited)
|| hasCore(matrix,rows,cols,row+1,col,str,pathLength,visited)
|| hasCore(matrix,rows,cols,row-1,col,str,pathLength,visited);
if(!flag){
pathLength--;
visited[row*cols+col]=false;
}
}
return flag;
}
public boolean hasPath(char[] matrix, int rows, int cols, char[] str){
if(matrix==null || rows==0 || cols==0 || str==null)
return false;
boolean [] visited = new boolean[rows*cols];
int pathLength = 0;
for(int i = 0;i< rows ;i++){
for(int j = 0;j< cols ;j++){
if(hasCore(matrix,rows,cols,i,j,str,pathLength,visited))
return true;
}
}
return false;
}
}
13.机器人的运动范围:
地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
解题思路:机器人的运动范围和求矩阵的路径问题相似,每一步有四种可能,只是不需要进行删除,如果各位数的和小于阈值,count+1即可。
public class Solution {
public int Sum(int number){
int result=0;
while(number!=0){
result += number%10;
number = number/10;
}
return result;
}
public int movingCore(int threshold, int rows, int cols, int row, int col, boolean[] visited){
int result = 0;
if(col<cols && col>=0 && row<rows && row>=0 && !visited[row*cols+col] && (Sum(col)+Sum(row))<=threshold){
visited[row*cols+col]= true;
result = 1+ movingCore(threshold,rows,cols,row,col+1,visited)
+ movingCore(threshold,rows,cols,row,col-1,visited)
+ movingCore(threshold,rows,cols,row+1,col,visited)
+ movingCore(threshold,rows,cols,row-1,col,visited);
}
return result;
}
public int movingCount(int threshold, int rows, int cols){
if(threshold<=0 || rows<=0 || cols<=0)
return 0;
boolean[] visited = new boolean[rows*cols];
for(int i=0; i<rows*cols;i++)
visited[i]=false;
int count =movingCore(threshold,rows,cols,0,0,visited);
return count;
}
}
三.动态规划和贪婪算法:
求某个问题的最优解,而且该问题能够分解成若干个子问题,并且子问题之间还有重叠的更小的问题,可以采用动态规划,从上往下计算小问题的最优解并存储下来,再以此为基础求取大问题的最优解。
贪婪算法每一步都可以做出一个贪婪的选择,基于这个选择,确定能够得到最优解。必须先证明一个问题可以通过贪婪策略得到最优解,才能采取这种方法。
14.剪绳子:
给你一根长度为n的绳子,请把绳子剪成m段 (m和n都是整数,n>1并且m>1)每段绳子的长度记为k[0],k[1],...,k[m].请问k [0]*k[1]*...*k[m]可能的最大乘积是多少?例如,当绳子的长度为8时,我们把它剪成长度分别为2,3,3的三段,此时得到的最大乘积是18.
解题思路:采用动态规划的方式,首先定义函数f(n)为把长度为n的绳子剪成若干段后各段长度乘积的最大值。在剪第一刀时,我们有n-1种选择,也就是说第一段绳子的可能长度分别为1,2,3.....,n-1。因此f(n)=max(f(i)*f(n-i)),其中0<i<n。计算过程可以按照从下而上的顺序,先得到f(2),f(3),再得到f(4), f(5),直到得到f(n)。当绳子的长度为2的时候,只能剪成长度为1的两段,所以f(2) = 1,当n = 3时,容易得出f(3) = 2;
public class Solution {
public static int maxAfterCutting(int len){
if(len<2)
return 0;
if(len==2)
return 1;
if(len==3)
return 2;
//子问题的最优解存储在result数组中,数组中的第i个元素表示把长度为i的绳子剪成若干段后各段长度乘积的最大值
int[] result=new int[len+1];
result[0]=0; result[1]=1;
result[2]=2; result[3]=3;
//自底向上开始求解
int max=0;
for(int i=4;i<=len;i++){
max=0;
for(int j=1;j<=i/2;j++){
int tempResult=result[j]*result[i-j];
if(max<tempResult)
max=tempResult;
result[i]=max;
}
}
max=result[len];
return max;
}
}
这道题可以证明,当在n>=5的情况下,剪长度为3的绳子段可以获得的乘积会更大。[3(n-3)>n,2(n-2)>n,3(n-3)>=2(n-2)==>n>=5],可以使用贪婪算法,剪尽可能多的长度为3的绳子段,当绳子的长度为4时,把绳子剪成长度为2的两段。
public class Solution {
public static int maxProductWithGreedy(int len){
if(len<2)
return 0;
if(len==2)
return 1;
if(len==3)
return 2;
//贪心算法,先尽可能减去长度为3的段
int timeOfThree=len/3;
//判断还剩下多少,再进行处理
if(len-timeOfThree*3==1)
timeOfThree-=1;
int timeOfTwo=(len-timeOfThree*3)/2;
return (int) ((Math.pow(3, timeOfThree))*(Math.pow(2, timeOfTwo)));
}
}
四、查找和排序:
查找包括顺序查找、二分查找、哈希表查找、二叉排序树查找,可以使用循环或者递归的方式实现。参见博客中的各专题。
二分查找:数组中面试题53.在排序数组中查找数字(数字在排序数组中出现的次数,0-n-1中缺失的数字,数组中数值和下标相等的元素);面试题 11.旋转数组的最小数字;
哈希表查找:
二叉排序树的查找:面试题33.二叉搜索树的后序遍历序列;面试题54.二叉搜索树的第K个结点。
五.位运算:
位运算是把数字用二进制表示后,对每一位上0或者1的运算。位运算包括五种,与、或、异或、左移、右移。注意,当数字进行右移时,需要考虑是否为无符号数,数字为有符号数时,用数字的符号位填补最左边的n位。
15.二进制中1的个数:
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
解题思路:为了避免输入的数为负数,在右移过程中出现死循环,最直接的方法是采用左移的思路,将1逐渐左移,按位与,累加1的个数。循环的次数等于二进制的位数。
public class Solution {
public int NumberOf1(int n) {
int count = 0;
int flag = 1;
while (flag != 0) {
if ((n & flag) != 0) {
count++;
}
flag = flag << 1;
}
return count;
}
}
更好的方法是,把一个整数减去1再和原来的整数相与,会把整数最右边的1变成0,统计一个整数的二进制有多少个1,转化为进行多少次这样的操作。
public class Solution {
public int NumberOf1(int n) {
int count=0;
while(n!=0){
n= n &(n-1);
count++;
}
return count;
}
}
举一反三:用一条语句判断一个整数是不是2的整数次方。一个整数如果是2的整数次方,则这个整数的二进制表示中只有1位为1,将这个整数减1后与原整数按位与,得到的结果为0则是,否则不是。
if(((n-1)&n)==0)
return true;
else
return false;
举一反三:输入两个整数m,n,计算需要改变m的二进制表示中的多少位可以得到n。首先求出两个数的异或(^),异或结果中1的个数即为两数中不同位的个数,统计1的个数采用【(异或数-1)& 异或数】的次数。
public class Solution {
public int GetCount(int N,int M){
int value=N^M;//先将两个数异或
int count=0;
while(value){
count++;
value=(value&(value-1));//求异或后1的个数
}
return count;
}
}
65.不用加减乘除做加法:
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
解题思路:不能使用四则运算,所以只能考虑位运算。计算两个数的和,首先将两个数按位进行异或,然后再考虑进位(按位与后向左移一位),最后相加即可,相加的过程可以再次使用上述步骤,直到进位为0即可
public class Solution {
public int Add(int num1,int num2) {
int sum = 0,add = 0;
while(num2 != 0){
sum = num1 ^ num2;
add = ( num1 & num2)<<1;
num1 = sum;
num2 = add ;
}
return num1;
}
}
相关问题:不使用新变量,交换两个变量的值。有两种方法,第一种采用加减法(a=a+b,b=a-b,a=a-b.);第二种采用异或的方式(a=a^b,b=a^b,a=a^b)。