代码随想录算法训练营第10天

代码随想录算法训练营第10天 | 栈与队列理论基础、232.用栈实现队列、225. 用队列实现栈、20. 有效的括号、1047. 删除字符串中的所有相邻重复项

一、刷题部分

1.1 栈与队列理论基础

栈是先进后出,队列是先进先出。

在 C++ 中,栈和队列是 STL 里面的两个数据结构,然而不同的 STL 版本是有区别的:

  1. HP STL:其他版本的 C++ STL,一般是以 HP STL 为蓝本实现出来的,HP STL 是 C++ STL 的第一个实现版本,而且开放源代码。
  2. P.J.Plauger STL 由 P.J.Plauger 参照 HP STL 为蓝本实现出来的,被 Visual C++ 编译器所采用,不是开源的。
  3. SGI STL 由 Silicon Graphics Computer System 公司参照 HP STL 实现,被 Linux 的 C++ 编译器 GCC 所采用,SGL STL 是开源软件,源码可读性甚高。

接下来介绍的栈与队列也是 SGI STL 里面的数据结构:

栈提供 push 和 pop 接口,所有元素必须符合先进后出规则,所以栈不能提供走访功能,也不提供迭代器。

栈以底层容器完成所有工作,对外提供统一接口,底层容器是可插拔的(可以控制使用哪种容器来实现)。因此 STL 中的栈往往不算作容器,而是叫做 container adapter (容器适配器).

以下示意图表示了 STL 栈与容器的关系:

SGI STL 默认使用 deque 来实现栈。deque 是一个双向队列,只要封住一端就可以当做栈来使用。

若要手动指定 vector 为栈的底层实现,初始化语句:

std::stack<int, std::vector<int>> third; //使用 vector 为底层容器的栈

队列

与前面的栈基本一样,也是默认用 deque 为底层数据结构,也可以指定 list 为底层实现:

std::queue<int, std::list<int>> third; //使用 list 为底层容器的队列

1.2 232.用栈实现队列

1.2.1 题目描述

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  • 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例 1

输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]

解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false

提示:

  • 1 <= x <= 9
  • 最多调用 100pushpoppeekempty
  • 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)

进阶:

  • 你能否实现每个操作均摊时间复杂度为 O(1) 的队列?换句话说,执行 n 个操作的总时间复杂度为 O(n) ,即使其中一个操作可能花费较长时间。

1.2.2 初见思路

这题貌似在本科学习数据结构的时候做过,现在回忆一下思路:

用 stack1 的栈口放数据,用 stack2 的栈口出数据。

push:先检查 stack2 是否为空,如果为空则将 stack1 里的数据倒入 stack2,否则不动。之后将数据 push 到 stack1 里。

pop:先检查 stack2 是否为空,如果为空则将 stack1 里的数据倒入 stack2,否则不动。之后将 stack2 里的数据 pop 出来。

peek:先检查 stack2 是否为空,如果为空则将 stack1 里的数据倒入 stack2,否则不动,之后将 stack2 顶端元素返回。

empty:先检查 stack2 是否为空,如果为空则将 stack1 里的数据倒入 stack2,否则不动。之后再检查 stack2 是否为空,如果为空则返回 true,否则返回 false。

思路有了接下来写一下成员对象:

private:
    stack<int> s1;
    stack<int> s2;

实现栈的转移(包含条件判断):

void transfer() {
    if (!s2.empty()) {
        return;
    }
    //实现栈的转移,是一个基本操作
    while(!s1.empty()) {
        s2.push(s1.top());
        s1.pop();
    }
}

实现push

void push(int x) {
    transfer();
    s1.push(x);
}

实现pop

int pop() {
    transfer();
    int res = s2.top();
    s2.pop();
    return res;
}

实现peek

int peek() {
    transfer();
    return s2.top();
}

实现empty

bool empty() {
    transfer();
    return s2.empty();
}

经测试,代码顺利通过力扣测试。接下来看一下录。

1.2.3 正式做题

与我的思路基本一致。

1.2.4 遇到的困难

🈚️

1.2.5 本题总结

本题拓展部分讲了在工业生产中,遇到重复的逻辑应该是将这个逻辑抽象成一个函数然后重复调用,而不是复制粘贴代码。


1.3 225. 用队列实现栈

1.3.1 题目描述

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false

注意:

  • 你只能使用队列的标准操作 —— 也就是 push to backpeek/pop from frontsizeis empty 这些操作。
  • 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

示例:

输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]

解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False

提示:

  • 1 <= x <= 9
  • 最多调用100pushpoptopempty
  • 每次调用 poptop 都保证栈不为空

进阶:你能否仅用一个队列来实现栈。

1.3.2 初见思路

先想想不进阶的题目,也就是用两个队列该怎么做:

由于单向队列不可能实现元素的逆序,因此类似于用队列实现栈的思路是不可行的。我简单设想了这样的方法,其实把队列当成栈那么最重要的是取数的时候应该取队尾的数,那么可以用一个队列主要存储内容,每次 栈push 操作就对该队列push,然后另一个队列作为辅助队列,每一次栈pop的时候,将第一个队列里的数全都按序pop到辅助队列只留最后一个元素,然后pop出来就行。

写一下代码:

先定义两个队列:

private:
	queue<int> q1;
	queue<int> q2;

实现push:

void push(int x) {
    q1.push(x);
}

实现pop:

int pop(){
    while (q1.size() > 1) {
        q2.push(q1.front());
        q1.pop();
    }
    int res = q1.front();
    q1.pop();
    while (!q2.empty()) {
        q1.push(q2.front());
        q2.pop();
    }
    return res;
}

实现top:

int top(){
    int res;
    while (!q1.empty()) {
        res = q1.front();
        q2.push(res);
        q1.pop();
    }
    while (!q2.empty()) {
        q1.push(q2.front());
        q2.pop();
    }
    return res;
}

实现empty:

bool empty() {
    if (q1.empty()) {
        return true;
    }
    return false;
}

通过了力扣的测试。

其实在写这个代码的时候就已经想到了怎么来用一个队列做了,就是将队头元素pop出来之后立即push回去,这样就不需要一个辅助队列了。

class MyStack {
private:
	queue<int> q;
public:
    MyStack() {
        
    }
    
    void push(int x) {
        q.push(x);
    }
    
    int pop(){
        int size = q.size();
        int res;
        while (size > 1) {
            //将队头pop出后立即push回来
            res = q.front();
            q.pop();
            q.push(res);
            size--;
        }
        //最后一个元素就丢掉了
        res = q.front();
        q.pop();
        return res;
    }
    
    int top(){
        int size = q.size();
        int res;
        while (size > 0) {
            res = q.front();
            q.pop();
            q.push(res);
            size--;
        }
        return res;
    }
    
    bool empty() {
        if (q.empty()) {
            return true;
        }
        return false;
    }
};

自然也是通过了力扣的测试。下面看看录里怎么讲的。

1.3.3 正式做题

双队列的思路与我自己写的基本一致。

单队列的思路与我自己写的也是基本一致。

1.3.4 遇到的困难

🈚️

1.3.5 本题总结

还是需要动点脑筋的,直接生搬硬套栈模拟队列的思路确实做不出来这道题。

1.4 20. 有效的括号

1.4.1 题目描述

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

示例 1:

输入:s = "()"

输出:true

示例 2:

输入:s = "()[]{}"

输出:true

示例 3:

输入:s = "(]"

输出:false

示例 4:

输入:s = "([])"

输出:true

提示:

  • 1 <= s.length <= 104
  • s 仅由括号 '()[]{}' 组成

1.4.2 初见思路

显然是需要使用栈的,思路大致如下:

使用一个栈来存储字符,每次遇到一个左括号就压栈,每次遇到一个右括号就检查栈顶元素是否匹配,匹配就出栈,否则直接返回不匹配;进行到最后如果栈不为空也是不匹配,栈为空说明匹配。具体写代码的时候可以给括号映射成数字,这样更好判断。

先做好映射,可以使用 map:

map<char,int> myMap;
myMap['('] = -1;
myMap['['] = -2;
myMap['{'] = -3;
myMap[')'] = 1;
myMap[']'] = 2;
myMap['}'] = 3;

定义好栈:

stack<int> myStack;

遍历string:

for (char c : s) {
}

左括号压栈:

if (myMap[c] < 0) {
    myStack.push(myMap[c]);
}

右括号检查:

else {
    if (myStack.empty() || (myMap[c] + myStack.top() != 0)) {
        return false;
    }
    else {
        myStack.pop();
    }
}

最后检查栈空与否:

if (myStack.empty()) {
    return true;
}
return false;

代码如下:

class Solution {
public:
    bool isValid(string s) {
        map<char,int> myMap;
        myMap['('] = -1;
        myMap['['] = -2;
        myMap['{'] = -3;
        myMap[')'] = 1;
        myMap[']'] = 2;
        myMap['}'] = 3;
        stack<int> myStack;

        for (char c : s) {
            if (myMap[c] < 0) {
                myStack.push(myMap[c]);
            }
            else {
                if (myStack.empty() || (myMap[c] + myStack.top() != 0)) {
                    return false;
                }
                else {
                    myStack.pop();
                }
            }
        }
        if (myStack.empty()) {
            return true;
        }
        return false;
    }
};

1.4.3 正式做题

思路一致。

1.4.4 遇到的困难

在写的时候没有考虑到出现右括号的时候栈为空的情况,判题报错了才发现这个问题。

1.4.5 本题总结

主要还是需要再做题之前先把几种可能得情况考虑清楚再来做题,这样再问题没那么容易看出来的时候,会很有意义。

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

1.5.1 题目描述

给出由小写字母组成的字符串 s重复项删除操作会选择两个相邻且相同的字母,并删除它们。

s 上反复执行重复项删除操作,直到无法继续删除。

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例:

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

提示:

  1. 1 <= s.length <= 105
  2. s 仅由小写英文字母组成。

1.5.2 初见思路

可以借助栈来一次做完。本题没有太多好说的,直接开写。

//定义栈
stack<char> st;
//定义返回值
string res;
//遍历字符串
for (char c : s) {
    if (st.empty() || st.top() != c) {
        st.push(c);
    }
    else {
        st.pop();
    }
}
//收尾工作
while (!st.empty()) {
    res += st.top();
    st.pop();
}

res = res.reverse();
return res

完整代码:

class Solution {
public:
    string removeDuplicates(string s) {
        //定义栈
        stack<char> st;
        //定义返回值
        string res;
        //遍历字符串
        for (char c : s) {
            if (st.empty() || st.top() != c) {
                st.push(c);
            }
            else {
                st.pop();
            }
        }
        //收尾工作
        while (!st.empty()) {
            res += st.top();
            st.pop();
        }

        reverse(res.begin(), res.end());
        return res;
    }
};

1.5.3 正式做题

思路一致。

1.5.4 遇到的困难

🈚️

1.5.5 本题总结

本题和上一题基本一样的思路,算是展示了栈的一些基本用法。

二、总结与回顾

今天简单了解了一下栈和队列的使用,整体难度不高。

posted @ 2025-01-26 20:22  xc0208  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示