题目一:二维数组中的查找
问题描述:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数.
思路:将target与二维数组array第一行最后一个数做比较,如果target大于该行数,则不考虑第一行,相反不考虑最后一列,相等返回true
假如:我们从右上角选取元素,如果相等则返回true;如果大于,显然该列的元素都大于目标整数,我们就可以去掉这一列然后对比剩下的前几列;如果小于,显然该行的所有元素都小于目标整数,我们就可以去掉这一行然后对比剩下的后几行。依次类推,即可找到或找不到相应的目标整数。
/**
* 面试题3:二维数组中的查找
*/
public static boolean Find(int[][] mat, int number){
boolean found = false;
if(mat != null && mat.length() > 0 && mat[0].length() > 0){
int m = mat.length();
int n = mat[0].length();
int row = 0;//行
int col = n-1;//列,最右上角
while(row < m && col >=0){
if(mat[row][col] == number){//右上角的数(行最大,列最小)
found = true;
break;
}else if(mat[row][col] > number){//该列最小数都大于,说明该列其他数肯定也大于,果断舍弃该列,
col--;//往前移一列
}else{//该行最大数都小于,说明该行其他数肯定也都小于,果断移到下行
row++;
}
}
}
return found;
}
面试题二:替换空格(假设在原来的字符串上替换,并且保证输入的字符串后面有足够多的内存空间)
题目:请实现一个函数,把字符串中的每个空格替换成“20%”。例如输入“hellow new world!”,则输出“hellow02%new02%world!”。
原因:在网络编程中,如果URL参数中含有特殊字符,如:空格、“#”等,可能导致服务器端无法获得正确的参数值。我们需要将这些特殊符号转换成服务器识别的字符。转换规则是在“%”后面跟上ASCII码的两位十六进制的表。比如:空格的ASCII玛是32,即十六进制的0x20,因此空格被替换成“%20”。
思路1:按顺序找出空格所在的位置(下标),然后利用字串相加,将去除空格的字串用“%20”连接起来
/**
* 面试题:替换空格
*/
public static String replaceSpace(StringBuffer str){
String s = "";
int start = 0;
while(start >= 0){
int begin = start;
start = str.indexOf(" ", start);//查找空格的位置可返回某个指定的字符串值在字符串中首次出现的位置。
if(start >= 0){
s = s + str.substring(begin, start) + "%20";
start++;
}else{
s = s + str.substring(begin, str.length());
break;
}
}
return s;
}
思路2:按照一般的想法,我们遍历字符,每遇到空格就将之替换,但是需要后面的字符的移动,这样做需要的移动次数很多。O(n^2)
/**
* 每个char进行遍历
*
* @param stringBuffer
* @return
*/
public static String replaceReplace(StringBuffer stringBuffer){
StringBuffer out = new StringBuffer();
for (int i = 0;i<stringBuffer.toString().length();i++){
char b = stringBuffer.charAt(i);
if (String.valueOf(b).equals(" ")){
out.append("%20");
}else {
out.append(b);
}
}
return out.toString();
}
/**
* 直接使用函数
* @param str
* @return
*/
public static String replace(StringBuffer str){
return str.toString().replaceAll("\\s","%20");
}
public static void main(String[] args) {
StringBuffer str = new StringBuffer();
str.append("hello world.");
System.out.println(replaceReplace(str));
System.out.println(replace(str));
}
思路3:基于思路2,如果从前往后替换字符串,那么保存在空格后面的字符串肯定会被覆盖 ,我们移动的时候从后开始,这样就会减少很多重复的元素移动
算法描述如下:
从字符串的后面开始复制和替换,首先准备两个指针,p1和p2,p1指向原始字符串的末尾,p2指向替换后字符串的末尾,接下来,向前移动指针p1,逐个把它指向的字符复制到p2,碰到一个空格之后,把p1向前移动1格,在p2处插入字符串“20%”,由于“20%”长度为3,同时也要把p2向前移动3格。直到p1=p2,表明所有空格都已经替换完毕。
package com.offer.www;
public class ReplaceBlank {
private static String testString = "hellow new world!";
// 计算字符串中包含的空格个数
public static int getBlankNum(String testString) {
int count = 0;
for (int i = 0; i < testString.length(); i++) {
String tempString = String.valueOf(testString.charAt(i));
if (tempString.equals(" ")) {
count++;
}
}
return count;
}
// 打印char[]数组
public static void printArray(char[] testArray) {
for (char i : testArray) {
System.out.print(i);
}
System.out.println();
}
// 将字符串空格转换为20%
public static void replaceAllBlank(String testString) {
if (testString == null || testString.length() <= 0) {
return;
}
// 字符数组初始长度
int originalLength = testString.length();
// 字符数组增加长度后
int newLength = getBlankNum(testString) * 2
+ originalLength;
char[] tempArray = new char[newLength];
System.arraycopy(testString.toCharArray(), 0, tempArray, 0, testString
.toCharArray().length);
int indexofOriginal = originalLength - 1;
int indexofNew = newLength - 1;
System.out.println("未替换空格时的字符串:");
printArray(tempArray);
while (indexofOriginal >= 0 && indexofOriginal != indexofNew) {
if (tempArray[indexofOriginal] == ' ') {
tempArray[indexofNew--] = '%';
tempArray[indexofNew--] = '2';
tempArray[indexofNew--] = '0';
} else {
tempArray[indexofNew--] = tempArray[indexofOriginal];
}
indexofOriginal--;
}
System.out.println("替换空格后的字符串:");
printArray(tempArray);
}
public static void main(String[] args) {
replaceAllBlank(testString);
}
}
剑指Offer面试题3(Java版)
:从尾到头打印链表
题目:输入一个链表的头结点,从尾到头反过来打印出每个节点的值。
是否允许在打印链表的时候修改链表的结构?这个取决于面试官的要求,
思路:假设面试官也要求这个题目不能改变链表的结构。接下来我们想到解决这个问题肯定要遍历链表。遍历的顺序是从头到尾的顺序,可输出的顺序却是从尾到头。也就是说第一个遍历的节点最后一个输出,而最后一个遍历到的节点第一个输出。这就是典型的'后进先出“,我们可以从栈实现这种顺序。没经过一个节点的时候,把该节点放到一个栈中。当遍历完整的链表后,再从栈顶开始逐个输出节点的值,此时输出的节点的顺序已经反转过来了。
<pre name="code" class="java">package offer;
import java.util.Stack;
class Node {
//注:此处的两个成员变量权限不能为private,因为private的权限是仅对本类访问。
public int data; //数据域
public Node nextNode;//指针域
public Node() {
}
public Node(int data) {
this.data = data;
}
}
public class E05FromTailToStart {
public static void main(String[] args) {
Node start = new Node(1);
Node two = new Node(2);
start.nextNode = two;
// Node two = new Node(2);
// start.nextNode = two;
printTailToStartRec(start);
printTailToStartStack(start);
}
// 采用递归调用的方法
private static void printTailToStartRec(Node start) {
if (start != null) {
if (start.nextNode != null) {
printTailToStartRec(start.nextNode);
}
System.out.println(start.data);
}
else{
System.out.println("list is null!");
}
}
// 采用栈--先进后出的方法
private static void printTailToStartStack(Node node) {
if (node == null) {
System.out.println("list is null");
return;
}
Stack<Node> stack = new Stack<Node>();
while (node != null) {
stack.push(node);
node = node.nextNode;
}
while (!stack.isEmpty()) {
System.out.println(stack.pop().data);
}
}
}
上面的基于递归的代码看起来很简洁,但有个问题:当链表非常长的时候,就会导致函数调用的层级很深,从而有可能导致函数调用栈溢出。显式用栈基于循环的代码的鲁棒性要好一些。
剑指Offer面试题4(Java版):重建二叉树
题目:输入某二叉树的前序遍历和中序遍历的结果,请重新构造出该二叉树。假设输入的前序遍历和中序遍历的结果中不包含重复的数字。例如输入的前序遍历序列为{1,2,4,7,3,5,6,8}和中序遍历为{4,7,2,1,5,3,6,8},则重建出二叉树并输出它的头结点。
既然我们已经分别找到了左、右子树的前序遍历序列和中序遍历序列,我们可以用同样的方法分别去构建左右子树。也就是说,接下来的事情可以用递归的方法去完成。
思路:先找到中序中和前序第一个结点相同的节点即为分界点
在二叉树的前序遍历中,第一个数字总是树的根节点。在中序遍历中,树的根节点在序列的中间,左子树的节点的值位于根节点的左边,右子树节点的值位于根节点值的右边。
因此需要扫描中序遍历序列才能找到很结点的值,由此可以找到左子树的节点的个数和右子树节点的个数,然后在前序遍历序列中找到左子树的根节点,再到中序遍历序列中找到左子树的左子树和右子树。依次递归。由于二叉树的构造本身就是用递归实现的,
package swordForOffer;
/**
*/
import utils.BinaryTreeNode;
public class E06ConstructBinaryTree {
/**
*
* @param preOrder 前序遍历数组
* @param inOrder 中序遍历数组
* @param length 数组长度
* @return 根结点
1
8 * / \
9 * 2 3
10 * / / \
11 * 4 5 6
12 * \ /
13 * 7 8
*/
public static BinaryTreeNode Construct(int[] preOrder, int[] inOrder,int length){
if (preOrder == null || inOrder == null || length <= 0) {
return null;
}
try {
return ConstructCore(preOrder, 0, preOrder.length - 1, inOrder, 0,inOrder.length - 1);
} catch (InvalidPutException e) {
e.printStackTrace();
return null;
}
}
/**
*
* @param PreOrder
* 前序遍历序列
* @param startPreIndex
* 前序序列开始位置
* @param endPreIndex
* 前序序列结束位置
* @param InOrder
* 中序遍历序列
* @param startInIndex
* 中序序列开始位置
* @param endInIndex
* 中序序列结束位置
* @return 根结点
* @throws InvalidPutException
*/
public static BinaryTreeNode ConstructCore(int[] preOrder,int startPreIndex, int endPreIndex,
int[] inOrder,int startInIndex, int endInIndex) throws InvalidPutException {
int rootValue = preOrder[startPreIndex];
System.out.println("rootValue = " + rootValue);
BinaryTreeNode root = new BinaryTreeNode(rootValue);
// 只有一个元素
if (startPreIndex == endPreIndex) {
if (startInIndex == endInIndex
&& preOrder[startPreIndex] == inOrder[startInIndex]) {
System.out.println("only one element");
return root;
} else {
throw new InvalidPutException();
}
}
// 在中序遍历中找到根结点的索引
int rootInIndex = startInIndex;
while (rootInIndex <= endInIndex && inOrder[rootInIndex] != rootValue) {
++rootInIndex;
}
if (rootInIndex == endInIndex && inOrder[rootInIndex] != rootValue) {
throw new InvalidPutException();
}
int leftLength = rootInIndex - startInIndex;
int leftPreOrderEndIndex = startPreIndex + leftLength;
if (leftLength > 0) {
// 构建左子树
root.leftNode = ConstructCore(preOrder, startPreIndex + 1,
leftPreOrderEndIndex, inOrder, startInIndex,
rootInIndex - 1);
}
if (leftLength < endPreIndex - startPreIndex) {
// 右子树有元素,构建右子树
root.rightNode = ConstructCore(preOrder, leftPreOrderEndIndex + 1,
endPreIndex, inOrder, rootInIndex + 1, endInIndex);
}
return root;
}
static class InvalidPutException extends Exception {
private static final long serialVersionUID = 1L;
}
public static void printPreOrder(BinaryTreeNode root) {
if (root == null) {
return;
} else {
System.out.print(root.value + " ");
}
if (root.leftNode != null) {
printPreOrder(root.leftNode);
}
if (root.rightNode != null) {
printPreOrder(root.rightNode);
}
}
public static void main(String[] args) throws Exception{
E06ConstructBinaryTree test=new E06ConstructBinaryTree();
int[] preOrder={1,2,4,7,3,5,6,8};
int[] inOrder={4,7,2,1,5,3,8,6};
printPreOrder(Construct(preOrder, inOrder, preOrder.length));
}
}
在函数ConstructCore中,我们先根据先序遍历的第一个数字创建根节点,接下来在中序遍历中找到根节点的位置,这样就能确定左右子树节点的数量。在前序遍历和中序遍历的序列中划分左右子树节点的值之后,我们就可以递归调用函数ConstructCore,去分别构建它的左右子树。
题目五: 用栈实现队列
用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
解题思路:
- 将所有数先存入第一个栈中;
- 第一个栈出栈,按照出栈顺序存入第二个栈中;
- 再从第二个栈中出栈,即得到队列;
我们通过一个具体的例子来分析该队列插入和删除元素的过程。首先插入一个元素a,不妨先把它插入到stack1,此时stack1 中的元素有{a},stack2为空。再压入两个元素b和c,还是插入到stack1中,此时stack1中的元素有{a,b,c},其中c位于栈顶,而stack2仍然为空。
这个时候,我们试着删除从队列中删除一个元素。按照队列先入先出的规则,由于a比b、c先插入到队列中,最先被删除的元素应该是a。元素a存储在stack1中,但并不在占顶上,因此不能直接被删除。注意到stack2我们还一直没有使用过,现在是让stack2发挥作用的时候了。如果我们把stack1中的元素逐个弹出并压入stack2,元素在stack2中的顺序正好和原来的stack1中的顺序相反。因此经过3次弹出stack1和压入stack2操作之后,stack1为空,而stack2中的元素是{c,b,a},这个时候就可以弹出stack2的栈顶a了。此时的stack1为空,而stack2的元素为{c,b},其中在栈顶,如图所示:
/**
*
*/
package swordForOffer;
import java.util.Stack;
public class E07QueueWithTwoStacks {
/**
* 用两个栈实现一个队列,完成两个函数appendTail和deletedHead,分别是在队列尾部插入节点
* 和在队列头部删除节点的功能
*/
private Stack<String> stack1 = new Stack<String>();
private Stack<String> stack2 = new Stack<String>();
public void appendTail(String s){
stack1.push(s);// 加入队列中的元素只加入到栈1中
}
// 删除一个元素时,检查栈2是否为空,栈2不为空则弹出栈2栈顶元素
// 栈2为空,则把栈1中的元素全部弹出、压入到栈2中,然后从栈2栈顶弹出元素
public String deletedHead() throws Exception{if(stack2.isEmpty()){while(!stack1.isEmpty()){stack2.push(stack1.pop());}}if(stack2.isEmpty()){throw new Exception("队列为空,不能删除");}return stack2.pop();}public static void main(String[] args) throws
Exception{E07QueueWithTwoStacks test = new E07QueueWithTwoStacks();test.appendTail("1");test.appendTail("2");test.deletedHead();}}
用两个队列实现栈。
通过一系列的栈的压入和弹出操作来分析用队列模拟一个栈的过程,如图所示,我们先往栈内压入一个元素a。由于两个队列现在都是空,我们可以选择把a插入两个队列中的任一个。我们不妨把a插入queue1。接下来继续网栈内压入b,c两个元素。我们把它们都插入queue1。这个时候 queue1包含3个元素a,b,c其中a位于队列的头部,c位于队列的尾部。
现在我们考虑从栈内弹出一个元素。根据栈的后入先出的原则,最后被压入栈的c应该最先被弹出。由于c位于queue1的尾部,而我们每次只能从队列的头部删除元素,因此我们可以从queueu中依次删除a/b/c并插入到queue2中,再从queue1中删除c。这就相当于从栈中弹出元素c了。我们可以用同样的方法从栈内弹出元素b。
接下来我们考虑从栈内压入一个元素d.此时queue1已经有了一个元素,我们就把d插入到queue1的尾部。如果我们再从栈内弹出一个元素,此时被弹出的应该是最后被压入的d.由于d位于queue1的尾部,我们只能先从头部删除 queue1的元素并插入到queue2,直到queue1中遇到d再直接把它删除。如图所示:
/**
*
*/
package swordForOffer;
import java.util.LinkedList;
public class E07StacksWithTwoQueue {
private LinkedList<String> queue1;
private LinkedList<String> queue2;
public E07StacksWithTwoQueue(){
queue1 = new LinkedList<String>();
queue2 = new LinkedList<String>();
}
public String pop(){
String re =null;
if(queue1.size() == 0 && queue2.size() == 0){
return null;
}
if(queue2.size() == 0){
while(queue1.size() >0){
re = queue1.removeFirst();
if(queue1.size() != 0){
queue2.addLast(re);
}
}
}else if(queue1.size() == 0){
while(queue2.size() >0){
re = queue2.removeFirst();
if(queue2.size()!=0){
queue1.addLast(re);
}
}
}
return re;
}
public String push(String str){
if(queue1.size() ==0 && queue2.size() == 0){
queue1.addLast(str);
}else if(queue1.size()!=0){
queue1.addLast(str);
}else if(queue2.size()!=0){
queue2.addLast(str);
}
return str;
}
public static void main(String[] args) {
E07StacksWithTwoQueue stack=new E07StacksWithTwoQueue();
String tmp;
stack.push("1");
stack.push("2");
stack.push("3");
tmp=stack.pop();
System.out.println(tmp);//3
stack.push("4");
tmp=stack.pop();
System.out.println(tmp);//4
tmp=stack.pop();
System.out.println(tmp);//2
stack.push("5");
stack.push("6");
tmp=stack.pop();
System.out.println(tmp);//6
tmp=stack.pop();
System.out.println(tmp);//5
tmp=stack.pop();
System.out.println(tmp);//1
}
}
题目六: 旋转数组的最小数字
题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增的排序的数组的一个旋转,输出旋转数组的最小元素。例如输入{1,2,3,4,5}的一个旋转为{3,4,5,1,2},该数组的最小值为1。
解析:旋转数组的特点:
(1)递增排序的数组旋转之后的数组可划分为两个排序的子数组;
(2)前面的子数组的元素都大于或等于后面子数组的元素;
(3)最小的元素刚好是两个子数组的分界线;
(4)旋转数组在一定程度上是有序的;
在有序的数组中可以用二分查找实现O(logn)的查找,我们也可用二分查找的思想寻找旋转数组的最小数字。
思路:
1. 设置两个指针,初始状态第一个指针指向前面子数组的第一个元素,第二个指针指向后面子数组的最后一个元素;
2. 找到两个指针的中间元素;
3.若其大于等于第一个指针指向的元素,则说明其在前面的子数组中,且显然最小元素在中间元素的右边,若其小于等于第二个指针指向的元素,则说明其在后面的子数组中,且显然最小元素在中间元素的左边。
如此,便可以缩小搜索范围,提高时间复杂度,最终第一个指针指向前面子数组的最后一个元素,而第二个指针指向后面子数组的第一个元素,它们处于相邻位置,而第二个指针指向的刚好是最小的元素。如下图:
注意:当两个指针指向的数字及它们中间的数字三者相等时,无法判断中间数字位于前面的子数组还是后面的子数组,也就无法移动两个指针来缩小查找的范围,此时只能用顺序查找的方法。如下图:
package Solution;
/**
* 剑指offer面试题8:旋转数组的最小数字
* 题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序数组的一个旋转,输出旋转数组的最小元素。
* 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1
* 旋转数组的特点:
* 1,旋转之后的数组可以分为两个排序的子数组,且前面的子数组的元素都大于或等于后面子数组的元素。
* 2,最小或者最大元素位于两个子数组的分界
* 解题思路:在排序数组中,可以使用二分查找法进行查找
* 利用左右两个指针和中间指针元素大小关系判断中间元素是位于前面的子数组还是后面的子数组,缩小查找范围
* 需要考虑的特例:
* 1,排序数组本身仍是该数组的一个旋转
* 2,如果数组内有重复元素,那么有可能出现左右两指针元素与中间元素相同,无法判断中间元素属于前面子数组还是后面子数组,此时用顺序查找
* @author GL
*
*/
public class No8FindMinInRotatedSortedArray {
public static void main(String[] args) {
int[] array1={3,4,5,1,2};
System.out.println("升序排序数组的一个旋转数组,无重复数字:"+findMin(array1));
int[] array2={1,2,3,4,5};
System.out.println("升序排序数组:"+findMin(array2));
int[] array3={1};
System.out.println("只包含一个数字的数组:"+findMin(array3));
int[] array4={2,4,2,2,2,2,2};
System.out.println("包含重复元素的数组:"+findMin(array4));
int[] array5=null;
System.out.println("null数组:"+findMin(array5));
}
public static int findMin(int[] array){
if(array==null||array.length<=0)
throw new RuntimeException("invalid array");
int indexLow=0;
int indexHigh=array.length-1;
//旋转数组为排序数组本身的特例
int indexMiddle=indexLow;
while(array[indexLow]>=array[indexHigh]){
if(indexHigh-indexLow==1){
indexMiddle=indexHigh;
break;
}
//考虑两侧数字和中间数字相同的特殊情况,采用顺序查找算法查找最小值
indexMiddle=(indexLow+indexHigh)/2;
if(array[indexMiddle]==array[indexLow]&&array[indexHigh]==array[indexLow])
return findMinInOrder(array,indexLow,indexHigh);
if(array[indexMiddle]>=array[indexLow])
indexLow=indexMiddle;
else
indexHigh=indexMiddle;
}
return array[indexMiddle];
}
//顺序查找
private static int findMinInOrder(int[] array,int indexLow,int indexHigh){
int result=array[indexLow];
for(int i=indexLow+1;i<indexHigh;i++){
if(array[i]<result)
result=array[i];
}
return result;
}
}
题目七: 斐波那契数列
其实改进的方法比并不复杂。上述的递归代码之所以慢是因为重复的计算太多,我们只要想避免重复计算就型了。比如我们可以把已经得到的数列中间项保存起来,如果下次需要计算的时候我们先查找一下,如果前面已经计算过就不用重复计算了。
更简单的方法是从下往上计算,首先计算f(0)和f(1)算出f(2),再根据f(1)和f(2)算出f(3)……依次类推就可以算出第n项了。很容易理解,这种思路的时间复杂度为O(n)。
package Solution;
/**
* 剑指offer面试题9:斐波那契数列
* 题目:写一个函数,输入n,求斐波那契数列的第n项。
* 0, n=1
* 斐波那契数列定义如下:f(n)= 1, n=2
* f(n-1)+f(n-2), n>2
* @author GL
*
*/
public class No9Fibonacci {
public static void main(String[] args) {
System.out.println("第4项斐波那契数列的值为:"+fibonacci(4));
}
/*
* 采用递归实现斐波那契数列生成函数,效率低
*/
public static int generateFibonacci(int n){
if(n==0)
return 0;
if(n==1)
return 1;
return generateFibonacci(n-1)+generateFibonacci(n-2);
}
/*
* 采用循环实现斐波那契数列
* 存储数列中间项,求得结果
*/
public static int fibonacci(int n){
int[] result={0,1};
if(n<2)
return result[n];
int fibNMinusOne=1;
int fibNMinusTwo=0;
int fibN=0;
for(int i=2;i<=n;i++){
fibN=fibNMinusOne+fibNMinusTwo;
fibNMinusTwo=fibNMinusOne;
fibNMinusOne=fibN;
}
return fibN;
}
}
题目八: 跳台阶/ 变态跳台阶
描述:
1, 如果青蛙一次可以跳上1级台阶, 2级台阶........ n级台阶. 求该青蛙跳上一个n级台阶总共有多少中跳法.
2,一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
1, 递归,斐波那契数列
2, 递归思路:
(1)一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
用Fib(n)表示青蛙跳上n阶台阶的跳法数,青蛙一次性跳上n阶台阶的跳法数,是定Fib(0)=1;
当n = 1时,只有一种跳法,即1阶跳:Fib(1) = 1;
当n = 2时,有两种跳法,一阶跳和二阶跳:Fib(2) = Fib(1)+FIb(0) = 2;
当n =3时,有三种跳法,第一次跳出一阶后,后面还有Fib(3-1)中跳法;第一次跳出二阶后,后面还有Fib(3-2)中跳法;第一次跳出三阶后,后面还有Fib(3-3)中跳法
Fib(3)= Fib(2)+Fib(1)+Fib(0) = 4
当n= n时,共有n种跳法方式,第一次跳出一阶后,后面还有Fib(n-1)种跳法;第一次跳出二阶后,后面还有Fib(n-2)种跳法,第一次跳出n阶后,后面还有Fib(n-n)种体哦啊发。
Fib(n) = Fib(n-1)+Fib(n-2)+Fib(n-3)+..........+Fib(n-n)=Fib(0)+Fib(1)+Fib(2)+.......+Fib(n-1)
又因为Fib(n-1)=Fib(0)+Fib(1)+Fib(2)+.......+Fib(n-2)
两式相减得:Fib(n)-Fib(n-1)=Fib(n-1) =====》 Fib(n) = 2*Fib(n-1) n >= 2
递归等式如下:
分析:这道题最近经常出现,包括MicroStrategy等比较重视算法的公司都曾先后选用过个这道题作为面试题或者笔试题。
首先我们考虑最简单的情况。如果只有1级台阶,那显然只有一种跳法。如果有2级台阶,那就有两种跳的方法了:一种是分两次跳,每次跳1级;另外一种就是一次跳2级。
现在我们再来讨论一般情况。我们把n级台阶时的跳法看成是n的函数,记为f(n)。当n>2时,第一次跳的时候就有两种不同的选择:一是第一次只跳1级,此时跳法数目等于后面剩下的n-1级台阶的跳法数目,即为f(n-1);另外一种选择是第一次跳2级,此时跳法数目等于后面剩下的n-2级台阶的跳法数目,即为f(n-2)。因此n级台阶时的不同跳法的总数f(n)=f(n-1)+(f-2)。
我们把上面的分析用一个公式总结如下:
题目八: 矩形覆盖
描述:我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
思路:
利用动态规划,一共有n列,若从左向右放小矩形,有两种放置方式:
第一种:横着放,即占用两列。此时第二行的前两个空格只能横着放,阵
第二种:竖着放,
观察题目中的矩形,2*n的,是个长条形。本来脑中想象的是复杂的华容道,但是既然只是简单的长条形,那么依然逆向分析。既然是长条形的,那么从后向前,最后一个矩形2*2的,只有两种情况:
public class CoverRec {
/**
* 解决方法:
* 依旧是斐波那契数列:
*
* @param target
* @return
*/
public static int RectCover(int target){
if (target <= 1){
return 1;
}else if (target == 2){
return 2;
}else {
return RectCover(target-1) + RectCover(target-2);
}
}
public static void main(String[] args) {
System.out.println(RectCover(10));
}
}
题目九: 二进制中1的个数
描述: 输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
解题思路:
- 解法一: 这是一个比较讨巧的做法,将二进制转换为String,同时将0全部用替换为"",然后返回String长度;
- 解法二: 将n与(n-1)相与,会将n最右边的1去掉;
举例: n=10 1010&1001=1000 --> 1000&0111=0000; 两次,即10的二进制数中有两个1;
其一:简单的转换成2进制数进行遍历
其二:通过与其差一位的数进行与运算,统计运算次数
原理:如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1(如果最右边的1后面还有0的话)。其余所有位将不会受到影响。
举个例子:一个二进制数1100,从右边数起第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成了1,而前面的1保持不变,因此得到的结果是1011.我们发现减1的结果是把最右边的一个1开始的所有位都取反了。这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。如1100&1011=1000.也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。
public class sumOf1 {
public int NumberOf1(int n) {
int result = 0;
char[] s = Integer.toBinaryString(n).toCharArray();
for (int i = 0; i < s.length; i++) {
if(s[i] == '1'){
result++;
}
}
return result;
}
public int NumberOf2(int n) {
int sum = 0;
while(n!=0){
sum++;
n = n & (n-1);
}
return sum;
}
public static void main(String[] args) {
int result = new sumOf1().NumberOf2(11);
System.out.println(result);
}
}
题目十: 数值的整数次方
描述:
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
解题思路: 代码
先判断指数的正负性,然后直接数学思路进行数相乘;
当然也可以直接使用Math.power(base,exponent);
package Solution;
/**
* 剑指offer面试题11:数值的整数次方
* 题目:实现函数double power(double base,int exponent),求base的exponent次方。
* 不得使用库函数,同时不需要考虑大数问题。
* 解题思路:实现数值的n次方就是将一个数自身连乘n次
* 底数要考虑到正数、负数和零的情况
* 指数要考虑到正整数,负整数和零的情况。因此可能的情况有九种,其中尤其要注意底数为0,指数为负数的情况下是无意义的,因此要做特殊处理
* 指数为负数的乘方运算,可先按照指数为正求得,然后求倒数得到真正结果
* 解法一:全面不高效,考虑到所有边界条件和负面测试
* 解法二:全面高效完美的算法,使用公式a^n=a^(n/2)*a^(n/2) n是偶數
* a^(n/2)*a^(n/2)&a n是奇数
* @author GL
*
*/
public class No11Power {
public static void main(String[] args) {
System.out.println("底数为2,指数为2运算结果为:"+power(2,2));
System.out.println("底数为2,指数为-2运算结果为:"+power(2,-2));
System.out.println("底数为2,指数为0运算结果为:"+power(2,0));
System.out.println("底数为-2,指数为2运算结果为:"+power(-2,2));
System.out.println("底数为-2,指数为-2运算结果为:"+power(-2,-2));
System.out.println("底数为-2,指数为0运算结果为:"+power(-2,0));
System.out.println("底数为0,指数为2运算结果为:"+power(0,2));
System.out.println("底数为0,指数为0运算结果为:"+power(0,0));
System.out.println("底数为2,指数为1运算结果为:"+power(2,1));
System.out.println("底数为0,指数为-2运算结果为:"+power(0,-2));
}
public static double power(double base,int exponent){
if(equal(base,0.0)&&exponent<0)
throw new RuntimeException("while exponent is minus,the base can't be zero");
int absExponent=exponent;
if(exponent<0)
absExponent=~exponent+1;//整数按位取反+1得到他的相反数
//double result=powerWithUnsignedExponent(base,absExponent);
double result= powerWithUnsignedExponentByRecursion( base, absExponent);
if(exponent<0)
result=1.0/result;
return result;
}
//解法一:求指数为非负数时候的乘方运算,连乘
private static double powerWithUnsignedExponent(double base, int absExponent) {
double result=1.0;
for(int i=1;i<=absExponent;i++){
result=result*base;
}
return result;
}
//解法二:计算一个数的乘方的时候,通过计算指数的一半次,得到的结果相乘即可得到,这样计算量会大大减少
private static double powerWithUnsignedExponentByRecursion(double base,int exponent){
if(exponent==0)
return 1.0;
if(exponent==1)
return base;
double result= powerWithUnsignedExponentByRecursion(base,exponent>>1);
result=result*result;
if((exponent&0x1)==1)
result=result*base;
return result;
}
//浮点数由于精度问题不能用==判断是否想等,如果两数满足一定条件精度,可以看做相等
private static boolean equal(double number1,double number2){
if(number1-number2>-0.0000001&&(number1-number2)<0.0000001)
return true;
return false;
}
}
这里利用了公式:
a^n = a^(n/2)* a^(n/2)*base
其中n为偶数时, base = 1; n为奇数时,base = a.
另外:使用位运算提高效率
除以2等价于 >> 1
模 2等价于 & 0x1
参考文章:http://www.jianshu.com/p/fa9dcbc88a8e http://blog.youkuaiyun.com/lilianforever/article/details/51836923
http://www.cnblogs.com/gl-developer/p/6475399.html