04 堆、栈、队列

(难)BM45 滑动窗口的最大值描述

给定一个长度为 n 的数组 nums 和滑动窗口的大小 size ,找出所有滑动窗口里数值的最大值。

例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

数据范围: 1≤size≤n≤10000,数组中每个元素的值满足 ∣val∣≤10000
要求:空间复杂度 O(n),时间复杂度 O(n)

思路:

我们都知道,若是一个数字A进入窗口后,若是比窗口内其他数字都大,那么这个数字之前的数字都没用了,因为它们必定会比A早离开窗口,在A离开之前都争不过A,所以A在进入时依次从尾部排除掉之前的小值再进入,而每次窗口移动要弹出窗口最前面值,因此队首也需要弹出,所以我们选择双向队列。

具体做法:

step 1:维护一个双向队列,用来存储数列的下标。
step 2:首先检查窗口大小与数组大小。
step 3:先遍历第一个窗口,如果即将进入队列的下标的值大于队列后方的值,依次将小于的值拿出来去掉,再加入,保证队列是递增序。
//先遍历一个窗口
for(int i = 0; i < size; i++){
    //去掉比自己先进队列的小于自己的值
    while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
        dq.pollLast();
    dq.add(i);
}
step 4:遍历后续窗口,每次取出队首就是最大值,如果某个下标已经过了窗口,则从队列前方将其弹出。
step 5:对于之后的窗口,重复step 3,直到数组结束。
import java.util.*;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size) {
        ArrayList<Integer> res = new ArrayList<Integer>();
        //窗口大于数组长度的时候,返回空
        if(size <= num.length && size != 0){
            //双向队列
            ArrayDeque <Integer> dq = new ArrayDeque<Integer>();  
            //先遍历一个窗口
            for(int i = 0; i < size; i++){
                //去掉比自己先进队列的小于自己的值
                while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
                     dq.pollLast();
                dq.add(i);
            }
            //遍历后续数组元素
            for(int i = size; i < num.length; i++){
                //取窗口内的最大值
                res.add(num[dq.peekFirst()]);
                while(!dq.isEmpty() && dq.peekFirst() < (i - size + 1))
                    //弹出窗口移走后的值
                    dq.pollFirst(); 
                //加入新的值前,去掉比自己先进队列的小于自己的值
                while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
                    dq.pollLast();
                dq.add(i);
            }
            res.add(num[dq.pollFirst()]);
        }     
        return res;
    }
}
时间复杂度:O(n)O(n)O(n),数组长度为nnn,只遍历一遍数组
空间复杂度:O(m)O(m)O(m),窗口长度mmm,双向队列最长时,将窗口填满

(中等,但我感觉难)BM49 表达式求值

写一个支持+ - *三种符号的运算器,其中优先级+ - 是一级,*更高一级
支持括号运算

思路:

对于上述两个要求,我们要考虑的是两点,一是处理运算优先级的问题,二是处理括号的问题。

处理优先级问题,那必定是乘号有着优先运算的权利,加号减号先一边看,我们甚至可以把减号看成加一个数的相反数,则这里只有乘法和加法,那我们优先处理乘法,遇到乘法,把前一个数和后一个数乘起来,遇到加法就把这些数字都暂时存起来,最后乘法处理完了,就剩余加法,把之前存起来的数字都相加就好了。

处理括号的问题,我们可以将括号中的部分看成一个新的表达式,即一个子问题,因此可以将新的表达式递归地求解,得到一个数字,再运算:

终止条件: 每次遇到左括号意味着进入括号子问题进行计算,那么遇到右括号代表这个递归结束。
返回值: 将括号内部的计算结果值返回。
本级任务: 遍历括号里面的字符,进行计算。

具体做法:

step 1:使用栈辅助处理优先级,默认符号为加号。
step 2:遍历字符串,遇到数字,则将连续的数字字符部分转化为int型数字。
step 3:遇到左括号,则将括号后的部分送入递归,处理子问题;遇到右括号代表已经到了这个子问题的结尾,结束继续遍历字符串,将子问题的加法部分相加为一个数字,返回。
step 4:当遇到符号的时候如果是+,得到的数字正常入栈,如果是-,则将其相反数入栈,如果是*,则将栈中内容弹出与后一个元素相乘再入栈。
step 5:最后将栈中剩余的所有元素,进行一次全部相加。

import java.util.*;
public class Solution {
    public ArrayList<Integer> function(String s, int index){
        Stack<Integer> stack = new Stack<Integer>(); 
        int num = 0;
        char op = '+';
        int i;
        for(i = index; i < s.length(); i++){
            //数字转换成int数字
            //判断是否为数字
            if(s.charAt(i) >= '0' && s.charAt(i) <= '9'){ 
                num = num * 10 + s.charAt(i) - '0';
                if(i != s.length() - 1)
                    continue;
            }
            //碰到'('时,把整个括号内的当成一个数字处理
            if(s.charAt(i) == '('){
                //递归处理括号
                ArrayList<Integer> res = function(s, i + 1);
                num = res.get(0);
                i = res.get(1);
                if(i != s.length() - 1)
                    continue;
            }            
            switch(op){
            //加减号先入栈
            case '+': 
                stack.push(num);
                break;
            case '-':
                //相反数
                stack.push(-num);
                break;
            //优先计算乘号
            case '*':  
                int temp = stack.pop();
                stack.push(temp * num);
                break;
            }
            num = 0;
            //右括号结束递归
            if(s.charAt(i) == ')') 
                break; 
            else 
                op = s.charAt(i);
        }
        int sum = 0;
        //栈中元素相加
        while(!stack.isEmpty())  
            sum += stack.pop();
        ArrayList<Integer> temp = new ArrayList<Integer>();
        temp.add(sum);
        temp.add(i);
        return temp; 
    }
    public int solve (String s) {
        ArrayList<Integer> res = function(s, 0);
        return res.get(0);
    }
}

BM42 两个栈实现队列

class CQueue {
    Deque<Integer> inStack;
    Deque<Integer> outStack;

    public CQueue() {
        inStack = new ArrayDeque<Integer>();
        outStack = new ArrayDeque<Integer>();
    }

    public void appendTail(int value) {
        inStack.push(value);
    }

    public int deleteHead() {
        if (outStack.isEmpty()) {
            if (inStack.isEmpty()) {
                return -1;
            }
            in2out();
        }
        return outStack.pop();
    }

    private void in2out() {
        while (!inStack.isEmpty()) {
            outStack.push(inStack.pop());
        }
    }
}

作者:LeetCode-Solution
链接:https://leetcode.cn/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/solution/mian-shi-ti-09-yong-liang-ge-zhan-shi-xian-dui-l-3/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

BM43 包含Min函数的栈

import java.util.Stack;

public class Solution {
    //用于栈的push 与 pop
    Stack<Integer> s1 = new Stack<Integer>(); 
    //用于存储最小min
    Stack<Integer> s2 = new Stack<Integer>(); 
    public void push(int node) {
        s1.push(node);  
        //空或者新元素较小,则入栈
        if(s2.isEmpty() || s2.peek() > node)  
            s2.push(node);
        else
            //重复加入栈顶
            s2.push(s2.peek());  
    }
    
    public void pop() {
        s1.pop();
        s2.pop();
    }
    
    public int top() {
        return s1.peek();
    }
    
    public int min() {
        return s2.peek();
    }
}

BM44 有效括号序列

利用栈

import java.util.*;
public class Solution {
    public boolean isValid (String s) {
        //辅助栈
        Stack<Character> st = new Stack<Character>(); 
        //遍历字符串
        for(int i = 0; i < s.length(); i++){ 
            //遇到左小括号
            if(s.charAt(i) == '(') 
                //期待遇到右小括号
                st.push(')'); 
            //遇到左中括号
            else if(s.charAt(i) == '[') 
                //期待遇到右中括号
                st.push(']'); 
            //遇到左打括号
            else if(s.charAt(i) == '{') 
                //期待遇到右打括号
                st.push('}'); 
            //必须有左括号的情况下才能遇到右括号
            else if(st.isEmpty() || st.pop() != s.charAt(i)) 
                return false;
        }
        //栈中是否还有元素
        return st.isEmpty(); 
    }
}

BM46 最小的k个数

在这里插入图片描述

方法一:排序

直接排序,然后去前k小数据。

import java.util.*;
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> res = new ArrayList<Integer>();
        //排除特殊情况
        if(k == 0 || input.length == 0) 
            return res;
        //大根堆 
        PriorityQueue<Integer> q = new PriorityQueue<>((o1, o2)->o2.compareTo(o1));
        //构建一个k个大小的堆  
        for(int i = 0; i < k; i++)
            q.offer(input[i]);
        for(int i = k; i < input.length; i++){
            //较小元素入堆
            if(q.peek() > input[i]){  
                q.poll();
                q.offer(input[i]);
            }
        }
        //堆中元素取出入数组
        for(int i = 0; i < k; i++) 
            res.add(q.poll());
        return res;
    }
}
时间复杂度:O(nlog2n),sort函数属于优化后的快速排序,复杂度为O(nlog2n)
空间复杂度:O(1),无额外辅助空间使用

方法二:堆排序

建立一个容量为k的大根堆的优先队列。遍历一遍元素,如果队列大小<k,就直接入队,否则,让当前元素与队顶元素相比,如果队顶元素大,则出队,将当前元素入队

import java.util.*;
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> res = new ArrayList<Integer>();
        //排除特殊情况
        if(k == 0 || input.length == 0) 
            return res;
        //大根堆 
        PriorityQueue<Integer> q = new PriorityQueue<>((o1, o2)->o2.compareTo(o1));
        //构建一个k个大小的堆  
        for(int i = 0; i < k; i++)
            q.offer(input[i]);
        for(int i = k; i < input.length; i++){
            //较小元素入堆
            if(q.peek() > input[i]){  
                q.poll();
                q.offer(input[i]);
            }
        }
        //堆中元素取出入数组
        for(int i = 0; i < k; i++) 
            res.add(q.poll());
        return res;
    }
}

时间复杂度:O(nlongk), 插入容量为k的大根堆时间复杂度为O(longk), 一共遍历n个元素
空间复杂度:O(k)

方法三:快排思想

对数组[l, r]一次快排partition过程可得到,[l, p), p, [p+1, r)三个区间,[l,p)为小于等于p的值
[p+1,r)为大于等于p的值。
然后再判断p,利用二分法

如果[l,p), p,也就是p+1个元素(因为下标从0开始),如果p+1 == k, 找到答案
2。 如果p+1 < k, 说明答案在[p+1, r)区间内,
3, 如果p+1 > k , 说明答案在[l, p)内 

时间复杂度:平均时间复杂度为O(n),每次partition的大小为n+n/2+n/4+… = 2n,最坏时间复杂度为O(n^2), 因为每次partition都只减少一个元素
空间复杂度:O(1)

BM47 寻找第k大

在这里插入图片描述思路:

本题需要使用快速排序的思想,快速排序:每次移动,可以找到一个标杆元素,然后将大于它的移到左边,小于它的移到右边,由此整个数组划分成为两部分,然后分别对左边和右边使用同样的方法进行排序,不断划分左右子段,直到整个数组有序。这也是分治的思想,将数组分化成为子段,分而治之。

放到这道题中,如果标杆元素左边刚好有k−1个比它大的,那么该元素就是第k大:

//若从0开始,刚好是第K个点,则找到
if(K == j + 1)
return a[p];

如果它左边的元素比k−1少,说明第k大在其右边,直接二分法进入右边,不用管标杆元素左边:

if(j + 1 < k)
return partition(a, j + 1, high, k);

同理如果它右边的元素比k−1少,那第k大在其左边,右边不用管。

return partition(a, low, j - 1, k);

具体做法:

step 1:进行一次快排,大元素在左,小元素在右,得到的标杆j点.在此之前要使用随机数获取标杆元素,防止数据分布导致每次划分不能均衡。
step 2:如果 j + 1 = k ,那么j点就是第K大。
step 3:如果 j + 1 > k,则第k大的元素在左半段,更新high = j - 1,执行step 1。
step 4:如果 j + 1 < k,则第k大的元素在右半段,更新low = j + 1, 再执行step 1.

Java实现代码:

import java.util.*;
public class Solution {
    //交换函数
    Random r = new Random();
    public static void swap(int arr[], int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    public int partition(int[] a, int low, int high, int k){
        //随机快排划分
        int x = Math.abs(r.nextInt()) % (high - low + 1) + low;
        swap(a, low, x);
        int v = a[low];
        int i = low + 1;
        int j = high;
        while(true){
            //小于标杆的在右
            while(j >= low + 1 && a[j] < v)
                j--;
            //大于标杆的在左
            while(i <= high && a[i] > v)
                i++;
            if(i > j)
                break;
            swap(a, i, j);
            i++;
            j--;
        }
        swap(a, low, j);
        //从0开始,所以为第j+1大
        if(j + 1 == k)
            return a[j];
        //继续划分
        else if(j + 1 < k)
            return partition(a, j + 1, high, k);
        else
            return partition(a, low, j - 1, k);
    }
    public int findKth(int[] a, int n, int K) {
        return partition(a, 0, n - 1, K);
    }
}

BM48 数据流中的中位数

插入排序

具体做法:

step 1:用一数组存储输入的数据流。
step 2:Insert函数在插入的同时,遍历之前存储在数组中的数据,按照递增顺序依次插入,如此一来,加入的数据流便是有序的。
step 3:GetMedian函数可以根据下标直接访问中位数,分为数组为奇数个元素和偶数个元素两种情况。记得需要类型转换为double。
import java.util.*;
public class Solution {
    private ArrayList<Integer> val = new ArrayList<Integer>();
    public void Insert(Integer num) {
        if(val.isEmpty())
            //val中没有数据,直接加入
            val.add(num); 
        //val中有数据,需要插入排序
        else{ 
            int i = 0;
            //遍历找到插入点
            for(; i < val.size(); i++){
                if(num <= val.get(i))
                   break;
            }
            //插入相应位置
            val.add(i, num); 
        }
    }

    public Double GetMedian() {
        int n = val.size();
        //奇数个数字
        if(n % 2 == 1) 
            //类型转换
            return (double)val.get(n / 2); 
        //偶数个数字
        else{ 
            double a = val.get(n / 2);
            double b = val.get(n / 2 - 1);
            return (a + b) / 2;
        }
    }
}

时间复杂度:Insert函数O(n)O(n)O(n),不管遍历还是插入都是O(n)O(n)O(n),GetMedian函数O(1)O(1)O(1),直接访问
空间复杂度:O(n)O(n)O(n),记录输入流的数组

堆排序

step 1:我们可以维护两个堆,分别是大顶堆min,用于存储较小的值,其中顶部最大;小顶堆max,用于存储较大的值,其中顶部最小,则中位数只会在两个堆的堆顶出现。
step 2:我们可以约定奇数个元素时取大顶堆的顶部值,偶数个元素时取两堆顶的平均值,则可以发现两个堆的数据长度要么是相等的,要么奇数时大顶堆会多一个。
step 3:每次输入的数据流先进入大顶堆排序,然后将小顶堆的最大值弹入大顶堆中,完成整个的排序。
step 4:但是因为大顶堆的数据不可能会比小顶堆少一个,因此需要再比较二者的长度,若是小顶堆长度小于大顶堆,需要从大顶堆中弹出最小值到大顶堆中进行平衡。
import java.util.*;
public class Solution {
    //小顶堆,元素数值都比大顶堆大
    private PriorityQueue<Integer> max = new PriorityQueue<>();
    //大顶堆,元素数值较小 
    private PriorityQueue<Integer> min = new PriorityQueue<>((o1, o2)->o2.compareTo(o1)); 
    //维护两个堆,取两个堆顶部即与中位数相关
    public void Insert(Integer num) {
        //先加入较小部分
        min.offer(num);
        //将较小部分的最大值取出,送入到较大部分
        max.offer(min.poll());  
        //平衡两个堆的数量
        if(min.size() < max.size())  
            min.offer(max.poll());
    }

    public Double GetMedian() {
        //奇数个
        if(min.size() > max.size()) 
            return (double)min.peek();
        else
            //偶数个
            return (double)(min.peek() + max.peek()) / 2; 
    }


}

时间复杂度:Insert函数 & O(log_2n) & ,维护堆的复杂度,GetMedian函数O(1),直接访问
空间复杂度:O(n),两个堆的空间,虽是两个,但是一个堆最多n/2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值