栈和队列详解

 作者主页

lovewold少个r博客主页

也许所有的人类都是孤独的,至少有孤独的可能。


目录

栈的应用场景

栈的实现

图解栈的操作

栈的代码实现

栈的初始化

⏬栈的入栈操作

⏫栈的出栈操作

⏩获取栈顶元素

获取栈的有效元素个数

判断栈是否为空

销毁栈

队列

队列的应用场景

队列的实现

图解队列的操作

                  队列的代码实现

队列的初始化

⏪队列的入队操作

⤴️获取队尾的元素

⤴️获取队头的元素

判断队列是否为空

队列的销毁操作

栈和队列

栈和队列的特征相反性

栈和队列的相互转换

传送门:力扣OJ-队列实现栈

传送门:力扣OJ-栈实现队列

✒️总结

⚠️栈(Stack)

⚠️队列(Queue)


前言

栈和队列都是一种常用的数据结构,它们的实现方式非常相似,都是通过线性结构来实现。但是二者的操作方式有所不同。本章将对栈和队列进行剖析,分析和实现栈和队列。


栈 (Stack) 是一种操作受限的线性数据结构,只允许在一端进行插入和删除操作。栈的特点是后进先出 (Last In First Out,LIFO),也就是说,最后插入的数据最先被删除。栈的常见操作包括 push 入栈操作、pop 出栈操作等。

栈的应用场景

栈作为一种基础数据结构,在很多场景上有应用。比如当我们在编辑器中编辑东西出现错误需要返回的时候:当我们在使用浏览器访问网页,发现内容并不是自己想要的,需要返回上一个页面的时候;亦或者编辑器总是能检查到我们左右括号并不匹配,更或者我们输入一串表达式,计算机就能为我们求值等等。

栈和队列详解_第1张图片

栈的实现

栈是一种“后进先出,先进后出”的结构,在数据的存储上依旧可以使用链表或者数组的方式去实现,使用什么各有优缺点,这里我们使用顺序表的实现方式,实现对栈的动态开辟和维护。

图解栈的操作

栈和队列详解_第2张图片栈和队列详解_第3张图片

        在逻辑上,入图示一样在栈顶进行入栈和出栈操作,栈底无出口。在使用链表时候操作也一样。两者本质一样。

栈和队列详解_第4张图片栈和队列详解_第5张图片

栈的代码实现

栈的常用功能

// 初始化栈 
void StackInit(Stack* ps);
// 入栈 
void StackPush(Stack* ps, STDataType data);
// 出栈 
void StackPop(Stack* ps);
// 获取栈顶元素 
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数 
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps);
// 销毁栈 
void StackDestroy(Stack* ps);
栈的初始化

栈的操作主要是在栈顶,所以我们要构建一个通过栈顶操作的top标识为顶,同时对于一个栈是有大小的,我们这里用内存开辟函数可以实现动态开辟,仍然需要一个容量标识对是否栈满进行特殊考虑。

// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	int top;		// 栈顶
	int capacity;  // 容量 
}Stack;
// 初始化栈 
void StackInit(Stack* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;

}

⏬栈的入栈操作

栈的入栈和出栈始终是在栈顶操作的,对于栈顶操作后的更新需要及时。

// 入栈 
void StackPush(Stack* ps, STDataType data)
{
	assert(ps);
	if (ps->top == ps->capacity)//栈满即扩容
	{
		int newcapacity = ps->capacity == 0 ? 4 :ps->capacity*2;
		STDataType* new = (STDataType*)realloc(ps->a,sizeof(STDataType)*newcapacity);
		if (new == NULL)
		{
			perror("realoc fail\n");
			exit(-1);
		}
		ps->a = new;
		ps->capacity = newcapacity;
	}
	ps->a[ps->top] = data;//压入栈空间
	ps->top++;//及时更新栈顶标识
}
⏫栈的出栈操作
// 出栈 
void StackPop(Stack* ps)
{
	assert(ps);
	assert(ps->top > 0);
	--ps->top;
}

出栈当栈无元素时候不可以继续出栈,合理运用断言可以快速定位错误。

⏩获取栈顶元素

获取栈顶元素我们一种有两种方式,即获取后删除和获取但保留。这里我们只采取第二种,

栈的释放和删除让使用者自行删除。

// 获取栈顶元素 
STDataType StackTop(Stack* ps)
{
	assert(ps);
	assert(ps->top > 0);
	return ps->a[ps->top-1];//栈的索引起始为0,栈顶类似下标,最后一位为N-1

}
获取栈的有效元素个数
// 获取栈中有效元素个数 
int StackSize(Stack* ps)
{
	assert(ps);
	return ps->top;
}

栈的top即为有效元素个数,直接返回即可。

判断栈是否为空

判断栈是否为空用bool类型返回也可以,或者使用值判定也可以,C语言默认也不带bool类型,因此需要使用stobool头文件。检测栈是否为空,如果为空返回非零结果,如果不为空返回0

// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps)
{
	assert(ps);
	if (ps->top == 0)
	{
		return 1;//ture为空
	}
	else
	{
		return 0;
	}
}
销毁栈
// 销毁栈 
void StackDestroy(Stack* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->capacity = ps->top = 0;
}

队列

        队列 (Queue) 也是一种操作受限的线性数据结构,不同的是,队列只允许在队列的一端进行插入操作,在另一端进行删除操作。队列的特点是先进先出 (First In First Out,FIFO),也就是说,最先插入的数据最先被删除。队列的常见操作包括 enqueue 入队操作、dequeue 出队操作等。队列顾名思义就像排队,前面先入队的先出队,而对于一个规范的队列,肯定是插队和中途离开皆不可以,入队只能在队尾入队,出队只能在队头出队。

栈和队列详解_第6张图片

队列的应用场景

        基于队列的基本数据结构在应用上有更多场景,比如网络请求队列:将需要进行网络请求的任务按照先后顺序加入队列中,确保每个请求按照顺序依次进行,避免网络并发请求带来的问题。又或者消息队列:用于异步通信,例如将用户发起的请求转化成消息,通过队列传递到后台服务,后台服务按照消息队列中的顺序依次进行处理。亦或者订单处理队列:对于电商等涉及订单的业务场景,可以将订单按照先后顺序加入队列,确保订单的处理顺序正确。还有比如电脑卡顿后你疯狂点击执行的操作在恢复正常运行后的依次执行。用户的任务调度总是需要顺利的依次完成,这便需要队列操作实现。

队列的实现

        队列的特点是先进先出 (First In First Out,FIFO),也就是说,最先插入的数据最先被删除。队列的常见操作包括 enqueue 入队操作、dequeue 出队操作等。这里我们使用链式结构,对队列动态开辟内存实现队列操作。

图解队列的操作

栈和队列详解_第7张图片队列的代码实现

队列的常用操作

// 初始化队列 
void QueueInit(Queue* q);
// 队尾入队列 
void QueuePush(Queue* q, QDataType data);
// 队头出队列 
void QueuePop(Queue* q);
// 获取队列头部元素 
QDataType QueueFront(Queue* q);
// 获取队列队尾元素 
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数 
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q);
// 销毁队列 
void QueueDestroy(Queue* q);
队列的初始化

        队列应该要频繁对队头和队伍进行操作,后续还会对队列的有效元素个数进行输出操作,因此我们额外封装一个结构题,封装头指针,尾指针和有效元素个数计数器。

// 链式结构:表示队列 
typedef int QDataType;
typedef struct QListNode
{
	struct QListNode* next;
	QDataType data;

}QNode;

// 队列的结构 
typedef struct Queue
{
	QNode* front;
	QNode* rear;
	int size;
}Queue;
// 初始化队列 
void QueueInit(Queue* q)
{
	assert(q);
	q->front = q->rear = NULL;
	q->size = 0;
}

⏪队列的入队操作

        入队操作需要进行对入队元素节点的创建和内存开辟,如果没有节点,需要队头指针和队尾指针同时改变;如果已有节点,入队操作只需要改变队尾节点,并且都需要对有效元素个数计数器进行更新。

void QueuePush(Queue* q, QDataType data)
{
	assert(q);
	QNode* newNode = (QNode*)malloc(sizeof(QNode));
	if (newNode == NULL)
	{
		perror("malloc fail\n");
		exit(-1);
	}
	newNode->data = data;
	newNode->next = NULL;
	if (q->front == NULL)
	{
		q->front = q->rear = newNode;
	}
	else
	{
		q->rear->next = newNode;
		q->rear = newNode;
	}
	q->size++;
}

队列的出队操作

队列的出队操作在队头进行,需要更新队头和有效元素个数。

// 队头出队列 
void QueuePop(Queue* q)
{
	assert(q);
	assert(!QueueEmpty(q));
	if (q->front->next == NULL)
	{
		free(q->front);
		q->front = q->rear = NULL;
	}
	else
	{
		QNode* tail = q->front->next;
		free(q->front);
		q->front = tail;
	}

	q->size--;
}

⤴️获取队尾的元素

队尾对头元素两者操作一样,直接返回对应的值即可。

// 获取队列头部元素 
QDataType QueueFront(Queue* q)
{
	assert(q);
	assert(!QueueEmpty(q));

	return q->front->data;
}

⤴️获取队头的元素
// 获取队列队尾元素 
QDataType QueueBack(Queue* q)
{
	assert(q);
	assert(!QueueEmpty(q));
	return q->rear->data;
}

ℹ️获取有效元素个数 

直接返回对应的计数器。

// 获取队列中有效元素个数 
int QueueSize(Queue* q)
{
	assert(q);
	return q->size;
}
判断队列是否为空
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q)
{
	assert(q);

	if (q->front == NULL)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}
队列的销毁操作
// 销毁队列 
void QueueDestroy(Queue* q)
{
	assert(q);
	QNode* cur = q->front;
	while (cur)
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}
	q->front = q->rear = NULL;
	q->size = 0;
}

栈和队列

栈和队列的特征相反性

栈和队列在特征上是截然相反的:

  • 栈: 栈采用后进先出(Last-In-First-Out,LIFO)的访问方式,也就是最后入栈的元素最先出栈。类比一个堆积的盘子,你只能从最上面取走或放置一个盘子。
  • 队列: 队列采用先进先出(First-In-First-Out,FIFO)的访问方式,也就是最早入队的元素最先出队。类比排队等待服务的情况,先来的人先被服务。

hello算法一文中这个描述很有意思,栈和队列就是叠叠猫和队列猫的区别。

栈和队列的相互转换

传送门:力扣OJ-队列实现栈

题目分析:

栈和队列详解_第8张图片

这道题的本质其实就是找到队列和栈的必然特征本质,要用队列实现栈,就要将队列中的先进先出的转变为先进后出的特征。

栈和队列详解_第9张图片栈和队列详解_第10张图片栈和队列详解_第11张图片

        也就是说,只需要将队列元素前n-1压入空队列,在将这个队列的最后一个元素出队即可取得队首出队的效果。达成了栈的先进后出的特点,将其封装一下便可以达成队列成栈的效果。

代码展示:

#include
#include
#include
#include
#include

// 链式结构:表示队列 
typedef int QDataType;
typedef struct QListNode
{
	struct QListNode* next;
	QDataType data;

}QNode;

// 队列的结构 
typedef struct Queue
{
	QNode* front;
	QNode* rear;
	int size;
}Queue;

// 初始化队列 
void QueueInit(Queue* q);
// 队尾入队列 
void QueuePush(Queue* q, QDataType data);
// 队头出队列 
void QueuePop(Queue* q);
// 获取队列头部元素 
QDataType QueueFront(Queue* q);
// 获取队列队尾元素 
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数 
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q);
// 销毁队列 
void QueueDestroy(Queue* q);


// 链式结构:表示队列 



// 初始化队列 
void QueueInit(Queue* q)
{
	assert(q);
	q->front = q->rear = NULL;
	q->size = 0;

}
// 队尾入队列 
void QueuePush(Queue* q, QDataType data)
{
	assert(q);
	QNode* newNode = (QNode*)malloc(sizeof(QNode));
	if (newNode == NULL)
	{
		perror("malloc fail\n");
		exit(-1);
	}
	newNode->data = data;
	newNode->next = NULL;
	if (q->front == NULL)
	{
		q->front = q->rear = newNode;
	}
	else
	{
		q->rear->next = newNode;
		q->rear = newNode;
	}
	q->size++;
}
// 队头出队列 
void QueuePop(Queue* q)
{
	assert(q);
	assert(!QueueEmpty(q));
	if (q->front->next == NULL)
	{
		free(q->front);
		q->front = q->rear = NULL;
	}
	else
	{
		QNode* tail = q->front->next;
		free(q->front);
		q->front = tail;
	}

	q->size--;
}
// 获取队列头部元素 
QDataType QueueFront(Queue* q)
{
	assert(q);
	assert(!QueueEmpty(q));

	return q->front->data;
}
// 获取队列队尾元素 
QDataType QueueBack(Queue* q)
{
	assert(q);
	assert(!QueueEmpty(q));
	return q->rear->data;
}
// 获取队列中有效元素个数 
int QueueSize(Queue* q)
{
	assert(q);
	return q->size;
}
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* q)
{
	assert(q);

	if (q->front == NULL)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}
// 销毁队列 
void QueueDestroy(Queue* q)
{
	assert(q);
	QNode* cur = q->front;
	while (cur)
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}
	q->front = q->rear = NULL;
	q->size = 0;
}

typedef struct 
{
    Queue q1;
    Queue q2;

} MyStack;


MyStack* myStackCreate() 
{
    MyStack* new = (MyStack*)malloc(sizeof(MyStack));
    QueueInit(&new->q1);
    QueueInit(&new->q2);
    return new;
}

void myStackPush(MyStack* obj, int x) 
{
    if(!QueueEmpty(&obj->q1))
    {
        QueuePush(&obj->q1,x);
    }
    else
    {
        QueuePush(&obj->q2,x);
    }
}

int myStackPop(MyStack* obj) 
{
    Queue* empty=&obj->q1;
    Queue* nonempty=&obj->q2;
    if(!QueueEmpty(&obj->q1))//看那一个队列为空就操作哪一个
    {
        empty=&obj->q2;
        nonempty=&obj->q1;
    }
    while(QueueSize(nonempty)>1)
    {
        QueuePush(empty,QueueFront(nonempty));
        QueuePop(nonempty);
    }
    int top=QueueFront(nonempty);
    QueuePop(nonempty);
    return top;
}
int myStackTop(MyStack* obj) 
{
    if(!QueueEmpty(&obj->q1))
    {
        return QueueBack(&obj->q1);
    }
    else
    {
         return QueueBack(&obj->q2);
    }

}

bool myStackEmpty(MyStack* obj) 
{

     return QueueEmpty(&obj->q1)&&QueueEmpty(&obj->q2);

}

void myStackFree(MyStack* obj) 
{
    QueueDestroy(&obj->q1);
    QueueDestroy(&obj->q2);
    free(obj);
}

/**
 * Your MyStack struct will be instantiated and called as such:
 * MyStack* obj = myStackCreate();
 * myStackPush(obj, x);
 
 * int param_2 = myStackPop(obj);
 
 * int param_3 = myStackTop(obj);
 
 * bool param_4 = myStackEmpty(obj);
 
 * myStackFree(obj);
*/

传送门:力扣OJ-栈实现队列


传送门:力扣OJ-栈实现队列

题目分析:

栈和队列详解_第12张图片

        栈的特点主要是先进后出,而对于另一个栈来说,从一个栈来说这个出的栈进入本身相当于逆序进栈。栈在两次联通入栈的过程中完成了一遍逆序,而对于队列特点,正好就是需要这逆序的元素。从先进后出完成了先进先出的转变。

栈和队列详解_第13张图片

        一个栈专门负责入数据,另一个栈入栈数据只当本身栈为空的时候从另一个栈取,这个时候栈二出栈完成了一遍逆序,达到了实现队列的要求。

代码展示:

#include
#include
#include
#include
#include

// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	int top;		// 栈顶
	int capacity;  // 容量 
}Stack;
// 初始化栈 
void StackInit(Stack* ps);
// 入栈 
void StackPush(Stack* ps, STDataType data);
// 出栈 
void StackPop(Stack* ps);
// 获取栈顶元素 
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数 
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps);
// 销毁栈 
void StackDestroy(Stack* ps);


// 初始化栈 
void StackInit(Stack* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;

}
// 入栈 
void StackPush(Stack* ps, STDataType data)
{
	assert(ps);
	if (ps->top == ps->capacity)
	{
		int newcapacity = ps->capacity == 0 ? 4 :ps->capacity*2;
		STDataType* new = (STDataType*)realloc(ps->a,sizeof(STDataType)*newcapacity);
		if (new == NULL)
		{
			perror("realoc fail\n");
			exit(-1);
		}
		ps->a = new;
		ps->capacity = newcapacity;
	}
	ps->a[ps->top] = data;
	ps->top++;
}
// 出栈 
void StackPop(Stack* ps)
{
	assert(ps);
	assert(ps->top > 0);
	--ps->top;
}
// 获取栈顶元素 
STDataType StackTop(Stack* ps)
{
	assert(ps);
	assert(ps->top > 0);
	return ps->a[ps->top-1];
}
// 获取栈中有效元素个数 
int StackSize(Stack* ps)
{
	assert(ps);
	return ps->top;
}
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps)
{
	assert(ps);
	if (ps->top == 0)
	{
		return 1;//ture为空
	}
	else
	{
		return 0;
	}
}
// 销毁栈 
void StackDestroy(Stack* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->capacity = ps->top = 0;
}

typedef struct 
{
    Stack q1;
    Stack q2;
} MyQueue;


MyQueue* myQueueCreate() 
{
    MyQueue* new=(MyQueue*)malloc(sizeof(MyQueue));
    StackInit(&new->q1);
    StackInit(&new->q2);
    return new;
}

void myQueuePush(MyQueue* obj, int x) 
{
    StackPush(&obj->q1,x);
}

int myQueuePop(MyQueue* obj) 
{
    if(StackEmpty(&obj->q2))
    {
        while(!StackEmpty(&obj->q1))
        {
            StackPush(&obj->q2,StackTop(&obj->q1));
            StackPop(&obj->q1);
        }
        
    }
    int top = StackTop(&obj->q2);
    StackPop(&obj->q2);
    return top;
}

int myQueuePeek(MyQueue* obj) 
{
    if(StackEmpty(&obj->q2))
    {
        while(!StackEmpty(&obj->q1))
        {
            StackPush(&obj->q2,StackTop(&obj->q1));
            StackPop(&obj->q1);
        }
    }
    return StackTop(&obj->q2);
}

bool myQueueEmpty(MyQueue* obj)
{
    return StackEmpty(&obj->q1)&&StackEmpty(&obj->q2);

}

void myQueueFree(MyQueue* obj) 
{
    StackDestroy(&obj->q1);
    StackDestroy(&obj->q2);
    free(obj);

}

/**
 * Your MyQueue struct will be instantiated and called as such:
 * MyQueue* obj = myQueueCreate();
 * myQueuePush(obj, x);
 
 * int param_2 = myQueuePop(obj);
 
 * int param_3 = myQueuePeek(obj);
 
 * bool param_4 = myQueueEmpty(obj);
 
 * myQueueFree(obj);
*/

✒️总结

⚠️栈(Stack):

  1. 定义:栈是一种线性数据结构,它按照后进先出(Last-In-First-Out,LIFO)的原则管理数据。最后进入栈的元素最先被访问和移除。

  2. 操作:栈支持两个主要操作:

    • push:将元素添加到栈的顶部。
    • pop:从栈的顶部移除并返回元素。
  3. 特点:栈的特点使其适合于一些应用,例如函数调用堆栈、表达式求值、回文字符串检测等。

  4. 实现:可以使用数组或链表来实现栈。

⚠️队列(Queue):

  1. 定义:队列是一种线性数据结构,它按照先进先出(First-In-First-Out,FIFO)的原则管理数据。最早进入队列的元素最先被访问和移除。

  2. 操作:队列支持两个主要操作:

    • enqueue:将元素添加到队列的尾部。
    • dequeue:从队列的头部移除并返回元素。
  3. 特点:队列通常用于处理需要按顺序处理的任务,例如任务调度、打印队列、广度优先搜索等。

  4. 实现:队列可以使用数组、链表或双端队列(Deque)来实现。


作者水平有限,文章难免出现纰漏,如有问题欢迎指正!


你可能感兴趣的:(数据结构)