牛客网完整java解法【剑指offer】 一

剑指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

 

   OK~第一篇就讲这么多,拜拜

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值