殷色玫瑰头像
关注
C++ STL:stack、queue、priority_queue 与容器适配器详解封面图

C++ STL:stack、queue、priority_queue 与容器适配器详解

C++ STL:stack、queue、priority_queue 与容器适配器详解

一、stack

1.1 stack 的基本概念

stack 是一种**后进先出(LIFO)**的数据结构

也就是说:

Last In First Out

最后进入栈中的元素,最先被取出

可以把栈理解成一摞盘子

入栈

    ↓
  ┌───┐
  │ 3 │  ← 栈顶
  ├───┤
  │ 2 │
  ├───┤
  │ 1 │
  └───┘

出栈时只能从最上面取

因此 stack 不支持随机访问,不能像 vector 一样通过下标访问中间元素

常用接口:

接口作用
stack()构造空栈
empty()判断栈是否为空
size()获取栈中元素个数
top()获取栈顶元素
push()将元素压入栈顶
pop()删除栈顶元素

其中最常用的就是:

s.push(x);
s.pop();
s.top();
s.empty();
s.size();

需要注意:

s.pop();

只是删除栈顶元素,并不会返回被删除的元素

如果需要先获取栈顶元素,再删除:

int x = s.top();
s.pop();

二、stack 的使用

使用 stack 需要包含头文件:

#include <stack>

基本使用:

stack<int> s;

s.push(1);
s.push(2);
s.push(3);

此时:

栈顶
 ↓
 3
 2
 1

执行:

cout << s.top() << endl;

得到:

3

执行:

s.pop();

此时:

栈顶
 ↓
 2
 1

三、最小栈

普通 stack 可以在 O(1) 时间内获取栈顶元素,但是如果要求:

随时获取当前栈中的最小值

普通 stack 无法直接完成

一种经典方法是使用两个栈

stack<int> _elem;
stack<int> _min;

其中:

_elem

保存正常的栈元素

_min

保存当前栈中的最小值


3.1 入栈

假设依次加入:

5 3 7 2

可以按照下面的方式维护 _min

_elem       _min

5            5
3            3
7            3
2            2

每次入栈时:

void push(int x)
{
    _elem.push(x);

    if (_min.empty() || x <= _min.top())
        _min.push(x);
}

关键代码:

if (_min.empty() || x <= _min.top())

如果当前元素小于等于 _min 栈顶的元素,就把它也压入 _min

为什么使用 <= 而不是 <

因为最小值可能出现多次

例如:

5
3
3

此时 _min:

3
3
5

如果连续弹出两个 3,需要让 _min 中的两个 3 也分别被弹出


3.2 出栈

void pop()
{
    if (_min.top() == _elem.top())
        _min.pop();

    _elem.pop();
}

例如:

_elem       _min

7            3
3            3
5            5

此时弹出 3

由于:

_elem.top() == _min.top()

所以 _min 中的 3 也需要删除

最终:

_elem       _min

7            5
5

3.3 获取最小值

int getMin()
{
    return _min.top();
}

因为 _min 的栈顶始终保存当前最小值,所以获取最小值的时间复杂度为:

O(1)

完整结构可以理解成:

class MinStack
{
public:
    void push(int x)
    {
        _elem.push(x);

        if (_min.empty() || x <= _min.top())
            _min.push(x);
    }

    void pop()
    {
        if (_min.top() == _elem.top())
            _min.pop();

        _elem.pop();
    }

    int top()
    {
        return _elem.top();
    }

    int getMin()
    {
        return _min.top();
    }

private:
    stack<int> _elem;
    stack<int> _min;
};

核心思想就是:

一个栈保存所有数据,另一个栈专门维护当前最小值


四、栈的弹出压入序列

一个经典问题:

给定一个入栈序列,判断一个出栈序列是否合法

例如:

入栈:

1 2 3 4 5

如果出栈:

4 5 3 2 1

是可以实现的

过程:

1入栈
2入栈
3入栈
4入栈
4出栈

5入栈
5出栈

3出栈
2出栈
1出栈

但是如果要求:

4 3 5 1 2

则无法实现


模拟栈解决

核心思想:

按照入栈序列不断入栈,只要栈顶等于当前要求的出栈元素,就立即出栈

代码:

bool IsPopOrder(vector<int> pushV, vector<int> popV)
{
    if (pushV.size() != popV.size())
        return false;

    int outIdx = 0;
    int inIdx = 0;

    stack<int> s;

    while (outIdx < popV.size())
    {
        while (s.empty() || s.top() != popV[outIdx])
        {
            if (inIdx < pushV.size())
                s.push(pushV[inIdx++]);
            else
                return false;
        }

        s.pop();
        outIdx++;
    }

    return true;
}

这个题最重要的是理解:

while (s.empty() || s.top() != popV[outIdx])

只要当前栈顶不是目标出栈元素,就继续从入栈序列中拿元素进栈

如果所有元素都已经入栈,栈顶仍然不是目标元素,那么说明这个出栈序列无法实现


五、逆波兰表达式求值

逆波兰表达式也叫后缀表达式

例如普通表达式:

3 + 4

逆波兰表达式:

3 4 +

再比如:

3 + 4 * 5

可以写成:

3 4 5 * +

这种表达式非常适合使用栈处理


5.1 基本思想

从左到右遍历表达式

如果遇到数字:

入栈

如果遇到运算符:

取出两个栈顶元素
进行计算
把结果重新压入栈

例如:

3 4 +

过程:

3入栈

4入栈

遇到+

取出4
取出3

3 + 4 = 7

7重新入栈

5.2 为什么减法和除法要注意顺序

假设栈中:

3
4

栈顶是:

4

因此:

int right = s.top();
s.pop();

int left = s.top();
s.pop();

计算:

left - right

而不能写成:

right - left

例如:

3 4 -

正确结果:

3 - 4 = -1

除法也是同样的道理

left / right

5.3 代码

int evalRPN(vector<string>& tokens)
{
    stack<int> s;

    for (size_t i = 0; i < tokens.size(); ++i)
    {
        string& str = tokens[i];

        if (!(str == "+" ||
              str == "-" ||
              str == "*" ||
              str == "/"))
        {
            s.push(atoi(str.c_str()));
        }
        else
        {
            int right = s.top();
            s.pop();

            int left = s.top();
            s.pop();

            switch (str[0])
            {
            case '+':
                s.push(left + right);
                break;

            case '-':
                s.push(left - right);
                break;

            case '*':
                s.push(left * right);
                break;

            case '/':
                s.push(left / right);
                break;
            }
        }
    }

    return s.top();
}

六、stack 的模拟实现

stack 本身是一种容器适配器

因此可以使用其他容器作为它的底层容器

例如使用 vector

#include <vector>

namespace bite
{
    template<class T>
    class stack
    {
    public:
        stack()
        {}

        void push(const T& x)
        {
            _c.push_back(x);
        }

        void pop()
        {
            _c.pop_back();
        }

        T& top()
        {
            return _c.back();
        }

        const T& top() const
        {
            return _c.back();
        }

        size_t size() const
        {
            return _c.size();
        }

        bool empty() const
        {
            return _c.empty();
        }

    private:
        vector<T> _c;
    };
}

可以发现:

push()

对应:

vector::push_back()
pop()

对应:

vector::pop_back()
top()

对应:

vector::back()

因此:

只要底层容器能够支持 push_back() 和 pop_back(),就可以用来实现 stack

例如:

vector
list
deque

都可以


七、queue

7.1 queue 的基本概念

queue 是一种**先进先出(FIFO)**的数据结构

即:

First In First Out

最先进入队列的元素最先出去

例如:

入队 →

1  2  3  4

↑           ↑
队头        队尾

出队时:

1

先出去

然后:

2

再出去

因此队列与栈最大的区别:

stack:

后进先出

queue:

先进先出

7.2 queue 的常用接口

接口作用
queue()构造空队列
empty()判断队列是否为空
size()获取元素个数
front()获取队头元素
back()获取队尾元素
push()从队尾入队
pop()从队头出队

例如:

queue<int> q;

q.push(1);
q.push(2);
q.push(3);

此时:

队头             队尾
 ↓                 ↓
 1   2   3

执行:

q.pop();

删除:

1

剩下:

2   3

八、queue 的底层容器

queue 本身也是一种容器适配器

底层容器需要支持:

empty()
size()
front()
back()
push_back()
pop_front()

标准库中:

deque
list

都可以满足要求

默认情况下:

queue<int>

使用:

deque<int>

作为底层容器


九、queue 的模拟实现

因为 queue 同时需要:

尾插
头删

如果使用 vector,头删需要移动大量元素,因此效率较低

所以可以使用 list 模拟

#include <list>

namespace bite
{
    template<class T>
    class queue
    {
    public:
        queue()
        {}

        void push(const T& x)
        {
            _c.push_back(x);
        }

        void pop()
        {
            _c.pop_front();
        }

        T& back()
        {
            return _c.back();
        }

        const T& back() const
        {
            return _c.back();
        }

        T& front()
        {
            return _c.front();
        }

        const T& front() const
        {
            return _c.front();
        }

        size_t size() const
        {
            return _c.size();
        }

        bool empty() const
        {
            return _c.empty();
        }

    private:
        list<T> _c;
    };
}

对应关系:

queue::push()
        ↓
list::push_back()

queue::pop()
        ↓
list::pop_front()

queue::front()
        ↓
list::front()

queue::back()
        ↓
list::back()

十、priority_queue

priority_queue 的基本概念

priority_queue 是优先级队列

普通 queue 遵循:

先进先出

而 priority_queue 不按照进入顺序出队

它按照元素的优先级决定谁先出来

默认情况下:

priority_queue<int>

是大堆

也就是说:

top()

返回当前最大的元素

例如:

priority_queue<int> q;

q.push(3);
q.push(7);
q.push(2);
q.push(9);

那么:

q.top()

得到:

9

十一、priority_queue 与堆

priority_queue 底层默认使用:

vector

然后利用堆算法维护堆结构

因此可以把:

priority_queue

理解为:

堆 + 容器适配器接口

常用接口:

接口作用
empty()判断是否为空
size()获取元素个数
top()获取堆顶元素
push()插入元素
pop()删除堆顶元素

其中:

top()

只是查看堆顶

而:

pop()

才是真正删除堆顶


十二、大堆和小堆

默认:

priority_queue<int>

是大堆

例如:

priority_queue<int> q;

q.push(3);
q.push(2);
q.push(7);
q.push(6);
q.push(0);
q.push(4);
q.push(1);
q.push(9);
q.push(8);
q.push(5);

cout << q.top() << endl;

输出:

9

创建小堆

如果希望最小的元素位于堆顶,可以使用:

greater<int>
priority_queue<int, vector<int>, greater<int>> q;

此时:

q.top()

得到的是最小值

例如:

priority_queue<int, vector<int>, greater<int>> q;

q.push(3);
q.push(7);
q.push(2);
q.push(9);

cout << q.top() << endl;

结果:

2

因此:

priority_queue<int>

表示大堆

priority_queue<int, vector<int>, greater<int>>

表示小堆


十三、自定义类型使用 priority_queue

如果 priority_queue 中存储的是自定义类型,就需要告诉它:

两个对象之间应该如何比较

例如定义:

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {}

    bool operator<(const Date& d) const
    {
        return (_year < d._year) ||
               (_year == d._year && _month < d._month) ||
               (_year == d._year && _month == d._month && _day < d._day);
    }

    bool operator>(const Date& d) const
    {
        return (_year > d._year) ||
               (_year == d._year && _month > d._month) ||
               (_year == d._year && _month == d._month && _day > d._day);
    }

private:
    int _year;
    int _month;
    int _day;
};

然后:

priority_queue<Date> q1;

默认构造大堆,因此需要提供:

operator<

如果使用:

priority_queue<Date, vector<Date>, greater<Date>> q2;

则需要提供:

operator>

十四、priority_queue 解决第 K 大元素

例如:

数组:

3 2 1 5 6 4

要求:

第 2 大元素

可以把所有元素放入大堆:

priority_queue<int> p(nums.begin(), nums.end());

此时:

top() = 最大值

不断删除最大的元素:

for (int i = 0; i < k - 1; ++i)
{
    p.pop();
}

最后:

return p.top();

对于:

3 2 1 5 6 4

建立大堆后:

6
5
4
3
2
1

删除一次最大值:

5
4
3
2
1

此时:

top()

就是第二大的元素:

5

十五、什么是容器适配器

适配器的核心思想就是:

将一个已有类的接口转换成另外一种接口

STL 中的:

stack
queue
priority_queue

都属于容器适配器

它们本身并不是传统意义上的底层数据结构

而是:

已有容器
    ↓
进行封装
    ↓
限制和重新组织接口
    ↓
形成新的数据结构

例如:

vector
    ↓
封装
    ↓
stack

stack 不允许用户直接访问中间元素,只提供:

push()
pop()
top()

这样就形成了后进先出的特性


十六、stack 和 queue 的底层结构

stack 和 queue 默认使用:

deque

作为底层容器

例如:

template<class T, class Con = deque<T>>
class stack
{
    ...
};

其中:

Con

就是底层容器类型

因此实际上:

stack<int>

可以理解成:

stack<int, deque<int>>

也可以指定:

stack<int, vector<int>>

或者:

stack<int, list<int>>

只要底层容器能够满足 stack 所需要的接口即可


十七、deque

deque 的基本特点

deque 可以理解为:

双端队列

它允许在头部和尾部进行插入和删除

头部                  尾部
 ↓                      ↓
┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │
└───┴───┴───┴───┴───┘
 ↑                      ↑
头删头插              尾删尾插

与 vector 相比:

deque
头插效率高

因为不需要像 vector 那样搬移大量元素

与 list 相比:

deque
空间利用率更高

十八、deque 并不是真正连续的空间

这是理解 deque 时非常重要的一点

deque 看起来像一段连续空间:

1 2 3 4 5 6 7 8

但实际上它由多个连续的小空间组成

可以理解成:

┌───────┐
│ 1 2 3 │
└───────┘
     ↓
┌───────┐
│ 4 5 6 │
└───────┘
     ↓
┌───────┐
│ 7 8 9 │
└───────┘

因此:

整体看起来连续
实际内部是分段连续

为了让用户感觉像是在访问连续空间,deque 的迭代器需要处理不同小块之间的切换

这也是 deque 迭代器实现比较复杂的原因


十九、deque 的缺陷

虽然 deque 有很多优点,但它也存在明显缺陷

最主要的问题:

不适合频繁遍历

因为 deque 实际上是分段连续空间

遍历过程中,迭代器需要不断判断当前是否已经到达某一段空间的边界

因此遍历效率不如真正连续的 vector

所以实际使用线性容器时:

需要频繁随机访问和遍历
    ↓
vector

需要频繁插入和删除
    ↓
list

而 deque 的典型应用之一就是作为:

stack
queue

的底层容器


二十、为什么 stack 和 queue 默认使用 deque

这是这部分内容中非常重要的一点

20.1 stack 的需求

stack 只需要:

push_back()
pop_back()

因此:

vector
list
deque

都可以作为底层容器


20.2 queue 的需求

queue 需要:

push_back()
pop_front()

因此:

list
deque

比较适合

vector 的 pop_front() 需要移动大量元素,因此不适合作为 queue 的底层容器


20.3 为什么最终选择 deque

因为:

stack

不需要遍历,只需要在固定的一端操作

queue

也不需要遍历,只需要在两端进行操作

而 deque:

头部插入删除效率高
尾部插入删除效率高
扩容时不需要搬移大量数据
空间利用率比 list 高

同时又避开了 deque 最明显的缺陷:

不适合频繁遍历

因为:

stack
queue

本身就不提供迭代器,也不需要进行遍历

所以:

deque 的优点
        +
stack / queue 不需要遍历
        ↓
非常适合作为 stack / queue 的默认底层容器

二十一、stack 的完整模拟实现

#include <deque>

namespace bite
{
    template<class T, class Con = deque<T>>
    class stack
    {
    public:
        stack()
        {}

        void push(const T& x)
        {
            _c.push_back(x);
        }

        void pop()
        {
            _c.pop_back();
        }

        T& top()
        {
            return _c.back();
        }

        const T& top() const
        {
            return _c.back();
        }

        size_t size() const
        {
            return _c.size();
        }

        bool empty() const
        {
            return _c.empty();
        }

    private:
        Con _c;
    };
}

这里最值得注意的是:

template<class T, class Con = deque<T>>

意味着:

T
↓
stack 中存储的数据类型

Con
↓
stack 使用的底层容器

默认:

Con = deque<T>

因此:

stack<int>

实际上相当于:

stack<int, deque<int>>

当然也可以指定:

stack<int, vector<int>>

或者:

stack<int, list<int>>

二十二、queue 的完整模拟实现

#include <deque>

namespace bite
{
    template<class T, class Con = deque<T>>
    class queue
    {
    public:
        queue()
        {}

        void push(const T& x)
        {
            _c.push_back(x);
        }

        void pop()
        {
            _c.pop_front();
        }

        T& back()
        {
            return _c.back();
        }

        const T& back() const
        {
            return _c.back();
        }

        T& front()
        {
            return _c.front();
        }

        const T& front() const
        {
            return _c.front();
        }

        size_t size() const
        {
            return _c.size();
        }

        bool empty() const
        {
            return _c.empty();
        }

    private:
        Con _c;
    };
}

核心对应关系:

queue::push()
    ↓
_c.push_back()

queue::pop()
    ↓
_c.pop_front()

queue::front()
    ↓
_c.front()

queue::back()
    ↓
_c.back()

因此 queue 的底层容器只需要满足相应接口即可


二十三、stack、queue、priority_queue 对比

容器适配器核心特点默认底层容器主要操作
stack后进先出deque一端进出
queue先进先出deque一端进一端出
priority_queue按优先级出队vector堆顶进出

从数据结构角度理解:

stack
    ↓
后进先出
    ↓
适合处理具有后进先出特点的问题
queue
    ↓
先进先出
    ↓
适合处理具有先进先出特点的问题
priority_queue
    ↓
堆
    ↓
快速获取当前最大值或最小值

其中 stack 和 queue 都属于容器适配器,而 priority_queue 底层则通过 vector 加堆算法维护优先级结构

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2501_93810221/article/details/167080815

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--