本篇适用于学习了C语言基础栈和队列及其相关实现,想进一步提升运用的同学
目录
1.由两个队列实现栈
“开幕雷击”
这是个什么东西?
这便是大家在学习C语言结构体时很容易时忽视的一个知识点:匿名结构体
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
这样的结构体仅仅能使用一次,编译器不会把和他相同结构的结构体认为是同一自定义类型。
具体思路:
比方我们先对栈1压入1 2 3 4,现在要top或者pop,我们需要取出4,由于队列的特性,我们只能先将1 2 3拿出来并压入另一个队列,再对剩下的4做top或者pop处理。
因此:
导数据即可。
实现push,紧接上文思路,再压入依然只能压入有数据的“另一个队列”,这样才能与上文的逻辑相吻合。
void myStackPush(MyStack* obj, int x) {
//谁不为空就传谁
if(!QueueEmpty(&obj->q1)){
QueuePush(&obj->q1,x);
}else{
QueuePush(&obj->q2,x);
}
}
实现pop,为了避免把重复的代码写两次,我们使用假设法。
最终题解:
typedef struct {
Queue q1;
Queue q2;
} MyStack;
MyStack* myStackCreate() {
MyStack* obj=(MyStack*)malloc(sizeof(MyStack));
QueueInit(&obj->q1);
QueueInit(&obj->q2);
return obj;
}
void myStackPush(MyStack* obj, int x) {
//谁不为空就传谁
if(!QueueEmpty(&obj->q1)){
QueuePush(&obj->q1,x);
}else{
QueuePush(&obj->q2,x);
}
}
int myStackPop(MyStack* obj) {
//先导数据,再删除
//使用假设法
Queue* pEmptyQueue=&obj->q1;
Queue* pNonEmptyQueue=&obj->q2;
if(!QueueEmpty(&obj->q1)){
pEmptyQueue=&obj->q2;
pNonEmptyQueue=&obj->q1;
}
//开导
while(pNonEmptyQueue->size>1){
QueuePush(pEmptyQueue,QueueFront(pNonEmptyQueue));
QueuePop(pNonEmptyQueue);
}
int val=QueueFront(pNonEmptyQueue);
QueuePop(pNonEmptyQueue);
return val;
}
int myStackTop(MyStack* obj) {
Queue* pEmptyQueue=&obj->q1;
Queue* pNonEmptyQueue=&obj->q2;
if(!QueueEmpty(&obj->q1)){
pEmptyQueue=&obj->q2;
pNonEmptyQueue=&obj->q1;
}
//开导
while(pNonEmptyQueue->size>1){
QueuePush(pEmptyQueue,QueueFront(pNonEmptyQueue));
QueuePop(pNonEmptyQueue);
}
int val=QueueFront(pNonEmptyQueue);
QueuePush(pEmptyQueue,QueueFront(pNonEmptyQueue));
QueuePop(pNonEmptyQueue);
return val;
}
bool myStackEmpty(MyStack* obj) {
return QueueEmpty(&obj->q1)&&QueueEmpty(&obj->q2);
}
void myStackFree(MyStack* obj) {
free(obj);
}
2.由两个栈实现队列
可以模仿上一题依靠导数据求解,但是没必要刻舟求剑。
先压入1 2 3 4
第一次pop时,只能先将栈1的内容导入栈2
当第一次栈1数据被导入栈2之后,数据的顺序发生了变化,栈2后入先出的数据已经变成了栈1先入后出的数据 (有一种将栈1和栈2连接到一起的感觉)
因此,栈1取名叫pushst 栈2取名叫popst!!!
所以,我们只需要在pop栈为空时进行导数据,其余时候可直接在push进,pop出。
易错!
定义了指针,但是没有开辟指针指向的结构
结构体里只放了两个指针,没有起到任何实质性的作用!!
typedef struct {
Queue q1;
Queue q2;
} MyStack;
MyStack* myStackCreate() {
MyStack* obj=(MyStack*)malloc(sizeof(MyStack));
QueueInit(&obj->q1);
QueueInit(&obj->q2);
return obj;
}
void myStackPush(MyStack* obj, int x) {
//谁不为空就传谁
if(!QueueEmpty(&obj->q1)){
QueuePush(&obj->q1,x);
}else{
QueuePush(&obj->q2,x);
}
}
int myStackPop(MyStack* obj) {
//先导数据,再删除
//使用假设法
Queue* pEmptyQueue=&obj->q1;
Queue* pNonEmptyQueue=&obj->q2;
if(!QueueEmpty(&obj->q1)){
pEmptyQueue=&obj->q2;
pNonEmptyQueue=&obj->q1;
}
//开导
while(pNonEmptyQueue->size>1){
QueuePush(pEmptyQueue,QueueFront(pNonEmptyQueue));
QueuePop(pNonEmptyQueue);
}
int val=QueueFront(pNonEmptyQueue);
QueuePop(pNonEmptyQueue);
return val;
}
int myStackTop(MyStack* obj) {
Queue* pEmptyQueue=&obj->q1;
Queue* pNonEmptyQueue=&obj->q2;
if(!QueueEmpty(&obj->q1)){
pEmptyQueue=&obj->q2;
pNonEmptyQueue=&obj->q1;
}
//开导
while(pNonEmptyQueue->size>1){
QueuePush(pEmptyQueue,QueueFront(pNonEmptyQueue));
QueuePop(pNonEmptyQueue);
}
int val=QueueFront(pNonEmptyQueue);
QueuePush(pEmptyQueue,QueueFront(pNonEmptyQueue));
QueuePop(pNonEmptyQueue);
return val;
}
bool myStackEmpty(MyStack* obj) {
return QueueEmpty(&obj->q1)&&QueueEmpty(&obj->q2);
}
void myStackFree(MyStack* obj) {
free(obj);
}
3.循环队列
一提及队列,我们的第一反应是用链表实现。
先根据需要,进行选型、分析:
依照队列的习惯,我们使用front、rear两个指针分别指向头和尾
初始化时front和rear都指向第一个
进行一次push
rear还指向第一个元素。有一个元素和没有元素时两个指针的指向一样并且相等,这是我们不希望看到的。可以通过增加size变量来控制,但是这样变量更多,更难操作
那是否可以借助栈的思想,无元素时让rear指向空,有元素指向第一个(模仿栈的初始top=-1)?
加满之后(rear->next==front)貌似没有问题,紧接着我们考虑进行top。
每top一个元素,front就向前走一个(队列先进先删),那么当front和rear重合的时候(希望将环形队列删空),又需要单独对删除最后一个数据进行处理。
因此,我们采取“rear指向尾节点下一个”的思想(模仿栈的初始top=0)
为了防止队列为空和队列全满的时候无法区分(均满足条件front==rear,也就是常说的假溢出问题),我们多使用一个节点来存放rear.
再检查我们需要实现的功能,如何取尾?是遍历链表(效率低下)还是造轮子实现双向链表?
显然都不是最优解..........
尽管以上方法都能实现,但是为了减少需要注意的细节和特殊情况的处理,我们选择使用顺序表而非链表,rear指向尾的下一个,这样就能随机访问下标元素了。自然的,front和rear就由指针变成了链表。
小结:
a.链表
front 头
rear 尾下一个——————————————————————》无法区分头尾
b.链表
front 头
rear 尾——————————————————————》删除最后一个要单独操作,并且刚开始时需要将rear指向空。
c.注意了以上所有,依然需要实现双向链表(不是不能做,只是选型的目的就是找最优解),所以我们使用数组
具体实现时,开辟k+1个空间,注意让下标正常++时,每进入新的循环(比如front由k又+1到了0)
利用取模(%(k+1))操作,去掉每一个整周期即可。
所以,每push一次rear都要+1后取模以去掉整周期,每pop一次front都要+1后取模来去掉整周期。
(判断空时不用取模,为空时不管两个下标轮回了多少次都一定相等,毕竟之前该取的时候都取过了)
push操作:
因为每次加入数据之后rear都会取模,所以始终控制在较小的数据上(第一轮正周期之内)
之所以判断是否满的函数还要再单独取模,就是因为可能在rear+1那一下刚好发生如下临界的尴尬情况
取尾数据时的特殊情况:
为了让rear-1可以继续,我们给rear再加一轮(k+1),多走一个周期,保障所有情况rear都可以在变化之后被访问到
全部实现:
typedef struct {
int* a;
int front;
int rear;
int k;
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
obj->k=k;
obj->front=obj->rear=0;
obj->a=(int*)malloc(sizeof(int)*(k+1));
return obj;
}
bool myCircularQueueIsFull(MyCircularQueue* obj);
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if(myCircularQueueIsFull(obj)){
return false;
}
obj->a[obj->rear]=value;
obj->rear=(obj->rear+1)%(obj->k+1);
return true;
}
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj)){
return false;
}
obj->front=(obj->front+1)%(obj->k+1);
return true;
}
int myCircularQueueFront(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
return obj->a[obj->front];
}
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj))
return -1;
return obj->a[(obj->rear+obj->k)%(obj->k+1)];
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->front==obj->rear;
}
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return (obj->rear+1)%(obj->k+1)==obj->front;
}
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
最后也是顺利通过了: