❤️[数据结构]动图+万字详解栈和队列(动图+实例)建议收藏❤️

Posted Linux猿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了❤️[数据结构]动图+万字详解栈和队列(动图+实例)建议收藏❤️相关的知识,希望对你有一定的参考价值。


🎈 作者:Linux猿

🎈 简介:CSDN博客专家🏆,华为云享专家🏆,C/C++、面试、刷题、算法尽管咨询我,关注我,有问题私聊!

🎈 关注专栏:C/C++面试通关集锦 (优质好文持续更新中……)🚀

🎈 欢迎小伙伴们点赞👍、收藏⭐、留言💬


目录

一、栈

1.1 什么是栈

1.2 实现方式

1.3 数组实现栈

1.3.0 类封装

1.3.1 push 操作

1.3.2 pop 操作

1.3.3 empty 操作

1.3.4 top 操作

1.3.5 size 操作

1.3.6 数组栈测试

1.4 链表实现栈

1.4.0 类封装

1.4.1 push 操作

1.4.2 pop 操作

1.4.3 empty 操作

1.4.4 top 操作

1.4.5 size 操作

1.4.6 链表栈测试

1.5 实战分析

1.6 复杂度分析

1.6.1 时间复杂度

1.6.2 空间复杂度

1.7 栈的应用

二、队列

2.1 什么是队列

2.2 实现方式

2.3 数组实现队列

2.3.0 类封装

2.3.1 push 操作

2.3.2 pop 操作

2.3.3 front 操作

2.3.4 empty 操作

2.3.5 size 操作

2.3.6 back 操作

2.3.7 数组队列测试

2.4 链表实现队列

2.4.0 类封装

2.4.1 push 操作

2.4.2 pop 操作

2.4.3 front 操作

2.4.4 empty 操作

2.4.5 size 操作

2.4.6 链表队列测试

2.5 实战分析

2.6 复杂度分析

 2.6.1 时间复杂度

2.6.2 空间复杂度

2.7 队列的应用

三、总结


在日常的学习以及求职面试中,栈和队列是一块非常重要的内容,经常被提及,本篇文章总结了栈和队列基本概念及常用操作,并且分别使用数组和链表实现了栈和队列,简单易懂,想不会都难!赶紧来看下吧!

一、栈

1.1 什么是栈

栈是一种抽象数据结构,也是一种线性数据结构,具有后进先出(LIFO,Last In First Out)的特性,即:后进入栈中的元素在先进入元素的上面,故后进入栈中的元素先出栈,当然也可以说是先进后出,即:先进入的元素后出栈,一样的原理,只是说法不同。如下图所示:

 从上图可以看到,栈只有一个口,即是入口也是出口,后进入栈的元素 C,比先进入栈的 B 和 A 先出栈,这就是所谓的后进先出。如上图所示,栈有栈顶和栈底。

来看一下动图,如下所示:

标题

1.2 实现方式

栈的实现可以通过数组或链表来实现,如下图所示:

标题
标题

 下面分别来看一下数组和链表来实现栈。

1.3 数组实现栈

1.3.0 类封装

下面用类封装了栈的常用操作,如下所示:

#define NUM 100000 // 栈的大小
class Stack {
    int data[NUM];  // 数组模拟栈
    int num;    // 栈指针,指向栈顶元素
public:
    Stack() {   // 初始化
        num = -1;
        memset(data, 0, sizeof(data));
    }
    void push(int val); // 添加元素
    int pop();          // 删除栈顶元素
    int top();          // 返回栈顶元素
    bool empty();       // 判断是否为空
    int size();         // 栈大小
};

上面是以 int 为例子,更好的封装是使用 C++ 的模板,这里为了便于理解采用了更简单的方式。

另一方面,数组还可以改成动态分配的形式,即:先分配一个初始数组,如果栈溢出了,则重新分配,将原先的内容拷贝到新分配的数组中,分配的方式可以参考 STL 的递增策略。

1.3.1 push 操作

向栈中添加元素,如下所示:

void Stack::push(int val) {
    if(num >= NUM) {
        cout<<"Stack Overflow!"<<endl;
        return;
    }
    data[++num] = val;
}

如上所示,先判断栈是否已满,未满则添加元素。可以将上面的 if 语句里的内容修改为增加栈容量。

1.3.2 pop 操作

删除栈顶元素,因为栈是后进先出的,如下所示:

int Stack::pop() {
    if(empty()) {
        cout<<"Stack Empty!"<<endl;
        return -1;
    }
    return data[num--];
}

如上所示,先判断栈是否为空,不空则删除栈顶元素,只要栈顶指针移动即可。

1.3.3 empty 操作

判断栈是否为空,空返回 true,否则返回 false ,如下所示:

bool Stack::empty() {
    return num == -1;
}

这里也许有人会说“为啥不用 int 类型,返回 0 和 1 呢? 用 true 和 false 更好是因为 bool 类型占一个字节,而 int 通常占 4 个字节。

1.3.4 top 操作

返回栈顶元素,如下所示:

int Stack::top() {
    if(empty()) {
        cout<<"Stack Empty!"<<endl;
        return -1;
    }
    return data[num];
}

先判断栈是否为空,如果非空则返回栈顶元素。

注意:这里不是删除,仅仅是返回栈顶元素的值。

1.3.5 size 操作

返回栈大小,如下所示:

int Stack::size() {
    return num + 1;
}

栈指针 num 是从 0 开始的,故返回 num+1。

1.3.6 数组栈测试

下面是测试上面的数组栈实现,如下所示:

int main()
{
    Stack st;

    st.push(10);
    st.push(20);
    cout<<"The size of stack is "<<st.size()<<endl;
    cout<<"The top of stack is "<<st.top()<<endl;
    cout<<"The stack empty is "<<st.empty()<<endl;
    cout<<"----------------------------------------"<<endl;

    st.pop();
    cout<<"The size of stack is "<<st.size()<<endl;
    cout<<"The top of stack is "<<st.top()<<endl;
    cout<<"----------------------------------------"<<endl;
    return 0;
}

输出为:

The size of stack is 2
The top of stack is 20
The stack empty is 0
----------------------------------------
The size of stack is 1
The top of stack is 10
----------------------------------------

1.4 链表实现栈

1.4.0 类封装

使用类封装栈,如下所示:

struct node { // 链表单个节点
    int val;  // 存储栈元素值
    struct node* next;// 指向下一个元素的指针
    node(int value) { // 赋初值
        val = value;
    }
};

class Stack {
    struct node *index; // 指向栈顶元素
    int s_size; // 记录栈容量
public:
    Stack() { // 初始化
        index = nullptr;
        s_size = 0;
    }
    ~Stack() ;
    void push(int val); // 添加元素
    int pop();          // 删除栈顶元素
    int top();          // 返回栈顶元素
    bool empty();       // 判断是否为空
    int size();         // 栈大小
};

struce node 是链表中的单个元素, class Stack 包含栈的各种操作,这里使用 s_size 记录栈的容量,便于操作。当然,这里是以 int 为例,可以将其修改为 C++ 的模板形式。

1.4.1 push 操作

向栈中添加元素,如下所示:

void Stack::push(int val) {
    struct node* tmp = new node(val);
    if(tmp == nullptr) {
        cout<<"Failed to allocate space!"<<endl;
        return;
    }
    tmp->next = index;
    index = tmp;
    s_size++;
}

如上所示,这里不用判断栈是否已满,需要判断一个是否分配成功。

1.4.2 pop 操作

删除栈顶元素,因为栈是后进先出的,如下所示:

int Stack::pop() {
    if(empty()) {
        cout<<"Stack Empty!"<<endl;
        return -1;
    }
    int tmpVal = index->val; // 暂存栈顶元素
    index = index->next; // 删除栈顶元素
    s_size--;            // 栈元素个数减一
    return tmpVal; // 这个返回下栈顶元素
}

如上所示,先判断栈是否为空,不空则删除栈顶元素,只要栈顶指针移动即可。

1.4.3 empty 操作

判断栈是否为空,空返回 true,否则返回 false ,如下所示:

bool Stack::empty() {
    return index == nullptr;
}

1.4.4 top 操作

返回栈顶元素,如下所示:

int Stack::top() {
    if(empty()) {
        cout<<"Stack Empty!"<<endl;
        return -1;
    }
    return index->val; // 仅仅返回栈顶元素
}

因为 index 一直指向栈顶元素,所以直接返回 index->val。

1.4.5 size 操作

返回栈大小,如下所示:

int Stack::size() {
    return s_size;
}

这里使用 s_size 记录栈大小,不然每次都要遍历链表。

1.4.6 链表栈测试

下面是测试上面的链表栈实现,如下所示:

int main()
{
    Stack st;

    st.push(10);
    st.push(20);
    cout<<"The size of stack is "<<st.size()<<endl;
    cout<<"The top of stack is "<<st.top()<<endl;
    cout<<"The stack empty is "<<st.empty()<<endl;
    cout<<"----------------------------------------"<<endl;

    st.pop();
    cout<<"The size of stack is "<<st.size()<<endl;
    cout<<"The top of stack is "<<st.top()<<endl;
    cout<<"----------------------------------------"<<endl;
    return 0;
}

输出为:

linuxy@linuxy:~/Stack$ g++ -o main Stack.cpp 
linuxy@linuxy:~/Stack$ ./main 
The size of stack is 2
The top of stack is 20
The stack empty is 0
----------------------------------------
The size of stack is 1
The top of stack is 10
----------------------------------------
linuxy@linuxy:~/Stack$ 

1.5 实战分析

模拟元素栈操作,如何实现从下面的入栈顺序到出栈顺序。

入栈顺序:A B C D

出栈顺序:C B D A

如上所示,需要找到一种方法,实现从入站次序到出站次序,这是栈经常考的题目,步骤如下:

(1)因为第一个出站的是 C,所以 A,B ,C依次入栈;

(2)这时,C 在栈顶,C 出栈;

(3)因为第二个出栈的是 B,当前栈顶为 B,所以直接让 B 出栈;

(4)B 出栈后,当前栈顶为 A,但是,应该是 D 出栈,所以先让 D 进栈;

(5)当前栈顶为 D,下一个出栈元素为 D,让 D 出栈;

(6)当前栈顶元素为 A,下一个出栈元素对应 A,所以 A 出栈;

下面来看下动图:

1.6 复杂度分析

1.6.1 时间复杂度

不管是链表实现的栈,还是数组实现的栈,push 和 pop 操作的时间复杂度都是O(1)的,链表实现的栈中 clear 操作是O(n)的,因为需要释放所有的链表空间,其它操作都是O(1)的。

1.6.2 空间复杂度

链表相对于数组更节省空间,因为链表使用到才会分配,数组是提前分配好,而且如果栈满时,数组还需要重新分配。

1.7 栈的应用

(1)深度优先搜索

深度优先搜索的非递归实现通常使用栈作为辅助的数组结构。

(2)软件中的回退和前进功能

(3)拓扑排序

二、队列

2.1 什么是队列

队列是一种线性结构的抽象数据类型,可以实现先进先出(FIFO,First In,First Out),即:先进入的元素,先出队列,可以比喻为日常排队买东西。

如下图所示:

标题

从上图可以看到,队列和栈不同,队列有两个口,而且是一个只允许进入元素,一个只允许出元素,后进入栈的元素C,比先进入栈的 B 和 A 先出栈,这就是所谓的后进先出。如上图所示,栈有栈顶和栈底。

来看一下动图,如下所示:

标题

2.2 实现方式

队列的实现可以通过数组或链表来实现,如下图所示:

标题
标题

2.3 数组实现队列

2.3.0 类封装

下面是使用类封装了队列,如下所示:

#define NUM 100000 // 队列大小
class Queue {
    int data[NUM];  // 数组模拟队列
    int first;      // 头指针,指向队列顶部
    int last;       // 尾指针,指向队列尾部
public:
    Queue() {   // 初始化
        first = 0;
        last = 0;
        memset(data, 0, sizeof(data));
    }
    void push(int val); // 添加元素
    int pop();          // 删除队列头元素
    int front();        // 返回队列头元素
    bool empty();       // 判断是否为空
    int size();         // 队列大小
    int back();         // 返回队列尾元素
};

data 存储队列数据,first 和 last 分别指向队列头部和尾部。

2.3.1 push 操作

向队列中添加元素,如下所示:

void Queue::push(int val) {
    if(last >= NUM) {
        cout<<"Queue Overflow!"<<endl;
        return;
    }
    data[last++] = val;
}

先判断队列是否已满,未满则添加元素,添加元素只需要移动尾指针即可。

2.3.2 pop 操作

删除队列头元素,如下所示:

int Queue::pop() {
    if(empty()) {
        cout<<"Queue Empty!"<<endl;
        return -1;
    }
    return data[first++];
}

如上所示,先判断队列是否为空,不空则删除队列头元素,只需要头指针 first 移动即可。

2.3.3 front 操作

返回头元素的值,和栈的 top 操作相同的原理,如下所示:

int Queue::front() {
    if(empty()) {
        cout<<"Queue Empty!"<<endl;
        return -1;
    }
    return data[first];
}

先判断队列是否为空,非空则返回头元素。

2.3.4 empty 操作

判断队列是否为空,如下所示:

bool Queue::empty() {
    return first == last;
}

直接比较 first 与 last 是否相等即可。

2.3.5 size 操作

判断队列元素个数,如下所示:

int Queue::size() {
    return last - first;
}

使用队列尾指针与头指针之前的举例便是元素个数。

2.3.6 back 操作

返回队列尾元素,如下所示:

int Queue::back() {
    if(empty()) {
        cout<<"Queue Empty!"<<endl;
        return -1;
    }
    return data[last-1];
}

先判断是否为空,不为空则返回 last - 1 所在的元素,因为 last 指向尾元素的下一个位置。

2.3.7 数组队列测试

下面是对上面数组实现的队列的测试,如下所示:

int main()
{
    Queue que;

    cout<<"The queue empty is "<<que.empty()<<endl;
    cout<<"The size of queue is "<<que.size()<<endl;
    cout<<"----------------------------------------"<<endl;
    que.push(10);
    que.push(20);
    cout<<"The size of queue is "<<que.size()<<endl;
    cout<<"The front of queue is "<<que.front()<<endl;
    cout<<"The queue empty is "<<que.empty()<<endl;
    cout<<"----------------------------------------"<<endl;

    que.pop();
    cout<<"The size of queue is "<<que.size()<<endl;
    cout<<"The front of queue is "<<que.front()<<endl;

    return 0;
}

输出为:

linuxy@linuxy:~/Stack$ ./main 
The queue empty is 1
The size of queue is 0
----------------------------------------
The size of queue is 2
The front of queue is 10
The queue empty is 0
----------------------------------------
The size of queue is 1
The front of queue is 20
linuxy@linuxy:~/Stack$

2.4 链表实现队列

2.4.0 类封装

struct node {   // 链表节点
    int val;    // 链表值
    struct node* next;
    node(int value) {
        val = value;
    }
};

class Queue {
    struct node *first; // 指向队列头元素
    struct node *rear;  // 指向队列尾元素
    int s_size;         // 记录队列元素个数
public:
    Queue() { // 初始化
        first = nullptr;
        rear = nullptr;
        s_size = 0;
    }
    ~Queue();
    void push(int val);  // 添加元素
    int pop();           // 删除队列头元素
    int front();         // 返回队列头元素
    bool empty();        // 判断是否为空
    int size();          // 队列元素个数
    void back();         // 返回队列尾元素
};

first 指向队列头元素,rear 指向队列尾元素,s_size 记录队列中的元素个数。

2.4.1 push 操作

向队列中添加元素,添加到队列尾部,因为队列是先进先出,如下所示:

void Queue::push(int val) {
    struct node* tmp = new node(val);
    if(tmp == nullptr) {
        cout<<"Failed to allocate space!"<<endl;
        return;
    }
    if(rear == nullptr) { // 添加第一个元素
        first = tmp;
        rear = tmp;
        tmp->next = nullptr;
    } else {              // 已有元素
        rear->next = tmp;
        tmp->next = nullptr;
        rear = tmp;
    }

    s_size++;
}

这里需要先分配空间,第一次添加元素需要将 first 和 rear 都指向该元素,否则添加到尾部即可,别忘记增加 s_size。

2.4.2 pop 操作

删除队列头元素,如下所示:

int Queue::pop() {
    if(empty()) {
        cout<<"Stack Empty!"<<endl;
        return -1;
    }
    struct node* tmp = first;
    first = first->next;
    int val = tmp->val;
    delete tmp;

    s_size--;
    return val;
}

先判断队列是否为空,不为空则移动队列头指针,别忘记释放空间。

2.4.3 front 操作

返回队列头元素的值,并不是删除哦!如下所示:

int Queue::front() {
    if(empty()) {
        cout<<"Stack Empty!"<<endl;
        return -1;
    }
    return first->val;
}

队列的 front 和 栈的 top 原理类似。

2.4.4 empty 操作

判断队列是否为空,如下所示:

bool Queue::empty() {
    return first == nullptr;
}

2.4.5 size 操作

判断队列元素个数,如下所示:

int Queue::size() {
    return s_size;
}

这里使用了一个辅助变量 s_size,不然每次求队列元素个数都需要遍历一次链表。

2.4.6 链表队列测试

下面是测试上面的链表队列的实现,如下所示:

int main()
{
    Queue que;

    cout<<"The queue empty is "<<que.empty()<<endl;
    cout<<"The size of queue is "<<que.size()<<endl;
    cout<<"----------------------------------------"<<endl;

    que.push(10);
    que.push(20);
    cout<<"The size of queue is "<<que.size()<<endl;
    cout<<"The top of queue is "<<que.front()<<endl;
    cout<<"The queue empty is "<<que.empty()<<endl;
    cout<<"----------------------------------------"<<endl;

    que.pop();
    cout<<"The size of queue is "<<que.size()<<endl;
    cout<<"The top of queue is "<<que.front()<<endl;
    cout<<"----------------------------------------"<<endl;

    return 0;
}

输出结果为:

linuxy@linuxy:~/Queue$ ./main 
The queue empty is 1
The size of queue is 0
----------------------------------------
The size of queue is 2
The top of queue is 10
The queue empty is 0
----------------------------------------
The size of queue is 1
The top of queue is 20
----------------------------------------
linuxy@linuxy:~/Queue$

2.5 实战分析

采用层次遍历的方法遍历如下二叉树:

标题

层次遍历上图的二叉树,即:从上到下一层一层的遍历二叉树的节点,按照这个思路,遍历步骤为:

(1)先访问 1 节点,1节点有两个孩子节点,1 节点出队列后,将 2 和 3 两个节点入队;

(2)访问 2 节点,2 节点出队,2 节点有两个子节点 4 和 5,2 节点出队后,4 和 5 节点入队;

(3)访问 3 节点,3 节点没有子节点,直接出队;

(4)依次访问 4  和 5 节点,这两个节点都没有子节点,所以出队即可。

(5)所有节点按照层次遍历的方式访问完毕!

来看一下动图:

标题

2.6 复杂度分析

 2.6.1 时间复杂度

不管是链表实现的队列,还是数组实现的队列,push、pop、empty、front 等操作的时间复杂度都是O(1)的。

2.6.2 空间复杂度

链表相对于数组更节省空间,因为链表使用到才会分配,数组是提前分配好,而且如果队列满时,数组还需要重新分配。

2.7 队列的应用

(1)广度优先搜索

广度优先搜索的实现通常使用队列作为辅助的数组结构。

(2)在一些资源请求或任务调度中,往往是先来先处理。

三、总结

栈和队列都有很强的特性,栈是后进先出,队列是先进先出,应用在不同的应用场景中。

欢迎关注下方👇👇👇公众号👇👇👇,获取更多优质内容🤞(比心)!

以上是关于❤️[数据结构]动图+万字详解栈和队列(动图+实例)建议收藏❤️的主要内容,如果未能解决你的问题,请参考以下文章

❤️五万字《十大排序算法》动图讲解❤️(建议收藏)

❤️五万字《十大排序算法》动图讲解❤️(建议收藏)

十大经典排序,你全都会了吗?(附源码动图万字详解)

❤️[数据结构]动图详解链表(单链表/双链表……)(动图+实例)建议收藏!❤️

❤️《画解数据结构》七张动图,画解单调队列❤️(建议收藏)

❤️《画解数据结构》全网最全队列总结,九张动图搞懂队列 ❤️(文末有投票)