剑指offer 一
首先声明下啊,本人是个辣鸡,各类题目都是在前辈们总结下完成的,并不全是自己的思想。我只是将自己的理解和看法整理了一下。题目由于二叉树之类的不太熟练,所以在这一章有关它的题会跳过。其他的题目尽量讲解。可能说不清楚,大家想看能看就看。还有请大家自行打开牛客网题目,对着看,因为我不想复制题目。
本人喜欢先贴代码,然后讲解,大家多担待~
二维数组中的查找:
public boolean Find(int target, int [][] array) {
int height=array.length-1;
int i=0;
//从左下角开始查找
while (height>=0 && i<=array[0].length-1){
if (array[height][i]>target)
height--;
else
if (array[height][i]<target)
i++;
else
return true ;
}
return false;
}
我是这样理解的,从左下角查找,比左上角找要好。如果从左上角找,比他大的数会有两个分支。但是如果选择左下角查找
比他大就往右,比他小就往下,清晰明了。同理选择右上角也是可以的。while的结束条件,就是不要超过数组范围就好。
替换空格:
/**
public String replaceSpace(StringBuffer str) {
String str1=str.toString();
String str2=str1.replaceAll(" ","%20");
return str2;
}
*/
正确解法
public String replaceSpace(StringBuffer str) {
String str1=str.toString();
char [] stchar=str1.toCharArray();
StringBuffer stb=new StringBuffer();
for (int i=0;i<stchar.length;i++){
if (stchar[i]==' ')
stb.append("%20");
else
stb.append(stchar[i]);
}
return stb.toString();
}
如果使用第一种方式,估计要被暴打。这他妈也太残暴了,面试官肯定是不让用的。
第二种的思想,就是先将stringbuffer转成string,然后转成字符数组。进行遍历,如果为 ' ' 就append%20,否则就appen 原来的
注意stchar[i]==' ',用的是单引号。怕用string用多了,改不了。
从尾到头打印链表
借助栈来实现
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list=new ArrayList<Integer>();
Stack<Integer> stack=new Stack<Integer>();
while (listNode!= null){
stack.push(listNode.val);
listNode=listNode.next;
}
while (!stack.isEmpty()){
list.add(stack.pop());
}
return list;
}
递归实现
public class Solution {
ArrayList<Integer> list=new ArrayList<Integer>();
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
if (listNode!= null){
this.printListFromTailToHead(listNode.next);
list.add(listNode.val);
}
return list;
}
}
还有一种没有想到的可以用add(0,val)的方法,这样不需要借助栈
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list=new ArrayList<Integer>();
while (listNode != null){
list.add(0,listNode.val);
listNode=listNode.next;
}
return list;
}
栈实现:看代码好理解,就是先将链表所有值按顺序存在栈中,这样栈中的值就是原来相反顺序的值,然后取出来给list。
递归实现:额,递归怎么说呢?就是不断的往里深入,要注意判断条件listNode!=空时才能继续调用自己方法往下深入(有时候条件比较难找,需自己找好递归条件),直到链表最后一个元素。有多人很多时候不知道递归调用自己方法之后的代码怎么写,其实你可以这样看,假设你不断递归到了最后一个节点,这个时候你需要做的事,就是你写的代码,比如这,你需要添加值到list里面去,所以这里就写这个代码。
arraylist实现:这个方法也简单,一时半会没想起,就是利用add(0,listNode.val)方法,意思是在0位添加值,如果还有元素加到0位,那么原来在0位的数就会往后移动到1位。如此往复。
用两个栈实现队列
public class Solution {
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
//进栈用stack1
public void push(int node) {
stack1.push(node);
}
public int pop() {
if (stack1.isEmpty()&&stack2.isEmpty()){
throw new RuntimeException("Queue is empty!");
}
if (stack2.empty()){
while (!stack1.empty()){
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
}
核心思想:一个栈作为存放元素的栈,一个栈作为取元素的栈。可以这样认为,存元素时用stack1,取元素时,在取元素之前判断一下,stack2有没有元素,没有的话,将stack1所有值给stack2.这样stack2中所有元素都是先进的按顺序的元素。且这一次加入到stack2中所有元素取完,才能去stack1中加入。如果没有取完就去加,会打乱顺序。也可以理解为两个容器,每次都是倾巢出动,stack1将所有元素加入stack2,stack2也要全部用完才能继续加。
旋转数组的最小数字
public int minNumberInRotateArray(int [] array) {
if (array.length == 0){
return 0;
}
int right=0,left=array.length-1;
while (right < left){
int mid= right+(left-right)/2;
if ( array[ left ]<array[ mid ])
right=mid+1;
else
if (array[ left ]==array[ mid ])
left=left-1;
else
left=mid;
}
return array[right];
}
其实这题考虑的问题还是挺多的。首先我们可以把他分为两个部分,左边部分一定是大于等于右边,二这两个部分的分界线就是最小值。那么现在可以根据二分法来操作,判断条件还是需要考虑的。如果中间值大于右边值说明中点一定在左边部分,因为左边部分一定比右边大。所以right=mid+1(别问我为啥+1,你应该知道的),如果右边值和中间值一样,这时候你最后别轻举妄动,这个时候你根本不知道中点属于右边还是左边。就像11111101和120111111一样,这个时候不要轻易将中点赋给两边。将left后退一个元素。如果右边值大于中间值,说明中点在右部分,将left=mid,有同学就要问了,为啥不是left=mid-1?
原因是这样的,就靠这二分法,咱最后肯定会出现只有两个元素的时候,就例如1 0 和 0 1 这两种情况,最小值是0;第一种经过判断right=mid+1,得到的就是最小值没有影响。但是如果第二种我们用left=mid-1;那么就糟糕了,此时mid就是中点不能减一了。
斐波那契数列
public int Fibonacci(int n) {
if (n<=1)
return n;
int result=0;
int one =0;
int two=1;
for (int i=2;i<=n;i++){
result=one+two;
one=two;
two=result;
}
return result;
}
十分抱歉没有找到特别好的算法。这题一拿到大部分人觉得有递归,但是递归在这里运算量有点大,容易造成栈溢出。所以我们还是用迭代吧。感觉这个算法没什么好讲的,3=2+1类似的算法规律。
跳台阶
public int JumpFloor(int target) {
if (target <= 2)
return target;
else {
return JumpFloor(target-1)+JumpFloor(target-2);
}
}
把前几个数列列出来就能找到规律了。
| 1, (n=1)
f(n) = | 2, (n=2)
| f(n-1)+f(n-2) ,(n>2,n为整数)
还是斐波那契数列。
变态跳台阶
public int JumpFloorII(int target) {
if(target<=1){
return target;
}
else {
return JumpFloorII(target-1)*2;
}
}
| 0 ,(n=0 )
f(n) = | 1 ,(n=1 )
| 2*f(n-1),(n>=2)
这个真的就是找规律,找不出规律那就别想用简单方法解开。
矩形覆盖
public int RectCover(int target) {
if (target<=2){
return target;
}
else{
return RectCover((target-1))+RectCover(target-2);
}
}
嗨,不说了,自己体会吧
二进制中1的个数
//---------------正解--------------------------------
//思想:用1(1自身左移运算,其实后来就不是1了)和n的每位进行位与,来判断1的个数
private static int NumberOf1_low(int n) {
int count = 0;
int flag = 1;
while (flag != 0) {
if ((n & flag) != 0) {
count++;
}
flag = flag << 1;
}
return count;
}
//--------------------最优解----------------------------
public static int NumberOf1(int n) {
int count = 0;
while (n != 0) {
++count;
n = (n - 1) & n;
}
return count;
}
首先讲解第一种:思想就是将1不断的与n&运算,之后左移动一位,如果与的结果不为0,就代表含有1.觉得唯一不好的地方就是,结束循环的条件是1不断向左移,直到溢出,到达int类型的最大的一位。很多人这时就会有疑问了,既然这个条件,为什么不把n向右移动呢?那样就可以用作为判断条件了。的确可以,但是忽略了一点,假如n为负数呢?向右移动,左边是用1来补位。到那时候就无限个1了。所以这个只能用于非负数。
第二种:觉得大家有两个疑惑,首先就是为什么n不等于0就可以++count?其实啊,如果一个数不为0,那么他的二进制一定是含1的,所以可以直接给他+1;第二个疑惑?为啥用n-1&n呢?其实就好比这样理解,将一个数-1后,是将右边第一个1减去,而1后面的所有0变成1 ,就好像11000-1=101111 一样。这样相与后10000,就直接去掉了一个1.n&n-1,可以这样理解得到的结果是右边第一个1之后的所有数都等于0(包括这个1位);那么第二个1,第三个1保持不变。
数值的整数次方
public double Power(double base, int exponent) {
double result =1.0;
for (int i=0;i<Math.abs(exponent);i++){
result*=base;
}
if (exponent>=0)
return result;
else
return 1/result;
}
另外一种比较牛逼的算法
ublic double Power(double base, int n) {
double res = 1,curr = base;
int exponent;
if(n>0){
exponent = n;
}else if(n<0){
if(base==0)
throw new RuntimeException("分母不能为0");
exponent = -n;
}else{// n==0
return 1;// 0的0次方
}
while(exponent!=0){
if((exponent&1)==1)
res*=curr;
curr*=curr;// 翻倍
exponent>>=1;// 右移一位
}
return n>=0?res:(1/res);
}
第一种算法:比较简单,先不管正负数,取绝对值。循环累乘。最后判断指数是否为负数,是负数就取倒数,不是返回原来结果
第二种:大家自己琢磨
调整数组的顺序使奇数位于偶数前面
public void reOrderArray(int [] array) {
int [] mixAaary=new int[array.length];
int j=0;
for (int i=0;i<array.length;i++){
if (array[i]%2 != 0){
mixAaary[j]=array[i];
++j;
}
}
for (int k=0;k<array.length;k++){
if (array[k]%2 == 0){
mixAaary[j]=array[k];
++j;
}
}
for (int i=0;i<array.length;i++){
array[i]=mixAaary[i];
}
}
第二种方法
public void reOrderArray(int [] array) {
for (int i=0;i<array.length-1;i++){
for (int j=0;j<array.length-i-1;j++){
if (array[j]%2==0 && array[j+1]%2!=0){
int temp=array[j+1];
array[j+1]=array[j];
array[j]=temp;
}
}
}
}
}
第一种比较简单:通过构造一个新的数组,一次循环先将奇数加入数组,第二次循环将偶数加入到数组,最后别忘了将临时变量数组赋给array。(他这题是要将array中数改变顺序,而不是得到一个新排序数组)
第二种:相当于冒泡法,前偶后奇就替换。
链表中倒数第k个结点
public ListNode FindKthToTail(ListNode head,int k) {
if (head==null){
return null;
}
int length=0;
ListNode node=head;
while (node != null){
node=node.next;
++length;
}
if (k>length || k<=0)
return null;
for (int i=1;i<=length-k;i++){
head=head.next;
}
return head;
}
第二种
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
if(head==null||k<=0){
return null;
}
ListNode pre=head;
ListNode last=head;
for(int i=1;i<k;i++){
if(pre.next!=null){
pre=pre.next;
}else{
return null;
}
}
while(pre.next!=null){
pre = pre.next;
last=last.next;
}
return last;
}
第一种:先进行一次遍历,求出长度然后求出倒数k个的值。
第二种:定义两个指针都指向头部,先让第一个指针走k-1步,然后两个指针一起向后走,直到第一个指针为空,此时第二个指针就是倒数第k个。就是个很简单的数学问题,画个图就明白了。
反转链表
public ListNode ReverseList(ListNode head) {
if(head==null)
return null;
//head为当前节点,如果当前节点为空的话,那就什么也不做,直接返回null;
ListNode pre = null;
ListNode next = null;
//当前节点是head,pre为当前节点的前一节点,next为当前节点的下一节点
//需要pre和next的目的是让当前节点从pre->head->next1->next2变成pre<-head next1->next2
//即pre让节点可以反转所指方向,但反转之后如果不用next节点保存next1节点的话,此单链表就此断开了
//所以需要用到pre和next两个节点
//1->2->3->4->5
//1<-2<-3 4->5
while(head!=null){
//做循环,如果当前节点不为空的话,始终执行此循环,此循环的目的就是让当前节点从指向next到指向pre
//如此就可以做到反转链表的效果
//先用next保存head的下一个节点的信息,保证单链表不会因为失去head节点的原next节点而就此断裂
next = head.next;
//保存完next,就可以让head从指向next变成指向pre了,代码如下
head.next = pre;
//head指向pre后,就继续依次反转下一个节点
//让pre,head,next依次向后移动一个节点,继续下一次的指针反转
pre = head;
head = next;
}
//如果head为null的时候,pre就为最后一个节点了,但是链表已经反转完毕,pre就是反转后链表的第一个节点
//直接输出pre就是我们想要得到的反转后的链表
return pre;
}
这是牛客网某位大佬的思路。上面也讲的挺清楚的,来讲讲我自己的看法。
为什么用head!=nul,而不是head.next!=null呢?是这样的,当head为空时,pre正好就是反转链表后的头结点。
用next=head.next是为了保证后面链表的完整性,不让他断了。因为后面head.next=pre,这时候断成两个链表了。
pre=head。将头结点赋给pre。我画了个图,大家将就看吧。
合并两个排序的链表
public ListNode Merge(ListNode list1,ListNode list2) {
if (list1 == null)
return list2;
if (list2 == null)
return list1;
ListNode node=null;
if (list1.val<list2.val){
node=list1;
node.next=Merge(list1.next,list2);
}
else {
node=list2;
node.next=Merge(list1,list2.next);
}
return node;
}
递归:因为两个链表都是单调递增的,所以当其中一个链表为空时,可以直接返回另一个。
如果lilst1.val小于list2.val,就直接赋给node,然后递归下去。node.next=Merge(list1.next,list2);
包含min函数的栈
public class Solution {
Stack<Integer> stack=new Stack<Integer>();
Stack<Integer> min=new Stack<Integer>();
public void push(int node) {
if (min.empty() || min.peek()>=node){
min.push(node);
}
stack.push(node);
}
public void pop() {
if (stack.peek() == min.peek())
min.pop();
stack.pop();
}
public int min() {
return min.peek();
}
}
定义两个栈,一个专门用来存进来的数,一个用来存最小值。
push时,如果min为空直接加入,或者顶上最小元素大于加入的元素,也加入min中,但是stack始终是加入的。
pop时,无论如何,stack是一定会出栈的,但是min栈栈顶元素要想出栈,必须等到stack出栈的元素和min栈顶元素相同时才能出栈,因为如果stack的栈顶元素都走了,min栈中栈顶元素也没用了,因为此时stack中已经不存在这个值了。
min:直接返回栈顶值就好了。
栈的压入弹出序列
public boolean IsPopOrder(int [] pushA,int [] popA) {
if (pushA.length==0 || popA.length==0){
return false;
}
Stack<Integer> S=new Stack <Integer>();
int j=0;
for (int i=0;i<pushA.length;i++){
S.push(pushA[i]);
while (!S.empty() && S.peek()==popA[j]){
S.pop();
++j;
}
}
return S.empty();
}
这一段是别人解析应用过来:
用一个辅助的栈,遍历压栈顺序,先讲第一个放入栈中,这里是1,然后判断栈顶元素是不是出栈顺序的第一个元素,这里是4,很显然1≠4,所以我们继续压栈,直到相等以后开始出栈,出栈一个元素,则将出栈顺序向后移动一位,直到不相等,这样循环等压栈顺序遍历完成,如果辅助栈还不为空,说明弹出序列不是该栈的弹出顺序。
举例:
入栈1,2,3,4,5
出栈4,5,3,2,1
首先1入辅助栈,此时栈顶1≠4,继续入栈2
此时栈顶2≠4,继续入栈3
此时栈顶3≠4,继续入栈4
此时栈顶4=4,出栈4,弹出序列向后一位,此时为5,,辅助栈里面是1,2,3
此时栈顶3≠5,继续入栈5
此时栈顶5=5,出栈5,弹出序列向后一位,此时为3,,辅助栈里面是1,2,3
….
依次执行,最后辅助栈为空。如果不为空说明弹出序列不是该栈的弹出顺序。
简单来说就是:进入辅助栈的数都是按照pushA进入的,每次进入都会和popA出栈的第一个数进行比较。不同就继续向stack加入,相同,辅助栈就出栈,而popA数组的数也向后移动,期待下一次比较。如果到最后,辅助栈中不为空,说明false,否则true