栈
概念及结构
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据的插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作被称为进栈/压栈/入栈,入数据的地方为栈顶、
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
出栈、入栈的过程我们可以想象为我们往羽毛球筒里放羽毛球和拿羽毛球,都是只能在一个口进出,是不是可以很形象的想象出来。下面我们来看一看栈是如何实现的。
栈的实现
栈的实现一般可以使用数组或者链表(包括单链表和双向链表)来实现。在使用之前,我们需要考虑哪一种的综合效率最高,从单个节点的空间占用上来看,双向链表是大于单链表和数组的,因此我们首先可以排除双向链表;然后我们来对比一下单链表和数组的优劣:数组在动态开辟空间上可能会有消耗,单链表在开辟空间上较为优良,数组在插入和删除数据上比单链表的操作更为简单,其他区别也都是相差无几,综合下来看,数组和单链表平分秋色,但是在上一章我们了解到了一个缓存利用率的概念,数组的缓存利用率是远大于单链表的,所以我们选择数组作为实现栈的优选。
下面我们先来看一看静态栈的结构:
cpp
//实际中一般不使用,所以简单了解
typedef int STDataType;
#define N 10
typedef struct Stack
{
STDataType _a[N];
int _top; // 栈顶
}Stack;
支持动态增长的栈的结构:
cpp
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct StackNode
{
STDataType* data; //动态开辟数组
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);
我们来看看相关函数的实现:
cpp
//创建和销毁
void StackInit(ST* pst)
{
assert(pst);
//初始化
pst->data = NULL;
pst->capacity = pst->top = 0;
}
void StackDestroy(ST* pst)
{
assert(pst);
free(pst->data);
free(pst);
}
//在栈的初始化和销毁上,我们要保证指针的有效性,并且在销毁时将数组也一并释放。
//入栈和出栈
void StackPush(ST* pst,STDataType x)
{
assert(pst);
//扩容操作我使用了三目操作符,兼顾了多种情况下容量的大小变化。
if (pst->top == pst->capacity)
{
int newcapacity = (pst->capacity == 0) ? 4 : pst->capacity * 2;
//使用realloc,在容量为0时,realloc == malloc。
pst->data = (STDataType*)realloc(pst->data,sizeof(STDataType)*newcapacity);
if (pst->data == NULL)
{
perror("malloc fail");
return;
}
}
//先赋值再后移,因为top是在栈顶元素的下一个位置,当前位置并没有元素。
pst->data[pst->top] = x;
pst->top++;
}
//删除函数很简单,不做解释
void StackPop(ST* pst)
{
assert(pst);
pst->top--;
}
//获取栈顶元素
STDataType StackTop(ST* pst)
{
assert(pst);
//只需一步
return pst->data[pst->top - 1];
}
//判空
bool StackEmpty(ST* pst)
{
assert(pst);
//只需一步
return (pst->top == 0);
}
//求栈中元素个数
int StackSize(ST* pst)
{
assert(pst);
//只需一步
return pst->top;
}
测试代码各位自行书写,这里不做测试。这里要注意,我们创建的结构体需要传指针参数。
队列
结构及概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)。
入队列:进行插入操作的一端,即队尾。
出队列:进行删除操作的一端,即队头。
队列的实现
队列的结构也可以用数组和链表来实现,因为出队列操作是在数组的头节点上,数组的效率会比较低,所以使用链表的结构实现更好一些。
链式结构表示:
cpp
//旧版本
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType val;
}QNode;
//因为我们在使用单链表时需要同时可以操作队头和队尾,因此我们设置两个指针的QNode结构体,用来便于我们操作链表,在新定义结构体中,我们还加入了size用来计算队列中元素个数。
//优化版本
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType val;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
头文件包含及函数声明文件:
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
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);
//栈空
bool QueueEmpty(Queue* pq);
//求队列元素个数
int QueueSize(Queue* pq);
队列函数实现:
cpp
//初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = 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;
}
//入队列
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->phead == NULL)
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = pq->ptail->next;
}
pq->size++;
}
//出队列
void QueuePop(Queue* pq)
{
assert(pq);
//一个节点
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);
return pq->phead->val;
}
//获取队列的尾元素
QDataType QueueBack(Queue* pq)
{
assert(pq);
return pq->ptail->val;
}
//栈空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return (pq->size == 0);
}
//求队列元素个数
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
测试及调试操作请各位自行编写。
循环队列
我们假设有k个元素,如果在顺序存储的队列需要建立大于k的数组,并把队列的所有元素存储在数组的前k个单元,数组的首元素就是队头。与栈不同的是,队列元素的出列是在队头,意味着如果我们用数组实现队列的话,队列的所有元素都要向前移动,以保证队列的队头有效性,此时的时间复杂度为O(n)。
循环队列是一种特殊的队列实现方式,我们可以仔细思考,为什么出队列时一定要全部移动呢,如果不去限制队列的元素必须存储到数组的前k个单元,出队的性能就会大大提高。也就是说,我们不需要一定在下标为0的位置去这只队头。
为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,我们选择队头指针指向队头元素,而队尾指针指向队尾元素的下一个位置,这样当队头等于队尾时,只为空栈;(队尾+1)%(k+1)== 队头时为栈满,如此就解决了假溢出问题。
下面我们来看相关题目:
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为"环形缓冲器"。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k)
: 构造器,设置队列长度为 k 。Front
: 从队首获取元素。如果队列为空,返回 -1 。Rear
: 获取队尾元素。如果队列为空,返回 -1 。enQueue(value)
: 向循环队列插入一个元素。如果成功插入则返回真。deQueue()
: 从循环队列中删除一个元素。如果成功删除则返回真。isEmpty()
: 检查循环队列是否为空。isFull()
: 检查循环队列是否已满。
代码实现如下:
cpp
typedef struct {
int* a;
int phead;
int ptail;
int k;
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* pq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
if (pq == NULL)
{
perror("malloc fail!");
return NULL;
}
//多开一个数组空间解决假溢出问题
pq->a = (int*)malloc(sizeof(int) * (k + 1));;
pq->k = k;
pq->phead = pq->ptail = 0;
return pq;
}
//判空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
assert(obj);
return (obj->ptail == obj->phead);
}
//判满
bool myCircularQueueIsFull(MyCircularQueue* obj) {
assert(obj);
return ((obj->ptail + 1) % (obj->k + 1)) == (obj->phead);
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
assert(obj);
if (myCircularQueueIsFull(obj))
{
return false;
}
obj->a[obj->ptail] = value;
obj->ptail++;
obj->ptail %= (obj->k + 1);
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
assert(obj);
if (myCircularQueueIsEmpty(obj))
{
return false;
}
obj->phead++;
obj->phead %= (obj->k + 1);
return true;
}
int myCircularQueueFront(MyCircularQueue* obj) {
assert(obj);
if (myCircularQueueIsEmpty(obj))
{
return -1;
}
else
{
return obj->a[obj->phead];
}
}
int myCircularQueueRear(MyCircularQueue* obj) {
assert(obj);
if (myCircularQueueIsEmpty(obj))
{
return -1;
}
else
{
//保证了当ptail为0时,不会有减1出现负数的情况。
return obj->a[(obj->ptail - 1 + obj->k + 1) % (obj->k + 1)];
}
}
void myCircularQueueFree(MyCircularQueue* obj) {
assert(obj);
free(obj->a);
free(obj);
}
经典OJ题目:
有效的括号
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
1.左括号必须用相同类型的右括号闭合。
2.左括号必须以正确的顺序闭合。
3.每个右括号都有一个对应的相同类型的左括号。
提示:
1 <= s.length <= 10^4,且s
仅由括号 '()[]{}'
组成。
解:
这就是一道经典的栈结构的题目,题中说明只有三种括号,需要我们判断s中的括号是否匹配。我们用栈的结构就可以解决。通过对字符串的数组访问,我们可以遍历整个数组,当遍历道德数组元素为左括号时,元素进栈,当遍历到的数组为右括号时,栈顶元素出栈,与该有括号匹配,如果两括号不匹配,说明字符串中的括号不匹配,如果括号匹配,则继续向后遍历。在遍历中,如果栈为空,而数组中还存在右括号,说明字符串中的括号不匹配;如果遍历结束,而栈不为空,说明字符串中的括号不匹配,否则括号匹配。
代码如下:
cpp
//所需头文件
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef char stDataType;
typedef struct Stack
{
stDataType* data;
int top;
int capacity;
}ST;
//栈的初始化
void InitStack(ST* pst)
{
assert(pst);
pst->data = NULL;
pst->top = 0;
pst->capacity = 0;
}
//销毁栈
void DestroyStack(ST* pst)
{
assert(pst);
free(pst->data);
pst->data = NULL;
pst->capacity = pst->top = 0;
}
//入栈
void PushStack(ST* pst, stDataType data)
{
assert(pst);
//扩容
if (pst->top == pst->capacity)
{
int newcapacity = (pst->capacity == 0) ? 2 : (pst->capacity * 2);
stDataType* tmp = (stDataType*)realloc(pst->data, sizeof(stDataType) * newcapacity);
if (tmp == NULL)
{
perror("Push malloc fail");
return;
}
else
{
pst->data = tmp;
pst->capacity = newcapacity;
}
}
//尾插
pst->data[pst->top] = data;
pst->top++;
}
//出栈
void PopStack(ST* pst)
{
assert(pst);
assert(pst->top > 0);
pst->top--;
}
//取栈顶数据
stDataType TopStack(ST* pst)
{
assert(pst);
assert(pst->top > 0);
//取栈顶元素
stDataType ret = pst->data[pst->top - 1];
//返回元素
return ret;
}
//判断空栈
bool EmptyStack(ST* pst)
{
assert(pst);
/*if (pst->top == 0)
{
return true;
}else
{
return false;
}*/
return (pst->top == 0);
}
//判断是否匹配
bool isValid(char* s)
{
ST st;
InitStack(&st);
while(*s)
{
if(*s == '(' || *s == '{' || *s == '[')
{
PushStack(&st,*s);
}
else if(*s == ')' || *s == '}' || *s == ']')
{
//先判断栈是否为空
if(EmptyStack(&st))
{
DestroyStack(&st);
return false;
}
stDataType tmp = TopStack(&st);
PopStack(&st);
if(tmp == '(' && *s != ')'
|| tmp == '{' && *s != '}'
|| tmp == '[' && *s != ']')
{
DestroyStack(&st);
return false;
}
}
++s;
}
//如果栈不为空,说明左括号多于有括号
bool ret = EmptyStack(&st);
DestroyStack(&st);
return ret;
}
用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
实现 MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。
注意:
- 你只能使用队列的标准操作 ------ 也就是
push to back
、peek/pop from front
、size
和is empty
这些操作。 - 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
提示:
1 <= x <= 9
- 最多调用
100
次push
、pop
、top
和empty
- 每次调用
pop
和top
都保证栈不为空
解:
这道题有些难度,但只要把思路捋清楚,结合上面的队列代码就可以简单解决。在这道题中,我们使用了两个队列来完成,栈的特点是后进先出,而队列是先进先出,我们只要利用两个队列来完成后进先出的操作,就可以了。
首先,在myStackCreate函数中我们开辟了自建栈的结构体空间,并利用指针将空间转移给实参。myStackPush函数中,我们需要做的就是检查那个函数不是空队列,然后向不为空队列的队列中继续入元素。myStackPop中首先我们需要判空,因为size不能为负数,接着我们先用假设法设其中一个为空队列,另一个为非空队列,如果不对,我们再讲两个变量转换一下。然后我们将非空队列中的前n-1个数据导入空队列中,再将最后一个数据记录后删除,最后返回该元素的值。剩余函数比较简单,这里不做讲解,各位如果有疑问,可以在评论区讨论。这位额外提一嘴,各位一定要注意断言的使用,我们在插入和删除时数据元素时,不仅要确保指针的有效性,还要确保部分指针头结点的有效性,以及队列为空时,调用删除函数的问题。
cpp
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType val;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
typedef struct {
Queue q1;
Queue q2;
} MyStack;
//初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = 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;
}
//入队列
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->phead == NULL)
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = pq->ptail->next;
}
pq->size++;
}
//求队列元素个数
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
//出队列
void QueuePop(Queue* pq)
{
assert(pq);
if(pq->phead == NULL)
{
return;
}
//一个节点
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 && pq->phead);
return pq->phead->val;
}
//获取队列的尾元素
QDataType QueueBack(Queue* pq)
{
assert(pq && pq->ptail);
return pq->ptail->val;
}
//栈空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return (pq->size == 0);
}
MyStack* myStackCreate()
{
MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
if (pst == NULL)
{
perror("malloc fail");
return NULL;
}
QueueInit(&(pst->q1));
QueueInit(&(pst->q2));
return pst;
}
void myStackPush(MyStack* obj, int x) {
assert(obj);
if(!QueueEmpty(&obj->q1))
{
QueuePush(&(obj->q1),x);
}
else
{
QueuePush(&(obj->q2),x);
}
}
int myStackPop(MyStack* obj) {
assert(obj);
Queue* empty = &(obj->q1);
Queue* noempty = &(obj->q2);
if(!QueueEmpty(&(obj->q1)))
{
empty = &(obj->q2);
noempty = &(obj->q1);
}
//将前n-1个元素移入到另一个队列,删除最后一个元素
while(QueueSize(noempty) > 1)
{
QueuePush(empty,QueueFront(noempty));
QueuePop(noempty);
}
int top = QueueFront(noempty);
QueuePop(noempty);
return top;
}
int myStackTop(MyStack* obj) {
assert(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) {
assert(obj);
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);
}
用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
):
实现 MyQueue
类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
说明:
- 你只能使用标准的栈操作 ------ 也就是只有
push to top
,peek/pop from top
,size
, 和is empty
操作是合法的。 - 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
提示:
1 <= x <= 9
- 最多调用
100
次push
、pop
、peek
和empty
- 假设所有操作都是有效的 (例如,一个空的队列不会调用
pop
或者peek
操作)
解:
用栈实现队列的思路和用队列实现栈的方式差不多。我们只需要在两个栈中导数据就能够完成,但这次的导数据只需要从一个栈导入另一个栈即可,但是在一次导入之后,如果这个导入之后的栈还有数据,就不能进行第二次导入,这样会导致自建队列的数据顺序改变。在push函数中我们也不需要再调整指针,将qs1作为一栈,qs2作为二栈,每一次push都将数据放入一栈,当需要导入时,就将一栈的数据导入二栈中。
cpp
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct StackNode
{
STDataType* data; //动态开辟数组
int top; // 栈顶元素的下一个位置
int capacity; // 容量
}ST;
//创建(栈)队列
typedef struct
{
ST s1;
ST s2;
}MyQueue;
//创建和销毁
void StackInit(ST* pst)
{
assert(pst);
//初始化
pst->data = NULL;
pst->capacity = pst->top = 0;
}
void StackDestroy(ST* pst)
{
assert(pst);
free(pst->data);
// 不需要释放 pst 本身,因为它不是动态分配的
// free(pst);
}
//入栈和出栈
void StackPush(ST* pst, STDataType x)
{
assert(pst);
if (pst->top == pst->capacity)
{
int newcapacity = (pst->capacity == 0) ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->data, sizeof(STDataType) * newcapacity);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
pst->data = tmp;
pst->capacity = newcapacity;
}
pst->data[pst->top] = x;
pst->top++;
}
void StackPop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
pst->top--;
}
//判空
bool StackEmpty(ST* pst)
{
assert(pst);
return (pst->top == 0);
}
//获取栈顶元素
STDataType StackTop(ST* pst)
{
assert(pst);
if (!StackEmpty(pst))
return pst->data[pst->top - 1];
else
return -1;
}
//求栈中元素个数
int StackSize(ST* pst)
{
assert(pst);
return pst->top;
}
MyQueue* myQueueCreate()
{
MyQueue* pq = (MyQueue*)malloc(sizeof(MyQueue));
if (pq == NULL)
{
perror("malloc fail");
return NULL;
}
StackInit(&(pq->s1));
StackInit(&(pq->s2));
return pq;
}
void myQueuePush(MyQueue* obj, int x)
{
assert(obj);
StackPush(&(obj->s1), x);
}
int myQueuePop(MyQueue* obj) {
assert(obj);
assert(!StackEmpty(&(obj->s1)) || !StackEmpty(&(obj->s2)));
ST* qs1 = &(obj->s1);
ST* qs2 = &(obj->s2);
if (StackEmpty(qs2))
{
while (!StackEmpty(&(obj->s1)))
{
StackPush(&(obj->s2), StackTop(&(obj->s1)));
StackPop(&(obj->s1));
}
}
int top = StackTop(&(obj->s2));
StackPop(&(obj->s2));
return top;
}
//返回队列开头的元素
int myQueuePeek(MyQueue* obj) {
assert(obj);
//同pop函数
if (StackEmpty(&(obj->s2))) {
while (!StackEmpty(&(obj->s1))) {
StackPush(&(obj->s2), StackTop(&(obj->s1)));
StackPop(&(obj->s1));
}
}
return StackTop(&(obj->s2));
}
bool myQueueEmpty(MyQueue* obj) {
assert(obj);
return StackEmpty(&(obj->s1)) && StackEmpty(&(obj->s2));
}
void myQueueFree(MyQueue* obj) {
assert(obj);
StackDestroy(&(obj->s1));
StackDestroy(&(obj->s2));
free(obj);
}