剑指OFFER思路总结与代码分享——栈和堆篇(Java实现)

剑指OFFER栈和堆相关

  • 09 用两个栈实现队列
  • 30 包含min函数的栈
  • 59-II 队列最大值
  • 59-I 滑动窗口的最大值
  • 40 最小的k个数
  • 41 数据流中的中位数

注:顺序是先筛选分类再按LeeCode上的通过率排的,每题最后的总结代码都是在LeeCode上跑过的,应该没啥问题。但是思路中的代码都是直接在CSDN编辑器里徒手敲的,若有笔误还烦请告知,蟹蟹~

09 用两个栈实现队列

剑指OFFER思路总结与代码分享——栈和堆篇(Java实现)_第1张图片
首先LinkedList有栈的功能,Stack继承自Vector,底层是用数组实现的,需要各种copyOf去扩容,会导致速度变慢,我们就用LinkedList的push()pop()来模拟栈即可。
它给我们两个栈,要求完成一个队列的操作,入队没啥好说的,进栈就完事了;出队就是把第一个栈内的东西统统倒到第二个栈里,就又变正了,然后我们出第二个栈最上面的那个即可,有种“正=>反=>反=>正”的感觉。
剑指OFFER思路总结与代码分享——栈和堆篇(Java实现)_第2张图片

class CQueue {
     

    LinkedList<Integer> stack1,stack2;
    
    public CQueue() {
     
        stack1 = new LinkedList<>();
        stack2 = new LinkedList<>();
    }
    
    public void appendTail(int value) {
     
        stack1.push(value);
    }
    
    public int deleteHead() {
     
        if(stack2.isEmpty()){
     
            if(stack1.isEmpty()){
     	
            	//两个栈都空,队列就是空的
                return -1;
            }
            while(!stack1.isEmpty()){
     		
            	//把栈1的东西全弹进栈2里,由stack1的反变成stack2的正
                stack2.push(stack1.pop());
            }   
        }
        //弹栈2最顶上那个
        return stack2.pop();
    }
}

/**
 * Your CQueue object will be instantiated and called as such:
 * CQueue obj = new CQueue();
 * obj.appendTail(value);
 * int param_2 = obj.deleteHead();
 */

30 包含min函数的栈

剑指OFFER思路总结与代码分享——栈和堆篇(Java实现)_第3张图片
不太会算时间复杂度啊,看了一下速度战胜了97%应该满足要求了吧,卑微。
读完题第一反应:
剑指OFFER思路总结与代码分享——栈和堆篇(Java实现)_第4张图片
这啥玩意…脑补了一下过程,写了个int min = 0;,然后发现不对,终于知道这题要考啥了,因为它要O(1)嘛,意思就是在不让你遍历的情况下直接告诉它你这个栈的最小值是啥,万一你min记录的那个值被弹出去了,然后它问你:现在最小值是啥啊?你就傻了,所以咱们要以空间换时间,怎么个换法就是这题的考点了。
我们另外再开一个栈,专门记录最小值,新的值进来我们要问自己,这个值是不是比我们以前记录的值小?要是小的话把它压入栈(相等的话也要压入栈哦~写个[3,2,1,1]想想,相等不压进去等1弹出记录的栈顶就是2了,但是其实最小值还是1),重复这么做,等弹出的时候问自己,这个值是我记录的栈顶的值么?如果是的话把它和要弹出的值一起弹出去,这样我的栈顶就是原来第二小的值了。花里胡哨的总结就是再另外维护一个递减的栈就完事了。
另外要在这提一个知识点,我一开始想当然的用linkedlist.getLast();去拿栈顶的元素,结果发现拿的是栈底的,查了一下发现:

LinkedList add 是加在list尾部.
LinkedList push 施加在list头部. 等同于addFirst.

切记切记,容易搞混。简单说来,你要用栈的那套你就一直用,包括push/pop/peek,用队列那套就用add/poll,这样不容易出错。

class MinStack {
     

   LinkedList<Integer> list1,list2;
   /** initialize your data structure here. */
   public MinStack() {
     
       list1 = new LinkedList<>();
       list2 = new LinkedList<>();
   }
   
   public void push(int x) {
     
       list1.push(x);
       //list2空就直接加,不空则看看栈顶的值是否比咱们的大或者相等
       //要是比咱们小的话就说明咱们不会影响到最小值的变更,就拉倒了
       if(list2.isEmpty() || list2.peek() >= x){
     
           list2.push(x);
       }
       
   }
   
   public void pop() {
     
   	   //相等则一起弹出
       if(list1.peek().equals(list2.peek())){
     
           list1.pop();
           list2.pop();
       }else{
     
       	   //不相等则自己弹出
           list1.pop();
       }
   }
   
   public int top() {
     
       return list1.peek();
   }
   
   public int min() {
     
       return list2.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.min();
*/

59-II 队列最大值

剑指OFFER思路总结与代码分享——栈和堆篇(Java实现)_第5张图片
fine我承认我对poll pop push搞得头都晕了,不如就写清楚了从前出还是从后出。这题其实和上一题蛮像的,思路都是维护两个队列,一个负责当正常队列存取数据,另一个维护一个递减队列负责记录最大值,完事了就。

class MaxQueue {
     

    Deque<Integer> queue;
    Deque<Integer> help;
    public MaxQueue() {
     
        queue = new ArrayDeque<>();
        help = new ArrayDeque<>();
    }
    
    public int max_value() {
     
        if(queue.isEmpty()){
     
            return -1;
        }else{
     
            return help.peekFirst();
        }
    }
    
    public void push_back(int value) {
     
        queue.add(value);
        while(!help.isEmpty() && help.peekLast() < value){
     
            help.pollLast();
        }
        help.add(value);
    }
    
    public int pop_front() {
     
        if(queue.isEmpty()){
     
            return -1;
        }
        int res = queue.pollFirst();
        if(res == help.peekFirst()){
     
            help.pollFirst();
        }
        return res;
    }
}

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue obj = new MaxQueue();
 * int param_1 = obj.max_value();
 * obj.push_back(value);
 * int param_3 = obj.pop_front();
 */

59-I 滑动窗口的最大值

剑指OFFER思路总结与代码分享——栈和堆篇(Java实现)_第6张图片
传说中滑动窗口法的板子题来了,这玩意在剑指offer里是easy,在LeeCode里是hard…
其实咱们前面几题已经用到滑动窗口法的思路了,就是再建一个单调递减双端队列来维护其最大值,不过这里我们存的是它的下标,因为这样取值比较方便些。也就是说我们维护的这个队列下标对应的nums的值是需要递减的。最核心的思路就是如果遇到大的,就把在队列中比大的还小的数全弹出去,再将大的压入。若遇到小的就直接压入(有点纸牌游戏小猫钓鱼的味道)。
如果板子不熟最好还是用个例子确定好各个边界,这里边界蛮多的:
剑指OFFER思路总结与代码分享——栈和堆篇(Java实现)_第7张图片

class Solution {
     
    public int[] maxSlidingWindow(int[] nums, int k) {
     
        if(nums.length < 2){
     
            return nums;
        }
        Deque<Integer> queue = new ArrayDeque<>();
        int[] res = new int[nums.length - k + 1];
        for(int i = 0; i < nums.length; i++){
     
            //若前面的数小则依次弹出
            while(!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]){
     
                queue.pollLast();
            }
            queue.addLast(i);
            //下标超过则取出
            if(queue.peekFirst() <= i - k){
     
                queue.pollFirst();
            }
            if(i >= k - 1){
     
                res[i - (k - 1)] = nums[queue.peekFirst()];
            }
        }
        
        return res;
    }
}

总结一下滑动窗口的板子:

Deque = new ArrayDeque
res = new int[l - k + 1]
for(i 0 -> l-1){
     
	while(队列非空 && 最后一个元素比我这个小){
     
		弹出最后一个
	}
	加上我自己
	if(下标过了窗口范围){
     
		取出头个元素
	}
	if(到窗口需要向后挪的时候了){
     
		挪一个写一个res
	}
} 
return res;

下面是堆的题目:

40 最小的k个数

剑指OFFER思路总结与代码分享——栈和堆篇(Java实现)_第8张图片
既然这题被分到堆了就用堆去做吧,其实说到TopK问题应该就是用堆做比较方便。记一下new出小根堆和大根堆的语句:

//小根堆
Queue<Integer> heap = new PriorityQueue<>();
//大根堆(重写了小根堆的比较器)
Queue<Integer> heap = new PriorityQueue<>((v1, v2) -> v2 - v1);

class Solution {
     
    public int[] getLeastNumbers(int[] arr, int k) {
     
        if(k == 0 || arr.length == 0){
     
            return new int[0];
        }

        Queue<Integer> heap = new PriorityQueue<>((v1, v2) -> v2 - v1);
        for(int n:arr){
     
            if(heap.size() < k){
     
            	//offer更好,为了看得方便用了add()
                heap.add(n);
            }else if(n < heap.peek()){
     
                heap.poll();
                heap.add(n);
            }
        }

        int[] res = new int[heap.size()];
        int index = 0;
        for(int n:heap){
     
            res[index++] = n;
        }
        return res;
    }
}

41 数据流中的中位数

剑指OFFER思路总结与代码分享——栈和堆篇(Java实现)_第9张图片
维护两个堆,一个大顶堆一个小顶堆。输入的数据首先放在大顶堆,然后从大顶堆吐出最上面的一个送给小顶堆,一直保持大顶堆的数据比小顶堆少1个(奇数)或者相等(偶数)的情况,这样我们算平均值就是小顶堆的堆顶或者小顶堆顶与大顶堆顶的平均数了。


通俗理解一下,一个门派分为一半的内门弟子(小顶堆)与一半的外门弟子(大顶堆),规定若为偶数就对半分,奇数的话多的那个名额是内门弟子的。
每次有新人进来,我们就先将他放在外门,然后从外门取最厉害的一个(大顶堆的peek)加入内门,当然,如果内门的人多了,我们就取内门最差的那个(小顶堆的peek)加入外门。
我们问这个门派弟子的平均实力,那若弟子数为奇数,就是内门最差的那个(小顶堆的peek),为偶数则是内门最差的(小顶堆peek)与外门最好的(大顶堆peek)的平均了。


class MedianFinder {
     

    Queue<Integer> big;
    Queue<Integer> small;
    /** initialize your data structure here. */
    public MedianFinder() {
     
        big = new PriorityQueue<>((v1 ,v2) -> v2 - v1);
        small = new PriorityQueue<>();
    }
    
    public void addNum(int num) {
     
        big.add(num);
        small.add(big.poll());
        if(big.size() + 1 < small.size()){
     
            big.add(small.poll());
        }
    }
    
    public double findMedian() {
     
        if(small.size() > big.size()){
     
            return small.peek();
        }
        return (double)(small.peek() + big.peek()) / 2;

    }
}

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */

到此为止栈和堆的部分就结束啦~求个关注啵啵啵(●´З`●)

你可能感兴趣的:(剑指Offer,剑指offer,面试,Java,算法,栈)