刷leetCode算法题+解析(十三)

作者: 唯有努力不欺人丶 | 来源:发表于2019-12-06 18:18 被阅读0次

    又是一个愉快的周五,继续刷题ing~

    最小栈

    题目:设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
    push(x) -- 将元素 x 推入栈中。
    pop() -- 删除栈顶的元素。
    top() -- 获取栈顶元素。
    getMin() -- 检索栈中的最小元素。
    

    示例:
    MinStack minStack = new MinStack();
    minStack.push(-2);
    minStack.push(0);
    minStack.push(-3);
    minStack.getMin(); --> 返回 -3.
    minStack.pop();
    minStack.top(); --> 返回 0.
    minStack.getMin(); --> 返回 -2.

    前言:这里先认个怂,这道题我思路不行。 我一直承认与非科班的短板还有工作以来急功近利的心态。至于这种很少见的数据结构(除了数组,集合,map之外的数据结构)链表和树都还是在LeetCode上学的。Queue队列是前一段时间树的遍历了解的。而这道题用到的栈。讲真我在这道题之前连单词都不会写。一开始我试图用自己的思路来解题,唯一这种类型先入后出的也就是队列。所以在尽量用Queue来解题,卡在了getMin这块。最终还是直接决定看解析了。现在不会,看了解析我会了不就行了么?还好网上那么多好心的大大出各种解析,解释和思路讲解。讲的很透彻和明白,尤其是有一个大大提供了多思路的讲解,每一种都让我豁然开朗,这里再次表达感谢。
    思路:这才是真正的解题思路。简单说一下这道题很多种做法可以做。首先Java提供的栈Stack。它本身已经有了前三个方法的实现,也就是这道题如果用了这个Stack只要实现最后一个最小值就可以了。当然也可以不用这个Stack,但是这种思路后面说。先说用Stack实现最小值
    首先一种最简单的办法:用空间换时间。这里只要求常熟时间内检索到最小元素的栈。并没有对空间有要求。我们完全可以使用两个栈,一个是正常的栈,另一个是专门记录最小值的栈。这样取最小值只需要一步,简单的多。直接上代码:

    class MinStack {
       
        private Stack<Integer> stack;
        private Stack<Integer> min;
        /** initialize your data structure here. */
        public MinStack() {
            stack = new Stack();
            min = new Stack();
        }
        
        public void push(int x) {
            stack.push(x);
            //最小值这个栈不为空
            if(!min.isEmpty()){
    //当新push的比最小栈中的首元素大就把这个push到最小栈中
                if(min.peek()>=x){
                    min.push(x);
                }
            }else{
                min.push(x);
            }
        }
        
        public void pop() {
            //这里有个坑,不能直接比较,不然两个Integer得判断equals
            int x =stack.pop();
            if(min.peek()==x){
                min.pop();
            }
        }
        
        public int top() {
            return stack.peek();
        }
        
        public int getMin() {
            return min.peek();
        }
    }
    
    /**
     * Your MinStack object will be instantiated and called as such:
     * MinStack obj = new MinStack();
     * obj.push(x);
     * obj.pop();
     * int param_3 = obj.top();
     * int param_4 = obj.getMin();
     */
    

    其实这个是最容易理解的了,也很简单,但是大佬在这个基础上改良优化了一下。这个是很明显的用空间换时间。能不能只用一个栈就完成这个需求?真的非常佩服大佬的思路广而且闲(绝对是褒义词,喜欢钻研的意思)这一个简单 题不断优化,提供多种不同思路。咱们继续说这个题上一版的优化版。如果用一个栈,则需要一个常量来表示最小值。但是会出现一种状态,每次这个常量表示的都是最小值。但是这个最小值上一个最小值记录找不到了。如果这个最小值出栈了就gg了、所以重点是要保存最小值的同时保存第二小的值。这样不管怎么出栈都不会gg。
    然后大佬的思路是在push最小值的时候,把之前最小值也压入栈。这样就算是最小值出栈了,我们还能知道第二小的(也就是现在的最小的)。说起来很复杂但是只要理解原理了还是很简单的。我这里直接上代码了。

    class MinStack {
       
        private Stack<Integer> stack;
        //因为这个min取值万一push进来的都大于初始值就会出错。所以直接min赋值最大值。
        private int min = Integer.MAX_VALUE;
        /** initialize your data structure here. */
        public MinStack() {
            stack = new Stack();
        }  
        public void push(int x) {
            if(min>=x){
                //把之前最小的push进去。
                stack.push(min);
                //把min改成最小值
                min=x;   
            }
            stack.push(x);
        }   
        public void pop() {
            int x =stack.pop();
            //说明最小值被pop了,min应该等于第二小的。并且顺便把第二小的也pop出去。
            if(min==x){
                min = stack.pop();
            }
            
        }    
        public int top() {
            return stack.peek();
        }
        
        public int getMin() {
            return min;
        }
    }
    
    /**
     * Your MinStack object will be instantiated and called as such:
     * MinStack obj = new MinStack();
     * obj.push(x);
     * obj.pop();
     * int param_3 = obj.top();
     * int param_4 = obj.getMin();
     */
    

    再然后大神还提供了几个思路,一个是取巧的减法。这个就是保存的数据是每一个值和最小值的差。然后如果遇到比这个最小值还小的数字这个差就会是负数。同时更新最小值min的值。这样每次取出值的时候判断正负就好了。如果是负的,我们用最小值减去这个负数得到的就是第二小的值了。
    其实想想也知道,这个方法运行起来跟上一个比不用往栈中插入额外的数据。但是每次取出值都要加以判断。具体哪个比较好还真不好说。而且因为保存差值,可能会溢出,所以要用long存,不能用int存。

    class MinStack {  
        //涉及到运算,可能会溢出,所以这里用long
        private Stack<Long> stack;
        //因为这个min取值万一push进来的都大于初始值就会出错。所以直接min赋值最大值。
        private long min;
        /** initialize your data structure here. */
        public MinStack() {
            stack = new Stack();
        }  
        public void push(int x) {
            if(stack.isEmpty()){
                min = x;
                stack.push(x-min);
            }else{
                stack.push(x-min);
                if(x<min){
                    //新插入的数据是负的。并且替换min值                
                    min = x;
                }
                
            }
        }   
        public void pop() {
            //因为这个是不用有返回值的,所以直接弹出去就行了。管他数据对不对呢
            long x =stack.pop();
            //说明最小值被pop的是最小值。
            if(x<0){
                min = min-x;
            }
            
        }    
        public int top() {
            //如果是负数这个出栈的值是min
            if(stack.peek()<0){
                return (int)min;
            }else{
                return (int)(stack.peek()+min);
            }
        }
        
        public int getMin() {
            return (int)min;
        }   
    }
    /**
     * Your MinStack object will be instantiated and called as such:
     * MinStack obj = new MinStack();
     * obj.push(x);
     * obj.pop();
     * int param_3 = obj.top();
     * int param_4 = obj.getMin();
     */
    

    最后一个是没采用java的Stack自己链表实现的方法。这个其实道理不难,而且思路也简单。但是因为是自己建造底层数据结构所以代码多点。
    原理就是链表,并且在每一个节点除了存储节点本身的值还存储最小值。所以每次获取最小值只要知道当前节点的最小值就可以了。 也是一种用空间换时间的做法。直接上代码:

    class MinStack {  
        class Node{
            int node;
            int min;
            Node next;
            Node(int val,int min){
                this.node = val;
                this.min = min;
                next =null;
            }
        } 
        
        private Node node;
        /** initialize your data structure here. */
        public MinStack() {
        }  
        //这块区别于常用的链表,每次新push的节点要放在头部。所以是把node往后挂
        public void push(int x) {
            if(node==null){
                node = new Node(x,x);
            }else{
                Node n = new Node(x,Math.min(x,node.min)); 
                n.next = node;
                node = n;
            }
        }   
        public void pop() {
            //把头节点拿走。因为之前存的时候每一个节点都有当前数据的最小值,所以根本不用考虑最小值
            if(node!=null){
                node = node.next;
            }
        }    
        public int top() {
            return node.node;
        }
        
        public int getMin() {
            return node.min;
        }   
    }
    
    /**
     * Your MinStack object will be instantiated and called as such:
     * MinStack obj = new MinStack();
     * obj.push(x);
     * obj.pop();
     * int param_3 = obj.top();
     * int param_4 = obj.getMin();
     */
    

    然后这道题终于结束了,整整用了半天,理大佬思路,一点点按照理解写代码。再一次次执行,最后写下来。虽然时间耽误挺多但是真的觉得学到了很多东西。顺便心得就是做题一定要踏下心来慢慢的分析琢磨,越着急思路越不清晰。下一题吧。
    LeetCode遇到需要会员才能做的题了。好蓝瘦,不能白嫖了~

    相交链表

    题目:编写一个程序,找到两个单链表相交的起始节点。
    题目截图
    题目截图
    题目截图

    思考:反正如图所示吧,单纯的找到焦点还是很简单的,暴力双循环吧。但是真的太low,人家都说了要尽量满足O(n)时间复杂度和O(1)内存了。所以还是想想怎么取巧吧。
    真遗憾,我没想出来满足要求的解题方法。因为我觉得这个题不取巧谁都能做,所以我也没暴力法写出来。直接看了解析。然后有一次被别人的思路所震惊。
    思路:直接说方法吧。思路就是两个指针分别遍历链表A,B。A遍历完接着遍历B。B遍历完接着遍历A。这两个指针的交叉点就是链表的交点。可能我这么说很难理解,我这里用图表示

    是时候展示我的画技了!

    所以这个题的解就是两个链表互相加一下,然后一直遍历到,直到两个链表相等的节点就是相交点,如果遍历完了也没有相交点说明就是不相交的两个链表。
    思路理解了代码很容易实现的:

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) {
     *         val = x;
     *         next = null;
     *     }
     * }
     */
    public class Solution {
        public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
            if(headA==null || headB==null){
                return null;
            }
            ListNode ha = headA;
            ListNode hb = headB; 
            //A+B只加一次,必须有个标,不然就循环追尾了
            boolean flag1 = true;
            boolean flag2 = true;       
            while(ha!=null){
                  if(ha==hb){
                      return ha;
                  }else{
                      if(ha.next == null && flag1){
                          //已经追加一次就要把标改状态。
                          ha = headB;
                          flag1 = false;
                      }else{
                          ha = ha.next;   
                      }
                      if(hb.next == null && flag2){
                           hb = headA;
                           flag2 = false;
                      }else{
                          hb = hb.next;
                      }                
                  }
            }
            return null;
        }
    }
    

    然后我看到了另一种写法,思路是一样的,我这里是用flag判断只追加一次的。还有一种思路就是判断条件是A==B,不管是A+B或者B+A,在走完一遍的时候最终会归于null。所以那么判断也可以。只要理解就行。我刚刚也改了那种方法运行了一下,两个方式其实性能差不多。选择能理解的喜欢的就好。

    两数之和二

    给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。说明:
    返回的下标值(index1 和 index2)不是从零开始的。
    你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
    

    示例:
    输入: numbers = [2, 7, 11, 15], target = 9
    输出: [1,2]
    解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。

    思路:这道题我做过,我不知道他和两数之和1 有什么区别,除了下标是从1开始。反正做是第一时间做出来了,怎么优化还得慢慢来。先把第一版贴上来

    class Solution {
        public int[] twoSum(int[] numbers, int target) {
            int [] result = new int[2];
            HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
            for(int i = 0;i<numbers.length;i++){
                //有答案则直接返回。这两个数对找到了.这有个优化点,就是因为升序排列,而且不会有重复,所以说两个加数中较大的那个肯定比target/2大。因为有负数问题,所以是大于等于
                if(numbers[i]>=target/2 && map.containsKey(target-numbers[i])){
                    result[0] = map.get(target-numbers[i]);
                    result[1] = i+1;
                    return result;
                }else{
                    //没有把这个数和下标存进map。因为下标不是从0开始,所以存i+1。
                    map.put(numbers[i],i+1);
                }     
            }
            return null;
        }
    }
    

    我觉得我已经绞尽脑汁的优化了,但是性能还是不行。其实能理解:因为可能是上面这种遍历的方法本身就有问题,所以我换个思路试试。
    实在没思路忍不住看了题解,然后豁然开朗。真的,一看就会。一点不夸张。差的就是那个思维模式。哎,其实就是双指针法。最大最小相加跟给定的比较。大了右指针往左移。小了左指针往右移。知道正好找到那两个数为止。就这么简单粗暴。因为题目已经说了肯定有一对是可以的。所以我也没做判断。再说溢出问题。其实这个不用考虑。不然你拿点数据测试一下就行了。因为这个溢出后结果肯定是不对的。然后开始挪位。最后肯定是挪到不溢出的地方了。直接上代码吧

    class Solution {
        public int[] twoSum(int[] numbers, int target) {
            int [] result = new int[2];
            int left= 0;
            int right =numbers.length-1;
            while(true){
                //这两个数相加比给定的大。则最大值往小
                if(numbers[left]+numbers[right]>target){
                    right--;
                }else if(numbers[left]+numbers[right]<target){//这两个数相加比给定值小,则最小值往大
                    left++;
                }else{
                    result[0] = left+1;
                    result[1] = right+1;
                    return result;
                }
            }
        }
    }
    

    好了,今天的三道题就到这,其实学习的乐趣不在于学会了怎么怎么样,而在于懂了的那一刻的成就感和自豪感。我是正在试图入门的算法小白,每天进步一点点~
    如果稍微帮到你了记得点个喜欢点个关注呦~也祝大家工作顺顺利利!周末愉快!

    相关文章

      网友评论

        本文标题:刷leetCode算法题+解析(十三)

        本文链接:https://www.haomeiwen.com/subject/qkesgctx.html