代码随想录算法训练营第9天|栈与队列part01

第五章 栈与队列part01

理论基础

        了解一下 栈与队列的内部实现机制,文中是以C++为例讲解的。

        文章讲解:代码随想录

        先学下最基础的知识点:队列是先进先出,栈是先进后出。

        栈的内部结构是这样的,可以看出来栈有点像一个类,用vector,deque和list实现存储数据,但是只给了外部服务接口就那么几个函数。

        默认用deque实现栈,当然,list和map,也可,代码如下:

std::stack<int, std::vector<int> > third;  // 使用vector为底层容器的栈
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列

        容器适配器

容器适配器通常是基于已有的标准容器(如std::vectorstd::deque等)构建的,适配器修改了它们的接口,使其表现得像另一种特定用途的容器。

C++标准库中有三种主要的容器适配器:

  1. std::stack:基于std::dequestd::vectorstd::list实现的后进先出(LIFO)数据结构。它只允许在容器的一端进行插入和删除操作,类似于堆栈。

  2. std::queue:基于std::deque或其他双端容器实现的先进先出(FIFO)数据结构。它允许从一端插入元素,从另一端移除元素。

  3. std::priority_queue:基于std::vector或其他随机访问容器实现的优先队列。这个适配器会根据元素的优先级进行排序,以确保优先级最高的元素总是位于队列的前端。

特点

  • 限制接口:容器适配器通常限制了底层容器的一部分接口,以提供更专注于特定用途的功能。例如,std::stack并不允许随机访问,只允许pushpoptop操作。

  • 灵活的底层容器:容器适配器可以使用不同的底层容器来实现,只要这些容器满足某些需求。例如,std::stack可以使用std::deque(默认)、std::vector或者std::list作为其底层容器。

232.用栈实现队列

        使用栈实现队列的下列操作:

        push(x) -- 将一个元素放入队列的尾部。
        pop() -- 从队列首部移除元素。
        peek() -- 返回队列首部的元素。
        empty() -- 返回队列是否为空。

        大家可以先看视频,了解一下模拟的过程,然后写代码会轻松很多。

        题目链接/文章讲解/视频讲解:代码随想录

        感觉没必要看视频,直接看下面的动图,思路就明显了。

        

只要一个进栈,一个出栈,就很好理解队列的操作。接下来就要学习如何实现,

push,还是很好实现的,直接导入stIn里面就可以了。

pop实现也很简单,把stIn里面的全导入到stout,弹出stOut里面最上面的,但是我最开始写的代码,又把stOut全导入到stIn。这种思路没有问题,但是很麻烦,卡尔给的方法很直接,stIn只是用来临时存储作用和导入的。stOut才是用来弹出的。记住一定要stOut是空的才好,只有这样,才能保证顺序。存储的新的一波stIn导入到stOut是正常顺序的。

class MyQueue {
public:
    stack<int> stIn;
    stack<int> stOut;
    MyQueue() {
            }
    
    void push(int x) {
        stIn.push(x);
    }
    
    int pop() {
    if(stOut.empty()==1)
    while(stIn.empty()==0)
    {
        stOut.push(stIn.top());
        stIn.pop();
    }
        int result= stOut.top();
        stOut.pop();
        return result;
    }
    
    int peek() {
        int temp= this->pop();
        stOut.push(temp);
        return temp;
    }
    
    bool empty() {
        return stIn.empty()&&stOut.empty();
    }
};

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue* obj = new MyQueue();
 * obj->push(x);
 * int param_2 = obj->pop();
 * int param_3 = obj->peek();
 * bool param_4 = obj->empty();
 */

写代码的时候发现:pop() 函数错误:stIn.pop() 实际上并不会返回值。你需要先获取栈顶元素的值,再调用 pop() 移除该元素。

在 C++ 的标准库中,std::stack<T>::pop() 函数的确不会返回被弹出的元素值。原因在于 C++ 设计者对栈(stack)的抽象模型及其使用方式进行了设计权衡,决定让 pop() 函数仅执行移除操作,而不返回值。

具体原因如下:

  1. 职责分离pop() 的设计原则是“职责单一”。它的唯一职责是将栈顶的元素移除,而不是返回这个元素。如果需要获取栈顶元素,应该调用 top(),而不是 pop()。C++ 标准库将这两个操作分开,以减少函数的职责,让代码更清晰,职责更明确。

  2. 避免误用:如果 pop() 同时移除并返回栈顶元素,程序员可能会忽略检查栈是否为空,直接使用返回值。这会导致未定义行为(Undefined Behavior),因为当栈为空时,pop() 将尝试移除不存在的元素。将这两个操作分开,可以更好地提醒程序员先调用 top() 获取值,再调用 pop() 进行移除。

225. 用队列实现栈

可能大家惯性思维,以为还要两个队列来模拟栈,其实只用一个队列就可以模拟栈了。

建议大家掌握一个队列的方法,更简单一些,可以先看视频讲解

使用两个的方法思路就是q2用来当作一个temp变量,用来临时存储。代码没有什么好讲的,整体上和用栈模拟队列有点不一样,几乎全程靠q1来C位,q2只是用来临时存储的工具。

题目链接/文章讲解/视频讲解:代码随想录

class MyStack {
public:
    queue<int> q1;
    queue<int> q2;
    MyStack() {

    }
    void push(int x) {
        q1.push(x);
    }
    
    int pop() {
   int size=q1.size();
    while(--size)
    {
        q2.push(q1.front());
        q1.pop();
    }
    int result=q1.front();
    q1.pop();
    q1=q2;
    while(q2.empty()==0)
    q2.pop();
    return result;
    }
    
    int top() {
        return q1.back();
    }
    
    bool empty() {
        return q1.empty();
    }
   
};

/**
 * Your MyStack object will be instantiated and called as such:
 * MyStack* obj = new MyStack();
 * obj->push(x);
 * int param_2 = obj->pop();
 * int param_3 = obj->top();
 * bool param_4 = obj->empty();
 */
1234567
start8
179
161514

13

121110

1<-
12<-
213
132
321

        感觉使用单栈来模拟思路更清晰简单。每次插入元素后,将队列中的元素重新排列,使得插入的元素成为队列的队首元素,这样在 pop()top() 操作时,直接访问队首元素即可。        

        根据上面表格,还是很好理解,先push(1);再push(2):push(2),pop(1),push(1);再push(3):push(3);pop(2),push(2),pop(1),push(1)。

  思路很简单,但是我在想可不可以push正常push,pop再排序呢。

1<-
12<-
123<-
231
312
12

        好像也可以,但是最大的问题,还是查找和pop,时间复杂度高太多了,各有利弊,如果需要大量查,感觉还是,push排序,pop正常,如果需要大量添加,正常push,pop再排序。

class MyStack {
public:
    queue<int> q1;
    MyStack() {
    }
    void push(int x) {
        q1.push(x);
      
    }   
    int pop() {
         int size=q1.size();
    while(--size)
    {
        q1.push(q1.front());
        q1.pop();}
       int result=q1.front();
        q1.pop();
         return  result;
    
    }
    int top() {
        
        int result=this->pop();
        q1.push(result);
        return result;   
    }
    
    bool empty() {
        return q1.empty();
    }
};

/**
 * Your MyStack object will be instantiated and called as such:
 * MyStack* obj = new MyStack();
 * obj->push(x);
 * int param_2 = obj->pop();
 * int param_3 = obj->top();
 * bool param_4 = obj->empty();
 */

20. 有效的括号

        讲完了栈实现队列,队列实现栈,接下来就是栈的经典应用了。

        大家先自己思考一下 有哪些不匹配的场景,在看视频 我讲的都有哪些场景,落实到代码其实就容易很多了。

        题目链接/文章讲解/视频讲解:代码随想录

        感觉和栈的思路基本上一致,把字符串s压入栈中,看下是不是右括号,如果是右括号一定有一个左括号可以配对。可以仔细观察就能发现,无论“(){}[]{}[]”这种并列的,还是“{({[()]})}”这种内嵌的,一定是可以相邻,可以对应删除掉。如果有一个右括号左括号没有匹配的,说明一定是错的,可以直接返回了。

        特别注意,一个for循环后,一定要判断下栈是否为空,为空的话才是匹配完,不为空说明有漏下的左括号。

class Solution {
public:
    bool isValid(string s) {
        stack<char> st;
      
for(int i=0;i<s.size();i++)
{
    if(s[i]==')'||s[i]==']'||s[i]=='}')
    {
        if(st.empty()==1)
        return false;
        if((st.top()=='('&&s[i]==')')||(st.top()=='['&&s[i]==']')||(st.top()=='{'&&s[i]=='}'))
            st.pop();
        else
            return false;
    } 
    else
    st.push(s[i]);
}
 if(st.empty()==1)
    return true;
 else 
     return false;
    }
};

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

栈的经典应用。

代码很简单,和之前的括号匹配很类似,所以挺简单的。

class Solution {
public:
    string removeDuplicates(string s) {
        stack<char> st;
        for(int i=0;i<s.size();i++)
    { 
        if(st.empty()||st.top()!=s[i])
            st.push(s[i]);
        else 
            st.pop();     
    } 
    string result;
    while (!st.empty()) {
        result += st.top();  
        st.pop();   
    }
    reverse(result.begin(), result.end());
    return result;
}
};

要知道栈为什么适合做这种类似于爱消除的操作,因为栈帮助我们记录了 遍历数组当前元素时候,前一个元素是什么。

生成字符串会与栈中字符顺序相反。如果需要保持原来的顺序,可以在生成字符串后进行反转,或者在构建字符串时从栈底开始。

题目链接/文章讲解/视频讲解:代码随想录

写代码的时候就想到,还要把栈导入,再导出,好麻烦,为什么不可以直接新建个string,在string上改呢。果然自己能想到的,别人肯定能想到。确实敲代码敲多了,确实能感觉到哪些地方重复浪费。原来字符串也可以push_back(),empty(),以及遍历,学到了。对,string的本质是个字符动态数组。

class Solution {
public:
    string removeDuplicates(string s) {
        string result;
        for(auto it : s) {
            if(result.empty() || result.back() != it) {
                result.push_back(it);
            }
            else {
                result.pop_back();
            }
        }
        return result;
    }
    
};

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第14训练营中,讲解了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论中还分享了相关的博客文章和配图,帮助学员更好地理解和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15的讨论中,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16的讨论中,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地解决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值