经典栈和队列面试题目解析(C语言)


因为我们使用的C语言做答,所以只能使用自己实现的栈和队列进行操作。

便于书写,插入操作和删除操作都会用push、pop代替
push: 插入操作
pop: 删除操作

如果有需要,这是栈和队列实现的博客链接:栈和队列的基本操作实现实现

这里前面三题都需要把前面栈或者队列的函数接口内容搬过来。我这里只写了解答,就不把栈或者队列的实现搬过来了,要不然内容太过冗余。前面有栈和队列的链接,有想法的可以点开,看一下。

一、括号匹配问题

题目链接:有效括号

需要栈的接口

解题思路

大致思路:

遇到三种左括号的任意一种进行压栈操作。如果字符不是三种左括号的任意一种,则取栈顶元素和该字符进行匹配,并且进行出栈操作。(如果看到这里有到一定的想法,可以自己操作一番)。

画图分析

示例1

第一种满足条件的情况

示例2

示例二的情况

代码

bool isValid(char * s)
{
    Stack st;
    StackInit(&st);

    while (*s)
    {
    	//这一步操作:如果字符是左括号就进栈
        if ((*s == '(') || (*s == '{') || (*s == '['))
        {
            StackPush(&st, *s);
        }
        else
        {
        	//如果字符串第一个字符是右括号,直接返回false
            if (StackEmpty(&st))
            {
            	//这里不能忘了对栈进行销毁操作,以防出现内存泄漏,做题网站是检查不出来内存泄漏的。下面也是一样,在返回的前面要对栈销毁。
                StackDestroy(&st);
                return false;
            }
            //取栈顶元素,并出栈
            STDataType top = StackTop(&st);
            StackPop(&st);

			//如果需要判断匹不匹配建议,反过来写,这样操作步骤更少。如果满足条件了,就会跳过这个语句,开始下一个字符。
            if((*s == ')' && top != '(')
            ||(*s == ']' && top != '[')
            ||(*s == '}' && top != '{'))
            {
                StackDestroy(&st);
                return false;
            }
        }
        //这里不能忘了,s++,因为是字符串,所以要继续向后访问
        s++;
    }

	//判断是否为空,为空满足条件 为真,不为空则为假
    bool ret = StackEmpty(&st);
    StackDestroy(&st);
    return ret;
}

二、用队列实现栈

题目链接:用队列实现栈

											需要队列的接口

解题思路

大致思路:建立两个不为空的队列

push:直接采用入队操作。
top:直接采用取队尾元素。
empty:判断两个队列为空。

主要问题还是pop。
pop:在push操作的时候,插入不为空的队列,如果都为空,随便入一个队列。在pop的时候需要把不为空的队列n-1个倒入为空的队列,然后在对剩下的一个数据进行删除操作即可。

画图分析

这里主要pop分析,队列中需要先有元素,然后才能进行pop

对pop操作画图分析

代码

建议:把判断为空的接口,放在前面,这样易对其他函数进行断言。当然也可以把判断为空的声明放在前面。

typedef struct 
{
	//建立两个队列
    Queue q1;
    Queue q2;
} MyStack;

//返回一个栈的指针
MyStack* myStackCreate() 
{
	//malloc一个栈的指针
    MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
    if (NULL == pst)
    {
        perror("MyStack::malloc");
        return NULL;
    }
    //对栈进行初始化,这里对两个队列初始化即可
    QueueInit(&pst->q1);
    QueueInit(&pst->q2);

	//返回栈的指针
    return pst;
}

bool myStackEmpty(MyStack* obj) 
{
	assert(obj);
    
    //判断是否为空,对两个队列一起判断
    return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}

void myStackPush(MyStack* obj, int x) 
{
	assert(obj);

	//push数据,push到不为空的队列。如果都为空,我这里的写法,会push到第二个队列
    if (!QueueEmpty(&obj->q1))
    {
        QueuePush(&obj->q1, x);
    }
    else
    {
        QueuePush(&obj->q2, x);
    }
}

//重中之重,删除操作
int myStackPop(MyStack* obj) 
{
	assert(obj);
    assert(!myQueueEmpty(obj));
    
	//因为我们也不知道哪一个为空,所以我们假设第一个队列为空,第二个队列不为空
    Queue* empty = &obj->q1;
    Queue* nonempty = &obj->q2;

	//这里判断我们的假设是否成立,如果不成立交换一下,成立直接跳过该if语句
    if (!QueueEmpty(&obj->q1))
    {
        empty = &obj->q2;
        nonempty = &obj->q1;
    }

    //倒数据,倒的还剩下一个
    while (QueueSize(nonempty) > 1)
    {
    	//空的导入一个,不为空的删除一个
        QueuePush(empty, QueueFront(nonempty));
        QueuePop(nonempty);
    }

	//题目要求移除并返回数据
    int top = QueueFront(nonempty);
    
    //这里就可以把最后一个数据删除了,模仿栈的后进先出的性质,
    QueuePop(nonempty);

     return top;

}

//取栈顶元素
int myStackTop(MyStack* obj) 
{
	assert(obj);
	
	//想象一下,不就是取队尾元素,这里也就显现出来在实现队列的时候,为什么还要取队尾元素了
    if (!QueueEmpty(&obj->q1))
    {
        return QueueBack(&obj->q1);
    }
    else
    {
        return QueueBack(&obj->q2);
    }
}


void myStackFree(MyStack* obj) 
{
	assert(obj);

	//直接对两个队列销毁就可以
    QueueDestroy(&obj->q1);
    QueueDestroy(&obj->q2);
    
    //obj也是malloc所以这里也要释放
    free(obj);
}

三、用栈实现队列

题目链接:用栈实现队列

											需要栈的接口

解题思路

大致思路:建立一个专门插入的栈和一个专门删除的栈。

push:直接在专门插入的栈插入即可
empty:判断两个栈为空
pop和peek相似

主要问题完成peek
在进行删除操作的时候,如果专门删除的栈为空,那么把专门插入的栈中的数据倒入到专门删除的栈。这是专门删除的栈的栈顶就是队尾。注意:每次只能专门删除的栈为空的时候才能再次进行数据的倒入。

画图分析

栈中需要先push元素,然后才能进行pop
对pop操作画图分析

代码

建议:把判断为空的接口,放在前面,这样易对其他函数进行断言。当然把判断为空的声明放在前面也可以。

//这是匿名结构体类型
typedef struct 
{
	//建立两个栈,一个专门push,一个专门pop
    Stack pushS;
    Stack popS;

} MyQueue;


MyQueue* myQueueCreate() 
{
    MyQueue* pq = (MyQueue*)malloc(sizeof(MyQueue));
    if(NULL == pq)
    {
        perror("Create::pq");
        return NULL;
    }

	//对该队列初始化,只需要初始化两个栈即可
    StackInit(&pq->pushS);
    StackInit(&pq->popS);

    return pq;
}


//  判断队列是否为空
bool myQueueEmpty(MyQueue* obj) 
{
	assert(obj);
	//两个栈都为空,则队列为空
    return StackEmpty(&obj->pushS) && StackEmpty(&obj->popS);
}


//  入队操作
void myQueuePush(MyQueue* obj, int x) 
{
	assert(obj);
	//直接无脑的向专门push的栈push
    StackPush(&obj->pushS, x);
}

//  取队头元素
int myQueuePeek(MyQueue* obj) 
{
	assert(obj);
    assert(!myQueueEmpty(obj));
    
	//这里需要判断popS是否为空,如果为空要从pushS倒入数据
    if(StackEmpty(&obj->popS))
    {
    	//倒入数据,在倒入数据的过程中,把pushS中所有的数据全部倒入到popS中,并且每向popS倒入一个数据pushS栈就要删除一个数据
        while (!StackEmpty(&obj->pushS))
        {
            StackPush(&obj->popS,StackTop(&obj->pushS));
            StackPop(&obj->pushS);
        }
    }

	//对头数据就是popS的栈顶数据
    return StackTop(&obj->popS);
}

//  将队列开头的元素移除,并返回该元素。因为pop和peek实现相似,所以这里借用一下
int myQueuePop(MyQueue* obj) 
{
	assert(obj);
    assert(!myQueueEmpty(obj));

	//保存之后,再进行释放
    int front = myQueuePeek(obj);
    StackPop(&obj->popS);
    return front;
}

//  销毁队列
void myQueueFree(MyQueue* obj) 
{
	assert(obj);
   
	//因为队列中就两个栈,所以销毁栈就可以了
    StackDestroy(&obj->pushS);
    StackDestroy(&obj->popS);

	//因为obj是malloc的所以也要释放,不然存在内存泄漏
    free(obj);
}

四、设计循环队列

因为是对循环队列(环形队列)的实现,所以这里的写法就和上面三个问题的形式有所区别

循环队列介绍

循环队列就是将队列存储空间的最后一个位置绕到第一个位置,形成逻辑上的环状空间,循环队列的大小也是固定的。

题目链接:设计循环队列

思路

大致思路:使用数组实现(也可以使用链表),主要解决判断队列中为满的情况。使用数组实现循环队列更简单一些,取模就行。

解决为满的情况:

  1. 多开辟一个空间。例如题目要求队列长度为k,则开辟k+1个空间。

数组形式的循环队列

  1. 也可以在结构体中维护一个计数,来判断循环队列为空还是为满,这两种方式都可以

因为使用数组实现,虽然不需要注意野指针问题,但是要注意数组越界的问题。
解决办法:
1.如果出现rear+1或者front+1的情况,这种都属于下标可能大于数组容量,则直接模队列的容量。在下面接口中插入和删除操作有例子。
2.如果出现rear-1的情况,这种可能会属于下标小于0,则需要加个队列的容量大小,然后再模个队列的容量大小。下面接口中判断是否为满的接口涉及该步骤。

下面进行画图分析和接口实现

接口实现 (数组实现)

本处对接口进行了拆分方便介绍。将接口合在一起就是整个循环队列的实现。
需要详细介绍的接口我会画图分析,因为循环队列的长度是固定的,所以假设循环队列的大小为k = 3(一个约定)。方便画图分析

  1. 循环队列的结构
typedef struct 
{
    int* a;         //存放数据

	//建立下标用来访问数组数据
    int front;      //队头的下标
    int rear;       //队尾的下标

	//取模的时候需要使用循环队列的大小
    int k;          //循环队列的大小,假设为3

	//第二种方式判空判满,如果count=k,那就是满,如果为0就是空,
	//所以即使队头和队尾的下标指向同一个位置,也可以通过这种方式判断队列空满
	//int count = 0;
} MyCircularQueue;
  1. 为循环队列开辟一块空间
MyCircularQueue* myCircularQueueCreate(int k) 
{
	//创建结构体指针
    MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    if (NULL == obj)
    {
        perror("Create::malloc");
        return NULL;
    }
    
    //开辟数组大小,要比k大1,方便判断为满的情况
    obj->a = (int*)malloc(sizeof(int) * (k+1));
    if (NULL == obj->a)
    {
        perror("obj->a::malloc");
        return NULL;
    }
    
    //初始化结构体成员
    obj->front = obj->rear = 0;
    obj->k = k;

	//返回开辟的结构体指针
    return obj;
}

下面都会使用这个图进行画图分析:(可以给它理解成连续的数组,使用链表更容易分析)
构建的逻辑结构的循环队列

  1. 判断循环队列是否为空
    建议:尽可能把下面函数需要的接口写在前面,例如下面写pop (DeQueue)接口的时候,需要判断循环队列是否为空,直接调用这个为空的接口就可以。
bool myCircularQueueIsEmpty(MyCircularQueue* obj) 
{
	//断言,如果obj为空,就没意义了,下面断言处一样的道理
    assert(obj);

	//当队头和队尾的下标一样,就代表循环队列为空。这里需要返回一个bool值,为空返回true,不为空返回false
    return obj->rear == obj->front;
}
  1. 判断循环队列是否为满
bool myCircularQueueIsFull(MyCircularQueue* obj) 
{
    assert(obj);

	//如下图所示两种为满的情况,结合在一起
    return (obj->rear+1) % (obj->k+1) == obj->front;
}

循环队列为满情况的画图分析图

  1. 循环队列插入操作,插入成功返回真
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) 
{
    assert(obj);

	//如果为满就无法插入,返回假
    if (myCircularQueueIsFull(obj))
        return false;
    
    //正常插入操作
    obj->a[obj->rear] = value;
    obj->rear++;

	//这里也需要取模,以防越界, 如下图所示:
    obj->rear %= obj->k+1;
    return true;
}

插入操作rear取模的原因

  1. 循环队列删除操作,删除成功,则返回真
bool myCircularQueueDeQueue(MyCircularQueue* obj) 
{
    assert(obj);

	//为空,就无法删除,直接返回假
    if (myCircularQueueIsEmpty(obj))
        return false;
    //删除操作,直接让队头的下标++,把原来的队头的空间还给操作系统就好。
    obj->front++;

	//这里也要取模,如下图所示:
    obj->front %= obj->k+1;
    return true;
}

删除操作front取模的原因
7. 从队首获取元素,如果队列为空返回 -1

int myCircularQueueFront(MyCircularQueue* obj) 
{
    assert(obj);
	
	//这一步比较简单,使用队列为空的接口判断,为空就返回-1。不为空,直接返回队头元素即可
    if (myCircularQueueIsEmpty(obj))
        return -1;
    else
        return obj->a[obj->front];
}
  1. 从队尾获取元素,如果队列为空返回 -1

这里需要注意一下,这里存在越界的风险,因为返回队尾元素实际是: obj->a[obj->rear-1],而rear-1就会存在越界的风险。所以这里需要加个 (k+1),然后再 %(k+1)。如下图所示:

int myCircularQueueRear(MyCircularQueue* obj) 
{
    assert(obj);

    if (myCircularQueueIsEmpty(obj))
        return -1;
    else
        return obj->a[(obj->rear + obj->k) % (obj->k+1)];
}

防止取队尾操作的越界情况

  1. 销毁循环队列
void myCircularQueueFree(MyCircularQueue* obj) 
{
    assert(obj);

	//因为malloc了两次,所以释放两次,对数组的操作可以置空,对obj的操作不能在这里置空,可以在外部置空。因为传的是obj,除非传的是obj的地址,
    free(obj->a);
    obj->a = NULL;
    free(obj);
}

总结

前面三题可以看出是对栈和队列的基础应用,而最后一题,是一种特殊的队列——循环队列。可以理解成是对前面栈和队列知识的一种拓展。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
栈和队列数据结构中常用的两种数据结构,它们可以被用来实现表达式求值。下面是一个使用栈和队列实现表达式求值的C语言例子: ```c #include <stdio.h> #include <stdlib.h> #include <ctype.h> #define MAX_STACK_SIZE 100 #define MAX_QUEUE_SIZE 100 typedef enum { lparen, rparen, plus, minus, times, divide, mod, eos, operand } precedence; int isp[] = { 0, 19, 12, 12, 13, 13, 13, 0 }; int icp[] = { 20, 19, 12, 12, 13, 13, 13, 0 }; precedence getToken(char *symbol, int *n) { *symbol = expr[(*n)++]; switch (*symbol) { case '(': return lparen; case ')': return rparen; case '+': return plus; case '-': return minus; case '*': return times; case '/': return divide; case '%': return mod; case '\0': return eos; default: return operand; } } void postfix(void) { char symbol; precedence token; int n = 0; int top = 0; int queue_front = 0, queue_rear = 0; int stack[MAX_STACK_SIZE]; char queue[MAX_QUEUE_SIZE][MAX_STACK_SIZE]; stack[0] = eos; while ((token = getToken(&symbol, &n)) != eos) { if (token == operand) { queue[queue_rear][0] = symbol; queue[queue_rear++][1] = '\0'; } else if (token == rparen) { while (stack[top] != lparen) { queue[queue_rear][0] = stack[top--]; queue[queue_rear++][1] = '\0'; } top--; } else { while (isp[stack[top]] >= icp[token]) { queue[queue_rear][0] = stack[top--]; queue[queue_rear++][1] = '\0'; } stack[++top] = token; } } while ((token = stack[top--]) != eos) { queue[queue_rear][0] = token; queue[queue_rear++][1] = '\0'; } printf("Postfix expression: "); for (int i = queue_front; i < queue_rear; i++) { printf("%s", queue[i]); } printf("\n"); } int main() { char expr[MAX_STACK_SIZE]; printf("Enter an infix expression: "); scanf("%s", expr); postfix(expr); return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kpl_20

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值