数据结构初阶:栈和队列
Posted AKA你的闺蜜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构初阶:栈和队列相关的知识,希望对你有一定的参考价值。
文章目录
栈和队列
栈和队列是两种数据结构,栈和队列这两种结构也是线性表。特殊在于它们的操作受到一些限制。
1 栈
1.1 栈的定义和结构
栈是一种特殊的线性表,栈只允许在其固定的一端进行插入和删除元素的操作。进行数据插入删除操作的一端被称为栈顶,另一端被称为栈底。栈中的数据元素遵循后进先出的原则。
后进先出,先进后出,即
LIFO
原则(Last In First Out)。
压栈:栈的插入操作被称为压栈,也可以叫做进栈、入栈。
出栈:栈的插入操作被称为出栈,或称弹栈。
数据的出入都在栈顶,类似于子弹上膛和发射的过程。元素像子弹一样一个个地被压入弹夹,再一个个地打出去,这个过程便是压栈和出栈,弹夹便是栈。
栈结构体定义
和线性表类似,栈结构可以使用数组栈和链式栈实现。相对来说,数组栈比链式栈的结构优势更大一点。
动态数组进行尾插尾删的效率高其次缓存命中率高,缺点是动态增容有一定的内存消耗。链式栈需使用双向链表,否则进行尾删的效率低,但以链表头作栈顶进行头插头删的效率要更高。
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
a
指向为栈开辟的空间,top
指向栈顶,相当于顺序表的size
。
1.2 栈的实现
//初始化栈
void StackInit(ST* ps);
//入栈
void StackPush(ST* ps, STDataType data);
//出栈
void StackPop(ST* ps);
//获取栈顶元素
STDataType StackTop(ST* ps);
//获取栈元素个数
int StackSize(ST* ps);
//检测空栈
bool StackEmpty(ST* ps);
//销毁栈
void StackDestroy(ST* ps);
栈初始化和销毁
void StackInit(ST* ps) {
ps->a = NULL;
ps->capacity = ps->top = 0;
}
void StackDestroy(ST* ps) {
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
top
可以初始化为0,也可以为-1。
- 若为0,则
top
总是即将插入元素的下标,或者说是栈顶的后一块空间,其值代表栈元素个。 - 若置为-1,则代表当前栈顶位置下标,其值加1代表元素个数。
栈的压入和弹出
void StackPush(ST* ps, STDataType data) {
assert(ps);
//检测容量
if (ps->capacity == ps->top) {
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* ptr = realloc(ps->a, sizeof(STDataType) * newCapacity);
if (ptr == NULL) {
perror("StackPush::realloc");
exit(-1);
}
ps->a = ptr;
ps->capacity = newCapacity;
}
ps->a[ps->top] = data;
ps->top++;
}
void StackPop(ST* ps) {
assert(ps);
assert(!StackEmpty(ps));
ps->top--;
}
入栈一定记得最后把
newCapacity
再赋值给capacity
,还有扩容时的大小应是数组元素的大小而不是结构体的大小。
删除数据前需要保证栈的非空状态。
获取栈顶元素
STDataType StackTop(ST* ps) {
assert(ps);
assert(!StackEmpty(ps));
return ps->a[ps->top - 1];
}
void test() {
while (!StackEmpty(&stack)) {
printf("%d ", StackTop(&stack));
StackPop(&stack);
}
}
top-1
为当前栈顶的下标。同样要保证栈非空。
加上该测试函数,可以实现循环打印栈元素的功能。
其他基本接口
//获取栈元素个数
int StackSize(ST* ps) {
assert(ps);
return ps->top;
}
//检测空栈
bool StackEmpty(ST* ps) {
assert(ps);
return !ps->top;
}
!top
的值正好可以表示栈的有无元素的状态。当然这样top
必须初始化为0。
2 队列
2.1 队列的定义和结构
队列同样是一种特殊的线性表,和栈相反,队列只允许在其一端进行插入而在另一端进行删除元素的操作。进行数据插入操作的一端被称为队尾,进行删除操作的另一端被称为队头。队列中的数据元素遵循先进先出的原则。
入队列即在队尾插入数据,出队列则是在队头删除数据。
先进先出,后进后出,即
FIFO
原则(First In First Out)。
队列结构体定义
typedef int QDataType;
typedef struct QueueNode
{
QDataType data;
struct QueueNode* next;
} QueueNode;
typedef struct Queue
{
struct QueueNode* head;
struct QueueNode* tail;
} Queue;
队列需要两个指针标识队头和队尾,以便管理队列的元素。而队列元素即结点用单链表的结构实现即可。把结点封装成一个结构体,队列再封装成一个结构体存入指向结点结构体的指针。
队尾指针是根据队列这个对象只在队尾进行插入操作的特点而设计的。原先的单链表表尾有插入删除两种操作,故定义尾指针的意义不大,将存储的功能交给双向链表完成。
结构的定义是很灵活的,不定义
Queue
结构体或者说不将头尾指针封装起来也是可以的。那么函数就需要定义成:void QueueInit(QueueNode** pphead, QueueNode** pptail);
显然,封装成结构体不失为一种良好的代码风格。
2.2 队列的实现
//队列初始化
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);
队列初始化和销毁
void QueueInit(Queue* pq) {
assert(pq);
pq->head = NULL;
pq->tail = NULL;
}
void QueueDestroy(Queue* pq) {
assert(pq);
QueueNode* cur = pq->head;
while (cur) {
QueueNode* next = cur->next;
free(cur);
cur = next;
}
}
初始化和销毁并没有传二级指针,因为传递结构体的地址,而两个指针是封装在结构体里的。创建队列在函数外,所以传其地址就行,同时加上断言以防空指针。
队尾入队和队头出队
void QueuePush(Queue* pq, QDataType x) {//Enqueue
assert(pq);
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
if (newNode == NULL) {
perror("Queue::malloc");
exit(-1);
}
newNode->data = x;
newNode->next = NULL;
//队列为空
if (pq->head == NULL) {
pq->head = pq->tail = newNode;
}
else {
pq->tail->next = newNode;
pq->tail = newNode;
}
}
void QueuePop(Queue* pq) {//Dequeue
assert(pq);
assert(!QueueEmpty(pq));
QueueNode* next = pq->head->next;
free(pq->head);
pq->head = next;
//链表为空时尾指针置空
if (pq->head == NULL) {
pq->tail = NULL;
}
}
在tail
所指的队尾后再新建并链上一个结点,再将tail
指针指向新结点,这便是入队的原理。出队只能在队头删除结点,也就是将head
指针指向下一个结点并将前一个释放掉即可。
head->next
对头指针解引用,就一定要保证head
有next
,也就是保证链表非空。
获取队头队尾元素
QDataType QueueFront(Queue* pq) {
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
QDataType QueueBack(Queue* pq) {
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
与出栈的实现类似,获取队列元素tail->data
,对指针解引用访问其所指空间,必然要检查指针是否有效,也就是判断链表是否为空。
void test() {
//...
while (!QueueEmpty(&q)) {
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
}
配合上述函数可以模拟实现循环出队。
其他基本接口
//获取队列元素个数
int QueueSize(Queue* pq) {
assert(pq);
int count = 0;
QueueNode* cur = pq->head;
while (cur) {
count++;
cur = cur->next;
}
return count;
}
//检测队列是否为空
bool QueueEmpty(Queue* pq) {
assert(pq);
return !pq->head;
}
获取队列元素个数,除了遍历计数的方式,还可以定义一个整型变量放在结点结构体中。
3 栈和队列面试题
Example 1 判断有效括号
给定一个只包括 (
,)
,{
,}
,[
,]
的字符串 s
,判断字符串是否有效。
bool isValid(char* s) {
ST st;
StackInit(&st);
while (*s) {
if (*s == '(' || *s == '[' || *s == '{') {
StackPush(&st, *s);
}
else {
//栈无元素,无法与右括号匹配
if (StackEmpty(&st)) {
StackDestroy(&st);
return false;
}
STDataType ret = StackTop(&st);
if ((ret == '(' && *s != ')') || (ret == '[' && *s != ']') || (ret == '{' && *s != '}')) {
StackDestroy(&st);
return false;
}
else {
StackPop(&st);
}
}
s++;
}
if (StackEmpty(&st)) {
StackDestroy(&st);
return true;
}
else {
StackDestroy(&st);
return false;
}
}
利用栈的先进后出,后进先出的特点。
- 将字符串
s
从前向后遍历将其中所有左括号依次入栈, - 等待遇到右括号时再利用后进先出的特点就可将最近的左括号与右括号对比。
- 若匹配成功则出栈一次,下一次就可以找到前一个左括号与之后的右括号进行匹配。
Example 2 队列实现栈
使用两个队列实现一个后入先出的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)
如用数组用链表实现,换成用队列实现栈。即利用队列的结构和接口函数,也就是队列的特点实现出一个结构,该结构具有栈的特点。
/**
* 结构体定义
**/
typedef struct QueueNode {
QDataType data;
struct QueueNode* next;
}QueueNode;
typedef struct Queue {
struct QueueNode* head;
struct QueueNode* tail;
}Queue;
typedef struct {
Queue q1;
Queue q2;
} MyStack;
/**
* 接口函数定义
**/
MyStack* myStackCreate() {
MyStack* st = (MyStack*)malloc(sizeof(MyStack));
if (st == NULL) {
exit(-1);
}
QueueInit(&st->q1);
QueueInit(&st->q2);
return st;
}
//调用函数创建堆区结构体并返回
void myStackPush(MyStack* obj, int x) {
assert(obj);
//向非空队列Push
if (!QueueEmpty(&obj->q1)) {
QueuePush(&obj->q1, x);
}
else {
QueuePush(&obj->q2, x);
}
}
int myStackPop(MyStack* obj) {
assert(obj);
//定义空与非空队列
Queue* emptyQ = &obj->q1;
Queue* nonEmptyQ = &obj->q2;
if (!QueueEmpty(&obj->q1)) {
nonEmptyQ = &obj->q1;
emptyQ = &obj->q2;
}
//将非空队列前n-1个元素Push到空队列
while (QueueSize(nonEmptyQ) > 1) {
QueuePush(emptyQ, QueueFront(nonEmptyQ));
QueuePop(nonEmptyQ);
}
//Pop最后一个元素并返回
int top = QueueFront(nonEmptyQ);
QueuePop(nonEmptyQ);
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) {
assert(obj);
//二者皆空才为空
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}
void myStackFree(MyStack* obj) {
assert(obj);
//释放队列结点
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
//释放结构体
free(obj);
}
Push
,由于栈和队列都是从固定的一端入,故模拟入栈直接向非空队列入即可。
Pop
,模拟出栈时,就要考虑到二者的不同,先删除队列中的前 n − 1 n-1 n−1个元素并将其入到另一个空队列中。直至第 n n n个元素再将其删除。
队头出数据,队尾入数据,正好能将非空队列前 n n n个元素按照原顺序插入到空队列中。非空队列仅剩最后一个元素再删除掉。将所插队列视为出栈后的栈,便实现模拟出栈的过程。
Top
,直接调用队列读取队尾元素的接口函数即可。
完成任意操作后都会产生一个空队列和一个非空队列。通过加以判断可以将非空队列视为待操作对象。也就是每次操作都是操作非空队列。
Example 3 栈实现队列
使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
)
/**
* 结构体定义
**/
typedef int STDataType;
typedef struct Stack {
STDataType* a;
int top;
int capacity;
}ST;
typedef struct {
ST pushST;
ST popST;
} MyQueue;
/**
* 接口函数定义
**/
MyQueue* myQueueCreate() {
MyQueue* pq= (MyQueue*)malloc(sizeof(MyQueue));
if (pq == NULL) {
exit(-1C++初阶Stack & Queue