栈和队列的4道面试题【详细解析】【代码实现】

news2025/1/17 6:14:37

栈和队列的面试题

1.有效的括号(栈实现)

题目:

有效的括号

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

示例 1:

输入:s = "()"
输出:true

示例 2:

输入:s = "()[]{}"
输出:true

示例 3:

输入:s = "(]"
输出:false

提示:

  • 1 <= s.length <= 104
  • s 仅由括号 '()[]{}' 组成

思路:

  1. 首先我们可以知道我们需要去比较字符串内的元素,并且我们需要用到后进先出的场景,因此这里我们考虑用栈来解决问题
  2. 我们将前括号放到栈内,s指针如果指向的是后括号,就让其和栈内的栈顶元素对比,如果匹配就将栈顶元素弹出,s继续遍历
  3. 一旦不匹配,或者栈空了,s还有后括号没有匹配,或者栈还有元素,s没有后括号匹配了就是无效字符串,返回false

代码实现:

由于我们是使用C语言写oj题,因此我们需要自己去编写栈的定义和栈的接口实现

如果是在leetcode上,头文件之类的自己会包含,我们不用去管

接口:
// 这里的栈我们用动态顺序表实现 (也可以用静态顺序表实现[不好扩容和定义空间大小])
# include<stdio.h>
# include<assert.h>
# include<stdlib.h>
# include<stdbool.h>

typedef char SLDataType;
typedef struct Stack
{
	SLDataType* _a;
	int _top; // 栈顶下标 [规定栈顶下标:最后一个有效数据的下一个位置]
	int _capacity; // 数组的有效空间大小 
}Stack;


// 栈的初始化
void StackInit(Stack* ps);

// 栈的销毁
void StackDestory(Stack* ps);

// 栈是能从栈顶  存数据或者取数据,因此不存在尾插头插之类的
// 入栈
void StackPush(Stack* ps, SLDataType x);

// 出栈
void StackPop(Stack* ps);

// 栈的数据个数获取
//int StackSize(Stack st); //其实理论上获取元素个数只需要传值调用就行 但是为了保持接口一致性,我们采用指针
int StackSize(Stack* ps);

// 获取栈顶元素
SLDataType StackTop(Stack* ps);

// 判断栈是否为空
int StackEmpty(Stack* ps); // 是空返回1  不是空的返回0

// 栈的初始化
void StackInit(Stack* ps)
{
	assert(ps); // ps不能为NULL

	// 栈的初始化
	/*ps->_a = NULL;
	ps->_top = 0;
	ps->_capacity = 0;*/

	// 除了上面这种初始化。也可以这样初始化
	SLDataType* tmp = (SLDataType*)malloc(sizeof(SLDataType) * 4); // 这样后面入栈时无需判断 空间是否为0 
	if (tmp == NULL)
	{
		perror("StackInit():malloc()");
		return;
	}

	ps->_a = tmp;
	ps->_top = 0;
	ps->_capacity = 4;
}

// 栈的销毁
void StackDestory(Stack* ps)
{
	assert(ps);

	free(ps->_a);
	ps->_a = NULL;

	ps->_top = ps->_capacity = 0;
}

// 入栈
void StackPush(Stack* ps, SLDataType x)
{
	assert(ps);

	// 插入之前 判断栈的空间是否足够新的数据插入
	if (ps->_top == ps->_capacity) // 判断空间是否足够 
	{
		int newcapacity = ps->_capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(ps->_a, sizeof(SLDataType) * newcapacity); // 增容
		if (tmp == NULL) // 判断是否增容成功
		{
			perror("StackPush():realloc()");
			return;
		}

		// 更新栈
		ps->_a = tmp;
		ps->_capacity = newcapacity;
	}

	ps->_a[ps->_top] = x; // 入栈
	ps->_top++; // 让top记录的是栈顶 也就是最后一个数据的下一个位置
}

// 出栈
void StackPop(Stack* ps)
{
	assert(ps);
	assert(ps->_top > 0); // 栈里面要有数据才能出栈

	ps->_top--; // 让top--就行 最后一个数据的下标是 top - 1
}

// 栈的数据个数获取
int StackSize(Stack* ps)
{
	assert(ps);

	return ps->_top; // top代表栈顶下标,是最后的一个数据的下标 + 1  其实就是栈的数据个数
}

// 获取栈顶元素
SLDataType StackTop(Stack* ps)
{
	assert(ps);
	assert(ps->_top > 0); // 没有数据还怎么获取

	return ps->_a[ps->_top - 1]; // top是栈顶下标,top - 1才是最后一个数据的下标
}

// 判断栈是否为空
int StackEmpty(Stack* ps) // 是空返回1  不是空的返回0
{
	assert(ps);

	return ps->_top == 0 ? 1 : 0; // ps->pos只要为0就说明栈内没有数据了
	//return !ps->_top; // ps->top 为0 就返回1,为真就返回 0 ,除了0的数都是真
}
代码:
bool isValid(char* s)
{
    // 由于这道题需要用到后进先出的特性,因此我们使用栈来解决
    // 创建一个栈
    Stack st;
    StackInit(&st); // 初始化
    bool ret = true; // 用来判断字符串是否有效

    // 遍历字符串
    while (*s != '\0')
    {
        // 如果s指针指向的是前括号就入栈
        if (*s == '(' || *s == '[' || *s == '{')
        {
            StackPush(&st, *s);
            s++; // 让s往后走
        }
        else
        {
            // 走到这里有可能是s后括号多,栈内已经没有前括号了,那后面去取栈顶元素自然无法取出
            if (StackEmpty(&st))// 判断栈是否空了
            {
                // 走进来就说明栈内没有元素了,但是s还有后括号
                ret = false; // 无效字符串
                break;
            }
            // 判断s下一步指向的是否是后括号,是否匹配栈顶的前括号
            char top = StackTop(&st); // 取出栈顶元素
            // 每一种括号都要判断一下是否匹配到
            if (*s == ')' && top != '(')
            {
                // 走到这里说明没有匹配上
                ret = false; // 无效字符串
                break; // 不在这里return false是因为会有内存泄漏问题,跳出循环去外面统一调用销毁函数
            }
            if (*s == ']' && top != '[')
            {
                // 走到这里说明没有匹配上
                ret = false; // 无效字符串
                break;
            }
            if (*s == '}' && top != '{')
            {
                // 走到这里说明没有匹配上
                ret = false; // 无效字符串
                break;
            }

            // 走到这里说明有括号配对成功,让s继续往后遍历
            s++;
            // 栈顶元素匹配成功之后要弹出来,防止后面还有括号要配对
            StackPop(&st);
        }

    }
    // 走到这里,有可能是全部匹配完是true。 
    //也有可能是s字符串只有前括号比后括号多 退出循环时,栈内还有许多前括号
    if (!StackEmpty(&st)) // 判断栈是否为空
        ret = false; // 不是空的就是无效字符串

    StackDestory(&st); // 销毁栈

    if (ret == false)
        return false;

    // 走到这里就说明是有效字符串
    return true;
}

2.用队列实现栈

题目:

用队列实现栈

请你仅使用两个队列实现一个后入先出(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;
}

3.用栈实现队列

题目:

用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

思路:

  1. 要通过两个栈实现先进先出的队列,我们要思考数据转移的特性

  2. 我们发现,我们把栈的数据转移到另外一个栈的时候,数据的顺序会倒转

  3. image-20240507223619789

  4. image-20240507223635988

  5. 然后我们发现,这样就是先进先出了,1,2, 3, 4压进去,出来也是从栈顶出来,1, 2, 3, 4。 也就是说 第一个栈的栈顶就是队列的队尾,第二个栈的栈顶就是队列的队头。

    • 那我们给这个队列插入数据时候,要从队尾插入,也就是要把数据从第二个栈全部转移到第一个栈。
    • 队列导出数据的时候,也就是从队头出,那就要把数据从第一个栈全部转移到第二个栈。

代码实现:

接口:
// 这里的栈我们用动态顺序表实现 (也可以用静态顺序表实现[不好扩容和定义空间大小])

typedef int SLDataType;
typedef struct Stack
{
	SLDataType* _a;
	int _top; // 栈顶下标 [规定栈顶下标:最后一个有效数据的下一个位置]
	int _capacity; // 数组的有效空间大小 
}Stack;


// 栈的初始化
void StackInit(Stack* ps);

// 栈的销毁
void StackDestory(Stack* ps);

// 栈是能从栈顶  存数据或者取数据,因此不存在尾插头插之类的
// 入栈
void StackPush(Stack* ps, SLDataType x);

// 出栈
void StackPop(Stack* ps);

// 栈的数据个数获取
//int StackSize(Stack st); //其实理论上获取元素个数只需要传值调用就行 但是为了保持接口一致性,我们采用指针
int StackSize(Stack* ps);

// 获取栈顶元素
SLDataType StackTop(Stack* ps);

// 判断栈是否为空
int StackEmpty(Stack* ps); // 是空返回1  不是空的返回0

// 栈的初始化
void StackInit(Stack* ps)
{
	assert(ps); // ps不能为NULL

	// 栈的初始化
	/*ps->_a = NULL;
	ps->_top = 0;
	ps->_capacity = 0;*/

	// 除了上面这种初始化。也可以这样初始化
	SLDataType* tmp = (SLDataType*)malloc(sizeof(SLDataType) * 4); // 这样后面入栈时无需判断 空间是否为0 
	if (tmp == NULL)
	{
		perror("StackInit():malloc()");
		return;
	}

	ps->_a = tmp;
	ps->_top = 0;
	ps->_capacity = 4;
}

// 栈的销毁
void StackDestory(Stack* ps)
{
	assert(ps);

	free(ps->_a);
	ps->_a = NULL;

	ps->_top = ps->_capacity = 0;
}

// 入栈
void StackPush(Stack* ps, SLDataType x)
{
	assert(ps);

	// 插入之前 判断栈的空间是否足够新的数据插入
	if (ps->_top == ps->_capacity) // 判断空间是否足够 
	{
		int newcapacity = ps->_capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(ps->_a, sizeof(SLDataType) * newcapacity); // 增容
		if (tmp == NULL) // 判断是否增容成功
		{
			perror("StackPush():realloc()");
			return;
		}

		// 更新栈
		ps->_a = tmp;
		ps->_capacity = newcapacity;
	}

	ps->_a[ps->_top] = x; // 入栈
	ps->_top++; // 让top记录的是栈顶 也就是最后一个数据的下一个位置
}

// 出栈
void StackPop(Stack* ps)
{
	assert(ps);
	assert(ps->_top > 0); // 栈里面要有数据才能出栈

	ps->_top--; // 让top--就行 最后一个数据的下标是 top - 1
}

// 栈的数据个数获取
int StackSize(Stack* ps)
{
	assert(ps);

	return ps->_top; // top代表栈顶下标,是最后的一个数据的下标 + 1  其实就是栈的数据个数
}

// 获取栈顶元素
SLDataType StackTop(Stack* ps)
{
	assert(ps);
	assert(ps->_top > 0); // 没有数据还怎么获取

	return ps->_a[ps->_top - 1]; // top是栈顶下标,top - 1才是最后一个数据的下标
}

// 判断栈是否为空
int StackEmpty(Stack* ps) // 是空返回1  不是空的返回0
{
	assert(ps);

	return ps->_top == 0 ? 1 : 0; // ps->pos只要为0就说明栈内没有数据了
	//return !ps->_top; // ps->top 为0 就返回1,为真就返回 0 ,除了0的数都是真
}
代码(自己实现的版本):
typedef struct 
{
    Stack _s1;
    Stack _s2;    
} MyQueue;


MyQueue* myQueueCreate() 
{
    // 创建栈
    MyQueue* pq = (MyQueue*)malloc(sizeof(MyQueue));
    StackInit(&pq->_s1);
    StackInit(&pq->_s2);

    return pq;
}

void myQueuePush(MyQueue* obj, int x) 
{
    // 给队列插入元素,要在第一个栈插入
    // 如果第二个栈有数据,要将其全部转移到第一个栈
    if(!StackEmpty(&obj->_s2))
    {
        // 第二个栈的数据有数据,将其全部转移到第一个栈
        while(StackSize(&obj->_s2) > 0)
        {
            // 转移
            StackPush(&obj->_s1, StackTop(&obj->_s2));
            // 让第二个栈的数据出栈
            StackPop(&obj->_s2);
        }
    }

    // 走到这里,如果第二个栈有数据,也全部转移到第一个栈
    // 如果第二个栈没有数据,那就直接在第一个栈插入数据就好
    StackPush(&obj->_s1, x);    
}

int myQueuePop(MyQueue* obj) 
{
    // 要找到队头(队列开头的元素)就要把全部数据都放在第二个栈,栈顶的数据就是队头
    if(!StackEmpty(&obj->_s1)) 
    {
        // 第一个栈的数据有数据,将其全部转移到第二个栈
        while(StackSize(&obj->_s1) > 0)
        {
            // 转移
            StackPush(&obj->_s2, StackTop(&obj->_s1));
            // 让第一个栈的数据出栈
            StackPop(&obj->_s1);
        }
    }
    // 由于题目说了一个空的队列不会调用 pop 或者 peek 操作
    // 因此这里不用判断两个栈是否为空

    // 走到这里数据一定在第二个栈
    int ret = StackTop(&obj->_s2);
    StackPop(&obj->_s2); // 移除元素
    return ret;
}

int myQueuePeek(MyQueue* obj) 
{
      // 要找到队头(队列开头的元素)就要把全部数据都放在第二个栈,栈顶的数据就是队头
    if(!StackEmpty(&obj->_s1))
    {
        // 第一个栈的数据有数据,将其全部转移到第二个栈
        while(StackSize(&obj->_s1) > 0)
        {
            // 转移
            StackPush(&obj->_s2, StackTop(&obj->_s1));
            // 让第一个栈的数据出栈
            StackPop(&obj->_s1);
        }
    }
    // 由于题目说了一个空的队列不会调用 pop 或者 peek 操作
    // 因此这里不用判断两个栈是否为空

    // 返回队头,也就是第二个栈的栈顶数据
    return StackTop(&obj->_s2);
}

bool myQueueEmpty(MyQueue* obj) 
{
    // 如果两个栈都为空,队列才是空
    return StackEmpty(&obj->_s1) && StackEmpty(&obj->_s2);
}

void myQueueFree(MyQueue* obj) 
{
    StackDestory(&obj->_s1);
    StackDestory(&obj->_s2);

    free(obj);
    obj = NULL;
}
优化后的代码:
typedef struct 
{
    Stack _pushST; // 用于插入数据
    Stack _popST; // 用于出数据
} MyQueue;


MyQueue* myQueueCreate() 
{
    // 创建栈
    MyQueue* pq = (MyQueue*)malloc(sizeof(MyQueue));
    StackInit(&pq->_pushST);
    StackInit(&pq->_popST);

    return pq;
}

void myQueuePush(MyQueue* obj, int x) 
{
    // 直接把数据插入到pushST栈内
    StackPush(&obj->_pushST, x);
}

int myQueuePop(MyQueue* obj) 
{
    // 这个函数的功能和peek函数的功能就多了一个要移除,也就是让队头数据弹出
    // 那我们就考虑让代码复用
    int ret = myQueuePeek(obj);
    StackPop(&obj->_popST); // 代码复用
    return ret;
}

int myQueuePeek(MyQueue* obj) 
{
    // 要找到队头 也就是popST的栈顶数据
    // 要分两种情况,
    //1.如果popST栈没有数据,那就把pushST栈的数据转移到popST栈内
    //2.如果popST有数据,直接返回栈顶的数据,这个数据就是队头
    if(!StackEmpty(&obj->_popST))
    {
        // popST有数据,直接返回栈顶数据,就是队头
        return StackTop(&obj->_popST);
    }
    else
    {
        // popST为空,将pushST栈的数据转移到popST栈内
        while(!StackEmpty(&obj->_pushST)) // 判断是否为空
        {
            StackPush(&obj->_popST, StackTop(&obj->_pushST));
            StackPop(&obj->_pushST); // 出栈
        } 

        return StackTop(&obj->_popST);
    }

}

bool myQueueEmpty(MyQueue* obj) 
{
    // 如果两个栈都为空,队列才是空
    return StackEmpty(&obj->_pushST) && StackEmpty(&obj->_popST);
}

void myQueueFree(MyQueue* obj) 
{
    StackDestory(&obj->_popST);
    StackDestory(&obj->_pushST);

    free(obj);
}

4.设计循环队列

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

题目:

设计循环队列

设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 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);
    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);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1653553.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++关键字、命名空间、输入输出

一、C C是在C的基础之上&#xff0c;容纳进去了面向对象编程思想&#xff0c;并增加了许多有用的库&#xff0c;以及编程范式等。 二、C关键字 C关键字有些是C语言中原带的&#xff0c;也有一些是C本身的关键字&#xff0c;对于这些关键字&#xff0c;大家只需在学习过程中去理…

2023年全国职业院校技能大赛(高职组)“云计算应用”赛项赛卷1(私有云)

#需要资源&#xff08;软件包及镜像&#xff09;或有问题的&#xff0c;可私聊博主&#xff01;&#xff01;&#xff01; #需要资源&#xff08;软件包及镜像&#xff09;或有问题的&#xff0c;可私聊博主&#xff01;&#xff01;&#xff01; #需要资源&#xff08;软件包…

C++之泛型编程---有限双端队列结构容器

引言 为了解决工业领域代码容器的通用化&#xff0c;可以考虑C里的泛型编程概念。假设一个场景需要实时保存最近的n个数据并按照顺序依次处理时&#xff0c;就需要定义一种新的容器来满足要求。当容器不满时&#xff0c;添加数据直接到队尾&#xff0c;当容器数据已经为n个时&a…

onlyoffice容器打包成镜像

书接上篇&#xff0c;onlyoffice容器已经更改在本地docker环境中了&#xff0c;之后需要部署到测试环境的docker中&#xff0c;采用容器打包成本地镜像 1、本地docker 查看容器&#xff1a;docker ps 生成镜像&#xff1a;docker commit -p blissful_lichterman 重命名镜像&a…

博睿数据将出席ClickHouse Hangzhou User Group第1届 Meetup

2024年5月18日&#xff0c;博睿数据数智能力中心负责人李骅宸将受邀参加ClickHouse Hangzhou User Group第1届 Meetup活动&#xff0c;分享《ClickHouse在可观测性的应用实践和优化》的主题演讲。 在当前数字化浪潮下&#xff0c;数据的规模和复杂性不断攀升&#xff0c;如何高…

Sam Altman 在斯坦福大学演讲的 10 个要点

最近在斯坦福大学举行的问答环节中&#xff0c;OpenAI 富有远见的首席执行官 Sam Altman 分享了关于人工智能的未来及其对社会的潜在影响的宝贵见解。作为 GPT 和 DALL-E 等突破性人工智能模型背后的研究组织的联合创始人&#xff0c;Altman 的观点对于企业家、研究人员以及任何…

uniapp+vue基于移动端的药品进销存系统r275i

最后我们通过需求分析、测试调整&#xff0c;与药品进销存管理系统管理系统的实际需求相结合&#xff0c;设计实现了药品进销存管理系统管理系统。 系统功能需求包含业务需求、功能需求用户需求&#xff0c;系统功能需求分析是在了解用户习惯、开发人员技术和实力等各个因素的前…

蓝鹏在线测宽仪有多少个常用系列?

蓝鹏测控专注几何尺寸智能测量仪的生产&#xff0c;其产品线丰富多样&#xff0c;测量仪涵盖了外径、椭圆度、螺纹钢肋高、直线度、宽度、厚度、边长、长度等各类几何尺寸&#xff0c;在线测宽仪主要应用于板材类产品的宽度尺寸检测。 在线测宽仪硬件技术与软件技术相结合&am…

第1章. STM32单片机入门知识介绍

目录 0. 《STM32单片机自学教程》专栏 1.1 嵌入式系统简介 1.1.1 什么是嵌入式系统 1.1.2 嵌入式系统的特点 1.1.3 嵌入式系统的应用领域 1.2 单片机基本概念 1.3 ARM简介 1.3.1 ARM公司简介 1.3.2 ARM处理器简介 1.4 STM32简介 1.4.1 基于Cortex内核的MCU 1.4.…

解析直播美颜SDK:计算机视觉在实时视频中的应用

今天&#xff0c;小编将带大家深入探讨直播美颜SDK的原理、应用及其在实时视频中的重要性。 一、直播美颜SDK的原理 直播美颜SDK的核心原理是基于计算机视觉技术&#xff0c;通过识别人脸、肤色、眼睛、嘴巴等关键特征点&#xff0c;对视频图像进行实时处理。其主要包括以下几…

【FFmpeg】Filter 过滤器 ① ( FFmpeg 过滤器简介 | 过滤器概念 | 过滤器用法 | 过滤器工作流程 | 过滤器文档 | 过滤器分类 )

文章目录 一、FFmpeg 过滤器 Filter 简介1、FFmpeg 过滤器概念2、FFmpeg 过滤器用法3、FFmpeg 过滤器工作流程4、FFmpeg 过滤器文档 二、FFmpeg 过滤器 分类1、过滤器分类 - 根据处理数据类型分类2、过滤器分类 - 根据编码器位置分类3、过滤器分类 - 根据功能分类 FFmpeg 相关文…

常见C语言基础笔试题

一. 简介 整理一些C语言常见的基础笔试题。 二. 常见C语言基础笔试题 1. 结构体指针加 1 结构体指针加 1操作&#xff1a; #include <stdio.h> #include <stdlib.h>typedef struct tagDev_INFO_S{int a;int b;int c;int d; } DEV_INFO_S;int main(void) { D…

力扣-21. 合并两个有序链表-js实现

/*** Definition for singly-linked list.* function ListNode(val, next) {* this.val (valundefined ? 0 : val)* this.next (nextundefined ? null : next)* }*/ /*** param {ListNode} list1* param {ListNode} list2* return {ListNode}*/ const mergeTwoList…

LeetCode 面试题 17.14 —— 最小 k 个数

阅读目录 1. 题目2. 解题思路一3. 代码实现一4. 解题思路二5. 代码实现二 1. 题目 2. 解题思路一 第一种方法就是利用快速排序&#xff0c;第一次排序后&#xff0c;数组被划分为了左右两个区间 [ 0 , i ] , [ i 1 , a r r . s i z e ( ) − 1 ] [0, i], [i1, arr.size()-1]…

【vue+vue-treeselect】根据指定字段,如isLeaf(是否末级节点),设置只允许末级节点可以选

1、当项目有特殊要求&#xff0c;必须根据某个字段的值去判断&#xff0c;是否节点可以选&#xff0c;即使已经是末级节点了&#xff0c;还是需要根据字段判断是否禁用 &#xff08;1&#xff09; :flat"true"一定要设置 (2)获取数据源的时候&#xff0c;设置下禁用…

制造业为什么需要质量管理系统

质量管理是一个企业最重要的核心竞争力之一。为了确保产品和服务的高质量&#xff0c;企业需要建立一个完善的质量管理体系。而质量管理系统&#xff08;QMS&#xff09;正是指导企业如何规范、组织和管理质量相关活动的框架和流程。 在智能制造时代&#xff0c;广大企业如何结…

数组中两个字符串的最小距离

给定一个字符串数组strs&#xff0c;再给定两个字符串str1和str2&#xff0c;返回在strs中str1和str2的最小距离&#xff0c;如果str1或str2为null&#xff0c;或不在strs中&#xff0c;返回-1。 输入描述&#xff1a; 输入包含有多行&#xff0c;第一输入一个整数n(1 ≤ n ≤…

苹果平板HOME键成历史,全面屏时代到来?2024平板电脑市场趋势分析

近期苹果公司在“放飞吧”发布会上推出了新款iPad Pro和iPad Air平板电脑&#xff0c;并下架了最后一款带有实体Home按键的iPad 9。这一变化标志着Home键在苹果iPad产品线中成为了历史&#xff0c;引起了不少网友的怀念和感慨。 与此同时&#xff0c;今年3月线上平板电脑市场迎…

js api part6

正则表达式 正则表达式 &#xff08;Regular Expression&#xff09;是用于 匹配字符串中字符组合 的模式。在 JavaScript中&#xff0c;正则表达式也是对象。通常用来查找、替换那些符合正则表达式的文本&#xff0c;许多语言都支持正则表达式。 正则表达式在 JavaScript中的…

猫不爱喝水是正常的?求求別再被洗脑了!日常可以补水的主食分享

猫不爱喝水正常吗&#xff1f;看给猫喂的什么&#xff0c;喂的罐头的话不爱喝水问题不大。喂的干粮猫还长期不喝水&#xff0c;处于缺水状态&#xff0c;可能会出现便秘、上火、尿黄、尿少等症状。在高温的夏季&#xff0c;猫还可能因脱水而中暑&#xff0c;严重时甚至可能导致…