stack与queue的介绍与使用与实现

简介: 栈(stack)是一种遵循先入后出(FILO)逻辑的线性数据结构。其只能从容器的一端进行元素的插入与提取操作。我们可以把他比作串串,我们在串肉的时候都是从底依次往上串肉,然后在吃的时候是从串顶依次向下吃,将串上的肉比作各种类型的元素(如整数、字符、对象等),串子比作适配器容器,就得到了栈这种数据结构。如图所示,我们把容器内元素的顶部称为“栈顶”,底部称为“栈底”。将把元素添加到栈顶的操作叫作“入栈”,删除栈顶元素的操作叫作“出栈”。(图片取自hello算法)


stack
栈(stack)是一种遵循先入后出(FILO)逻辑的线性数据结构。其只能从容器的一端进行元素的插入与提取操作。

我们可以把他比作串串,我们在串肉的时候都是从底依次往上串肉,然后在吃的时候是从串顶依次向下吃,将串上的肉比作各种类型的元素(如整数、字符、对象等),串子比作适配器容器,就得到了栈这种数据结构。

如图所示,我们把容器内元素的顶部称为“栈顶”,底部称为“栈底”。将把元素添加到栈顶的操作叫作“入栈”,删除栈顶元素的操作叫作“出栈”。(图片取自hello算法)

栈的常用操作
成员函数 功能
empty() 判断栈是否为空,空返回真,不为空返回假
size() 获取栈中有效元素个数,返回值为size_t
top() 获取栈顶元素
push() 元素入栈
pop() 元素出栈
swap() 交换两个栈中的数据(可以部分也可以交换全部)
其实在c语言中就已经存在了stack,但是c++后又引入了模板,所以c++STL标准库里面就存在了stack的模板,但是要需要引用头文件#include

include

include

using namespace std;

int main()
{
stack st;

// 元素入栈
st.push(1);
st.push(3);
st.push(2);
st.push(5);
st.push(4);

//访问栈顶元素 
int top = st.top();

// 元素出栈 
st.pop(); // 无返回值

// 获取栈的长度 
int size = st.size();

// 判断是否为空 
bool empty = st.empty();
return 0;

}

栈的实现:
以下是基于deque实现栈的示例代码:

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;

};

那么如果用链表怎么来实现呢?

下面是基于链表实现stack:

template<class T>
struct stack_node
{
    stack_node(const T& _val)
        :next(nullptr)
        , val(_val)
    {}
    stack_node* next;
    T val;
};
template<class T>
class stack
{
public:
    stack()
    {
        stackTop = nullptr;
        stkSize = 0;
    }

    ~stack()
    {
        delete[]stackTop;
        stackTop = nullptr;
        stkSize = 0;
    }

    int size()
    {
        return stkSize;
    }

    bool empty()
    {
        return size() == 0;
    }

    void push(int num)
    {
        stack_node<T>* node = new stack_node<T>(num);
        node->next = stackTop;
        stackTop = node;
        stkSize++;
    }

    int pop()
    {
        assert(!empty());
        int num = top();
        stack_node<T>* tmp = stackTop;
        stackTop = stackTop->next;
        // 释放内存
        delete tmp;
        stkSize--;
        return num;
    }

    int top()
    {
        assert(!empty());
        return stackTop->val;
    }

private:
    stack_node<T>* stackTop; // 将头节点作为栈顶
    int stkSize;        // 栈的长度
};

同样还存在着用数组来实现栈

class stack{
public:
int size()
{
return stack.size();
}

bool empty() 
{
    return stack.size() == 0;
}

void push(int num) 
{
    stack.push_back(num);
}

int pop() 
{
    int num = top();
    stack.pop_back();
    return num;
}

int top() 
{
    assert(!empty());
    return stack.back();
}

private:
vector stack;
}
};

三种实现方法的对比:
三种实现对此来说第一种实现是标准库的标准实现方法,二三都是我们自己可以通过自己思考可以想出来的实现方法,两种实现都支持栈定义中的各项操作。数组实现额外支持随机访问,但这已超出了栈的定义范畴,因此一般不会用到。

首先第三种方法的缺点之一就是如果入栈时超出数组容量,会触发扩容机制,导致该次入栈操作的时间复杂度变为0(n);

缺点二就是方法三在扩容上会存在大量的浪费空间,比如已经存在100个元素了,恰好数组正好填满,但是如果还要添加数据,链表只需要对应数据个数添加结点,但是数组要进行倍数扩容,会又大量浪费;

栈的典型应用
(取自hello算法)

浏览器中的后退与前进、软件中的撤销与反撤销。每当我们打开新的网页,浏览器就会对上一个网页执行入栈,这样我们就可以通过后退操作回到上一个网页。后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么需要两个栈来配合实现。
程序内存管理。每次调用函数时,系统都会在栈顶添加一个栈帧,用于记录函数的上下文信息。在递归函数中,向下递推阶段会不断执行入栈操作,而向上回溯阶段则会不断执行出栈操作。
queue
队列(queue)是一种遵循先入先出规则的线性数据结构。顾名思义,队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。

他就像我们打饭的时候,先去的人先打完饭,所以队列相对应就是(FIFO),

如图所示,我们将队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。(图片取自hello算法)

队列常用操作
成员函数 功能
empty() 判断队列是否为空
size() 获取队列中有效元素个数
front() 获取队头元素
back() 获取队尾元素
push() 队尾入队列
pop() 队头出队列
swap() 交换两个队列中的数据,可以交换部分也可以交换全部
int main()
{
/ 初始化队列 /
queue queue;

/* 元素入队 */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);

/* 访问队首元素 */
cout << queue.front() << endl;

/* 元素出队 */
queue.pop();

/* 获取队列的长度 */
cout << queue.size() << endl;


/* 判断队列是否为空 */
cout << queue.empty() << endl;
return 0;

}

队列的实现:
利用deque实现:

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;

};

本人有点懒,以后有空会补充用链表来实现队列:

省略......

利用数组来实现:(全部摘自hello算法)

注意:里面有一点小优化,要注意哦

在数组中删除首元素的时间复杂度为 0(n) ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。

我们可以使用一个变量 front 指向队首元素的索引,并维护一个变量 size 用于记录队列长度。定义 rear = front + size ,这个公式计算出的 rear 指向队尾元素之后的下一个位置。

基于此设计,数组中包含元素的有效区间为 [front, rear - 1],各种操作的实现方法如图所示。

入队操作:将输入元素赋值给 rear 索引处,并将 size 增加 1 。
出队操作:只需将 front 增加 1 ,并将 size 减少 1 。
可以看到,入队和出队操作都只需进行一次操作,时间复杂度均为 0(1) 。

你可能会发现一个问题:在不断进行入队和出队的过程中,front 和 rear 都在向右移动,当它们到达数组尾部时就无法继续移动了。为了解决此问题,我们可以将数组视为首尾相接的“环形数组”。

对于环形数组,我们需要让 front 或 rear 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现,代码如下所示:

/ 基于环形数组实现的队列 /
class ArrayQueue {
private:
int *nums; // 用于存储队列元素的数组
int front; // 队首指针,指向队首元素
int queSize; // 队列长度
int queCapacity; // 队列容量

public:
ArrayQueue(int capacity) {
// 初始化数组
nums = new int[capacity];
queCapacity = capacity;
front = queSize = 0;
}

~ArrayQueue() {
    delete[] nums;
}

/* 获取队列的容量 */
int capacity() {
    return queCapacity;
}

/* 获取队列的长度 */
int size() {
    return queSize;
}

/* 判断队列是否为空 */
bool isEmpty() {
    return size() == 0;
}

/* 入队 */
void push(int num) {
    if (queSize == queCapacity) {
        cout << "队列已满" << endl;
        return;
    }
    // 计算队尾指针,指向队尾索引 + 1
    // 通过取余操作实现 rear 越过数组尾部后回到头部
    int rear = (front + queSize) % queCapacity;
    // 将 num 添加至队尾
    nums[rear] = num;
    queSize++;
}

/* 出队 */
int pop() {
    int num = peek();
    // 队首指针向后移动一位,若越过尾部,则返回到数组头部
    front = (front + 1) % queCapacity;
    queSize--;
    return num;
}

/* 访问队首元素 */
int peek() {
    if (isEmpty())
        throw out_of_range("队列为空");
    return nums[front];
}

/* 将数组转化为 Vector 并返回 */
vector<int> toVector() {
    // 仅转换有效长度范围内的列表元素
    vector<int> arr(queSize);
    for (int i = 0, j = front; i < queSize; i++, j++) {
        arr[i] = nums[j % queCapacity];
    }
    return arr;
}

};

队列典型应用
淘宝订单。购物者下单后,订单将加入队列中,系统随后会根据顺序处理队列中的订单。在双十一期间,短时间内会产生海量订单,高并发成为工程师们需要重点攻克的问题。
各类待办事项。任何需要实现“先来后到”功能的场景,例如打印机的任务队列、餐厅的出餐队列等,队列在这些场景中可以有效地维护处理顺序。

目录
相关文章
|
搜索推荐 安全 UED
浅谈AARRR模型
浅谈AARRR模型
|
存储 NoSQL Java
Java如何生成序列号/订单号
Java如何生成序列号/订单号
1452 0
|
SQL JSON 数据可视化
权限开发手册,数据权限和接口权限配置
权限开发手册,数据权限和接口权限配置
1902 0
权限开发手册,数据权限和接口权限配置
|
6月前
|
消息中间件 架构师 Java
美团面试:对比分析 RocketMQ、Kafka、RabbitMQ 三大MQ常见问题?
美团面试:对比分析 RocketMQ、Kafka、RabbitMQ 三大MQ常见问题?
美团面试:对比分析 RocketMQ、Kafka、RabbitMQ 三大MQ常见问题?
|
设计模式 算法 安全
【C/C++ 关键字 函数说明符 】C++ final关键字(修饰成员函数无法被子类重写覆盖)
【C/C++ 关键字 函数说明符 】C++ final关键字(修饰成员函数无法被子类重写覆盖)
532 1
|
8月前
|
存储 缓存 算法
G1原理—3.G1是如何提升垃圾回收效率
本文深入探讨了G1垃圾回收器提升GC效率的核心机制,包括记忆集(RSet)、位图(BitMap)和卡表(CardTable)的设计与作用。记忆集通过记录跨代引用避免了不必要的老年代遍历,位图用于高效描述内存使用状态以优化标记过程,而卡表则在节约记忆集内存的同时提供更详细的引用信息。此外,文章还解析了DCQ(Dirty Card Queue)和DCQS(Dirty Card Queue Set)机制如何异步更新RSet,确保在高并发场景下的性能与准确性。这些设计共同提升了G1在标记、清理及整理内存时的效率。
386 10
|
存储 并行计算 Java
C++线程 并发编程:std::thread、std::sync与std::packaged_task深度解析(二)
C++线程 并发编程:std::thread、std::sync与std::packaged_task深度解析
576 0
|
敏捷开发 监控 测试技术
敏捷软件质量保证的方法与实践
本文介绍了软件质量保证(SQA)的重要性及其在敏捷开发中的实践方法。文章首先指出了传统测试方法的问题,如成本高昂和项目风险加大。为解决这些问题,文中提出了需求审核、代码审核与演练、基于会议的测试及基于风险的测试等多种实践方法。此外,文章还探讨了衡量软件质量的常见指标,如源代码行数、代码段/模块/时间段内的Bug数和代码覆盖率等。文中还详细描述了敏捷开发过程中QA的角色与活动,强调了QA需与开发人员、业务人员及客户密切协作,以确保产品质量。最后,文章指出了在敏捷开发中QA的特殊性及其对团队构成、测试阶段、工作方式等方面的影响。
368 3
敏捷软件质量保证的方法与实践
|
域名解析 编解码 负载均衡
【域名解析DNS专栏】域名解析中的EDNS扩展:提升DNS协议灵活性
在互联网中,DNS作为连接用户与网络资源的关键桥梁,其传统协议在面对复杂网络环境时显现出局限性。EDNS(扩展机制)应运而生,通过在DNS请求和响应中添加额外选项和字段,提升了DNS的功能和灵活性。EDNS不仅提高了查询效率和支持更大范围的数据类型,还能增强安全性并通过负载均衡提升系统稳定性。例如,允许指定更大的UDP数据包大小以减少分片和重传,支持DNSSEC加强安全性验证,以及通过Python示例代码展示了如何在DNS查询中使用EDNS选项。随着技术发展,EDNS将在域名解析领域扮演更重要角色。
795 0
|
存储 JSON 缓存
MessagePack - 简介及使用
MessagePack - 简介及使用
1474 1
MessagePack - 简介及使用