目录
栈的基本概述
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除
操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)
的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
栈的程序实现
栈的实现
顺序表 | 解决了头删头插的难题,但需要增容 |
链表 | 如果是单链表,则在尾删尾插时,需要找到尾节点的前驱,这是比较麻烦的: 1.双向链表,在节点内多定义一个prev指针 2.逆向思维,出栈入栈看作链表的头插头删 |
Stack.h
#pragma once
#include<stdio.h>
#include<stdbool.h>
#include<stdlib.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;//栈顶
int capacity;
}ST;
void StackInit(ST* ps);
void StackDestory(ST* ps);
//入栈
void StackPush(ST* ps,STDataType x);
//出栈
void StackPop(ST* ps);
STDataType StackTop(ST* ps);
int StackSize(ST* ps);
bool StackEmpty(ST* ps);
Stack.c
StackInit
和动态顺序表极其类似,malloc一个空间
值得注意的是:
如果top = 0,则意味着 ps->a[top]没有存储数据,入栈的时候先录入数据再++,且 top == 实际栈中的数据个数。
如果top = -1,则意味着 ps->a[top]存储了有效数据,入栈时先++再录入数据,且top == 实际栈中数据个数 - 1
void StackInit(ST* ps)
{
assert(ps);
ps->a = (STDataType*) malloc(sizeof(STDataType) * 4);
if (ps->a == NULL)
{
printf("malloc fail\n");
exit(-1);
}
ps->capacity = 4;
ps->top = 0;
}
StackDestory
void StackDestory(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
StackPush
void StackPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
STDataType*tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * ps->capacity * 2);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
ps->a = tmp;
ps->capacity *= 2;
}
}
ps->a[ps->top] = x;
ps->top++;
}
StackPop
出栈
void StackPop(ST* ps)
{
assert(ps);
assert(ps->top > 0);
ps->top--;
}
StackTop
获取栈顶元素
STDataType StackTop(ST* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->a[ps->top - 1];
}
注意assert,如果top == 0时,则说明栈中没有数据了
StackSize
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
StackEmpty
判断栈是否为空
bool StackEmpty(ST* ps)
{
assert(ps);
return !(ps->top);
}
注意,这里使用了布尔值,如果ps->top == 0,则返回true
Test.c
#define _CRT_SECURE_NO_WARNINGS
#include"Stack.h"
void TestStack()
{
ST st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
printf("%d\n", StackSize(&st));
while (!StackEmpty(&st))
{
printf("%d ", StackTop(&st));
StackPop(&st);
}
printf("\n");
printf("%d", StackSize(&st));
StackDestory(&st);
}
{
TestStack();
return 0;
}
队列的基本概述
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先
进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一
端称为队头
队列的程序实现
队列的实现
队列最重要的性质是:队头出队,队尾入队,而我们之前学过的单链表,头删和尾插是能够十分便捷的完成这一功能,所以我们需要使用单链表作为队列的基本结构。
但是值得注意的是:在进行尾插的时候,我们需要知道尾节点的地址,而遍历会让程序变得冗余。所以与单链表不同的是,我们需要定义一个tail,目的是指向尾节点,这样子可以极大提高程序运行效率。
Queue.h
这里与单链表不同的是,在一个队列中,我们需要定义两个指针,一个指向队头,进行头删;一个指向队尾,进行尾删。所以我们不妨将这两个量也封装成一个结构体,每次都从这个结构体内调用head和tail。
第二个结构体,就是我们常规的单链表的结构体了,节点中有一个后驱和存储的数据。
值得注意的是:由于我们在函数中会修改结构体的值,所以在函数传参的时候,我们会采用传址传参的方法,即传入结构体的指针
#pragma once
#include<stdio.h>
#include<stdbool.h>
#include<stdlib.h>
#include<assert.h>
typedef int QDataType;
//定义节点
typedef struct QueueNode
{
QDataType data;
struct QueueNode* next;
}QNode;
//定义队头队尾
typedef struct Queue
{
QNode* head;
QNode* tail;
}Queue;
void QueueInit(Queue* pq);
void QueueDestory(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);
Queue.c
QueueInit
初始化队列:即将pq内的tail和head置空
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
}
QueueDestory
销毁队列:这里我们每次入队,都会malloc一个新的内存空间,所以我们需要依次释放这些内存空间,需要遍历整个队列,而不是简单地置空pq这个结构体
void QueueDestory(Queue* pq)
{
QNode* cur = pq->head;
//销毁需要将所有申请的内存空间全部置空
//所以需要遍历+free
while (cur)
{
QNode* tmp = cur->next;
free(cur);
cur = tmp;
}
pq->head = pq->tail = NULL;
}
QueuePush
入队,即单链表中的尾插。这里与尾插有不同的地方是:我们多定义了一个tail指针,当队列没有节点时,即head和tail均为空,我们需要额外将head和tail赋值为新节点。
void QueuePush(Queue* pq, QDataType x)
{
QNode* newnode = (QNode*)malloc(sizeof(QNode));
newnode->next = NULL;
newnode->data = x;
if (newnode == NULL)
{
printf("malloc fail!\n");
exit(-1);
}
assert(pq);
if (pq->head == NULL)
{
pq->head = pq->tail = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
}
QueuePop
出队,相当于单链表中的头删法
我们也是需要分情况讨论
1.当队列中还有一个以上的节点时:
进行头删法,注意先保存head->next,再释放head
2.当队列中只有一个节点时,即head == tail:
为了防止tail成为野指针,我们需要直接将head释放,并将head和tail置空
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->head);
//删除到最后一个节点时,tail会变成野指针
//还剩多个节点时
if (pq->head->next)
{
QNode* tmp = pq->head->next;
free(pq->head);
pq->head = tmp;
}
//删除到只剩最后一个节点时:head->next为空
else
{
free(pq->head);
pq->head = pq->tail = NULL;
//将head和tail置空,恢复成初始化后的情形
}
}
QueueFront
取队头的节点,注意断言:没有节点时需要断言
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->head);
return pq->head->data;
}
QueueBack
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(pq->tail);
return pq->tail->data;
}
QueueSize
注意循环条件 cur不为空,当cur为最后一个节点时,仍需要进入循环,size++
int QueueSize(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
int size = 0;
while (cur)
{
cur = cur->next;
size++;
}
return size;
}
QueueEmpty
判断队列是否为空,返回值是布尔值
bool QueueEmpty(Queue* pq)
{
assert(pq);
if (pq->head)
{
return false;
}
else
{
return true;
}
}
Test.c
#include"Queue.h"
void TestQueue()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
QueuePop(&q);
printf("%d\n", QueueSize(&q));
while (!QueueEmpty(&q))
{
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
QueueDestory(&q);
}
int main()
{
TestQueue();
return 0;
}
栈和队列oj练习题
有效的括号
给定一个只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串s
,判断字符串是否有效。有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()" 输出:true示例 2:
输入:s = "()[]{}" 输出:true示例 3:
输入:s = "(]" 输出:false提示:
1 <= s.length <= 104
s
仅由括号'()[]{}'
组成来源:力扣(LeetCode)
思路:
括号匹配的本质是:如果出现一个左括号,在这个左括号后有且只有一个右括号与之匹配。
这题难点在于:我们不知道和左括号匹配的右括号具体位置在哪。
我们先考虑括号匹配的情况:
例如:
"(){}{}" "[(){}]" "{[()]}"
我们可以注意到,括号匹配的字符串有一个关键,至少会有一个相连的括号对存在!
"(){}{}" "[(){}]" "{[()]}"
然后我们可以发现:如果将这个相连的括号对删去,我们可以很欣喜的发现:
再次出现了至少一个相连的括号对
"(){}" "[]" "{[]}"
如此反复,全部元素被删除完毕后,则一定是匹配的
那我们接下来考虑其他不匹配的情况:
1.例如:
"{{" "{{{}}"
只有左括号和右括号,或者左右括号数量不相等:
这显然不成立,依据上述算法,不可能将所有括号删除
2.例如
"(([)])" "(}"
左右括号数量相等,但是没有相连,这也不能进行删除,所以不会清空字符串。
所以,我们可以得到一个思路:找到相连的括号对,进行删除。
所以,我们可以利用栈来实现这个思路。
typedef char STDataType;
typedef struct Stack
{
STDataType* s;
int Top;
int capacity;
}ST;
void StackInit(ST* ps)
{
assert(ps);
ps->Top = 0;
ps->capacity = 4;
ps->s = (STDataType*)malloc(sizeof(STDataType) * 4);
}
void StackDestory(ST* ps)
{
assert(ps);
free(ps->s);
ps->capacity = ps->Top = 0;
}
void StackPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->capacity == ps->Top)
{
STDataType* tmp = (STDataType*)realloc(ps->s, sizeof(STDataType) * ps->capacity * 2);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
ps->s = tmp;
ps->capacity *= 2;
}
}
ps->s[ps->Top] = x;
ps->Top++;
}
void StackPop(ST* ps)
{
assert(ps);
assert(ps->Top);
ps->Top--;
}
STDataType StackTop(ST* ps)
{
assert(ps);
assert(ps->Top);
return ps->s[ps->Top - 1];
}
bool StackEmpty(ST* ps)
{
assert(ps);
return !ps->Top;
}
bool isValid(char* s)
{
//初始化一个栈,用于存放字符串s
ST st;
StackInit(&st);
//遍历字符串中所有的字符
int i = 0;
while (s[i] != '\0')
{
switch (s[i])
{
//如果是这三个左括号,则入栈
case '(':
case '{':
case '[':
{
StackPush(&st, s[i]);
break;
}
//如果是这三个右括号,则取出栈顶的元素与之进行匹配
//如果匹配成功,则继续;如果匹配不成功,则返回false
case ')':
case '}':
case ']':
{
if (StackEmpty(&st))
{
StackDestory(&st);
return false;
}
char Top = StackTop(&st);
StackPop(&st);
//不匹配
if ((Top == '(' && s[i] != ')')
|| (Top == '{' && s[i] != '}')
|| (Top == '[' && s[i] != ']'))
{
StackDestory(&st);
return false;
}
}
}
i++;
}
//实际上,只有左括号可以入栈,如果有匹配的右括号,则出栈
//所以,当栈为空时,说明s中所有的字符都匹配
bool ret = StackEmpty(&st);
StackDestory(&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(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
示例:
输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False
提示:
1 <= x <= 9
最多调用100 次 push、pop、top 和 empty
每次调用 pop 和 top 都保证栈不为空
进阶:你能否仅用一个队列来实现栈。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/implement-stack-using-queues
思路:
定义两个队列:
Queue1 | 1 | 2 | 3 | 4 |
Queue2 |
队列的特点是:先入先出 栈的特点是:后入先出
所以我们需要将队尾的元素依次取出
1.将Queue1中除队尾所有的元素依次入队至Queue2中
Queue1 | 4 | |||
Queue2 | 1 | 2 | 3 |
2.将Queue1中的元素出队
Queue1 | ||||
Queue2 | 1 | 2 | 3 |
从此循环,直至所有元素出队
这就模拟了入栈和出栈的模式,基本实现了栈的功能
初始化
MyStack* myStackCreate()
{
MyStack* ps = (MyStack*)malloc(sizeof(MyStack));
QueueInit(ps->Queue1);
QueueInit(ps->Queue2);
if (ps == NULL)
{
printf("malloc fail\n");
exit(-1);
}
return ps;
}
入栈
按照上述思路,应该往有数据的队列中入数据
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)
{
//先假设q1为空,q2不为空
QNode* emptyq = &obj->q1;
QNode* nonemptyq = &obj->q2;
//找到不为空的队列
if (!QueueEmpty(&obj->q1))
{
emptyq = &obj->q2;
nonemptyq = &obj->q1;
}
//倒数据
while (QueueSize(nonemptyq) > 1)
{
QueuePush(emptyq,QueueFront(nonemptyq));
QueuePop(nonemptyq);
}
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);
QueueDestory(&obj->q1);
QueueDestory(&obj->q2);
free(obj);
}