在编程面试中,栈(Stack)队列(Queue)是数据结构的基础,也是面试官最爱的考点之一。无论是大厂笔试还是技术面,几乎都会遇到它们的变形题。本文精选了四道经典题目:括号匹配用队列实现栈设计循环队列用栈实现队列,并配以详细的图解和代码解析,帮助你彻底掌握这些核心概念。

1. 括号匹配问题:栈的经典应用

题目链接:有效的括号

这道题是栈最典型的应用场景之一。 核心思路是:遍历字符串,遇到左括号(({[)就将其入栈;遇到右括号()}])则检查栈顶元素是否与之匹配,匹配则出栈,否则返回 false

⚠️ 需要注意两个边界情况:

  • 遍历结束后,栈可能不为空(说明有左括号未匹配),此时应返回 false
  • 遇到右括号时栈已空,说明没有对应的左括号,直接返回 false

完整实现代码如下:

typedef char SLDataType;
typedef struct Stact
{
	SLDataType* a;
	int top;
	int capacity;
}ST;
// 初始化和销毁
void STInit(ST* pst);
void STDestroy(ST* pst);
// 入栈  出栈
void STPush(ST* pst,SLDataType x);
void STPop(ST* pst);
// 取栈顶数据
SLDataType STTop(ST* pst);
// 判空
bool STEmpty(ST* pst);
// 获取数据个数
int STSize(ST* pst);
void STInit(ST* pst)
{
	assert(pst);
	pst->a = NULL;
	pst->capacity = pst->top = 0;
}
void STDestroy(ST* pst)
{
	assert(pst);
	free(pst->a);
	pst->a = NULL;
	pst->capacity = pst->top = 0;
}
// 入栈  出栈
void STPush(ST* pst, SLDataType x)
{
	assert(pst);
	if (pst->capacity == pst->top)
	{
		int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(pst->a, newcapacity * sizeof(SLDataType));
		if (tmp == NULL)
		{
			perror("STpush");
			return;
		}
		pst->a = tmp;
		pst->capacity = newcapacity;
	}
	pst->a[pst->top] = x;
	pst->top++;
}
void STPop(ST* pst)
{
	assert(pst);
	assert(pst->top > 0);
	pst->top--;
}
// 取栈顶数据
SLDataType STTop(ST* pst)
{
	assert(pst);
	assert(pst->top > 0);
	return pst->a[pst->top - 1];
}
// 判空
bool STEmpty(ST* pst)
{
	assert(pst);
	/*if (pst->top == 0)
	{
		return true;
	}
	return false;*/
	return pst->top == 0;
}
// 获取数据个数
int STSize(ST* pst)
{
	assert(pst);
	return pst->top;
}
bool isValid(char* s)
{
    ST st;
    STInit(&st);
    while(*s!='\0')
    {
        if(*s=='('||*s=='{'||*s=='[')
        {
            STPush(&st,*s);
        }
        else
        {
            if(STEmpty(&st))
            {
                STDestroy(&st);
                return false;
            }
            if(STTop(&st)=='('&&*s!=')'
            ||STTop(&st)=='{'&&*s!='}'
            ||STTop(&st)=='['&&*s!=']')
            {
                STDestroy(&st);
                return false;
            }
            STPop(&st);
        }
        s++;
    }
    bool ret = STEmpty(&st);
    STDestroy(&st);
    return ret;
}

为了让代码更健壮,在返回 false 前记得释放栈内存,避免内存泄漏。✅

2. 用队列实现栈:双队列的巧妙配合

题目链接:用队列实现栈

队列是先进先出(FIFO),而栈是后进先出(LIFO),直接用单个队列无法实现栈的行为。这里我们使用两个队列来模拟栈。

核心策略:

  • 入栈(push):将元素插入非空队列(若两个都空,则插入任意一个)。
  • 出栈(pop):将非空队列的前 n-1 个元素依次出队并存入另一个空队列,最后剩下的一个元素就是栈顶元素(即要出栈的元素)。

核心思路:保持一个存数据,一个为空。入数据不入空的队列,出数据通过空的导一下

完整代码:

typedef int QDataType;
typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType val;
}QNode;
typedef struct Queue
{
	QNode* phead;
	QNode* ptail;
	int size;
}Queue;
void QueueInit(Queue* pq);
void QueueDestroy(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);
void QueueInit(Queue* pq)
{
	assert(pq);
	pq->phead = pq->ptail = NULL;
	pq->size = 0;
}
void QueueDestroy(Queue* pq)
{
	assert(pq);
	QNode* cur = pq->phead;
	while (cur)
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}
	pq->phead = pq->ptail = NULL;
	pq->size = 0;
}
void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("QueuePush");
		return;
	}
	newnode->val = x;
	newnode->next = NULL;
	if (pq->phead == NULL)
	{
		pq->phead = pq->ptail = newnode;
	}
	else
	{
		pq->ptail->next = newnode;
		pq->ptail = newnode;
	}
	pq->size++;
}
void QueuePop(Queue* pq)
{
	assert(pq);
	assert(pq->size != 0);
	if (pq->phead == pq->ptail)
	{
		free(pq->phead);
		pq->phead = pq->ptail = NULL;
	}
	else
	{
		QNode* next = pq->phead->next;
		free(pq->phead);
		pq->phead = next;
	}
	pq->size--;
}
// 取队头和队尾的数据
QDataType QueueFront(Queue* pq)
{
	assert(pq);
	assert(pq->phead);
	return pq->phead->val;
}
QDataType QueueBack(Queue* pq)
{
	assert(pq);
	assert(pq->ptail);
	return pq->ptail->val;
}
int QueueSize(Queue* pq)
{
	assert(pq);
	return pq->size;
}
bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->size == 0;
}
typedef struct
{
    Queue q1;
    Queue q2;
} MyStack;
MyStack* myStackCreate()
{
    MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
    QueueInit(&pst->q1);
    QueueInit(&pst->q2);
    return pst;
}
void myStackPush(MyStack* obj, int x)
{
    if(QueueEmpty(&(obj->q1)))
    {
        QueuePush(&(obj->q2),x);
    }
    else
    {
        QueuePush(&(obj->q1),x);
    }
}
int myStackPop(MyStack* obj)
{
    Queue* empty = &(obj->q1);
    Queue* nonempty = &(obj->q2);
    if(QueueEmpty(&(obj->q2)))
    {
        empty = &(obj->q2);
        nonempty = &(obj->q1);
    }
    while(QueueSize(nonempty)>1)
    {
        QueuePush(empty,QueueFront(nonempty));
        QueuePop(nonempty);
    }
    int ret = QueueFront(nonempty);
    QueuePop(nonempty);
    return ret;
}
int myStackTop(MyStack* obj)
{
    if(QueueEmpty(&(obj->q1)))
    {
       return QueueBack(&(obj->q2));
    }
    else
    {
       return QueueBack(&(obj->q1));
    }
}
bool myStackEmpty(MyStack* obj)
{
    return QueueEmpty(&(obj->q1))&&QueueEmpty(&(obj->q2));
}
void myStackFree(MyStack* obj)
{
    QueueDestroy(&(obj->q1));
    QueueDestroy(&(obj->q2));
    free(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);
*/

⚠️ 释放 obj 前,务必先释放内部的 q1q2,避免内存泄漏。这道题逻辑稍复杂,但多练几次就能掌握。

[AFFILIATE_SLOT_1]

3. 设计循环队列:有限空间的复用艺术

题目链接:设计循环队列

循环队列是一种有限空间、可复用的队列结构。当队列满时无法插入,但一旦有元素出队,空出的位置可以再次使用。这里我们选择用数组实现,使用 headtail 两个指针来管理数据。

关键难点:如何区分队列的?如果只用 head == tail 来判断,在空和满两种状态下条件都成立,无法区分。

方法1:增加一个size记录队列的大小
方法2:额外多开一个空间

我们采用多开一个空间的方法:

  • 队列为空head == tail
  • 队列为满(tail + 1) % (k+1) == head

完整代码:

typedef struct
{
    int* a;
    int head;
    int tail;
    int k;
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k)
{
    MyCircularQueue* obj = ( MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    obj->a = (int*)malloc(4*(k+1));
    obj->head=obj->tail=0;
    obj->k=k;
    return obj;
}
bool myCircularQueueIsFull(MyCircularQueue* obj);
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 myCircularQueueIsEmpty(MyCircularQueue* obj);
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;
    else
        return obj->a[obj->head];
}
int myCircularQueueRear(MyCircularQueue* obj)
{
    if(myCircularQueueIsEmpty(obj))
        return -1;
    else
        return obj->tail==0?obj->a[obj->k]:obj->a[obj->tail-1];
}
bool myCircularQueueIsEmpty(MyCircularQueue* obj)
{
    return obj->head==obj->tail;
}
bool myCircularQueueIsFull(MyCircularQueue* obj)
{
    return (obj->tail+1)%(obj->k+1)==obj->head;
}
void myCircularQueueFree(MyCircularQueue* obj)
{
    free(obj->a);
    free(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);
*/

特别提醒:实现 myCircularQueueRear 函数时,需要返回队列的最后一个元素。由于 tail 指向下一个插入位置,最后一个元素实际在 tail - 1 处。当 tail == 0 时,应返回数组末尾元素(下标 k)。可以用取模技巧处理:

return obj->a[(obj->tail+obj->k)%(obj->k+1)]

4. 用栈实现队列:双栈的“反转魔法”

题目链接:用栈实现队列

这道题与“用队列实现栈”有相似之处,但实现方式不同。 我们发现:当数据从一个栈弹出并压入另一个栈时,数据的顺序会被反转,这恰好符合队列的先进先出特性。

因此,我们固定一个栈 pushStack 用于入队,另一个栈 popStack 用于出队

  • 入队(push):直接压入 pushStack
  • 出队(pop):如果 popStack 为空,则将 pushStack 中的所有元素依次弹出并压入 popStack,然后从 popStack 弹出栈顶元素。

此时我们再去数据就可以顺利的把1给取出来。那我们下次取数据还需要再移动数据吗?

完整代码:

typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}ST;
// 初始化和销毁
void STInit(ST* pst);
void STDestroy(ST* pst);
// 入栈  出栈
void STPush(ST* pst,STDataType x);
void STPop(ST* pst);
// 取栈顶数据
STDataType STTop(ST* pst);
// 判空
bool STEmpty(ST* pst);
// 获取数据个数
int STSize(ST* pst);
// 初始化和销毁
void STInit(ST* pst)
{
	assert(pst);
	pst->a = NULL;
	pst->capacity = pst->top = 0;
}
void STDestroy(ST* pst)
{
	assert(pst);
	free(pst->a);
	pst->a = NULL;
	pst->capacity = pst->top = 0;
}
// 入栈  出栈
void STPush(ST* pst, STDataType x)
{
	assert(pst);
	if (pst->capacity == pst->top)
	{
		int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("STpush");
			return;
		}
		pst->a = tmp;
		pst->capacity = newcapacity;
	}
	pst->a[pst->top] = x;
	pst->top++;
}
void STPop(ST* pst)
{
	assert(pst);
	assert(pst->top > 0);
	pst->top--;
}
// 取栈顶数据
STDataType STTop(ST* pst)
{
	assert(pst);
	assert(pst->top > 0);
	return pst->a[pst->top - 1];
}
// 判空
bool STEmpty(ST* pst)
{
	assert(pst);
	/*if (pst->top == 0)
	{
		return true;
	}
	return false;*/
	return pst->top == 0;
}
// 获取数据个数
int STSize(ST* pst)
{
	assert(pst);
	return pst->top;
}
typedef struct
{
    ST psuhst;
    ST popst;
} MyQueue;
MyQueue* myQueueCreate()
{
    MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
    STInit(&obj->psuhst);
    STInit(&obj->popst);
    return obj;
}
void myQueuePush(MyQueue* obj, int x)
{
    STPush(&obj->psuhst,x);
}
int myQueuePeek(MyQueue* obj);
int myQueuePop(MyQueue* obj)
{
    int pop = myQueuePeek(obj);
    STPop(&obj->popst);
    return pop;
}
int myQueuePeek(MyQueue* obj)
{
    if(STEmpty(&obj->popst))
    {
        while(!STEmpty(&obj->psuhst))
        {
            int top = STTop(&obj->psuhst);
            STPush(&obj->popst,top);
            STPop(&obj->psuhst);
        }
    }
    return STTop(&obj->popst);
}
bool myQueueEmpty(MyQueue* obj)
{
    return STEmpty(&obj->psuhst)&&STEmpty(&obj->popst);
}
void myQueueFree(MyQueue* obj)
{
    STDestroy(&obj->psuhst);
    STDestroy(&obj->popst);
    free(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);
*/

这种“双栈反转”的思路非常巧妙,也是面试中常见的考察点。

[AFFILIATE_SLOT_2]

结语

本文通过四道经典面试题,深入剖析了队列的核心特性与常见变形。无论是括号匹配中的栈顶匹配,还是双队列/双栈模拟对方数据结构,都体现了数据结构的灵活运用。 关键要点回顾

  • ✅ 括号匹配:注意栈空和遍历结束后的判断。
  • ✅ 用队列实现栈:利用两个队列转移前 n-1 个元素。
  • ✅ 循环队列:多开一个空间区分空和满。
  • ✅ 用栈实现队列:利用双栈反转数据顺序。

希望这篇手写原创文章能帮助你更好地理解这些数据结构题。如果觉得有收获,别忘了点赞关注哦!