目录
1、栈:
1.1、栈的概念及结构:
![](https://img-blog.csdnimg.cn/1d75143a3fb74f7f8b15a363c1a4b429.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6ISx57yw55qE6YeO6am044CB,size_20,color_FFFFFF,t_70,g_se,x_16)
例题1:
1.2、栈的实现:
![](https://img-blog.csdnimg.cn/f3a7ed3e3d4b458e8a05b5515562fead.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6ISx57yw55qE6YeO6am044CB,size_20,color_FFFFFF,t_70,g_se,x_16)
若使用顺序表来实现栈的话是可以的,实现栈的功能就相当于是顺序表的尾插和尾删,不需要挪动数据,如果真要说出一个缺点,那就是当空间
不够使用的时候,要进行扩容才可以、
![](https://img-blog.csdnimg.cn/a3979020be55444bbe3487d29b047a8b.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6ISx57yw55qE6YeO6am044CB,size_20,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/74e5ae92890c410f83fc2d893811ca6b.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6ISx57yw55qE6YeO6am044CB,size_20,color_FFFFFF,t_70,g_se,x_16)
总结:
对于栈的实现,选择上面的结构都是可以的,若是选择最优的,则首先排除双向带头循环链表,是因为,能够使用单链表时,最好不要使用双链
表,其次排除单链表而选择顺序表,是因为,顺序表的CPU高速缓冲命中率较高,而链表的CPU高速缓冲命中率较低,其次是因为,虽然顺序表
会存在增容的问题,但是,并不是每次插入数据都需要进行增容,只是偶尔需要增容,只要增容完毕后,插入数据的速度会比单链表更快,只需
要把数据放进去即可,而单链表在插入数据的时候还需要申请节点,申请节点也需要一定的代价,所以顺序表可以在一定程度上弥补增容的代
价,所以综合下来,选用顺序表最合适,当能够选择顺序表时,就不使用链表、
1.2.1、test.c源文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
//在数据结构中,都要通过调用函数来实现对应的功能、
int main()
{
//结构体变量,在这最好不进行初始化,后面通过调用函数来实现相应的功能、
ST st;
//初始化栈、
StackInit(&st);
//入栈、
StackPush(&st, 1);
//出栈,取出栈顶元素进行打印、
printf("%d ", StackTop(&st));
StackPop(&st);
//入栈、
StackPush(&st, 2);
//出栈,取出栈顶元素进行打印、
printf("%d ", StackTop(&st));
StackPop(&st);
//入栈、
StackPush(&st, 3);
StackPush(&st, 4);
//打印栈时不可再写print函数,否则打印出来的是1,2,3,4,不符合 先进后出 的规则、
//栈不为空时进入while循环、
//逻辑取反、
while (!StackEmpty(&st))
{
//出栈,取出栈顶元素进行打印、
printf("%d ", StackTop(&st));
StackPop(&st);
}
printf("\n");
//销毁栈、
StackDestory(&st);
return 0;
}
1.2.2、Stack.c源文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
//初始化、
void StackInit(ST* ps)
{
//此处如果传值调用的话,形参是实参的一份临时拷贝,操作的就是形参中的结构体,而不能改变实参中的结构体,所以采用传址调用、
assert(ps);
ps->a = NULL;
ps->top = 0;
//在数组栈/顺序表栈中,top的初始化的值不同,则表示的意义不同、
//若top初始化为0,则表示的是top指向了栈顶元素的下一个元素,该方法是先向top所指的位置放数据,然后top再++、
//若top初始化为-1,则表示的是top指向了栈顶元素,该方法是top先++,然后再向top所指的位置放数据、
//在此我们选择前者,即将top初始化为0这种方法、
ps->capacity = 0;
//在之前写顺序表时,单独把扩容写成一个函数,是因为有尾插,头插,任意位置插,这些都需要先判断一下是否扩容,所以封装成一个函数比较方便,
//但是对于栈这个数据结构而言,只有一个尾插,所以不需要单独把扩容封装成一个函数,直接写到尾插,即入栈调用函数里面即可、
}
//销毁栈、
void StackDestory(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->top = 0;
}
//入栈、
void StackPush(ST* ps, STDataType x)
{
assert(ps);
//判断是否需要扩容、
if (ps->top == ps->capacity)
{
//需要扩容、
size_t newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newCapacity*sizeof(STDataType));
if (tmp == NULL)
{
//由于 ps->a 初始化的结果是NULL,再使用realloc函数的话,相当于是动态开辟内存空间,而不是增容、
//增容失败、
printf("realloc fail\n");
return;
}
else
{
//增容成功
ps->a = tmp;
ps->capacity = newCapacity;
}
}
ps->a[ps->top] = x;
ps->top++;
//ps->a[ps->top++] = x;
}
//出栈、
void StackPop(ST* ps)
{
assert(ps);
//确保栈中还存在数据,若不存在数据则不能再进行出栈操作、
//确保栈不为空、
assert(ps->top > 0);
ps->top--;
}
//判断是否栈为空、
//如果为空返回非零结果,如果不为空返回0、
bool StackEmpty(ST* ps)
{
assert(ps);
方法一:
//if (ps->top > 0)
//{
// //栈不为空、
// return false;
//}
//else
//{
// //栈为空、
// return true;
//}
//方法二:
return ps->top == 0;
//若栈为空,则上面为真,VS默认返回1,表示栈为空,若栈不为空,则上面为假,VS返回0,表示栈不为空、
}
//访问栈顶的数据、
STDataType StackTop(ST* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->a[(ps->top) - 1];
}
//记录栈内数据的个数、
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
1.2.3、Stack.h头文件:
#pragma once //防止头文件被重复包含、
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
//实现数组(顺序表)栈、
//静态、
//下面是定长的静态栈的结构,实际中一般不实用,所以我们主要实现下面的支持动态增长的栈、
//#define N 100
//typedef int STDataType;
//typedef struct Stack
//{
// STDataType a[N];
// int top;//记录栈顶的位置、
//}Stack;
//动态、
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);
//判断是否栈为空、
bool StackEmpty(ST* ps);
//访问栈顶的数据、
STDataType StackTop(ST* ps);
//记录栈内数据的个数、
int StackSize(ST* ps);
1.3、例题1:
//动态、
typedef char 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);
//判断是否栈为空、
bool StackEmpty(ST* ps);
//访问栈顶的数据、
STDataType StackTop(ST* ps);
//记录栈内数据的个数、
int StackSize(ST* ps);
//初始化、
void StackInit(ST* ps)
{
//此处如果传值调用的话,形参是实参的一份临时拷贝,操作的就是形参中的结构体,而不能改变实参中的结构体,所以采用传址调用、
assert(ps);
ps->a = NULL;
ps->top = 0;
//在数组栈/顺序表栈中,top的初始化的值不同,则表示的意义不同、
//若top初始化为0,则表示的是top指向了栈顶元素的下一个元素,该方法是先向top所指的位置放数据,然后top再++、
//若top初始化为-1,则表示的是top指向了栈顶元素,该方法是top先++,然后再向top所指的位置放数据、
//在此我们选择前者,即将top初始化为0这种方法、
ps->capacity = 0;
//在之前写顺序表时,单独把扩容写成一个函数,是因为有尾插,头插,任意位置插,这些都需要先判断一下是否扩容,所以封装成一个函数比较方便,
//但是对于栈这个数据结构而言,只有一个尾插,所以不需要单独把扩容封装成一个函数,直接写到尾插,即入栈调用函数里面即可、
}
//销毁栈、
void StackDestory(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->top = 0;
}
//入栈、
void StackPush(ST* ps, STDataType x)
{
assert(ps);
//判断是否需要扩容、
if (ps->top == ps->capacity)
{
//需要扩容、
size_t newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newCapacity*sizeof(STDataType));
if (tmp == NULL)
{
//由于 ps->a 初始化的结果是NULL,再使用realloc函数的话,相当于是动态开辟内存空间,而不是增容、
//增容失败、
printf("realloc fail\n");
return;
}
else
{
//增容成功
ps->a = tmp;
ps->capacity = newCapacity;
}
}
ps->a[ps->top] = x;
ps->top++;
//ps->a[ps->top++] = x;
}
//出栈、
void StackPop(ST* ps)
{
assert(ps);
//确保栈中还存在数据,若不存在数据则不能再进行出栈操作、
//确保栈不为空、
assert(ps->top > 0);
ps->top--;
}
//判断是否栈为空、
//如果为空返回非零结果,如果不为空返回0、
bool StackEmpty(ST* ps)
{
assert(ps);
方法一:
//if (ps->top > 0)
//{
// //栈不为空、
// return false;
//}
//else
//{
// //栈为空、
// return true;
//}
//方法二:
return ps->top == 0;
//若栈为空,则上面为真,VS默认返回1,表示栈为空,若栈不为空,则上面为假,VS返回0,表示栈不为空、
}
//访问栈顶的数据、
STDataType StackTop(ST* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->a[(ps->top) - 1];
}
//记录栈内数据的个数、
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
//直接使用数组的话,逻辑不太清晰,如果题目不告诉字符串的长度,数组就不知道开辟多大的空间,所以要使用顺序表,即使用栈即可、
bool isValid(char * s)
{
//定义栈、
ST st;
//初始化、
StackInit(&st);
//遍历字符串、
//字符串的结束标志是\0、
while(*s)
{
if(*s == '(' || *s == '[' || *s=='{')
{
//遇到左括号则让该左括号入栈、
StackPush(&st,*s);
s++;
}
else
{
//若栈为空,则StackEmpty返回值为1,真,进入if语句,直接返回false,若栈不为空,则该函数返回0,不进入if语句,继续往下执行,则下面的StackTop函数中断言就不会报错了、
if(StackEmpty(&st))
{
//如果栈为空,则说明if语句就没进去,那么在遍历字符串时遇到某一个右括号时,在该右括号之前,字符串中就一定没有与之对应的左括号,而若想使得某一种类型的左右括号能够按照顺序对应起来的话,则必须保证这种类型的左括号一定要在该种类型的右括号的前面才是可以的,否则这种类型的括号一定对应不起来,即该种类型的括号只有右括号而没有左括号,则该种类型的左右括号一定是不匹配的,所以,这样的话,字符串一定不是有效字符串,则直接返回false即可、
//销毁栈,否则会出现内存泄漏,但是内存泄漏一般是检查不出来的,所以不销毁也可以通过,最好要销毁一下、
StackDestory(&st);
return false;
}
//由于字符串中只有左括号和右括号,上面已经把所有的左括号包括,所以到此处的数据则为右括号、
//遇到右括号则让栈内中最后一个入栈的元素即栈顶元素出栈,与该右括号进行比较、
//定义char类型的变量top来存储栈顶元素、
char top=StackTop(&st);
//出栈、
StackPop(&st);
//让拿出来的元素与该右括号进行匹配、
if((*s==')' && top!='(') || (*s==']' && top!='[') || (*s=='}' && top!='{'))
{
//不匹配、
//无效字符串、
//销毁栈,否则会出现内存泄漏,但是内存泄漏一般是检查不出来的,所以不销毁也可以通过,最好要销毁一下、
StackDestory(&st);
return false;
}
else
{
//匹配,则继续遍历、
s++;
}
}
}
//假设字符串中只有左括号(时,进入while循环,入栈,s++,第二次不进入while循环,直接到此,但并不能说明是有效字符串,此时只需要判断一下栈是否为空即可、
//若栈为空,则说明所有的左括号都已经匹配了,则全部匹配,字符串有效,若栈不为空,则为无效字符串、
bool ret=StackEmpty(&st);
//若栈为空,则ret=1,直接返回ret,又因为isValid返回类型为bool,返回1的话则为真,说明是有效字符串、
//若栈不为空,则ret=0,则直接返回ret,又因为isValid返回类型为bool,返回0的话则为假,说明是无效字符串、
//销毁栈,否则会出现内存泄漏,但是内存泄漏一般是检查不出来的,所以不销毁也可以通过,最好要销毁一下、
StackDestory(&st);
return ret;
//当字符串中只有 ] 时,会报错说StackTop中的断言ps->top > 0 出现了问题,这是因为,当我们取栈顶的元素时,要保证栈内不为空,即ps->top>0,,
//但是当字符串中只有 ] 时,进入while循环,但不进入if语句,然后执行StackTop函数,但此时栈内都没有元素,所以ps->top等于0,所以断言过不去,就会报错,如果在StackTop中不断言ps->top > 0的话,该函数中的return时就会越界,越界更加麻烦,所以在while循环中的else中再添加条件才可、
}
2、队列:
2.1、队列的概念及结构:
![](https://img-blog.csdnimg.cn/fe6f84f32a93438faf45c99eaaae28dd.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6ISx57yw55qE6YeO6am044CB,size_20,color_FFFFFF,t_70,g_se,x_16)
2.2、队列的实现:
![](https://img-blog.csdnimg.cn/38ccd4d73f4c421ab2010b2cf915b112.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6ISx57yw55qE6YeO6am044CB,size_20,color_FFFFFF,t_70,g_se,x_16)
2.2.1、test.c函数:
#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"
int main()
{
//定义结构体变量q、
Queue q;
//初始化、
QueueInit(&q);
//入队、
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
//打印与出队、
printf("%d ", QueueFront(&q));
QueuePop(&q);
printf("\n");
//判断队列是否为空、
bool ret = QueueEmpty(&q);
if (ret == 1)
{
//空队列、
printf("队列为空\n");
}
else
{
//非空队列、
printf("队列不为空\n");
}
//计算队列的长度、
printf("队列长度为:%d\n", QueueSize(&q));
//取出队头的数据、
printf("队头的数据为: %d\n",QueueFront(&q));
//取出队尾的数据、
printf("队尾的数据为: %d\n", QueueBack(&q));
QueuePush(&q, 4);
QueuePush(&q, 5);
//打印与出队、
while (!QueueEmpty(&q))
{
printf("%d ", QueueFront(&q));
QueuePop(&q);
}
printf("\n");
//判断队列是否为空、
ret=QueueEmpty(&q);
if (ret == 1)
{
//空队列、
printf("队列为空\n");
}
else
{
//非空队列、
printf("队列不为空\n");
}
//计算队列的长度、
printf("队列长度为:%d\n", QueueSize(&q));
//销毁队列、
QueueDestory(&q);
printf("销毁成功\n");
return 0;
}
2.2.2、Queue.c函数:
#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"
//初始化两个指针变量,不是初始化节点、
//对于单链表而言,节点不需要进行初始化、
void QueueInit(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
pq->head = NULL;
pq->tail = NULL;
}
//销毁队列、
void QueueDestory(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
QNode* cur = pq->head;
while (cur)
{
//记录指针变量cur所指节点的下一个节点的地址、
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
}
//入队(尾插)、
void QueuePush(Queue* pq, QDataType x)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
//创建新节点,只有在入队时需要开辟新的节点,所以不需要把该过程封装成一个函数、
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
//动态开辟失败、
printf("malloc fail\n");
return;
}
else
{
//动态开辟成功、
newnode->data = x;
newnode->next = NULL;
}
//判断不带头单向不循环链表,即这里的队列是否为空、
//if (pq->tail == NULL)
if (pq->head == NULL)
{
//队列中不存在节点,即队列为空、
//进一步保证两个指针变量都为空指针NULL、
assert(pq->tail == NULL);
pq->head = newnode;
pq->tail = newnode;
}
else
{
//队列中存在节点,即队列不为空、
pq->tail->next = newnode;
pq->tail = newnode;
}
}
//出队(头删)、
void QueuePop(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
//当队列为空时,就不能再出队了、
assert(pq->head && pq->tail);
//判断队列中是否只有一个节点、
if (pq->head->next == NULL)
{
//队列中只有一个节点、
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
//队列中有多个节点、
//记录头指针所指节点的下一个节点的地址、
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
}
//判断队列是否为空、
bool QueueEmpty(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
//return pq->tail == NULL;
return pq->head == NULL;
}
//计算队列的长度、
size_t QueueSize(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
//方法一:
size_t size = 0;
QNode* cur = pq->head;
while (cur)
{
size++;
cur = cur->next;
}
return size;
//方法二:
//在结构体Queue成员变量中增加变量size_t size , 初始化时给成0,入队则++,出队则--,到最后直接在该调用函数内返回size即可,不需要再进行计算了,如果使用该方法
//那么在该函数内部时间复杂度就是:O(1)、
//不可以使用指针减指针的方法,是因为对于链表而言,物理结构是不连续的,所以不可以使用该方法、
}
//取出队头的数据、
QDataType QueueFront(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
assert(pq->head);//在保证队列不是空队列的情况下,再去取队头的数据、
return pq->head->data;
}
//取出队尾的数据、
QDataType QueueBack(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
assert(pq->tail);//在保证队列不是空队列的情况下,再去取队尾的数据、
return pq->tail->data;
}
2.2.3、Queue.h函数:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef int QDataType;
typedef struct QueueNode
{
QDataType data;
struct QueueNode* next; //不可以使用QNode* next来代替该行代码,这个重定义只有在12行以下才生效、
}QNode;
//在此由于带不带哨兵位的头节点影响不大,所以选择不带头的单链表,即不带头单向不循环链表、
//
//typedef struct QueueNode
//{
// QDataType data;
// struct QueueNode* next;
//}*QNode; //此时,QNode是struct QueueNode* 的重定义、
//为了方便,将 头指针和尾指针 写入一个结构体中、
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);
//判断队列是否为空、
bool QueueEmpty(Queue* pq);
//计算队列的长度、
size_t QueueSize(Queue* pq);
//取出队头的数据、
QDataType QueueFront(Queue* pq);
//取出队尾的数据、
QDataType QueueBack(Queue* pq);
2.3、例题2:
![](https://img-blog.csdnimg.cn/5e1e96007b534e9cab1ef61f35c8e5cb.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6ISx57yw55qE6YeO6am044CB,size_20,color_FFFFFF,t_70,g_se,x_16)
/*解题思路:
此题可以用两个队列去实现一个栈,每次始终保持一个队列为空,
入栈操作相当于给非空队列进行入队操作
出栈操作相当于非空队列的队尾元素出队,此时需要把非空队列除最后一个元素之外的其余元素入队到空队列,然后出队最后一个队尾元素*/
typedef int QDataType;
typedef struct QueueNode
{
QDataType data;
struct QueueNode* next; //不可以使用QNode* next来代替该行代码,这个重定义只有在12行以下才生效、
}QNode;
//在此由于带不带哨兵位的头节点影响不大,所以选择不带头的单链表,即不带头单向不循环链表、
//
//typedef struct QueueNode
//{
// QDataType data;
// struct QueueNode* next;
//}*QNode; //此时,QNode是struct QueueNode* 的重定义、
//为了方便,将 头指针和尾指针 写入一个结构体中、
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);
//判断队列是否为空、
bool QueueEmpty(Queue* pq);
//计算队列的长度、
size_t QueueSize(Queue* pq);
//取出队头的数据、
QDataType QueueFront(Queue* pq);
//取出队尾的数据、
QDataType QueueBack(Queue* pq);
//初始化两个指针变量,不是初始化节点、
//对于单链表而言,节点不需要进行初始化、
void QueueInit(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
pq->head = NULL;
pq->tail = NULL;
}
//销毁队列、
void QueueDestory(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
QNode* cur = pq->head;
while (cur)
{
//记录指针变量cur所指节点的下一个节点的地址、
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
}
//入队(尾插)、
void QueuePush(Queue* pq, QDataType x)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
//创建新节点,只有在入队时需要开辟新的节点,所以不需要把该过程封装成一个函数、
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
//动态开辟失败、
printf("malloc fail\n");
return;
}
else
{
//动态开辟成功、
newnode->data = x;
newnode->next = NULL;
}
//判断不带头单向不循环链表,即这里的队列是否为空、
//if (pq->tail == NULL)
if (pq->head == NULL)
{
//队列中不存在节点,即队列为空、
//进一步保证两个指针变量都为空指针NULL、
assert(pq->tail == NULL);
pq->head = newnode;
pq->tail = newnode;
}
else
{
//队列中存在节点,即队列不为空、
pq->tail->next = newnode;
pq->tail = newnode;
}
}
//出队(头删)、
void QueuePop(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
//当队列为空时,就不能再出队了、
assert(pq->head && pq->tail);
//判断队列中是否只有一个节点、
if (pq->head->next == NULL)
{
//队列中只有一个节点、
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
//队列中有多个节点、
//记录头指针所指节点的下一个节点的地址、
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
}
//判断队列是否为空、
bool QueueEmpty(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
//return pq->tail == NULL;
return pq->head == NULL;
}
//计算队列的长度、
size_t QueueSize(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
//方法一:
size_t size = 0;
QNode* cur = pq->head;
while (cur)
{
size++;
cur = cur->next;
}
return size;
//方法二:
//在结构体Queue成员变量中增加变量size_t size , 初始化时给成0,入队则++,出队则--,到最后直接在该调用函数内返回size即可,不需要再进行计算了,如果使用该方法
//那么在该函数内部时间复杂度就是:O(1)、
//不可以使用指针减指针的方法,是因为对于链表而言,物理结构是不连续的,所以不可以使用该方法、
}
//取出队头的数据、
QDataType QueueFront(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
assert(pq->head);//在保证队列不是空队列的情况下,再去取队头的数据、
return pq->head->data;
}
//取出队尾的数据、
QDataType QueueBack(Queue* pq)
{
//队列可以为空,即head和tail可以为空指针,但是管理两个指针的结构体变量的地址不能为空指针、
assert(pq);
//如果只把结构体变量传过来的话,即传值调用,形参是实参的一份临时拷贝,改变形参不会改变实参,要想通过调用函数来改变实参结构体中的内容,则需要把结构体变量的地址
//传过来,即传址调用、
assert(pq->tail);//在保证队列不是空队列的情况下,再去取队尾的数据、
return pq->tail->data;
}
//定义一个存储队列q1和q2的结构体、
typedef struct
{
Queue q1;
Queue q2;
} MyStack;
//结构构造和初始化、
//gcc是编译c的,g++是编译C++的、
MyStack* myStackCreate()
{
/*//定义一个结构体变量、
MyStack st;
//初始化、
//.....
//.....
return &st;
//到此返回出来的就是存放两个队列的结构体的地址,下面的调用函数则要通过该地址找到该结构体中的队列q1和q2,但是由于结构体变量st是局部变量,当出了生命周期即调用函数内部就会销毁,所以把该结构体变量的地址返回出去,则该地址就是野指针,这样就没办法再通过该指针找到该结构体中的两个队列了,现在要返回一个指针,并且可以通过该指针找到它所指的结构体,即该指针指向的结构体变量还没有被销毁,所以可以在堆区上进行开辟,这样他的生命周期是整个工程,出了这个调用函数不会被销毁,也可以写成static MyStack st,让局部变量st变为静态变量,而静态变量的生命周期也是整个工程,所以上述两种方法均可,在此选择前者*/
MyStack* pst=(MyStack*)malloc(sizeof(MyStack));
assert(pst);
//初始化、
QueueInit(&(pst->q1));
QueueInit(&(pst->q2));
return pst;
}
//一级指针pst传参,一级指针obj接收、
void myStackPush(MyStack* obj, int x)
{
//存放队列p1和p2的结构体变量的地址不可能为空指针NULL;
assert(obj);
//当两个队列都为空队列时,入栈就相当于随机往某一个队列中入队数据即可,当两个队列中有一个队列是非空队列时,入栈就相当于是往非空队列中入队数据,只要栈内还存在数据,则队列q1和q2一定一个为空,一个不为空,若栈内无数据,比如没有数据入栈或者是入栈的所有数据均已出栈时,则q1和q2均为空队列、
//不知道队列q1和q2到底那个为空队列,则要进行判断、
//->d的优先级高于&、
//逻辑取反、
if(!QueueEmpty(&obj->q1))
{
//队列q1为非空队列、
QueuePush(&obj->q1,x);
}
else
{
//队列q1为空,q2可能为空,也可能不为空、
//当栈内还存在数据时,q2一定不是空队列,所以要往队列q2中入队数据,若是数据未入栈或者入栈的数据均已出栈时,即栈内不存在数据时,q2则是空队列,由于这种情况时,随机选一个队列即可,则默认在q2中入队数据,所以不管何种情况,在else中直接往q2里放数据即可、
QueuePush(&obj->q2,x);
}
}
//一级指针pst传参,一级指针obj接收、
int myStackPop(MyStack* obj)
{
//存放队列p1和p2的结构体变量的地址不可能为空指针NULL;
assert(obj);
//该调用函数用来出栈数据,所以要考虑栈内是否还存在数据、
//1、栈内存在数据、
//由于栈内存在数据,所以只考虑队列q1和q2一个为空队列,一个为非空队列即可、
//假设队列q1为空队列,q2位非空队列、
Queue* empty=&obj->q1;
Queue* noempty=&obj->q2;
if(!QueueEmpty(&obj->q1))
{
//队列p1为非空队列,则p2一定为空队列、
empty=&obj->q2;
noempty=&obj->q1;
}
//到此就不用考虑到底是p1还是p2为空或者非空队列,只需要往空队列中 倒数据 即可、
//把非空队列中的 部分数据 倒进空队列中、
//把非空队列中前N-1个数据倒入空队列,再把剩下的一个删除,就实现了后进先出、
while(QueueSize(noempty) > 1)
{
QueuePush(empty,QueueFront(noempty));
QueuePop(noempty);
}
//返回该myStackPop调用函数要删除的那个数据、
//即noempty队列中的剩余的最后一个数据、
QDataType top=QueueFront(noempty);
//QDataType top=QueueBack(noempty);
QueuePop(noempty);
return top;
//2、栈内无数据、
//当栈内无数据时,q1和q2均为空队列,不进入if语句,也不进入while循环,当调用函数QueueFront时,由于noempty所指的队列是空队列,所以会进行报错,这是因为在QueueFront函数内部进行了断言、
}
//一级指针pst传参,一级指针obj接收、
//返回栈顶元素、
int myStackTop(MyStack* obj)
{
//存放队列p1和p2的结构体变量的地址不可能为空指针NULL;
assert(obj);
//此过程不需要倒数据,直接取非空队列中队尾的数据即可,该数据就是栈顶元素、
//由于队列只能在队头删数据,队尾插数据,所以不可以删除此处队尾的数据,但是可以取它的data值、
//不知道队列q1和q2到底那个为空队列,则要进行判断、
//->d的优先级高于&、
//逻辑取反、
if(!QueueEmpty(&obj->q1))
{
//队列q1为非空队列、
return QueueBack(&obj->q1);
}
else
{
//队列q1为空,q2可能为空,也可能不为空、
//当q2为非空队列时,直接取其队尾数据,若q2为空队列的话,不能够取其队尾的数据,在调用函数QueueBack中就会报错、
return QueueBack(&obj->q2);
}
}
//一级指针pst传参,一级指针obj接收、
//bool型本质上就是int整型、
bool myStackEmpty(MyStack* obj)
{
//存放队列p1和p2的结构体变量的地址不可能为空指针NULL;
assert(obj);
//当栈为空时,两个队列一定都为空,当栈不为空时,两个队列中,一个为空,一个不为空,不会出现两个队列都不为空的情况、
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}
//一级指针pst传参,一级指针obj接收、
void myStackFree(MyStack* obj)
{
//存放队列p1和p2的结构体变量的地址不可能为空指针NULL;
assert(obj);
//此时指针变量obj指向了存储队列q1和q2的结构体,该结构体中存储了队列q1和q2,而队列q1和q2中分别都有一个头指针head和尾指针tail,当栈内无数据时,则两个队列都是空队列,所以队列q1和q2中的头指针head和尾指针tail均指向空指针NULL,所以这两个队列中都没有要进行释放的空间,若栈内存在数据,则队列q1和q2中,一个为空队列,一个为非空队列,那么在此种情况下,空队列中的头指针head和尾指针tail均指向空指针NULL,所以对于空队列而言没有要进行释放的空间,但是非空队列中的头指针head和尾指针tail不指向NULL,所以此时要把动态开辟的节点释放掉,不会出现两个队列都是非空队列的情况,所谓释放空间只是用来释放动态开辟的内存空间,在本题中只有队列中的节点和存储队列的结构体是动态开辟的,所以他们要进行释放,其他的都不需要进行释放,如果在此直接free(obj)的话,是不可以的,这样只能把存储队列的结构体释放掉,而不能把队列中动态开辟的节点一起释放掉,free的功能只是把参数中指针指向的空间释放掉,所以不可以直接对obj进行释放,要在free(obj)之前把两个队列动态开辟的内存空间先进行释放,再对obj进行释放,由于不清楚队列q1和q2到底那个是非空队列,可能两个队列都为空队列,则都没有动态开辟节点,所以都不需要释放,也有可能q1和q2中一个为空队列,一个为非空队列,对于空队列而言,不需要进行释放,对于非空队列而言,要对其动态开辟的节点进行释放,不会出现两个队列都是非空队列的情况,那么按理说要进行判断,得出那个是非空队列,然后再进行释放,但是这样就比较麻烦,所以在此就不进行判断,对两个队列都进行释放,即对两个队列都进行销毁操作,然后再进行free(obj),从内往外依次释放空间,如果先对obj进行free的话,在VS编译器下就会把该空间中的内容置为随机值,就找不到队列中的节点了,即,那么就没办法再通过obj找到结构体成员变量中的q1和q2了,所以要从内往外依次进行释放、
QueueDestory(&obj->q1);
QueueDestory(&obj->q2);
free(obj);
obj=NULL;
//此处的obj是形参,实参是pst,属于传值调用,在此对obj进行置空并不影响外面的pst,所以若是自己实现的话,要在调用函数外面手动把pst置空,此处最好也要在调用函数内部把obj置空、
}
/**
* Your MyStack struct will be instantiated and called as such:
* MyStack* obj = myStackCreate();
* myStackPush(obj, x);
* int param_2 = myStackPop(obj);
* int param_3 = myStackTop(obj);
* bool param_4 = myStackEmpty(obj);
* myStackFree(obj);
*/
2.4、例题3:
力扣,用栈实现队列、
思路:
定义两个栈,分别为栈1,栈2,要想通过两个栈实现队列,则队列入队就相当于是往栈内入数据,即入栈,当两个栈内都无数据时,即两个栈为
空栈时,就可以任选其中一个栈往里入栈,即入数据,比如要入队数据1,2,3,4,即往其中一个栈内假设往栈1中入数据1,2,3,4,现在要实现一个
队列,而队列是先进先出,所以当队列出队时,出队顺序应该也是1,2,3,4,即从1开始出队,但是由于栈的性质是先进后出,所以在栈1内的数据
1,2,3,4,只能从数据4开始出栈,现在就要把该栈1内存储的所有数据倒进栈2中,由于栈是先进后出的性质,所以把数据倒至栈2内时,顺序会发
生改变,所以在栈2内倒完之后的数据的顺序则为:4,3, 2,1,此时对于队列而言,出队即相当于把栈2内所有元素直接出栈即可,当栈2中出完第
一个数据1,再出第二个数据2时,不需要再把数据倒进栈1中,这是因为,当两个队列进行倒数据时,顺序是不变的,但是当两个栈进行倒数据
时,顺序要发生改变,并且是倒序的,所以此时当把栈1内的所有的数据倒进栈2内时,在栈2内的数据的顺序若再出栈2,正好符合队列先进先出
的性质,对于队列而言,在还没有入队数据时,那么此时的两个栈一定都是空栈,现在要想入队数据,就可以任选一个栈往里入栈数据,假设往
栈1内入栈数据,那么就假设栈1为push栈,则栈2为pop栈,栈1,即push栈只用来入栈数据,而栈2,即pop栈只用来出栈数据,当数据1,2,3,4
都出栈完毕后,若再要想入队数据,则直接往push栈内入栈数据即可,比如入栈数据为:5,6,7,8,9,现在把这些数据倒进栈2,即pop栈内,倒完
之后顺序则为:9,8,7,6,5,,要注意,从push栈内往pop栈内倒数据的前提是,当pop栈内的所有数据均已出栈完毕后,即当pop栈为空栈时,再
往里倒数据,则在pop栈内的数据顺序为:9,8,7,6,5,然后直接将pop栈内的所有数据直接出栈即可、
/*
解题思路:
此题可以用两个栈实现,一个栈进行入队操作,另一个栈进行出队操作
出队操作: 当出队的栈不为空是,直接进行出栈操作,如果为空,需要把入队的栈元素全部导入到出队的栈,然后再进行出栈操作
*/
//实现数组(顺序表)栈、
//静态、
//下面是定长的静态栈的结构,实际中一般不实用,所以我们主要实现下面的支持动态增长的栈、
//#define N 100
//typedef int STDataType;
//typedef struct Stack
//{
// STDataType a[N];
// int top;//记录栈顶的位置、
//}Stack;
//动态、
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);
//判断是否栈为空、
bool StackEmpty(ST* ps);
//访问栈顶的数据、
STDataType StackTop(ST* ps);
//记录栈内数据的个数、
int StackSize(ST* ps);
//初始化、
void StackInit(ST* ps)
{
//此处如果传值调用的话,形参是实参的一份临时拷贝,操作的就是形参中的结构体,而不能改变实参中的结构体,所以采用传址调用、
assert(ps);
ps->a = NULL;
ps->top = 0;
//在数组栈/顺序表栈中,top的初始化的值不同,则表示的意义不同、
//若top初始化为0,则表示的是top指向了栈顶元素的下一个元素,该方法是先向top所指的位置放数据,然后top再++、
//若top初始化为-1,则表示的是top指向了栈顶元素,该方法是top先++,然后再向top所指的位置放数据、
//在此我们选择前者,即将top初始化为0这种方法、
ps->capacity = 0;
//在之前写顺序表时,单独把扩容写成一个函数,是因为有尾插,头插,任意位置插,这些都需要先判断一下是否扩容,所以封装成一个函数比较方便,
//但是对于栈这个数据结构而言,只有一个尾插,所以不需要单独把扩容封装成一个函数,直接写到尾插,即入栈调用函数里面即可、
}
//销毁栈、
void StackDestory(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->top = 0;
}
//入栈、
void StackPush(ST* ps, STDataType x)
{
assert(ps);
//判断是否需要扩容、
if (ps->top == ps->capacity)
{
//需要扩容、
size_t newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newCapacity*sizeof(STDataType));
if (tmp == NULL)
{
//由于 ps->a 初始化的结果是NULL,再使用realloc函数的话,相当于是动态开辟内存空间,而不是增容、
//增容失败、
printf("realloc fail\n");
return;
}
else
{
//增容成功
ps->a = tmp;
ps->capacity = newCapacity;
}
}
ps->a[ps->top] = x;
ps->top++;
//ps->a[ps->top++] = x;
}
//出栈、
void StackPop(ST* ps)
{
assert(ps);
//确保栈中还存在数据,若不存在数据则不能再进行出栈操作、
//确保栈不为空、
assert(ps->top > 0);
ps->top--;
}
//判断是否栈为空、
//如果为空返回非零结果,如果不为空返回0、
bool StackEmpty(ST* ps)
{
assert(ps);
方法一:
//if (ps->top > 0)
//{
// //栈不为空、
// return false;
//}
//else
//{
// //栈为空、
// return true;
//}
//方法二:
return ps->top == 0;
//若栈为空,则上面为真,VS默认返回1,表示栈为空,若栈不为空,则上面为假,VS返回0,表示栈不为空、
}
//访问栈顶的数据、
STDataType StackTop(ST* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->a[(ps->top) - 1];
}
//记录栈内数据的个数、
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
//定义一个存储 栈pushST和popST 的结构体、
typedef struct
{
ST pushST;
ST popST;
} MyQueue;
//结构构造和初始化、
//gcc是编译c的,g++是编译C++的、
MyQueue* myQueueCreate()
{
/*//定义一个结构体变量、
ST st;
//初始化、
//.....
//.....
return &st;
//到此返回出来的就是存放两个栈的结构体的地址,下面的调用函数则要通过该地址找到该结构体中的栈pushST和popST,但是由于结构体变量st是局部变量,当出了生命周期即调用函数内部就会销毁,所以把该结构体变量的地址返回出去,则该地址就是野指针,这样就没办法再通过该指针找到该结构体中的两个栈了,现在要返回一个指针,并且可以通过该指针找到它所指的结构体,即该指针指向的结构体变量还没有被销毁,所以可以在堆区上进行开辟,这样他的生命周期是整个工程,出了这个调用函数不会被销毁,也可以写成static ST st,让局部变量st变为静态变量,而静态变量的生命周期也是整个工程,所以上述两种方法均可,在此选择前者*/
MyQueue* q=(MyQueue*)malloc(sizeof(MyQueue));
assert(q);
//初始化、
//直接调取栈的函数接口,不要直接去操作栈,通过调用不同的接口来实现不同的功能,这就叫做隔离,减少耦合,当使用函数接口时,不需要关心栈的底层是如何实现的,即,不管栈的底层是使用顺序表实现还是链表实现的,如果只调用接口的话,就不需要去考虑这些内容,现在的栈是通过顺序表来实现的,假设改为使用链表实现,则下面使用函数接口的地方的代码都不受影响、
StackInit(&q->pushST);
StackInit(&q->popST);
return q;
}
//一级指针q传参,一级指针obj接收、
//入队数据、
void myQueuePush(MyQueue* obj, int x)
{
//存放栈pushST和popST的结构体变量的地址不可能为空指针NULL;
assert(obj);
//此时是使用两个栈来实现先入先出的队列,对于队列而言,当队列还未进行入队数据时,即当队列为空队列时,那么此时的两个栈一定都是空栈,若该队列要进行入队数据,由于此时的两个栈都是空栈,则可以任选一个栈往里入栈数据,就把该要进行入栈数据的栈记作pushST,另外一个栈则记作是popST,这是与上一题的做法是不同的,在本题中,pushST栈被确定下来,则另外一个栈即为popST栈,一旦pushST和popST被确定下来之后,就不再发生改变了,而在第一次入队数据时,由于两个栈都为空栈,所以把两者任意一个栈记作pushST栈即可,另外一个栈记作popST栈,所以本题在创建MyQueue结构体类型的结构体时,直接在该结构体中定义pushST栈和popST栈,接下来凡是入栈数据则直接入栈到pushST栈中,凡是出栈数据,则直接出栈popST中的数据即可、
//不管popST栈中是否存在数据,都可以直接往pushST栈中入栈数据、
StackPush(&obj->pushST,x);
}
//一级指针q传参,一级指针obj接收、
//出队数据、
int myQueuePop(MyQueue* obj)
{
/*//方法一:
//存放栈pushST和popST的结构体变量的地址不可能为空指针NULL;
assert(obj);
//判断popST栈中是否存在数据、
if(StackEmpty(&obj->popST))
{
//popST栈内无数据,则要把pushST栈内中的数据倒过来、
while(!StackEmpty(&obj->pushST))
{
StackPush(&obj->popST,StackTop(&obj->pushST));
StackPop(&obj->pushST);
}
}
//在此处不可以加else与上面的if进行对应,这是因为,当调用myQueuePop函数时,若popST栈内存在数据,则不进入if语句,直接出栈这些数据即可,当popST栈内不存在数据时,此时要先进入if语句,把pushST栈中的数据全部倒进popST栈内,出if语句说明已经完成该动作,但是这只是把pushST中的全部数据倒进popST栈内,还没有进行出队操作,所以出了if语句之后还要进行一次popST数据出栈的操作,若加上else,则会缺少这一步、
//popST栈内存在数据,这些数据可能是原来popST栈内原有的数据,也有可能是刚从pushST栈中倒过来的数据,不管怎么来的,只要popST栈内存在数据,则直接出栈这些数据即可、
//由于题目要求从队列的开头移除并返回元素,还要把队列开头的数据返回,但是return返回的这个过程必须在该调用函数最后,而在返回之前还要把该数据进行出栈,一旦出栈就不知道该数据的内容了,所以要先进行保存,然后出栈该数据,最后把保存的数据返回即可、
STDataType top=StackTop(&obj->popST);
StackPop(&obj->popST);
return top;*/
//方法二:
//存放栈pushST和popST的结构体变量的地址不可能为空指针NULL;
assert(obj);
//该调用函数要求从队列的开头移除并返回该元素,由于在该调用函数中要把return放在最后一步,而在返回之前还要把该数据进行出栈,一旦出栈就不知道该数据的内容了,所以要先进行保存,然后出栈该数据,最后把保存的数据返回即可、
//此时,保存队列开头的数据,就相当于是保存popST栈中的栈顶元素,,可以调用myQueuePeek函数,该调用函数的返回值就是队列的开头的元素,保存即可、
//保存调用函数myQueuePeek的返回值放到变量top中、
STDataType top=myQueuePeek(obj);//一级指针传参,一级指针接收、
//移除队列的开头数据就等价于让popST栈中的栈顶元素出栈、
StackPop(&obj->popST);
return top;
//若要进行复用,不能在myQueuePeek函数内部调用myQueuePop函数,这是因为,myQueuePeek函数只要求返回队列开头的元素,并未要求移除该元素,若要这样的话,一旦在myQueuePeek函数内部调用了myQueuePop函数,myQueuePop函数内部就会把队列开头的数据移除掉,与题意不符、
//若要在myQueuePop函数内部调用myQueuePeek函数,由于myQueuePop函数在myQueuePeek函数的前面,则必须要在myQueuePop函数的前面对myQueuePeek函数进行声明或者定义,由于这是接口型OJ题,在后台可能就已经把这些队列要实现的接口函数进行了声明,所以这里就算不在myQueuePop前面对myQueuePeek进行声明,也没有报错,但是如果自己实现的话,不能忽略这一步,否则一定会报错的、
}
//一级指针q传参,一级指针obj接收、
//返回队列开头的元素、
//栈只能获取栈顶的数据,不能获取栈底的数据、
int myQueuePeek(MyQueue* obj)
{
//存放栈pushST和popST的结构体变量的地址不可能为空指针NULL;
assert(obj);
//判断popST栈中是否存在数据、
if(StackEmpty(&obj->popST))
{
//popST栈内无数据,则要把pushST栈内中的数据倒过来、
while(!StackEmpty(&obj->pushST))
{
StackPush(&obj->popST,StackTop(&obj->pushST));
StackPop(&obj->pushST);
}
}
//在此处不可以加else与上面的if进行对应,这是因为,当调用myQueuePeek函数时,若popST栈内存在数据,则不进入if语句,直接获取栈顶的数据即可,当popST栈内不存在数据时,此时要先进入if语句,把pushST栈中的数据全部倒进popST栈内,出if语句说明已经完成该动作,但是这只是把pushST中的全部数据倒进popST栈内,还没有进行获取popST栈顶数据的操作,所以出了if语句之后还要进行一次获取popST栈顶数据的操作,若加上else,则会缺少这一步、
STDataType top=StackTop(&obj->popST);
return top;
//popST栈内存在数据,这些数据可能是原来popST栈内原有的数据,也有可能是刚从pushST栈中倒过来的数据,不管怎么来的,只要popST栈内存在数据,则直接获取popST栈内栈顶数据即可、
}
//一级指针q传参,一级指针obj接收、
bool myQueueEmpty(MyQueue* obj) {
//存放栈pushST和popST的结构体变量的地址不可能为空指针NULL;
assert(obj);
//bool型本质上就是int整型、
//当队列为空时,即还未向队列中入队数据或者是队列中的数据均已出队时,两个栈一定都为空,当队列不为空时,则两个栈中至少有一个栈是非空的,,两个栈要么其中一个栈存在数据,另外一个不存在数据,要么两个栈都存在数据,但是至少有一个栈是非空的、
//则两个栈只有全部为空栈时,队列此为空队列,两个栈只要有一个栈不是空栈,则队列就不为空队列、
return StackEmpty(&obj->pushST) && StackEmpty(&obj->popST);
}
//一级指针q传参,一级指针obj接收、
void myQueueFree(MyQueue* obj)
{
//存放栈pushST和popST的结构体变量的地址不可能为空指针NULL;
assert(obj);
//从 内往外 依次进行空间释放,避免出现内存泄漏、
//从 内往外 依次进行空间释放,避免出现内存泄漏、
//若直接先free(obj)的话,那么就没办法再通过obj找到结构体成员变量中的pushST和popST了,所以要从内往外依次free、
//对于栈pushST和popST而言,可能会因为没进行入栈操作从而没进行动态开辟内存空间,但是在此时不要再进行判断了,比较麻烦,直接都对这两个栈进行销毁即可、
StackDestory(&obj->pushST);
StackDestory(&obj->popST);
free(obj);
obj=NULL;
//此处的obj是形参,实参是q,属于 传值 调用,在此对obj进行置空并不影响外面的q,所以若是自己实现的话,要在调用函数外面手动把q置空,此处最好也要在调用函数内部把obj置空、
}
/**
* Your MyQueue struct will be instantiated and called as such:
* MyQueue* obj = myQueueCreate();
* myQueuePush(obj, x);
* int param_2 = myQueuePop(obj);
* int param_3 = myQueuePeek(obj);
* bool param_4 = myQueueEmpty(obj);
* myQueueFree(obj);
*/
2.5、例题4:
![](https://img-blog.csdnimg.cn/da4b16c8de5a428b9e481a159585be93.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6ISx57yw55qE6YeO6am044CB,size_20,color_FFFFFF,t_70,g_se,x_16)
循环队列也符合先进先出,环形队列的大小是固定的, 即设置队列的长度是K,是固定值、
循环队列的底层实现有两种实现方式:
1、循环链表实现,可以使用单向不带头循环链表来实现,当循环队列已经满了,若再插入,则就会失败,没有足够的空间,当删除队列数据
时,即出队数据时,不要释放节点的空间,直接让front++即可,不需要抹去原来front所指向的节点中的值、
2、顺序表实现,所谓循环队列,即指当下标 tail 出了顺序表所动态开辟的内存空间后,即越界时,就赋值 tail 为0,这样就回到了队列的头,起
到了循环的作用,逻辑上就是一个环形,当删除一个数据时,即循环队列出队一个数据时,顺序表中的数据不进行挪动,只需要让front++即可,
则就代表队列中出队了一个数据,此时,原先front所指的位置中的数据并没有被删除掉,所以对于顺序表而言,仍是连续存放的,当再入队数据
时,若tail指向了front原来指向的位置,则直接将新的数据覆盖原来的数据即可,这是在有空间的前提下进行的,即,当循环队列满时,是无法进
行入队数据的,但如果再进行了出队数据的操作,front就会改变了位置,但是原front所在的位置中的值并没有被抹去,所以此时再入队数据是可
以的,有空间,则入队的新数据就会覆盖原来front所指的位置上面的数据、
如果按照题目要求的话,即设置队列的长度是K,对于顺序表而言,则是直接动态开辟K个空间,对于链表而言,也是直接动态开辟K个空间,无
论使用哪种方式来实现循环队列,当队列进行出队数据时,只需要让front++即可,此时,由于没有抹去front原来所指向的值,对于顺序表而言也
是连续存放的,这两种方法都在最后的时候,再把动态开辟的内存空间释放掉即可、
无论是使用何种方法来实现循环队列,则循环队列为空的标准都是front==tail,,由于是循环队列,所以当front==tail时,会有两种可能,即,当
tail==front时,可能是空队列,也可能是队列满了,会存在歧义,所以对于循环队列而言,题目要求队列长度为K,即队列能够存储K个数据,那
么不管何种方法,都开辟K+1个空间,顺序表是直接开辟K+1个空间,而链表也是直接开辟K+1个空间,这K+1个空间中,有一个空间永远不存储
数据,空出来的那个空间的位置是随机的,动态的,永远是tail接近front的那一个空间,但他的位置是随机动态变化的,就可以解决了,当tail
== front时代表循环队列为空队列,当tail+1 == front时或者是tail->next=front时,则循环队列已满,tail指向队列中最后一个数据的下一个位置、
如果使用链表实现的话,方便判空,判满,不用判尾,直接循环,要想满足当tail == front时代表循环队列为空队列,当tail+1 == front时或者是
tail->next=front时,则循环队列已满,则必须保证tail所指的为循环队列中最后一个数据的下一个数据才可以,其次就是若使用链表来实现循环队
列的话,也不方便找队列中最后一个数据,是因为单向链表不能反向寻找,不可以让tail指向循环队列中的最后一个数据,否则,如果假设循环队
列中不存在数据,那么tail就不知指向哪里了,所以,也让指针变量tail指向循环队列中最后一个数据的后一个数据,就和使用顺序表来实现循环
队列一样,当tail == front时,循环队列为空队列,此时,K+1个空间已经开辟成功,只需要往里放数据,即循环队列入队数据即可,在最初时,
tail和front均指向链表的头节点,循环队列入队一个数据,则tail=tail->next,这样才可以,或者可以写成双向链表,或者再加一个指针去记录循环
队列中的最后一个数据,但是这比较麻烦,所以在此直接选顺序表即可,就可以避免这种上面的麻烦、
typedef struct
{
//示例中入队的数据都是int类型,在此直接定义为int类型即可、
int* a;
//下标的类型都为int类型、
int front;
int tail;
//队列长度为 k 为int类型、
int k;
} MyCircularQueue;
//若不加13,14行代码,则编译出错说不认识下面这两个函数,就表明在该OJ接口外并没有对这两个函数进行声明,所以在此声明一下即可,但是必须要保证在11行代码之后进行声明,是因为MyCircularQueue要在11行代码之后才起作用、
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
bool myCircularQueueIsFull(MyCircularQueue* obj);
//由于 myCircularQueueEnQueue,myCircularQueueDeQueue,myCircularQueueFront,myCircularQueueRear这些函数均调用了13,14行代码中的两个函数,所以这两个函数的声明必须放在前4个调用函数的定义之前,还要在11行代码之后才可以、
//结构构造和初始化、
MyCircularQueue* myCircularQueueCreate(int k)
{
MyCircularQueue* obj=( MyCircularQueue*)malloc(sizeof( MyCircularQueue));
assert(obj);
//在此处只能先把构造的结构体的地址得出来,放在指针变量obj中,然后通过该地址再找到结构体中的指针变量a,然后再动态开辟指针变量a所指向的空间,在此不能够先动态开辟指针变量a所指向的空间,是因为如果不先找到构造的结构体的地址,是没有办法找到指针变量a的,其次就是,已经知道队列的长度是一个固定值,为什么不在构造结构体时就直接开辟出数组(静态顺序表)呢,是因为,虽然队列的长度是固定值K,但是每次执行时,K的值可以改变,不固定,如果直接开辟出数组要保证数组元素个数为常数个,要么就直接给数组元素个数一个确定的数,要么就#define定义符号,但是定义的符号的值也是不确定的,虽然牛客和力扣都支持变长数组,但是在结构体成员变量内部定义数组时,是不可以使用变长数组的,没有这种用法,又发现只有当调用myCircularQueueCreate函数时,才知道K的大小,所以只能在该调用函数内部动态开辟顺序表、
//初始化、
//开辟K+1个空间,空出来的那个空间的位置是随机的、
obj->a=(int*)malloc(sizeof(int)*(k+1));
assert(obj->a);
obj->front=obj->tail=0;
obj->k=k;
return obj;
}
//向循环队列插入一个元素,如果成功插入则返回真、
//由于队列长度是固定的,所以当循环队列满了,再入队数据时,是不行的,即插入失败、
//一级指针obj传参,一级指针obj接收、
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
//构造的结构体的地址不可能为空指针NULL;
assert(obj);
//判断循环队列是否已满、
//在此处调用了myCircularQueueIsFull函数,由于是接口型OJ题,可能在外面已经把循环队列对应的函数接口都声明在最上面了,所以这里就算不声明,也不会报错、
if(myCircularQueueIsFull(obj))
{
//循环队列已满,则插入失败、
return false;
}
else
{
//循环队列未满,则插入成功、
//方法一:
/*obj->a[obj->tail]=value;
//判断下标tail是否要再从0开始、
//若使用链表来实现循环队列的话,在此就不需要进行判断了、
if(obj->tail == obj->k)
{
obj->tail=0;
}
else{
obj->tail++;
}
return true;*/
//方法二:
obj->a[obj->tail]=value;
obj->tail++;
obj->tail %= (obj->k+1);
return true;
}
//对于外层if,else语句而言,不进if语句,则一定进else语句,执行逻辑和语法逻辑也是这样,即满足if就进if,不满足if一定进else,语法逻辑就是如此,但是如果两个if语句的话,即使执行逻辑一定进入两个if语句,但是语法逻辑的话可能会不进入两个if语句,若在调用函数最后不写return的话,会报错,但是对于if,else语句来说,语法逻辑就保证了肯定进入两者之一,所以调用函数内部不写return也是可以的、
}
//一级指针obj传参,一级指针obj接收、
//从循环队列中删除一个元素,如果成功删除则返回真、
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
//构造的结构体的地址不可能为空指针NULL;
assert(obj);
//当循环队列为空队列时,就不能再进行删除元素,即出队操作了、
if(myCircularQueueIsEmpty(obj))
{
//循环队列为空队列,删除失败、
return false;
}
else
{
//循环队列不为空队列,删除成功、
//方法一:
/*//判断下标front是否要再从0开始、
//若使用链表来实现循环队列的话,在此就不需要进行判断了、
if(obj->front == obj->k)
{
obj->front=0;
}
else
{
obj->front++;
}
return true;*/
//方法二:
obj->front++;
obj->front %= (obj->k+1);
return true;
}
}
//一级指针obj传参,一级指针obj接收、
//从队首获取元素,如果队列为空,返回 -1 、
int myCircularQueueFront(MyCircularQueue* obj)
{
//构造的结构体的地址不可能为空指针NULL;
assert(obj);
//判断循环队列是否为空、
if(myCircularQueueIsEmpty(obj))
{
//循环队列为空队列、
return -1;
}
else
{
//循环队列为非空队列、
//当循环队列不为空队列时,front所指的数据永远是队头数据、
return obj->a[obj->front];
}
}
//一级指针obj传参,一级指针obj接收、
//获取队尾元素,如果队列为空,返回 -1 、
int myCircularQueueRear(MyCircularQueue* obj) {
//构造的结构体的地址不可能为空指针NULL;
assert(obj);
//判断循环队列是否为空、
if(myCircularQueueIsEmpty(obj))
{
//循环队列为空队列、
return -1;
}
else
{
//循环队列为非空队列、
//判断tail的值是否为0,当tail的值为0时,该位置的前一个位置下标为-1,在通过下标访问的时候就是越界访问,所以要判断、
if(obj->tail == 0)
{
return obj->a[obj->k];
}
else
{
//->的优先级高于-
return obj->a[obj->tail-1];
}
}
}
//一级指针obj传参,一级指针obj接收、
//检查循环队列是否为空、
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
//构造的结构体的地址不可能为空指针NULL;
assert(obj);
//任何情况下,obj->front == obj->tail时,则循环队列为空队列、
return obj->front == obj->tail;
}
//一级指针obj传参,一级指针obj接收、
//检查循环队列是否已满、
bool myCircularQueueIsFull(MyCircularQueue* obj) {
//构造的结构体的地址不可能为空指针NULL;
assert(obj);
//下面两种情况时,循环队列均已满、
if(obj->tail==obj->k && obj->front==0)
{
return true;
}
else
{
return obj->tail+1 == obj->front;
}
}
//一级指针obj传参,一级指针obj接收、
void myCircularQueueFree(MyCircularQueue* obj) {
//构造的结构体的地址不可能为空指针NULL;
assert(obj);
//从内往外依次free,若直接先free(obj)的话,那么就没办法再通过obj找到结构体成员变量中的指针变量a,所以要先对obj->a进行free,然后再对obj进行释放、
free(obj->a);
obj->a=NULL;
//此处由于obj是形参,所以通过该形参找到的指针变量a并把其置为空指针NULL,并不影响实参中的指针变量a,所以要在调用函数外面手动把结构体成员变量中的指针变量a置空,此处最好也要在调用函数内部把obj->a置空、
free(obj);
obj=NULL;
//此处的obj是形参,属于 传值 调用,在此对obj进行置空并不影响外面,所以若是自己实现的话,要在调用函数外面手动置空,此处最好也要在调用函数内部把obj置空、
}
/**
* Your MyCircularQueue struct will be instantiated and called as such:
* MyCircularQueue* obj = myCircularQueueCreate(k);
* bool param_1 = myCircularQueueEnQueue(obj, value);
* bool param_2 = myCircularQueueDeQueue(obj);
* int param_3 = myCircularQueueFront(obj);
* int param_4 = myCircularQueueRear(obj);
* bool param_5 = myCircularQueueIsEmpty(obj);
* bool param_6 = myCircularQueueIsFull(obj);
* myCircularQueueFree(obj);
*/
2.6、例题5:
![](https://img-blog.csdnimg.cn/ed2f2c2f2d81428cbb83bc3192992cee.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6ISx57yw55qE6YeO6am044CB,size_20,color_FFFFFF,t_70,g_se,x_16)
关于栈和队列的知识点已经全部结束,谢谢大家!