剑指offer
概述
本文记录自己在刷剑指offer时候的解题,无他。。。
第一题
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
解题思路:
假设数组为:
[[1 2 3]
[2,3,4]
[3,4,5]]
从第一行的最后一个元素开始比较,也就是是3,如果目标元素比他大,那一定比第一行的所有元素都大,直接把第一行的所有元素都排除了,之后从第二行最后一个元素开始,也就是4,同样的判断,直到第三行第一个元素结束判断。
解题是遇见的问题:
- 不清楚怎么使用Java构建二维数组,牛客网那个提示太垃圾,其实一开始写对了
- 不清楚怎么获取二维数组的行的长度和列的长度,同样,牛客网那个不能实验。
代码
public class Solution { public boolean Find(int target, int [][] array) { int column = array.length-1; int row = 0; while(column >= 0 && row < array[0].length){ if(array[row][column] > target){ column--; }else if(array[row][column] < target){ row++; }else{ return true; } } return false; } }
注:牛客网中自己写main函数是不管用的,因为他们已经为这个定义好的方法写好了单元测试代码,直接使用那个代码来判断你写的代码是否正确
第二题
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
解题思路:
这个相对来说比较简单,就是把空格替换成%20。
解题遇到的问题:
牛客网上使用的是StringBuffer,这个玩意的api用的确实是不熟练,在网站上又无法查看api,而且这个玩意没有repalceAll这个方法,他的replace方法和String的replace也不同,整个来说体验很差。
代码
public class Solution { public String replaceSpace(StringBuffer str) { //StringBuffer stringBuffer = new StringBuffer("we are happy"); for (int i = 0; i < str.length(); i++) { if (str.charAt(i) == ' '){ str.replace(i,i+1,"%20"); } } return str.toString(); } }
第三题
输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
解题思路:
这道题其实就是把链表中的数据给反转一下,有两种思路一种是使用栈,由于栈后进先出,就导致列表末尾的元素先出来,就可以倒叙了,使用递归也可以解决。
解题遇到的问题:
牛客网上返回的ArrayList,在平时写程序的时候都是使用泛型,就是List list = new ArrayList();但是在牛客网上这个就不行,看来牛客网还有很大的改进空间。
代码
使用栈实现
import java.util.ArrayList; import java.util.List; import java.util.Stack; public class Solution { public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { Stack stack = new Stack(); while(listNode != null){ stack.push(listNode.val); listNode = listNode.next; } ArrayList<Integer> list = new ArrayList(); while(!stack.isEmpty()){ list.add((Integer)stack.pop()); } return list; } }
使用递归实现
import java.util.ArrayList; import java.util.Stack; public class Solution { private ArrayList<Integer> re = new ArrayList<Integer>(); public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { if(listNode==null) return re; printListFromTailToHead(listNode.next); re.add(listNode.val); return re; } }
第四题
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
解题思路:这个主要是根据前序遍历和中序遍历的性质来解决,前序遍历是先遍历根节点,而中序遍历是先遍历左节点,之后是根节点,就利用这个性质,通过不断地缩小二叉树的大小(实际上是不断缩小前序遍历和中序遍历的范围,当缩小到最小的二叉树的时候,比如根节点只有一个子节点,或者根节点有两个子节点的时候,根据前序和中序遍历的结果可以直接知道这个最小子树到底是什么样子的,当知道了最小的,然后再找次小的。。。)
过程解析:
前序:{1,2,4,7,3,5,6,8}
中序:{4,7,2,1,5,3,8,6}
第一次递归:前序的第一个节点为根节点,也就是1,根据中序遍历,根节点1的左子树为{2,4,7},右子树子树为{3,5,6,8},然后左右子树单独处理
第二次递归:先看左子树,{2,4,7},把2作为左子树根节点,根据中序遍历{4,7,2}可以知道,2的左子树为{4,7},没有右子树。对应的右子树{3,5,6,8}和上面的分析方式一致
第三次递归:同样把4作为根节点,根据中序遍历{4,7}可知,4没有左子树,有一个右子树{7},通过这三次递归,其实就把每个根节点(注意这里其实二叉树只有一个根节点,这里为了方便说明,准确来说应该是非叶子节点)的左子树都搞好了
....
之后把右子树也按照上面的方式处理就可以了。
总结:以上的过程主要使用的逻辑就是把二叉树不断地拆分,直到拆分成可以确定一个最简单的二叉树为止,只有一点点的在拼接到一起就构成二叉树了。
解题遇到的问题:这个题如果不是通过程序实现,我是可以构建出来二叉树的,但是通过程序实现就有点难想,主要是两个遍历怎么结合,很难想
代码
import java.util.Map; import java.util.HashMap; public class Solution { public TreeNode reConstructBinaryTree(int [] pre,int [] in) { Map<Integer,Integer> map = new HashMap(); for(int i=0;i<in.length;i++){ map.put(in[i],i); } int index = map.get(pre[0]);
//这里注意是length-1,不是length,不然会发生数组越界异常 return pre(pre,0,pre.length-1,in,0,in.length-1,map); } public static TreeNode pre(int[] pre,int preStart,int preEnd,int[] in,int inStart,int inEnd,Map<Integer,Integer> map){ if(preStart > preEnd){ return null; } TreeNode node = new TreeNode(pre[preStart]); int index = map.get(pre[preStart]); node.left = pre(pre,preStart+1,preStart+index-inStart,in,inStart,index-1,map); node.right = pre(pre,preStart+index-inStart+1,preEnd,in,index+1,inEnd,map); return node; } }
第五题
用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
解题思路:第一个栈只负责队列的插入操作,第二个栈负责队列的出队操作,第一个栈一直插入即可,第二个栈一直出栈即可,如果发现第二个栈为空了,就把第一个栈中的数据放入到第二个栈中就可以了。
代码
import java.util.Stack; public class Solution { Stack<Integer> stack1 = new Stack<Integer>(); Stack<Integer> stack2 = new Stack<Integer>(); public void push(int node) { stack1.push(node); } public int pop() { if(stack2.isEmpty()){ while(!stack1.isEmpty()){ stack2.push((Integer)stack1.pop()); } } return (Integer)stack2.pop(); } }
第六题
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
解题思路:本题如果直接从数组中找最小的数,那方法太多了,但是直接遍历查找时间复杂度为O(n),所以这个题其实主要考察优化的,其实这个题中我已开始看有一个误解,题中说输入一个非递减排序数组,我理解成了不是递减排序的数组,那他的反面就是未排序和递增排序数组,导致我在解决本体时出现偏差,其实作者意思应该是严格递增排序和非严格递增排序。
举例:严格递增排序[1,2,3,4,5],旋转数组可以为:[4,5,1,2,3]
非严格递增排序[0,1,1,1,1,1],旋转数组可以为[1,1,1,1,0,1],也可以为[1,0,1,1,1,1]
思路:采用二分查找法
第一种情况:array[high] < array[mid],那最小值一定在mid右边
第二种情况:array[high] = array[mid],这种情况就不确定了,比如上面举例中的非严格递增排序,就不确定 最小值在mid的左边还是右边了,这是让high = high - 1;
第三种情况:array[high] > array[mid],那最小值一定为mid,或者在mid左边
代码
import java.util.ArrayList; public class Solution { public int minNumberInRotateArray(int [] array) { if(array.length == 0){ return 0; } int mid; int low = 0; int high = array.length-1; while(low < high){ mid = (low + high)/2; if(array[mid]>array[high]){ low = mid + 1; }else if(array[mid] == array[high]){ high = high - 1; }else{ high = mid; } } return array[low]; } }
第七题
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。 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-2)+Fibonacci(n-1); } }
使用迭代实现
public class Solution { public int Fibonacci(int n) { if(n == 0){ return 0; } if(n == 1){ return 1; } int a = 0; int b = 1; int c = 0; for(int i = 0;i < n-1;i++){ c = a + b; a = b; b = c; } return c; } }
第八题
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)
解题思路:
先说一下正解:
这个题从第一步来看,其实只有两种调法,要么跳1级,要么跳2级,下面看一下具体的分析
- 如果第一步跳1级,剩下n-1级,那剩下的台阶跳法就是f(n-1)
- 如果第一步跳2级,剩下的就是n-2级,那剩下的跳法就是f(n-2)
- 所以总结下来f(n) = f(n-1)+f(n-2),其实就是个斐波那契数列
只要把上面那个第七题的初始条件改一下就可以了,当n = 1,的时候f(1) = 1;当n = 2的时候f(2)=2。
代码
public class Solution { public int JumpFloor(int target) { if(target <= 0){ return 0; } if(target == 1){ return 1; } if(target == 2){ return 2; } int a = 1; int b = 2; int c = 0; for(int i = 3;i <= target;i++){ c = a + b; a = b; b = c; } return c; } }
在说一下我当时第一次做这个题的时候是怎么想的(提示:这个答案是有问题,当n<24,结果和上面的结果一致,当n>=24的时候就不一致了,暂时不知道哪里有问题)
思路:
既然n级的台阶,只能由1级和2级组成,那就可以使用排列组合的思想,举例如下:
比如:4级的台阶,有以下几种情况
- 只有1级跳,没有2级跳,总共跳了4次,对应的跳法就是C04 = 1;
- 有一个2级跳,两个1级跳,总共跳了3次,对应的跳法就是C13 = 3;
- 有两个2级跳,没有1级跳,总共跳了2次,对应的跳法就是C22 = 1;
- 所以总数就是 1 + 3 + 1 = 5;
上面的思路中那个C31的意义我想大家都知道,我就不解释了,这个思路其实就分为两步:
- 第一步:既然所有的跳法都是由1级和2级构成,那就穷举所有跳2级的方法,比如0个,1个,2个...........
- 第二步:计算每一种跳2级的方法中总共有多少种跳法
代码实现如下
public static int JumpFloor(int target) { if(target <= 0){ return 0; } int num = 0;
//有多少种跳法 int method = 0;
//为了计算阶乘,因为没有使用公式,下面是使用for循环做的 int temp1 = 1; int temp2 = 1; while(target-num*2>=0){ if(num == 0){ method++; }else{ if(target == num*2){ method++; }else{ int n = target-num; temp1 = 1; temp2 = 1;
//计算阶乘 for(int i = n;i>=n-num+1;i--){ temp1 = temp1*i; } for(int i=1;i<=num;i++){ temp2 = temp2*i; } method = method + temp1/temp2; } } num++; } return method; }
总结:我和斐波那契的解法做了个对比,当n<24结果都是一致的,当n>=24就不同了,目前还没有明白为什么,有大佬看出来可以告诉我一下
写在最后的话
8是个吉利的数字,就8结束,刷题确实很慢,不过慢慢总结规律也还好,很多题自己慢慢可以想出来怎么处理,但是效果往往很差,无论是否是为了面试,觉得训练思维也挺不错的,如果有看到这里的小伙伴,其实可以直接去牛客网刷题,而不用看我在这里bb,我只是为了记录一下在做这些题时候的一些新得,为了以后自己在看的时候,容易想起来自己当初是怎么想的,怎么解决的。