栈与队列及其基础应用
一.栈
1.栈的定义
栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。其结构可以参考羽毛球筒
在现实中我们通常用栈解决对称性问题,如经典的括号匹配问题。
2.栈的实现
由上结构图可看出,我们用一个指针bottom指向栈底元素,一个指针top指向栈顶元素的下一个位置。实现栈可以用数组。
typedef int STDataType;
typedef struct Stack {
STDataType *a;//一个动态创建的数组
int top;//栈顶数据的下个位置,也是当前栈内元素个数
int capacity;//当前栈的最大数据容量
} ST;
为了实现栈的增删改查等功能,我们可以定义若干接口
//初始化
void StackInit(ST *pst);
//销毁
void StackDestroy(ST *pst);
//入栈
void StackPush(ST *pst, STDataType x);
//出栈
void StackPop(ST *pst);
//获取栈顶元素
STDataType StackTop(ST* pst);
//获取栈是否为空
bool StackEmpty(ST* pst);
//获取栈中元素的个数
int StackSize(ST *pst);
现在对这些接口实现。
#include "Stack.h"
//注意top的指向会影响到他的初始值
//初始化
void StackInit(ST* pst) {
assert(pst != NULL);
pst->a= NULL;
//top指向栈顶元素的下一个空间,即表示当前栈内没有元素
pst->top = 0;
pst->capacity = 0;
}
//销毁
void StackDestroy(ST* pst) {
assert(pst != NULL);
free(pst->a);
pst->top=pst->capacity=0;
}
//入栈
//先判断当前空间是否足够,不够就动态开辟
//将入栈元素的值x赋予top,更新top位置
void StackPush(ST* pst, STDataType x) {
assert(pst != NULL);
if (pst->top == pst->capacity) {
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));
if (tmp == NULL) {
perror("realloc fail");
return;
}
}
pst->a[pst->top] = x;
pst->top++;
}
//出栈
//从栈顶出元素,直接令top--
void StackPop(ST* pst){
assert(pst != NULL);
pst->top--;
}
//获取栈顶元素
//top为栈顶元素下个位置,返回top-1处值即可
STDataType StackTop(ST* pst) {
assert(pst);
assert(pst->top != 0);
return pst->a[pst->top - 1];
}
//获取栈是否为空
bool StackEmpty(ST* pst) {
assert(pst != NULL);
return pst->top == 0;
}
//获取栈中元素的个数
int StackSize(ST* pst) {
assert(pst != NULL);
return pst->top;
}
二.队列
1.队列的定义
队列也是一种特殊的线性表,其只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头
在实际应用中我们常用队列解决公平性问题。如排队取号,以及并发编程中锁的竞争分配问题。
队列由于其队尾插入,队头删除的特性,若使用数组实现会比较困难:无论是插入还是删除,都需要考虑空间大小和数据移动的问题,因此在这里我仅展示用链表实现。因此队列的功能我们仅对链表进行尾插头删即可
声明队列节点结构
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType val;
}QNode;
为了避免次次寻找队头队尾,直接定义另一个结构存储头尾指针
typedef struct Queue
{
QNode* phead;//队头
QNode* ptail;//队尾
int size;//队列数据数量
}Queue;
要实现的接口
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
// 队尾插入
void QueuePush(Queue* pq, QDataType x);
// 队头删除
void QueuePop(Queue* pq);
// 取队头和队尾的数据
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
int QueueSize(Queue* pq);
bool QueueEmpty(Queue* pq);
实现
#include"Queue.h"
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->phead;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
// 队尾插入
//申请节点,插入节点后更新size
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->next = NULL;
newnode->val = x;
if (pq->ptail == NULL)//若此时队列没有节点
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
// 队头删除
//存头节点下个位置的节点
//释放头节点位置节点
//更新头节点和size
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->size != 0);
/*QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
if (pq->phead == NULL)
pq->ptail = NULL;*/
// 一个节点
if (pq->phead->next == NULL)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else // 多个节点
{
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
//输出队头数据
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->phead);
return pq->phead->val;
}
//输出队尾数据
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(pq->ptail);
return pq->ptail->val;
}
//求当前队列的数据个数
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
//当前队列数据个数0则为空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
三.栈和队列的简单应用
1.用双栈实现队列
该问题的核心是:如何用后进先出的结构,实现一个先进先出的结构。这个核心会同时影响到插入和删除的设计逻辑。
不妨先大致分析一下出队的逻辑
我们向Stack1中插入数据[1,2,3]。若要进行出队,应该出最先入队的1
然而在Stack1中,1显然位于栈底,想要将其最先出队,不难观察出:
我们只要将Stack1中数据以此出栈到Stack2中即可。
此时将Stack2的栈顶出栈,就完成了出队的操作。
typedef struct{
int*a;
int top;
int capacity;
}Stack;
void stackInit(Stack*pst){
assert(pst!=NULL);
pst->top=pst->capacity=0;
pst->a=NULL;
}
void stackPush(Stack*pst,int x){
assert(pst!=NULL);
if(pst->top==pst->capacity){
int newcapacity=pst->capacity==0?4:pst->capacity*2;
int *tmp=(int*)realloc(pst->a,newcapacity*sizeof(int));
pst->a=tmp;
pst->capacity=newcapacity;
}
pst->a[pst->top]=x;
pst->top++;
}
void stackPop(Stack*pst){
assert(pst!=NULL);
pst->top--;
}
int stackTop(Stack*pst){
return pst->a[pst->top-1];
}
bool stackEmpty(Stack*pst){
return pst->top==0;
}
void stackFree(Stack*pst){
free(pst->a);
pst->a=NULL;
}
//至此都是栈的结构和接口,因为C语言库没有内置栈。。
//可以看到这里的队列由双栈实现
typedef struct {
Stack stack1;
Stack stack2;
} MyQueue;
//建队,即将队中的两个栈初始化即可
MyQueue* myQueueCreate() {
MyQueue*obj=(MyQueue*)malloc(sizeof(MyQueue));
stackInit(&(obj->stack1));
stackInit(&(obj->stack2));
return obj;
}
int myQueuePop(MyQueue* obj){
// 当stack2为空时,转移所有元素
if(stackEmpty(&obj->stack2)){
while(!stackEmpty(&obj->stack1)){
int tmp = stackTop(&obj->stack1);
stackPush(&obj->stack2, tmp);
stackPop(&obj->stack1);
}
}
int x = stackTop(&obj->stack2);
stackPop(&obj->stack2);
return x;
}
void myQueuePush(MyQueue* obj, int x){
stackPush(&obj->stack1,x);
}
int myQueuePeek(MyQueue* obj) {
if (stackEmpty(&obj->stack2)) {
// 循环转移所有元素
while (!stackEmpty(&obj->stack1)) {
int tmp = stackTop(&obj->stack1);
stackPush(&obj->stack2, tmp);
stackPop(&obj->stack1);
}
}
return stackTop(&obj->stack2); // 正确返回队首元素
}
bool myQueueEmpty(MyQueue* obj) {
return stackEmpty(&obj->stack1)&&stackEmpty(&obj->stack2);
}
void myQueueFree(MyQueue* obj) {
stackFree(&obj->stack1);
stackFree(&obj->stack2);
}
这段代码就是上面提到要实现出队时,将栈满的元素一个个入栈到栈空的栈中
a.取stack1栈顶元素值
b.将stack1栈顶元素入空栈stack2
c.将当前stack1栈顶元素出栈,更新新的栈顶元素
由此直至stack1变为空栈,stack2栈满,stack1的栈底为stack2的栈顶。
while(!stackEmpty(&obj->stack1)){
int tmp = stackTop(&obj->stack1);
stackPush(&obj->stack2, tmp);
stackPop(&obj->stack1);
}
然而上述代码存在一定的问题。
int myQueuePop(MyQueue* obj){
// 当stack2为空时,转移所有元素
if(stackEmpty(&obj->stack2)){
while(!stackEmpty(&obj->stack1)){
int tmp = stackTop(&obj->stack1);
stackPush(&obj->stack2, tmp);
stackPop(&obj->stack1);
}
}
int x = stackTop(&obj->stack2);
stackPop(&obj->stack2);
return x;
}
void myQueuePush(MyQueue* obj, int x){
stackPush(&obj->stack1,x);
}
int myQueuePeek(MyQueue* obj) {
if (stackEmpty(&obj->stack2)) {
// 循环转移所有元素
while (!stackEmpty(&obj->stack1)) {
int tmp = stackTop(&obj->stack1);
stackPush(&obj->stack2, tmp);
stackPop(&obj->stack1);
}
}
return stackTop(&obj->stack2); // 正确返回队首元素
}
在进行转移元素的操作时,我们默认向空栈中转移。在初次创建栈时空栈即为Stack2(因为上述接口中首先向Stack1插入元素),但若测试用例中对同一个myQueue进行出队时,此时的空栈又变成了Stack1。这里建议使用if-else判断空栈或者用假设法判定空栈,否则可能无法通过某些测试用例。
2.用两个队列实现栈
这题的思想其实与第一个完全一致。在这里直接放出代码,并解决以上提到的问题
#define LEN 20
typedef struct queue {
int *data;
int head;
int rear;
int size;
} Queue;
typedef struct {
Queue *queue1, *queue2;
} MyStack;
Queue *initQueue(int k) {
Queue *obj = (Queue *)malloc(sizeof(Queue));
obj->data = (int *)malloc(k * sizeof(int));
obj->head = -1;
obj->rear = -1;
obj->size = k;
return obj;
}
void enQueue(Queue *obj, int e) {
if (obj->head == -1) {
obj->head = 0;
}
obj->rear = (obj->rear + 1) % obj->size;
obj->data[obj->rear] = e;
}
int deQueue(Queue *obj) {
int a = obj->data[obj->head];
if (obj->head == obj->rear) {
obj->rear = -1;
obj->head = -1;
return a;
}
obj->head = (obj->head + 1) % obj->size;
return a;
}
int isEmpty(Queue *obj) {
return obj->head == -1;
}
MyStack *myStackCreate() {
MyStack *obj = (MyStack *)malloc(sizeof(MyStack));
obj->queue1 = initQueue(LEN);
obj->queue2 = initQueue(LEN);
return obj;
}
void myStackPush(MyStack *obj, int x) {
if (isEmpty(obj->queue1)) {
enQueue(obj->queue2, x);
} else {
enQueue(obj->queue1, x);
}
}
int myStackPop(MyStack *obj) {
if (isEmpty(obj->queue1)) {
while (obj->queue2->head != obj->queue2->rear) {
enQueue(obj->queue1, deQueue(obj->queue2));
}
return deQueue(obj->queue2);
}
while (obj->queue1->head != obj->queue1->rear) {
enQueue(obj->queue2, deQueue(obj->queue1));
}
return deQueue(obj->queue1);
}
int myStackTop(MyStack *obj) {
if (isEmpty(obj->queue1)) {
return obj->queue2->data[obj->queue2->rear];
}
return obj->queue1->data[obj->queue1->rear];
}
bool myStackEmpty(MyStack *obj) {
if (obj->queue1->head == -1 && obj->queue2->head == -1) {
return true;
}
return false;
}
void myStackFree(MyStack *obj) {
free(obj->queue1->data);
obj->queue1->data = NULL;
free(obj->queue1);
obj->queue1 = NULL;
free(obj->queue2->data);
obj->queue2->data = NULL;
free(obj->queue2);
obj->queue2 = NULL;
free(obj);
obj = NULL;
}
3.设计循环队列
循环队列我们可以理解为一个座位有限的图书馆,当座位满时就不能再坐人,但是一旦有人离开,就可以再来一个人坐在那个空位。
我们可以直接用笔画一画这个结构,在这里rear为队尾元素的下一个位置。假设这是一个可以存放五个数据的循环队列。先对这个队列进行入队。
接着开始出队 (注意队列从队尾入队,队头出队)
其实从刚刚插入的图我们就能发现一个比较大的问题,当栈空和栈满时的front指针和rear指针指向的都是同一个位置。。。他有一个专门的称为,叫假溢出
为了解决假溢出,可以对要存储k个数据的循环队列,初始化k+1个空间。
若此时的k为4,开辟k+1个元素。此时的队空队满如下:
直接来做这道题
typedef struct {
int front;//指向队头
int rear;//指向队尾的下一个元素
int capacity;//空间容量
int *elements;//用数组实现循环队列
} MyCircularQueue;
//建立存k个数据的循环队列,这里开辟的空间为k+1
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue *obj = (MyCircularQueue *)malloc(sizeof(MyCircularQueue));
obj->capacity = k + 1;
obj->rear = obj->front = 0;
obj->elements = (int *)malloc(sizeof(int) * obj->capacity);
return obj;
}
//入队,成功return true
//若此时队满,直接return false
//这时判断队满的条件是rear的下一个元素为队头
//注意这里的取模操作,是为了消除rear+1大于capacity,并且也是让rear和front循环起来的基本操作
//更新rear位置
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if ((obj->rear + 1) % obj->capacity == obj->front) {
return false;
}
obj->elements[obj->rear] = value;
obj->rear = (obj->rear + 1) % obj->capacity;
return true;
}
//出队,成功return true
//若此时队空,直接return false
//更新front位置
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if (obj->rear == obj->front) {
return false;
}
obj->front = (obj->front + 1) % obj->capacity;
return true;
}
//求队头元素
//若队空,return
//返回front处值
int myCircularQueueFront(MyCircularQueue* obj) {
if (obj->rear == obj->front) {
return -1;
}
return obj->elements[obj->front];
}
//求队尾元素
//若队空,return
//rear为队尾元素的下一个,因此返回rear-1处位置即可
//但此时rear恰好为0,为让其正确指向队尾元素,-1加上capacity再模capacity
int myCircularQueueRear(MyCircularQueue* obj) {
if (obj->rear == obj->front) {
return -1;
}
return obj->elements[(obj->rear - 1 + obj->capacity) % obj->capacity];
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->rear == obj->front;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return (obj->rear + 1) % obj->capacity == obj->front;
}
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->elements);
free(obj);
}