栈和队列都常用的数据结构,这里使用C语言实现栈和队列,
栈:是一种特殊的线性表,其只允许在一端出数据和入数据,插入数据和删除数据的一端加栈顶,另一端叫栈底。其数据遵守先进后出的原则(Last in First out)。
因为这个特点那么如果进入一段数据是 1 2 3 4 5,那么其出数据就是反的 5 4 3 2 1。
压栈(push):往栈顶插入数据。
出栈(pop):把数据在栈顶删除。
实现:
typedef int STdatatype;
typedef struct stack
{
STdatatype *arr;
int top;
int capacity;
}stack;
//初始化
void StackInit(stack* ps);
//销毁堆栈
void StackDestory(stack* ps);
//压栈
void StackPush(stack* ps, STdatatype x);
//出栈
void StackPop(stack* ps);
//取栈顶的值
STdatatype StackTop(stack* ps);
//判断栈是否为空
bool StackIsEmpty(stack* ps);
//栈的长度
int StackSize(stack* ps);
void StackInit(stack* ps)
{
assert(ps);
ps->arr = NULL;
ps->capacity = 0;
ps->top = 0;
}
void StackDestory(stack* ps)
{
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->capacity = 0;
ps->top = 0;
}
void StackPush(stack* ps, STdatatype x)
{
assert(ps);
if (ps->capacity == ps->top){
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STdatatype* p = (STdatatype*)realloc(ps->arr, sizeof(STdatatype)* newCapacity);
if (p==NULL)
{
perror("申请内存失败\n");
exit(0);
}
ps->capacity = newCapacity;
ps->arr = p;
}
ps->arr[ps->top] = x;
ps->top++;
}
void StackPop(stack* ps)
{
assert(ps);
assert(!StackIsEmpty(ps));
ps->top--;
}
STdatatype StackTop(stack* ps)
{
assert(ps);
assert(!StackIsEmpty(ps));
return ps->arr[ps->top-1];
}
bool StackIsEmpty(stack* ps)
{
assert(ps);
return ps->top == 0;
}
int StackSize(stack* ps)
{
assert(ps);
assert(!StackIsEmpty(ps));
return ps->top;
}
这里使用了数组来模拟实现栈的特点。
测试:
PS:栈是不能被遍历的,要访问下一个数据,必须把现在的数据弹出栈顶。
PS:但是,还是要注意,并不是遍历的时候,出数据就只能是5 4 3 2 1,如果这里一边访问一边出数据,数据还是顺序的。
举例:
因为其只能在队头出数据,所以进入一段数据 1 2 3 4 5,那么出数据的顺序也该是 1 2 3 4 5。
实现:
typedef int Qdatatype;
struct QueNode
{
Qdatatype data;
struct QueNode* next;
};
struct Queue
{
QueNode* head;
QueNode* tail;
};
//初始化
void QueueInit(Queue* st);
//销毁队列
void QueueDestroy(Queue* st);
//插入数据 这里不用二级指针,因为传入的是结构体Queue,而不是头节点。
void QueuePush(Queue* st, Qdatatype x);
//删除数据
void QueuePop(Queue* st);
//获得队头的数据
Qdatatype QueueTop(Queue* st);
//获得队尾的数据
Qdatatype QueueBack(Queue* st);
//检查队列是否为空
bool QueueIsEmpty(Queue* st);
//队列长度
int QueueSize(Queue* st);
PS:这里求队列长度也可以在结构体中定义一个变量来记录,每次插入一个数据记录一下。
struct QueNode
{
Qdatatype data;
struct QueNode* next;
int size;
};
void QueueInit(Queue* st)
{
st->head = NULL;
st->tail = NULL;
}
void QueueDestroy(Queue* st)
{
assert(st);
QueNode* cur = st->head;
while (cur) {
QueNode* p = cur;
cur = cur->next;
free(p);
}
st->head = NULL;
st->tail = NULL;
}
void QueuePush(Queue* st, Qdatatype x)
{
assert(st);
QueNode* node = (QueNode*)malloc(sizeof(QueNode));
node->data = x;
node->next = NULL;
//队列只能往尾节点插入数据,直接让节点连接NULL,后面就不用置空了
if (st->head == NULL){ //如果头节点为空,把节点当做头节点
st->head = node;
st->tail = node;
}
else { //头节点不为空,连接在尾结点后面
st->tail->next = node;
st->tail = node;
}
}
//队列出数据只能从头部出去
void QueuePop(Queue* st)
{
assert(st);
assert(!QueueIsEmpty(st));
if (st->head == st->tail) {
free(st->head);
st->head = NULL;
st->tail = NULL;
}
else {
QueNode* p = st->head;
st->head = st->head->next;
free(p);
}
}
Qdatatype QueueTop(Queue* st)
{
assert(st);
assert(!QueueIsEmpty(st));
return st->head->data;
}
Qdatatype QueueBack(Queue* st)
{
assert(st);
assert(!QueueIsEmpty(st));
return st->tail->data;
}
bool QueueIsEmpty(Queue* st)
{
assert(st);
return st->head == NULL;
}
int QueueSize(Queue* st)
{
assert(st);
QueNode* cur = st->head;
int size = 0;
while (cur) {
size++;
cur = cur->next;
}
return size;
}
因为其队列的特点,这里定义了一个头节点和尾结点,这样每次在后面插入数据的时候就不用遍历链表了。
这里使用的链表来模拟队列的特点。
测试:
PS:注意队列也不能遍历。
1:使用栈实现队列
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(
push
、top
、pop
和empty
)。
- void push(int x) 将元素 x 压入栈顶。
- int pop() 移除并返回栈顶元素。
- int top() 返回栈顶元素。
- boolean empty() 如果栈是空的,返回 true ;否则,返回 false
PS:这里就使用上面已经写好的接口来实现。
typedef struct {
stack pushst;
stack popst;
} MyQueue;
bool myQueueEmpty(MyQueue* obj) {
assert(obj);
//如果两个栈都是空的,那么队列就是空的
return StackIsEmpty(&obj->pushst) && StackIsEmpty(&obj->popst);
}
MyQueue* myQueueCreate() {
MyQueue* obj=(MyQueue*)malloc(sizeof(MyQueue));
StackInit(&obj->pushst);
StackInit(&obj->popst);
return obj;
}
void myQueuePush(MyQueue* obj, int x) {
assert(obj);
StackPush(&obj->pushst,x);
}
int myQueuePop(MyQueue* obj) {
assert(obj);
assert(!myQueueEmpty(obj));
//如果pop栈是空
if(StackIsEmpty(&obj->popst)){
//把push栈全部放到pop栈里面
while(!StackIsEmpty(&obj->pushst)){
StackPush(&obj->popst,StackTop(&obj->pushst));
StackPop(&obj->pushst);
}
}
int temp=StackTop(&obj->popst);
StackPop(&obj->popst);
return temp;
}
int myQueuePeek(MyQueue* obj) {
assert(obj);
assert(!myQueueEmpty(obj));
if(StackIsEmpty(&obj->popst)){
while(!StackIsEmpty(&obj->pushst)){
StackPush(&obj->popst,StackTop(&obj->pushst));
StackPop(&obj->pushst);
}
}
return StackTop(&obj->popst);
}
void myQueueFree(MyQueue* obj) {
assert(obj);
StackDestory(&obj->pushst);
StackDestory(&obj->popst);
free(obj);
obj=NULL;
}
思路:
使用两个栈结构,一个pushst,一个popst, 往队列里面插入数据的时候把数据插入到pushst里面,而删除数据的时候弹出popst里面的数据。
而popst里面的数据就是pushst里面的数据倒过来的,因为以栈的特点,倒过来的数据刚好的反的,而这时候pop掉popst里面的数据,就相当于删除原来数据的头部数据。
题目链接:https://leetcode.cn/problems/implement-queue-using-stacks/
2:使用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
- void push(int x) 将元素 x 压入栈顶。
- int pop() 移除并返回栈顶元素。
- int top() 返回栈顶元素。
- boolean empty() 如果栈是空的,返回 true ;否则,返回 false
typedef struct {
Queue q1;
Queue q2;
} MyStack;
bool myStackEmpty(MyStack* obj) {
assert(obj);
return QueueIsEmpty(&obj->q1) && QueueIsEmpty(&obj->q2);
}
MyStack* myStackCreate() {
MyStack* node=(MyStack*)malloc(sizeof(MyStack));
QueueInit(&node->q1);
QueueInit(&node->q2);
return node;
}
void myStackPush(MyStack* obj, int x) {
assert(obj);
if(!QueueIsEmpty(&obj->q1)){
QueuePush(&obj->q1,x);
}
else{
QueuePush(&obj->q2,x);
}
}
int myStackPop(MyStack* obj) {
assert(obj);
assert(!myStackEmpty(obj));
//判断有数据的队列,和空队列
Queue* emptyQ = &obj->q1;
Queue* nonemptyQ = &obj->q2;
if(!QueueIsEmpty(&obj->q1)){
emptyQ=&obj->q2;
nonemptyQ=&obj->q1;
}
while(QueueSize(nonemptyQ) > 1){
//把非空的队列中的数据,倒入到空的队列中去,留一个
QueuePush(emptyQ,QueueTop(nonemptyQ));
QueuePop(nonemptyQ);
}
int temp=QueueTop(nonemptyQ);
QueuePop(nonemptyQ);
return temp;
}
int myStackTop(MyStack* obj) {
assert(obj);
assert(!myStackEmpty(obj));
if(!QueueIsEmpty(&obj->q1)){
return QueueBack(&obj->q1);
}
else{
return QueueBack(&obj->q2);
}
}
void myStackFree(MyStack* obj) {
assert(obj);
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);
obj=NULL;
}
思路:
使用两个队列结构,一个Q1,一个Q2,插入数据的时候,插入有数据的队列,如果两个队列都为空,随便插入一个空的队列,但是一定要有一个队列为空。
插入数据也和上面一样倒数据,但是因为其队列的特点,倒完数据其还是顺序摆放,但是栈删除数据,只能在栈顶删除。那么其实可以倒数据的时候,留下队尾的数据,全部倒完后再删除队尾的数据,就相当于删除了原数据的尾部。
题目链接:https://leetcode.cn/problems/implement-stack-using-queues/
循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
typedef struct {
int *a;
int k;
int head;
int tail;
} MyCircularQueue;
bool myCircularQueueIsFull(MyCircularQueue* obj) {
int t = obj->tail+1;
t %= obj->k+1;
if(t == obj->head){
return true;
}
return false;
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->head == obj->tail;
}
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* p=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
p->a=(int*)malloc(sizeof(int)*(k+1));
p->head=0;
p->tail=0;
p->k=k;
return p;
}
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if(myCircularQueueIsFull(obj))
return false;
obj->a[obj->tail]=value;
obj->tail++;
obj->tail %= obj->k+1;
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return false;
obj->head++;
obj->head %= obj->k+1;
return true;
}
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
return obj->a[obj->head];
}
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
int t=obj->tail-1 + obj->k+1;
t %= obj->k+1;
return obj->a[t];
}
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
这里使用了数组来模拟环形队列。
因为是环形的,所以这里队列一开始初始化的时候就要给定固定的长度,在结构体中定义一个变量 k 来记录环形队列的长度。但是要注意这里为了方便下面判断队列是满还是空,要多添加一个虚设空间,所以其队列真实长度为K+1。
head指向队列的头部,tail指向链表的尾部。插入数据的时候往下标为tail的空间插入,然后tail往后走一个(tail固定指向队尾的下一个)。
当队列都插入满数据后,tail会在head的前面。
那么这里就可以判断,如果tail+1==head,那么说明队列是满的,就不能插入数据了。(虚设空间不记录数据)。
删除数据,就让head往后走一个。
PS:这里前面的数据不用删除,后面插入数据的时候,会把数据覆盖掉。
当队列的数据全部删除后,tail和head相遇。
那么这里就可以判断,如果head==tail,那么说明队列为空。
PS:这里正方形内部是记录的数据,外面是数组的下标。
注意:
有几个情况要注意,因为其循环队列是用数组模拟的,所以一些特定的情况要避免。
这里判断队列是否是满的时候,tail+1=5超过了数组下标,而head是0,虽然是满的但是计算不正确。
如果这里tail超出了队列的长度,可以让其模上队列的长度,就可以把tail的变为0。
这里 t=tail+1=5,让 t%=k+1,5%5=0,这里t就等于0,head也是0,t==head,那么就说明队列满了。
而且这里如果tail不等于5,假设等于3,模上数组长度还是其自己,还是3。
PS:注意这里只是做判断,并没有改变tail现在的位置。
同理,这里队列没满,要继续往队尾插入数据,插入后tail往后走一步,这里也tail也会超出下标,那么也可以让其模上长度,tail++;tail%=k+1;其也会走到数组起始位置。
PS:这里插入数据了,要让tail往下走一个,要改变tail。
删除数据时也要注意,如果head在边界位置,也要让其模上长度,head++;head%=k+1;
最后是读取队尾数据的时候,这里队尾是tail的上一个,tail-1变为-1了,这样怎么模呢?
这里其实可以让tail先加上一个k+1,然后再模其长度,t=tail-1+k+1;t%=k+1;非常巧妙的写法。如果tail不在边界,加上长度也没什么影响,因为后面也会被模没了。