算法学习打卡day41|栈和队列:栈和队列相互实现、括号匹配、逆波兰表达式、滑动窗口最大值问题、求前 K 个高频元素

栈和队列相互实现

力扣题目链接用栈实现队列用队列实现栈
题目描述:

  • 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
    实现 MyQueue 类:
    void push(int x) 将元素 x 推到队列的末尾
    int pop() 从队列的开头移除并返回元素
    int peek() 返回队列开头的元素
    boolean empty() 如果队列为空,返回 true ;否则,返回 false
  • 请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
    实现 MyStack 类:
    void push(int x) 将元素 x 压入栈顶。
    int pop() 移除并返回栈顶元素。
    int top() 返回栈顶元素。
    boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

思路:

  • 用栈实现队列的思路是遇到 push 就放到stk1里,如果遇到pop那么就把stk1的元素一次存到stk2里,然后从stk2的栈顶pop,就可以实现先入先出了。
  • 用队列实现栈的思路是一个队列que1用来存储元素,用另外一个队列que2来备份元素,但要pop元素时,就把que1的元素都存到que2的元素里,然后把que1最后一个元素删掉就行。

代码实现:

  • 用栈实现队列
class MyQueue {
public:
    MyQueue() {
    }
    
   void push(int x) {
        stack1.push(x);
   }
   int pop() {
       int x = 0;
       if (!stack2.empty()) {
           x = stack2.top();
           stack2.pop();
           return x;
       }
       
       while (!stack1.empty()) {
           x = stack1.top();
           stack1.pop();
           stack2.push(x);
       }
       x = stack2.top();
       stack2.pop();
       return x;
   }
   int peek() {
        int result = this->pop();//降低代码耦合度
        stack2.push(result);
        return result;
   }
   bool empty() {
       return stack1.empty() && stack2.empty();
   }
private:
    stack<int> stack1; //用来压栈
    stack<int> stack2;  //从stack1出占,压入stack2就是栈顶元素了
};
  • 用队列实现栈
class MyStack {
public:
    queue<int> que1;
    MyStack() {
    }
    void push(int x) {
        que1.push(x);
    }
    int pop() {
        int size = que1.size();
        int x = 0;
        while (--size) {
            x = que1.front();
            que1.pop();
            que1.push(x);
        }
        int result = que1.front();
        que1.pop();
        return result;
    }
    int top() {
        int result = pop();
        que1.push(result);
        return result;
    }
    bool empty() {
        return que1.empty();
    }
    
};

括号匹配

力扣题目链接

题目描述:
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。

有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。

示例 1
输入:s = “()”
输出:true
示例 2
输入:s = “()[]{}”
输出:true
示例 3
输入:s = “(]”
输出:false

思路:

  • 这道题因为所有的元素都是括号,所以,我们直接把符号压入栈中,然后遇到右括号就和栈顶元素比较,如果匹配不到(不匹配或栈为空都是无效)那么肯定是无效的。当字符串遍历完后如果栈非空,那么字符串也是无效的。
  • 另外奇数一定是无法匹配的,可以在开头判断一下。

代码实现:

bool isValid(string s) {
        if (s.size() % 2)   return false;   //奇数一定无法匹配
        stack<char> stk;
        for (char& i : s) {
            if (i == '{' || i == '(' || i == '[') {
                stk.push(i);//左括号入栈
            } else {
                if (stk.empty())    return false;//如果是右括号,但是此时栈空,说明右边多了直接return
                if (i == ')') {
                    if (stk.top() != '(')   return false;//匹配不到就return
                } else if (i == ']') {
                    if (stk.top() != '[')   return false;
                } else {
                    if (stk.top() != '{')   return false;
                }
                stk.pop();//匹配到了,别忘了pop
            }
        }
        return stk.empty();//不用单独if判断了,直接在这里判断就行
    }
  • 代码实现进行了一定的优化,遇到左括号可以直接存对应右括号,那么遇到右括号可以直接进行比较是否相同了。
bool isValid(string s) {
        if (s.size() % 2)   return false;   //奇数一定无法匹配
        stack<char> stk;
        for (char& i : s) {
            if (i == '{')   stk.push('}');//换个思路,存右括号,那么遇到右括号直接比较是否相等就行了
            else if (i == '(') stk.push(')');
            else if (i == '[')  stk.push(']');
            else if (stk.empty() || stk.top() != i) return false;//右边多了或者没匹配到
            else stk.pop();//匹配到了,别忘了pop
        }
        return stk.empty();//不用单独if判断了,直接在这里判断就行
    }
  • 应用场景:
    • 编译器在词法分析的过程中处理括号、花括号等这个符号的逻辑,就是使用了栈来进行匹配的。
    • linux的cd命令也涉及到栈对路径处理。
    • 函数调用时的调用栈。
    • 递归也会借助栈来实现,每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中。

1047. 删除字符串中的所有相邻重复项

力扣题目链接
题目描述:
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例:
输入:“abbaca”
输出:“ca”
解释:
例如,在 “abbaca” 中,我们可以删除 “bb” 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 “aaca”,其中又只有 “aa” 可以执行重复项删除操作,所以最后的字符串为 “ca”。

思路:

  • 这道题有两种写法,栈的实现思路比较简单。
  • 借助栈:如果栈为空或者当前元素和栈顶元素不相等,就入栈,如果相等就把栈顶元素删除,遍历结束后就得到了最终结果。注意:字符串和数组是天然的栈!!!
  • 双指针法: 双指针法就是原地移除元素了,定义一个left和right指针,别激动和普通的删除重复项还不一样,是慢指针和快指针一起走,而且不是right和left比较,而是right和left-1去比较。! 而一般双指针法是left在从0走,right从1走,
    • 只有当慢指针的前一个元素和当前值相等时再退一格,只有这样才能保证奇数时留一个,偶数时都干掉(奇数时和前一个元素不一样了)。
    • 然后赋值时不是++left而是left++了,因为如果是偶数,那就是回退,然后偶数序列右边界下一个元素一定和左边界前一个不相等,这时就left就直接覆盖都给删了,如果是奇数那在右边界的时候已经和左边界前一个元素不相等了,可以留一个
  • 总结:遇到删除相邻元素一般两个思路:双指针法(slow、fast,数组有个通解),另外是借助栈去删除。

代码实现:

string removeDuplicates(string s) {
        string result;
        for (char& i : s) {
            if (result.empty() || i != result.back()) {
                result.push_back(i);
                continue;
            }  
            result.pop_back();
        }
        return result;
    }
  • 双指针法
string removeDuplicates(string s) {
        int left = 0, right = 0;
        for (right; right < s.size(); ++right) {
            if (left > 0 && s[left - 1] == s[right]) {
                left--;
            } else {
                 s[left++] = s[right];
            }
        }
        s.resize(left);
        return s;
    }

逆波兰表达式

力扣题目链接
题目描述:
给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。

注意:
有效的算符为 ‘+’、‘-’、‘*’ 和 ‘/’ 。
每个操作数(运算对象)都可以是一个整数或者另一个表达式。
两个整数之间的除法总是 向零截断 。
表达式中不含除零运算。
输入是一个根据逆波兰表示法表示的算术表达式。
答案及所有中间计算结果可以用 32 位 整数表示。

示例 1

输入:tokens = [“2”,“1”,“+”,“3”,“*”]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

思路:

  • 这个题很简单,遇到加减乘除就从栈里取两个元素,然后将计算结果再放入栈里即可,没有遇到运算符就把其他元素放入栈。

代码实现:

int evalRPN(vector<string>& tokens) {
        stack<int> stk;
        int num1, num2;
        for (string& s : tokens) {
            if (s == "+" || s == "-" || s == "*" || s == "/") {
                num1 = stk.top();
                stk.pop();
                num2 = stk.top();
                stk.pop();
                if (s == "+")   stk.push(num2 + num1);
                if (s == "-")   stk.push(num2 - num1);
                if (s == "*")   stk.push(num2 * num1);
                if (s == "/")   stk.push(num2 / num1);
            } else {
                stk.push(stoi(s));
            }
        }
        return stk.top();
    }

滑动窗口最大值问题

力扣题目链接
题目描述:
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值 。

示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
在这里插入图片描述

思路:

  • 这道题涉及到单调队列的应用,需要自己实现一个单调队列,什么是单调队列? 即让队列里的元素单调递增或者递减。
  • 看完题目首先想到的肯定是暴力循环去找最大值即可,这样的话时间复杂度是n*k,而优先级队列的话每次队列首位是最大值,没有办法确定是否是是我们滑动窗口要删除的元素。
  • 所以,我们这道题需要实现一个单调递减的队列,队列里保持单调性,并且最大值在队列头部,这里采用deque实现,C++底层默认也是deque实现的,主要涉及到push和pop两个操作:
    • 我们的需求是每次从队列里拿到窗口最大值,因为我们遍历数组的时候是知道要push哪个元素,pop哪个元素,那队列里其实只需要维护可能最大的元素就好了,每次pop的时候拿需要pop的元素去和队列首元素比较,如果相等就pop,不相等就不需要任何操作。
    • push:push的时候如果需要push的值比栈尾大时,就把尾部的值删掉,直到push的value比尾部值小停止删除,然后将新值存到栈尾。
    • pop:pop的话,需要和队列的front元素比较,如果相等就pop(),否则不需要进行操作,因为最大值没有发生改变,不需要执行pop操作(其实在push的时候比当前队首元素小的话就已经被pop出去了)。
    • front:这个就是返回队列开始的元素,记录窗口最大值。
  • 注意:
    1. deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。
    2. 单调队列的写法不唯一,遇到实际情况的时候,具体问题具体分析。

代码实现:

class Solution {
public:
    //定义单调队列
    class MyQueue {
    public:
        void push(int value) {
            while (!que.empty() && value > que.back()) {
                que.pop_back();
            }
            que.push_back(value);
        }
        void pop(int value) {
            if (!que.empty() && value == que.front()) {
                que.pop_front();
            }
        }
        int front() {
            return que.front();
        }
    private:
        deque<int> que;
    };
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        MyQueue my_que;
        //先放入k - 1个元素
        for (int i = 0; i < k - 1; ++i) {
            my_que.push(nums[i]);
        }
        int i = k - 1;
        vector<int> results;
        //依次取最大值
        while (i < nums.size()) {
            my_que.push(nums[i]);
            results.push_back(my_que.front());
            my_que.pop(nums[i - k + 1]);
            i++; 
        }
        return results;
    }
};

求前 K 个高频元素

力扣题目链接
题目描述:
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]

思路:

  • 一般遇到前k个或者k个元素合并之类的都可以试着用优先级队列来解题,本题也是使用优先级队列。
  • 分为以下三步:
    1. 首先先统计数组所有元素出现的次数,使用哈希map
    2. 建立小根堆,一直维持k个最大元素即可,既然是要求高频元素为啥不用大根堆? 因为由于我们需要保持K个元素不变,使用大根堆我们每次push一个元素进去后要pop一个元素出去,而大顶堆把最大元素pop出去了,最终得到的结果是前K个最小的元素了,而我们本题只需要维持最大的k个元素即可,每次利用小根堆把最小的元素干掉即可。
    3. 输出优先级队列的元素。
  • 另外本体需要自定义优先级队列的cmp,由于第三个参数是个类参数,所以我们需要自己定义一个类。

代码实现:

class mycomparison {
    public:
        bool operator () (const pair<int, int>& a, const pair<int, int>& b) {
            return a.second > b.second;
        }
    }; 
    vector<int> topKFrequent(vector<int>& nums, int k) {
        //统计次数
        unordered_map<int, int> maps;
        for (int& i : nums) {
            maps[i]++;
        }
        //建堆
        priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
        for (auto& i : maps) {
            pri_que.push(i);
            if (pri_que.size() > k) {
                pri_que.pop();
            }
        }

        //输出结果
        vector<int> results;
        while (k--) {
            results.push_back(pri_que.top().first);
            pri_que.pop();
        }
        return results;
    }
  • 总结:
    • 如果自定义对象直接重载对应的运算符(>或<),如果是标准库容器里的对象,可以自己定义一个结构体重载()
      • 还有为什么左大于右就会建立小顶堆,而不是建立大顶堆?

    例如我们在写快排的cmp函数的时候,return left>right 就是从大到小,return left<right 就是从小到大,优先级队列的定义正好反过来了,可能和源码实现有关。

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值