LeetCode 题解随笔:栈与队列

LeetCode 题解随笔:栈与队列,第1张

目录

一、栈与队列基础知识

232.用栈实现队列

225. 用队列实现栈

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

二、栈的应用

20. 有效的括号

150. 逆波兰表达式求值

71. 简化路径

 三、队列的应用

239. 滑动窗口最大值

四、优先队列

347.前 K 个高频元素


一、栈与队列基础知识 232.用栈实现队列
class MyQueue {
public:
    MyQueue() {

    }

    void push(int x) {
        this->sIn.push(x);
    }

    int pop() {
        //sOut为先入元素,为空时将sIn内元素全部导入
        if (this->sOut.empty()) {
            while (!this->sIn.empty()) {
                this->sOut.push(this->sIn.top());
                this->sIn.pop();
            }
        }
        int res = this->sOut.top();
        this->sOut.pop();
        return res;
    }

    int peek() {
        int res = this->pop();
        this->push(res);
        return res;
    }

    bool empty() {
        return this->sIn.empty() && this->sOut.empty();
    }

private:
    stack sIn;
    stack sOut;
};

利用两个栈实现,一个入队用,一个出队用。

注意两点:1.enpty()函数的简化写法;2.peek()函数复用了pop()函数,能使用代码复用的地方,尽量不要复制粘贴代码。

225. 用队列实现栈
class MyStack {
public:
    MyStack() {

    }

    void push(int x) {
        this->qMain.push(x);
    }

    int pop() {
        int size = this->qMain.size() - 1;
        //把队列1中的元素导入队列2,但保留最后一个元素
        while (size--) {
            this->qBackup.push(this->qMain.front());
            this->qMain.pop();
        }
        int res = this->qMain.front();
        this->qMain.pop();
        //把队列2拷贝给队列1,然后清空队列2
        this->qMain = this->qBackup;
        while (!this->qBackup.empty()) {
            this->qBackup.pop();
        }
        return res;
    }

    int top() {
        int res = this->pop();
        this->push(res);
        return res;
    }

    bool empty() {
        return this->qMain.empty();
    }

private:
    queue qMain;
    queue qBackup;
};

用第二个队列作为第一个队列的备份,每次pop()时除最后一个元素外全部入队第二个队列。

 1047. 删除字符串中的所有相邻重复项
string removeDuplicates(string s) {
        stack filter;
        for (int i = s.size() - 1; i >= 0; i--) {
            if (filter.empty())  filter.push(s[i]);
            else if (s[i] == filter.top())  filter.pop();
            else  filter.push(s[i]);
        }
        string res;
        while (!filter.empty()) {
            res += filter.top();
            filter.pop();
        }
        return res;
    }

本题也可以将字符串作为栈,利用push_back和pop_back直接实现。 


二、栈的应用 20. 有效的括号
bool isValid(string s) {
        stack rightRecord;
        for (auto i : s) {
            if (i == '(') rightRecord.push(')');
            else if (i == '{') rightRecord.push('}');
            else if (i == '[') rightRecord.push(']');
            //若需要输出左括号时,无该元素或无元素,说明不匹配或右括号多了
            else if (rightRecord.empty() || i != rightRecord.top()) return false;
            else rightRecord.pop();
        }
        //栈不为空,说明左括号多了
        return rightRecord.empty();
    }

不论括号是分段的,例如()[]{[]}有三段 ;还是括号只有囊括的,例如{[()]}只有一段。有效的字符串每一段对应的栈一定可以被清空。

另一个技巧是进栈右括号而非进栈左括号,这样只需要比较迭代元素i和栈顶元素是否一致即可。

150. 逆波兰表达式求值
    void GetTwoValues(stack& s, int& tempA, int& tempB) {
        tempB = s.top();
        s.pop();
        tempA = s.top();
        s.pop();
    }
    
    int evalRPN(vector& tokens) {
        stack s;
        int tempA;
        int tempB;
        for (auto i : tokens) {
            if (i == "+") {
                GetTwoValues(s, tempA, tempB);
                s.push(tempA + tempB);
            }
            else if (i == "-") {
                GetTwoValues(s, tempA, tempB);
                s.push(tempA - tempB);
            }
            else if (i == "*") {
                GetTwoValues(s, tempA, tempB);
                s.push(tempA * tempB);
            }
            else if (i == "/") {
                GetTwoValues(s, tempA, tempB);
                s.push(tempA / tempB);
            }
            else {
                s.push(stoi(i));
            }
        }
        return s.top();
    }

后缀表达式适合用栈 *** 作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。逆波兰表达式相当于是二叉树中的后序遍历。 

71. 简化路径
string simplifyPath(string path) {
        stack simplePath;
        // 字符串末尾添加一个/用于处理最后一级目录
        path += '/';
        for (auto str : path) {
            // 每次遇到/时进行判断和 *** 作
            if (str == '/' && !simplePath.empty()) {
                // 两个目录名之间必须只有一个斜杠 '/'
                if (simplePath.top() == '/') {
                    while (!simplePath.empty() && simplePath.top() == '/') {
                        simplePath.pop();
                    }
                }
                else if (simplePath.top() == '.') {
                    simplePath.pop();
                    // 一个点(.)表示当前目录本身
                    if (simplePath.top() == '/') {
                        simplePath.pop();
                    }
                    // 两个点 (..) 表示将目录切换到上一级
                    else if (simplePath.top() == '.') {
                        simplePath.pop();
                        // 需要返回上级目录的情况
                        if (simplePath.top() == '/') {
                            // 删除/..的/
                            simplePath.pop();
                            // 若存在上级目录,删除上级目录名以及/
                            if (!simplePath.empty()) {
                                while (simplePath.top() != '/') {
                                    simplePath.pop();
                                }
                                simplePath.pop();
                            }
                        }
                        // 多个.表示的是文件名
                        else {
                            simplePath.push('.');
                            simplePath.push('.');
                        }
                    }
                    // 其余情况.也表示文件名
                    else {
                        simplePath.push('.');
                    }
                }
            }
            simplePath.push(str);
        }
        // 最后一个目录名(如果存在)不能 以 '/' 结尾
        if (!simplePath.empty() && simplePath.top() == '/' && simplePath.size() != 1) {
            simplePath.pop();
        }
        string res;
        while (!simplePath.empty()) {
            res += simplePath.top();
            simplePath.pop();
        }
        reverse(res.begin(), res.end());
        return res;
    }

每种情况都考虑清楚即可。本题采用方式是遇到“/”时再进行处理。 


 三、队列的应用 239. 滑动窗口最大值
//用deque实现单调队列
class MyQueue {
public:
    //滑动窗口第一个值等于队列最大值是才出队,否则不进行 *** 作
    void pop(int value) {
        if (!this->m_queue.empty() && this->m_queue.front() == value) {
            this->m_queue.pop_front();
        }
    }
    //保持从头到尾由大到小
    void push(int value) {
        while (!this->m_queue.empty() && this->m_queue.back() < value) {
            this->m_queue.pop_back();
        }
        this->m_queue.push_back(value);
    }
    int front() {
        return this->m_queue.front();
    }
private:
    deque m_queue;
};

class Solution {
public: 
    vector maxSlidingWindow(vector& nums, int k) {
        MyQueue mq;
        vector res;
        //前k-1个元素进入单调队列
        for (int i = 0; i < k - 1; i++) {
            mq.push(nums[i]);
        }
        //从0到size()-k+1次循环,先入队(窗口内含k个元素),然后输出结果,再出队(窗口内含k-1个元素)
        for (int j = 0; j + k <= nums.size(); j++) {
            mq.push(nums[j + k - 1]);
            res.push_back(mq.front());
            mq.pop(nums[j]);
        }
        return res;
    }
};

本题采用单调队列这种数据结构来完成,一次循环的过程如下图所示(来源:代码随想录):

单调队列的队头元素存放滑动窗口内元素的最大值,并且队列递减排列,保证队头元素始终是可能的最大值。这样从0到size()-k+1次循环中,每次输出myQueue.front()即可。

实现单调队列的递减排列,并不是要将滑动窗口中的全部元素递减排列,而是在入队的过程中,只保留可能的最大元素:从deque的队尾入队,当待入队元素大于当前队尾时,deque()从队尾不断出队。

但这样会导致队列中的元素不足k个,因此在实现myQueue的pop() *** 作时,需要判断num[i]是否等于当前队头,只有等于时才执行deque的pop() *** 作。

单调队列不是一成不变的,针对不同场景要开发不同的写法。


四、优先队列 347.前 K 个高频元素
class MyCompare {
public:
    bool operator()(const pair& lhs, const pair& rhs) {
        return lhs.second > rhs.second;
    }
};

class Solution {
public: 
    vector topKFrequent(vector& nums, int k) {
        unordered_map freq;
        for (auto num : nums) {
            freq[num]++;
        }
        // 定义一个大小为k的小顶堆,用其扫描整个map
        // priority_queue,Container为保存数据的容器
        priority_queue, vector>, MyCompare> priQueue;
        for (unordered_map::iterator it = freq.begin(); it != freq.end(); it++) {
            priQueue.push(*it);
            // 元素数量大于k时,出队元素为最小元素
            if (priQueue.size() > k) {
                priQueue.pop();
            }
        }
        // 结果数组从大到小排列
        vector res;
        for (int i = k - 1; i >= 0; i--) {
            res.push_back(priQueue.top().first);
            priQueue.pop();
        }
        return res;
    }
};

首先,可以很自然地想到用map来统计频率,由于不需要对键值排序,此处采用unorder_map。

要对map中的频率值排序,此处采用堆排序,实现方法是优先队列(小顶堆/大顶堆)。 

大顶堆:每个结点的值都大于或等于其左右孩子结点的值。
小顶堆:每个结点的值都小于或等于其左右孩子结点的值。
如果是排序,求升序用大顶堆,求降序用小顶堆。

而对于topK问题,最大的 K 个:小顶堆;最小的 K 个:大顶堆。因为只保留K个元素,而优先队列每次出队队头元素,若采用大顶堆,保留的是最小的K个元素;采用小顶堆则保留了最大的K个元素。利用仿函数可以改变优先队列的排序方式。

算法的时间复杂度为O(nlog(k)),尤其注意排序过程中的时间复杂度为O(log(k))。

树结构和STL相关知识还要继续学习。

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/langs/867522.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-05-12
下一篇2022-05-12

发表评论

登录后才能评论

评论列表(0条)

    保存