队列的2个面试题【用队列实现栈】【设计循环队列(细节较多)】

队列的面试题

1.用队列实现栈

题目:

用队列实现栈

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false

思路:

  1. 我们知道栈是后进先出的,队列是先进先出的
  2. 插入数据(入栈),找到队列的队尾然后插入数据就行
  3. 弹出数据(出栈),就需要我们另想办法了
  4. 这里我们创建两个队列,在弹出队列的时候我们获取队尾的数据把剩下的数据放到另外一个空的队列,然后让队尾的数据从队头出去。这样可以实现后进先出的效果。

image-20240507151334488

代码实现:

队列的接口:
// 这里的队列的底层数据结构是单链表
// 定义节点的结构体

typedef int QDataType;
typedef struct QueueNode
{
	QDataType _data;
	struct QueueNode* _next;
}QueueNode;

// 和单链表不一样的是队列最好要有指向第一个节点和尾节点的指针
typedef struct Queue
{
	QueueNode* _head;
	QueueNode* _tail;
}Queue;


// 队列的接口(也就是函数) 
// 为什么这里的接口和单链表的时候不一样,不需要传二级指针呢,因为我们把指针放到了结构体内部,传的是结构体指针
// 通过结构体指针找到结构体,再从结构体内部拿到节点的指针,再从这个节点指针找到节点,这里起到的作用就类似于二级指针

// 队列的初始化
void QueueInit(Queue* pq);

// 队列的销毁
void QueueDestory(Queue* pq);

// 入队
void QueuePush(Queue* pq, QDataType x);

// 出队
void QueuePop(Queue* pq);

// 获取队头的数据
QDataType QueueFront(Queue* pq);

// 获取队尾的数据
QDataType QueueBack(Queue* pq);

// 判断队列是否为空  [返回1就是空,返回0就是非空]
int QueueEmpty(Queue* pq);

// 获取队列的数据个数
int QueueSize(Queue* pq);

// 队列的打印
void QueuePrint(Queue* pq);


// 队列的初始化
void QueueInit(Queue* pq)
{
	assert(pq);//pq不能为NULL

	// 初始化
	pq->_head = NULL;
	pq->_tail = NULL;

}

// 队列的销毁
void QueueDestory(Queue* pq)
{
	assert(pq);

	// 遍历队列,删除每一个节点
	QueueNode* cur = pq->_head;
	while (cur) 
	{
		QueueNode* next = cur->_next;
		free(cur);
		cur = next;
	}

	pq->_head = pq->_tail = NULL;
}

// 入队
void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);

	// 入队其实就是让新节点尾插到链表中
	QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
	if (newnode == NULL)
	{
		perror("QueuePush():malloc()");
		exit(-1);
	}

	newnode->_data = x;
	newnode->_next = NULL;

	// 判断列队是否为空
	if (pq->_head == NULL)
	{
		pq->_head = pq->_tail = newnode;
	}
	else
	{
		// 尾插
		pq->_tail->_next = newnode;
		pq->_tail = newnode;
	}

}

// 出队
void QueuePop(Queue* pq)
{
	assert(pq);
	assert(pq->_head); // 队列是空的怎么出队

	// 头删
	QueueNode* next = pq->_head->_next; // 把第一个节点的下一个节点存储起来
	free(pq->_head);
	pq->_head = next;

	// 这里有个问题,当最后一个节点删除完之后,pq->_head = NULL
	// 但是pq->_tail 就变成野指针了
	if (pq->_head == NULL)
	{
		pq->_tail = NULL;
	}

}

// 获取队头的数据
QDataType QueueFront(Queue* pq)
{
	assert(pq);
	assert(pq->_head);// 队列为空怎么获取队头数据

	return pq->_head->_data;
}

// 获取队尾的数据
QDataType QueueBack(Queue* pq)
{
	assert(pq);
	assert(pq->_tail); // 等价于assert(pq->_head); 头为空,尾也肯定为空,

	return pq->_tail->_data;
}

// 判断队列是否为空 [返回1就是空,返回0就是非空]
int QueueEmpty(Queue* pq)
{
	assert(pq);

	return pq->_head == NULL ? 1 : 0;
}

// 获取队列的数据个数
int QueueSize(Queue* pq)
{
	assert(pq);

	// 遍历队列统计数据个数
	QueueNode* cur = pq->_head;
	int size = 0;
	while (cur)
	{
		size++;
        cur = cur->_next;
	}

	return size;
}

// 队列的打印
void QueuePrint(Queue* pq)
{
	assert(pq);
	assert(pq->_head);

	while (!QueueEmpty(pq))
	{
		printf("%d ", QueueFront(pq));
		QueuePop(pq); // 从队头拿出一个数据要将其删除
	}
	printf("\n");
	
}
代码:
// 这个栈由两个队列实现
typedef struct 
{
    Queue _q1;
    Queue _q2;
} MyStack;


MyStack* myStackCreate() 
{
    // 创建我们的栈
    // MyStack st; // 局部变量的生命周期只存在函数,这样创建的st无法传递给外部使用
    MyStack* st = (MyStack*)malloc(sizeof(MyStack));
    QueueInit(&st->_q1);
    QueueInit(&st->_q2);

    return st;
}

void myStackPush(MyStack* obj, int x)
{
    //  为了实现栈的先进后出,我们创建了两个队列,一个队列是空的,一个队列存储数据
    if(!QueueEmpty(&obj->_q1)) // 判断q1队列是否 不为空
    {
        // 如果q1队列不是空的就插入到q1队列
        QueuePush(&obj->_q1, x);
    }
    else // 如果两个队列都是空,会走到这里
    {
        // q2队列不是空的就插入q2队列
        QueuePush(&obj->_q2, x);
    }
}

int myStackPop(MyStack* obj) 
{
    // 实现后进先出,让有数据的队列除了队尾元素,剩下的移动到空队列,然后在弹出剩下的队尾元素即可
    //这里我们假设 q1是空的,q2不是空的
    Queue* empty = &obj->_q1;
    Queue* noempty = &obj->_q2;
    // 判断q2是否为空
    if(QueueEmpty(&obj->_q2)) 
    {
        // q2是空的,说明前面的假设错误,更正
        empty = &obj->_q2;
        noempty = &obj->_q1;
    }
    
    // 让有数据的队列 除了队尾元素,全部都转移到空队列
    while(QueueSize(noempty) > 1) // 只转移队尾元素之前的元素
    {
        // 让有数据的队列的队头尾插到空队列
        QueuePush(empty, QueueFront(noempty));
        QueuePop(noempty); // 把有数据队列的队头元素弹出,这样队头才能更新
    }

    // 走到这里 有数据队列就剩下一个数据了
    int top = QueueBack(noempty);
    QueuePop(noempty); // 出栈
    return top;
}

int myStackTop(MyStack* obj) 
{
    // 找到有数据的队列的队尾
    if(!QueueEmpty(&obj->_q1))
    {
        return QueueBack(&obj->_q1);
    }
    else // 题目说了每次调用 pop 和 top 都保证栈不为空 因此这里无需对两个队列为空的情况做处理
    {
        return QueueBack(&obj->_q2);
    }

}

bool myStackEmpty(MyStack* obj) 
{
    // 两个队列都为空,栈才为空
    return QueueEmpty(&obj->_q1) && QueueEmpty(&obj->_q2);
}

void myStackFree(MyStack* obj) 
{
    QueueDestory(&obj->_q1);
    QueueDestory(&obj->_q2);
    free(obj);
    obj = NULL;
}

2.设计循环队列

实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列可以使用数组实现,也可以使用循环链表实现

题目:

设计循环队列

设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

  • MyCircularQueue(k): 构造器,设置队列长度为 k 。
  • Front: 从队首获取元素。如果队列为空,返回 -1 。
  • Rear: 获取队尾元素。如果队列为空,返回 -1 。
  • enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
  • deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
  • isEmpty(): 检查循环队列是否为空。
  • isFull(): 检查循环队列是否已满。

示例:

MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3
circularQueue.enQueue(1);  // 返回 true
circularQueue.enQueue(2);  // 返回 true
circularQueue.enQueue(3);  // 返回 true
circularQueue.enQueue(4);  // 返回 false,队列已满
circularQueue.Rear();  // 返回 3
circularQueue.isFull();  // 返回 true
circularQueue.deQueue();  // 返回 true
circularQueue.enQueue(4);  // 返回 true
circularQueue.Rear();  // 返回 4

思路:

这里的队列我们底层使用数组实现。

  1. 由于是循环队列,队头出数据,队尾入数据,首尾要相连。因此我们才用两个指针来实现数据的弹出和插入。
  2. 但是需要注意的是,我们要给数组留一个空间,是空的,不能使用的,不然的话我们无法判断该队列是空的还是满的

image-20240508103624619

image-20240508105626453

如图所示,当头和尾指针在一起的时候无法判断队列是空的还是满的。

  1. 为了解决这个问题,我们再数组中留一个空间不使用,当rear尾指针指向空的时候,队列就是满的。当front和rear再一起的时候,队列就是空的

image-20240508105925701

image-20240508105934563

  1. 但是这个空的空间是会变动的,我们如何去判断,rear此时刚好指向的是front前一个的空间呢(这个空间就是空的).。我们通过一个公式 (rear+1) % (k + 1) == front。 只要满足这个公式,就说明rear此时在front的前一个空间,就说明此时rear指向空的空间,就说明此时队列已经满了

image-20240508111546237

  1. 当front == rear的时候,队列就是空的

image-20240508111919779

代码实现:

题目中有很多函数接口要实现,这里我们先完成一些简单的,方便我们后面进行函数复用。

  • MyCircularQueue(k): 构造器,设置队列长度为 k 。

要想完成其他函数功能的实现,我们要先有一个循环队列的构造

MyCircularQueue* myCircularQueueCreate(int k) 
{
    // 给结构体申请空间
    MyCircularQueue* q = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    // 给结构体内的数组申请空间
    q->_a = malloc(sizeof(int) * (k + 1)); // 要多申请一个空间
    q->_front = 0;
    q->_rear = 0;
}

代码中的k+1 为了给数组多申请一个空间,前面我们的思路说了,这个空间是不插入数据的,一旦rear指针走到这个空间就代表队列满了。

  • isEmpty(): 检查循环队列是否为空。
bool myCircularQueueIsEmpty(MyCircularQueue* obj) 
{
    // 只要两个指针走到一起就说明队列满了
    return obj->_front == obj->_rear;
}
  • isFull(): 检查循环队列是否已满。
bool myCircularQueueIsFull(MyCircularQueue* obj) 
{
    // 只要rear指针走到了front指针的前一个空间,也就是我们留着不插入数据的空间。就说明队列满了
    return obj->_front == (obj->_rear + 1) % (k + 1);
    
}

代码中的(obj->_rear + 1) % (k + 1)是为了应对多种情况,大部分情况下,队列满了,rear+1 就是front,但是如果rear刚好在数组最后一个空间,这个时候+1就越界了。如图所示:

image-20240508150943220

因此让其% 上 数组空间个数,可以处理全部清弃情况。

  • enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
    // 插入元素之前,要判断该队列是否满了
    if(myCircularQueueIsFull(obj))
        return false; // 满了直接返回false

    // 走到这里就是没满,那就插入
    obj->_a[obj->_rear] = value;
    obj->_rear++; // rear下标指向的是队尾数据的下一个空间
    // 这里要注意 如果rear已经指向最后一个空间了,此时+1会越界
    obj->_rear %= (k + 1); // 如果越界了就会回到0,不越界没有影响
    
    return true;
}

代码中要注意obj->_rear %= (k + 1);的处理。没有这个处理在下图的情况会报错。

image-20240508150943220

  • deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
bool myCircularQueueDeQueue(MyCircularQueue* obj) 
{
    // 删除元素之前要看看队列是不是空的
    if(myCircularQueueIsEmpty(obj))
        return false;

    // 走到这里就说明不是空的
    obj->_front++; //直接让队头往后移动
    // 这里同样要考虑front是否会越界的问题
    obj->_front %= (k + 1); 

    return true;
}

越界的情况如下图:

front一直要++到rear才能删除干净,但是front会越界,因此需要处理。

image-20240508145324746

  • Front: 从队首获取元素。如果队列为空,返回 -1 。
int myCircularQueueFront(MyCircularQueue* obj) 
{
    // 获取队头数据之前,要判断队列是否为空
    if(myCircularQueueIsEmpty(obj))
        return -1;

    // 获取队头
    int head = obj->_a[obj->_front];
    return head;
}
  • Rear: 获取队尾元素。如果队列为空,返回 -1 。
int myCircularQueueRear(MyCircularQueue* obj)
{
    // 要获取队尾的数据,同样要判断队列是否为空
    if(myCircularQueueIsEmpty(obj))
        return -1;

    // 获取队尾
    int tail = obj->_rear - 1;
    // 要注意rear指针越界的情况
    if(tail == -1)
        tail = obj->_k;// 一共有k + 1个空间,最后一个下标就是k

    return obj->_a[tail];
}

image-20240508153543022

最后的全部代码:


typedef struct 
{
    int* _a; // 不知道数组空间要开多大,给个指针
    int _front; // 对头
    int _rear; // 队尾
    int _k; // 队列长度为 k 
} MyCircularQueue;


MyCircularQueue* myCircularQueueCreate(int k) 
{
    // 给结构体申请空间
    MyCircularQueue* q = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    // 给结构体内的数组申请空间
    q->_a = (int*)malloc(sizeof(int) * (k + 1)); // 要多申请一个空间
    q->_front = 0;
    q->_rear = 0;
    q->_k = k;

    return q;
}

// 手动加两个声明,才能使用让我们的函数复用。也可以自己去调换一下函数的定义顺序
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
bool myCircularQueueIsFull(MyCircularQueue* obj);

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
    // 插入元素之前,要判断该队列是否满了
    if(myCircularQueueIsFull(obj))
        return false; // 满了直接返回false

    // 走到这里就是没满,那就插入
    obj->_a[obj->_rear] = value;
    obj->_rear++; // rear下标指向的是队尾数据的下一个空间
    // 这里要注意 如果rear已经指向最后一个空间了,此时+1会越界
    obj->_rear %= (obj->_k + 1); // 如果越界了就会回到0,不越界没有影响

    return true;
}

bool myCircularQueueDeQueue(MyCircularQueue* obj) 
{
    // 删除元素之前要看看队列是不是空的
    if(myCircularQueueIsEmpty(obj))
        return false;

    // 走到这里就说明不是空的
    obj->_front++; //直接让队头往后移动
    // 这里同样要考虑front是否会越界的问题
    obj->_front %= (obj->_k + 1); 

    return true;
}

int myCircularQueueFront(MyCircularQueue* obj) 
{
    // 获取队头数据之前,要判断队列是否为空
    if(myCircularQueueIsEmpty(obj))
        return -1;

    // 获取队头
    int head = obj->_a[obj->_front];
    return head;
}

int myCircularQueueRear(MyCircularQueue* obj)
{
    // 要获取队尾的数据,同样要判断队列是否为空
    if(myCircularQueueIsEmpty(obj))
        return -1;

    // 获取队尾
    int tail = obj->_rear - 1;
    // 要注意rear指针越界的情况
    if(tail == -1)
        tail = obj->_k;// 一共有k + 1个空间,最后一个下标就是k

    return obj->_a[tail];
}

bool myCircularQueueIsEmpty(MyCircularQueue* obj) 
{
    // 只要两个指针走到一起就说明队列满了
    return obj->_front == obj->_rear;
}

bool myCircularQueueIsFull(MyCircularQueue* obj) 
{
    // 只要rear指针走到了front指针的前一个空间,也就是我们留着不插入数据的空间。就说明队列满了
    return (obj->_rear + 1) % (obj->_k + 1) == obj->_front;
    
}

void myCircularQueueFree(MyCircularQueue* obj) 
{
    free(obj->_a);
    free(obj);
}

注意:

在使用函数复用的时候要注意函数的声明问题

手动加两个声明,才能使用让我们的函数复用。也可以自己去调换一下函数的定义顺序

bool myCircularQueueIsEmpty(MyCircularQueue* obj);
bool myCircularQueueIsFull(MyCircularQueue* obj);
  • 34
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值