【剑指offer】Java:栈、队列、堆

这篇博客记录了使用Java实现数据结构中栈、队列和堆的典型问题解决方案,包括两个栈实现队列、带有最小值功能的栈、验证栈的压入弹出序列、找到最小的k个数、数据流中中位数的计算、滑动窗口最大值以及队列的最大值。每个问题都提供了详细的题解思路。
摘要由CSDN通过智能技术生成


参考了很多大佬的题解,仅作为自己学习笔记用。


1、09.两个栈实现队列

题意:

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]
--------------------------------------------------
输入:
["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]

题解:

双栈可实现列表倒序,倒序之后删除栈顶元素。in 栈用来处理入栈(push)操作,另一个栈用来处理出栈(pop)操作。
假设 in 栈先进 1 再进 2,那么从 in 栈进入 out 栈的时候顺序就是先进 2 后进 1,再从 out 出栈的时候就是先出 1 后出 2,这已经和队列的 FIFO 顺序一样了,大家可以自行画图一目了然。

class CQueue {
    Deque<Integer> in;		// 此处准备用 LinkedList,用 Stack 也行
    Deque<Integer> out;

    public CQueue() {
        in = new LinkedList<>();
        out = new LinkedList<>();
    }
    
    public void appendTail(int value) {
        in.push(value);
    }
    
    public int deleteHead() {		// 一共三种情况
        if(out.isEmpty()){
            if(in.isEmpty())		// out 为空,in 为空,直接返回 -1
                return -1;
            while(!in.isEmpty())	// out 为空,in 不空,把 in 栈都放到 out 栈,返回 out 栈顶
                out.push(in.pop());
            return out.pop();
        }else						// out 不空,直接返回 out 栈顶
            return out.pop();
    }
}

2、30.包含 min 函数的栈

题意:

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.min();   --> 返回 -2.

题解:

数据栈 + 辅助栈
数据栈 data 存放栈数据,辅助栈 min 存放当前数据栈中最小值。所以在数据栈 data 每次放入数据 x 之后,都要将 x 和 min 栈顶进行比较,将较小值再放入 min 栈。
这样每次调用 min 函数,直接取辅助栈 min 的栈顶值即可。data 每次 pop 的时候,min 也要 pop,保持两个栈大小相等
在这里插入图片描述
但是这里会发现辅助栈有重复放的情况。所以可以这样:
放入辅助栈之前加个判断,如果 x <= 辅助栈顶,那么就放入 x,否则就不放。
弹栈也加个判断,如果数据栈弹出的值 = 辅助栈顶,说明最小值被弹出了,这个时候辅助栈也弹出,否则不弹出。
在这里插入图片描述

class MinStack {
    Stack<Integer> data;
    Stack<Integer> min;

    /** initialize your data structure here. */
    public MinStack() {
        data = new Stack<>();
        min = new Stack<>();
    }
    
    public void push(int x) {
        data.push(x);
        if(min.isEmpty() || x <= min.peek())
        	min.push(x);
    }
    
    public void pop() {
        if(data.pop().equals(min.peek()))
        	min.pop();
    }
    
    public int top() {
        return data.peek();
    }
    
    public int min() {
        return min.peek();
    }
}

3、31.栈的压入、弹出序列

题意:

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
-------------------------------------------------------
输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。

题解:

使用一个栈来模拟压入弹出操作。每次入栈一个元素后,都要判断一下栈顶元素是不是当前出栈序列popped 的第一个元素,如果是的话则执行出栈操作并将 j 往后移一位,继续进行判断。
在这里插入图片描述

    public boolean validateStackSequences(int[] pushed, int[] popped) {
        int length = pushed.length, j = 0;
        Stack<Integer> stack = new Stack<>();
        
        for(int i=0; i<length; i++){
            stack.push(pushed[i]);
            while(j<length && !stack.isEmpty() && stack.peek() == popped[j]){
                stack.pop();
                j++;
            }              
        }
        return stack.isEmpty();
    }

4、40.最小的 k 个数

题意:

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
--------------------------
输入:arr = [0,1,2,1], k = 1
输出:[0]

题解1:

维护一个大小为 K 的最大顶堆。往堆中先加入 k 个元素,这些元素会自动变为大顶堆的排列方式,当再往里面加一个元素,又会重新排列,这时候就弹出堆顶元素,这个元素肯定堆中最大值。到最后的时候,堆中留下的全是较小的元素,因为堆中最大元素一直在被删除。

    public int[] getLeastNumbers(int[] arr, int k) {
    	if(arr == null || arr.length == 0)
    		return new int[0];
    		
		int[] res = new int[k];
		// PriorityQueue实现了堆,需要传入比较器,改为大顶堆,默认是小顶堆
		PriorityQueue<integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
		
		for(int a : arr){
			maxHeap.add(a);
			if(maxHeap.size() > k){
				maxHeap.poll();
			}
		}	
		for(int i = 0; i < k; i++){
			res[i] = maxHeap.poll();
		}
		return res;
    }

题解2:

快排变形。其实是快速选择算法。只不过和快速排序比较像而已。允许修改数组才可以用。
partition 后左边的元素都比 v 小,右边的都比 v 大。
此时如果 v 的下标为 j,且 j = k,那么左边的元素正好是 k 个,就是要找的前 k 个小值,否则递归寻找 j = k 。
在这里插入图片描述

public int[] getLeastNumbers(int[] arr, int k) {
        if (k == 0 || arr.length == 0) 
            return new int[0];
        
        // 最后一个参数表示找的是下标为 k 的数,最后返回下标 k 之前数即可
        quickSelect(arr, 0, arr.length-1, k);

        int[] res = new int[k];
        for(int i=0; i<k; i++)
            res[i] = arr[i];

        return res;
    }

    private void quickSelect(int[] arr, int lo, int hi, int k) { 
        if(lo >= hi) return;
        // 切分1次,基准元素的下标为 j,如果 j 恰好等于下标 k 就返回 j 左边所有的数,否则切分左或右
        int j = partition(arr, lo, hi);
        if(j == k)      
            return ;
        else if(j > k)       
            quickSelect(arr, lo, j-1, k);   //基准下标 > k,说明前k小的数在左边数组中
        else       
            quickSelect(arr, j+1, hi, k);   //基准下标 < k,说明右边数组也有前 k 小的
    }

    // 切分,返回基准元素最后的下标j,使得比nums[j]小的数都在j的左边,比nums[j]大的数都在j的右边
    private int partition(int[] arr, int lo, int hi) {
        int i = lo, j = hi + 1;  // 左右扫描指针
        int v = arr[lo];       // 切分元素,基准值
        while (true) {
            while (arr[++i] < v)	if(i == hi)	break;
            while (arr[--j] > v)	if(j == lo)	break;
            if (i >= j)     break;
            swap(arr, i, j);
        }
        swap(arr, lo, j);
        return j;
    }

    public void swap(int[] a, int i, int j){
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

5、41.数据流中的中位数

题意:

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
设计一个支持以下两种操作的数据结构:

  • void addNum(int num) - 从数据流中添加一个整数到数据结构中。
  • double findMedian() - 返回目前所有元素的中位数。
输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
------------------------------------------
输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]

题解:

一个大顶堆,存放左半边元素,一个小顶堆,存放右半边元素。保持两个堆处于平衡状态
在这里插入图片描述

class MedianFinder {
    PriorityQueue<Integer> left, right;
    int cnt;

    /** initialize your data structure here. */
    public MedianFinder() {
        // 左边大顶堆,存储左半边元素
        left = new PriorityQueue<>((a, b) -> b - a);
        // 右边小顶堆,存储右半边元素,存储的都比左边的大
        right = new PriorityQueue<>();
        // 当前数据流读入的元素个数
        cnt = 0;
    }
    
    public void addNum(int num) {
        // 保证两个堆处于平衡状态, 右边有时会多一个

        // 先插入到右边,但是插入的元素不一定比左边的大,所以先插入左边,从左边弹出最大的到右边
        if(cnt % 2 == 0){
            left.add(num);
            right.add(left.poll());
        }else{
            right.add(num);
            left.add(right.poll());
        }
        cnt++;
    }
    
    public double findMedian() {
        if(cnt % 2 == 0){
            return (left.peek() + right.peek()) / 2.0;
        }else{
            return (double)right.peek();
        } 
    }
}

6、59-1.滑动窗口的最大值

题意:

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7] 
解释: 
		滑动窗口的位置          最大值
-----------------------------------
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

题解:

双端队列
如果待加入的值比队尾值小,那么就加入;
如果待加入的值比队尾值大,那么队尾值肯定不是最大值,可以删掉,继续比较待加入的值和队尾值,如果队尾值还是小,继续删,直到队尾值比待加入的值大,再将待加入的值放入。

形成窗口后,每次都要将队首元素放入结果数组 res 中。如果滑动窗口左边出去的元素 = 队首元素,那么队首元素也需要删除。队列是非严格递减的。
在这里插入图片描述

    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length == 0 || k == 0) 
            return new int[0];
        
        Deque<Integer> deque = new LinkedList<>();
        int[] res = new int[nums.length - k + 1];

		// 形成窗口前
        for(int i = 0; i < k; i++) {
            while(!deque.isEmpty() && deque.peekLast() < nums[i]){
                deque.removeLast();
            }
            deque.addLast(nums[i]);
        }
        res[0] = deque.peekFirst();		// 已经形成窗口,队首元素放入 res

		// 形成窗口后
        for(int i = k; i < nums.length; i++) {
            if(deque.peekFirst() == nums[i-k]){
                deque.removeFirst();
           	}
            while(!deque.isEmpty() && deque.peekLast() < nums[i]){
                deque.removeLast();
          	}
            deque.addLast(nums[i]);
            res[i - k + 1] = deque.peekFirst();
        }
        return res;
    }

7、59-2.队列的最大值

题意:

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数 max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。若队列为空,pop_front 和 max_value 需要返回 -1。

输入: 
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
-------------------------------
输入: 
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]

题解:

定义队列并不难,获得最大值我们可以定义一个变量保存最大值,每次元素加入都进行更新,但是弹出元素后,次最大值就不知道了。所以,定义变量不行,此时利用双端队列。和上题类似。

class MaxQueue {
    Queue<Integer> queue;   // 要求定义的队列
    Deque<Integer> deque;   // 辅助的双端队列

    public MaxQueue() {
        queue = new LinkedList<>();
        deque = new LinkedList<>();
    }
    
    public int max_value() {
        if(queue.isEmpty())
            return -1;
        return deque.peekFirst();
    }
    
    public void push_back(int value) {
        queue.add(value);
        while(!deque.isEmpty() && value > deque.peekLast()){
            deque.removeLast();
        }
        deque.addLast(value);
    }
    
    public int pop_front() {
        if(queue.isEmpty())
            return -1;

        int temp = queue.poll();
        if(temp == deque.peekFirst())
            deque.removeFirst();
        return temp;
    }
}

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值