栈和队列经典面试题--(用队列实现栈,设计循环队列)

个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创

栈和队列经典面试题--(用队列实现栈,设计循环队列)

收录于专栏【数据结构初阶
本专栏旨在分享学习数据结构学习的一点学习笔记,欢迎大家在评论区交流讨论💌

目录

1. 括号匹配问题

题目描述:

思路分析:

代码展示:

2. 用队列实现栈

题目描述:

思路分析:

代码展示:

3. 用栈实现队列

题目描述:

思路分析:

代码展示:

4. 设计循环队列

题目描述:

思路分析:

代码展示:


1. 括号匹配问题

OJ链接--. - 力扣(LeetCode)


题目描述:

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

有效字符串需满足:

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

示例 1:

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

示例 2:

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

示例 3:

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

提示:

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

思路分析:

这道题我在力扣经典面试题中详细讲解过,这里就不再多说,直接给出代码,家人们可以点击下面链接看详细解析:有效的括号--力扣经典面试题-CSDN博客

代码展示:

 
//栈的结构
typedef char STDataType;
 
typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}ST;
 
//函数的声明
//初始化和销毁
void STInit(ST* pst);
void STDestory(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;
	//top指向栈顶数据的下一个位置
	pst->top = 0;
 
	//top指向栈顶数据
	//pst->top = -1;
	pst->capacity = 0;
}
void STDestory(ST* pst)
{
	assert(pst);
 
	free(pst->a);
	pst->a = NULL;
	pst->top = pst->capacity = 0;
}
//入栈和出栈
void STPush(ST* pst, STDataType x)
{
	assert(pst);
	//扩容
	if (pst->top == pst->capacity)
	{
		int newcapcacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(pst->a, newcapcacity * sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			return;
		}
		pst->a = tmp;
		pst->capacity = newcapcacity;
	}
	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);
 
	return pst->top == 0;
}
//获取数据个数
int STSize(ST* pst)
{
	assert(pst);
 
	return pst->top;
}
 
bool isValid(char* s) {
    ST st;
    STInit(&st);
    
    while(*s)
    {
        //左括号入栈
        if(*s == '(' || *s == '[' || *s == '{')
        {
            STPush(&st, *s);
        }
        //右括号取栈顶与左括号尝试匹配
        else
        {
            if(STEmpty(&st))
            {
                return false;
            }
            char top = STTop(&st);
            STPop(&st);
 
            //不匹配
            if(top == '(' && *s != ')'
            || top == '{' && *s != '}'
            || top == '[' && *s != ']')
            {
                STDestory(&st);
                return false;
            }
        }
        ++s;
    }
    //如果栈不为空,说明左括号必右括号多
    bool ret = STEmpty(&st);
    STDestory(&st);
    return ret;
}

2. 用队列实现栈

OJ链接--225. 用队列实现栈 - 力扣(LeetCode)

题目描述:

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

实现 MyStack 类:

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

注意:

  • 你只能使用队列的标准操作 —— 也就是 push to backpeek/pop from frontsize 和 is empty 这些操作。
  • 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

示例:

输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]

解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False

提示:

  • 1 <= x <= 9
  • 最多调用100 次 pushpoptop 和 empty
  • 每次调用 pop 和 top 都保证栈不为空

思路分析:

题目要求使用两个队列实现栈的功能,这里我们要注意:

队列的性质是先进先出

栈的性质是后进先出

如下图所示:

也就是说,我们的栈要实现4 3 2 1 这样的顺序

我们这道题目的栈需要我们实现四个功能:

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

 关于构建栈的思路:

我们其实可以发现栈中数据的存储可以通过两个队列实现,但是就是移除并返回栈顶元素无法通过队列实现,队列的最后一个元素才是我们的栈顶元素,那我们可不可以将有数据的队列中的数据全部导入我们另一个空队列,只剩最后一个数据即为栈顶元素,如下图所示:

注意:

我们入栈时一定要向有数据的队列插入数据,这样才能实现上述操作. 

 构建队列

之前发布过如何使用C语言构建队列,这里就直接给代码,想看的宝子们可以点击下方链接:

--数据结构之队列的超详细讲解-CSDN博客

typedef int QDateType;

//节点结构体
typedef struct QueueNode
{
	struct QueueNode* next;
	QDateType val;
}QNode;

//避免使用二级指针
typedef struct Queue
{
	QNode* phead;
	QNode* ptail;
	int size;
}Queue;

//初始化
void QueueInit(Queue* pq);
//销毁
void QueueDestroy(Queue* pq);

//队尾插入
//void QueuePush(QNode** phead, QNode** ptail, QDateType x);
//void QueuePop(QNode** phead, QNode** ptail);

//队尾插入
void QueuePush(Queue* pq, QDateType x);
//队头删除
void QueuePop(Queue* pq);

//取队头队尾的数据
QDateType QueueFront(Queue* pq);
QDateType QueueBack(Queue* pq);

//队中的元素个数
int QueueSize(Queue* pq);

//队是否为空
bool QueueEmpty(Queue* pq);

//初始化
void QueueInit(Queue* pq)
{
	assert(pq);
	pq->phead = NULL;
	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, QDateType x)
{
	assert(pq);

	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		return;
	}
	newnode->next = NULL;
	newnode->val = x;

	if (pq->phead == NULL)
	{
		pq->phead = pq->ptail = newnode;
	}
	else
	{
		pq->ptail->next = newnode;
		pq->ptail = pq->ptail->next;
	}
	pq->size++;
}


//从队头删除
void QueuePop(Queue* pq)
{
	assert(pq);
	assert(pq->size != 0);

	//一个节点
	if (pq->phead->next == NULL)
	{
		free(pq->phead);
		pq->phead = pq->ptail = NULL;
		pq->size = 0;
	}
	else
	{
		QNode* next = pq->phead->next;
		free(pq->phead);
		pq->phead = next;

		pq->size--;
	}
	
}

QDateType QueueBack(Queue* pq)
{
	assert(pq);
	assert(pq->ptail);

	return pq->ptail->val;
}

QDateType QueueFront(Queue* pq)
{
	assert(pq);
	assert(pq->phead);

	return pq->phead->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* obj = (MyStack*)malloc(sizeof(MyStack));
    QueueInit(&obj->q1);
    QueueInit(&obj->q2);

    return obj;
}
  • void push(int x) 将元素 x 压入栈顶。

这里我们只需要判断那个队列不为空,我们就将数据入队

void myStackPush(MyStack* obj, int x) {
    if(!QueueEmpty(&obj->q1))
    {
        QueuePush(&(obj->q1),x);
    }
    else
    {
        QueuePush(&(obj->q2),x);
    }
}

  • int pop() 移除并返回栈顶元素。

这里我们要将有数据的队列除最后一个数据的其他数据导入空队列,队列中的最后一个数据即为栈顶

int myStackPop(MyStack* obj) {
    //假设法
    Queue* empty = &(obj->q1);
    Queue* nonEmpty = &(obj->q2);
    if(!QueueEmpty(&(obj->q1)))
    {
        nonEmpty = &(obj->q1);
        empty = &(obj->q2);
    }
    //不为空前size-1导走,删除最后一个数据就是栈顶元素
    while(QueueSize(nonEmpty) > 1)
    {
        QueuePush(empty,QueueFront(nonEmpty));
        QueuePop(nonEmpty);
    }
    int top = QueueFront(nonEmpty);
    QueuePop(nonEmpty);
    return top;
}
  • int top() 返回栈顶元素。

栈顶即为队列的队尾元素

int myStackTop(MyStack* obj) {
    if(!QueueEmpty(&(obj->q1)))
    {
        return QueueBack(&(obj->q1));
    }
    else
    {
        return QueueBack(&(obj->q2));
    }
}
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

这里只需要判断我们两个队列中是否有元素

bool myStackEmpty(MyStack* obj) {
    return QueueEmpty(&(obj->q1)) && QueueEmpty(&(obj->q2));
}

销毁队列和栈

这里一定要注意,不可以直接free(obj),因为这样的话我们队列空间并没有释放

void myStackFree(MyStack* obj) {
    QueueDestroy(&(obj->q1));
    QueueDestroy(&(obj->q2));

    free(obj);
}

代码展示:

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* empty = &(obj->q1);
    Queue* nonEmpty = &(obj->q2);
    if(!QueueEmpty(&(obj->q1)))
    {
        nonEmpty = &(obj->q1);
        empty = &(obj->q2);
    }
    //不为空前size-1导走,删除最后一个数据就是栈顶元素
    while(QueueSize(nonEmpty) > 1)
    {
        QueuePush(empty,QueueFront(nonEmpty));
        QueuePop(nonEmpty);
    }
    int top = QueueFront(nonEmpty);
    QueuePop(nonEmpty);
    return top;
}

int myStackTop(MyStack* obj) {
    if(!QueueEmpty(&(obj->q1)))
    {
        return QueueBack(&(obj->q1));
    }
    else
    {
        return QueueBack(&(obj->q2));
    }
}

bool myStackEmpty(MyStack* obj) {
    return QueueEmpty(&(obj->q1)) && QueueEmpty(&(obj->q2));
}

void myStackFree(MyStack* obj) {
    QueueDestroy(&(obj->q1));
    QueueDestroy(&(obj->q2));

    free(obj);
}

3. 用栈实现队列

OJ链接--232. 用栈实现队列 - 力扣(LeetCode)

题目描述:

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

实现 MyQueue 类:

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

说明:

  • 你 只能 使用标准的栈操作 —— 也就是只有 push to toppeek/pop from topsize, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例 1:

输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]

解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false

思路分析:

这个思路和用两个队列实现栈是有点区别的:

两个队列,一个队列入队到另一个队列顺序是一样的,而栈是不一样的,如下图:

 

那这道题的思路就来了

我们两个栈,一个存储数据,然后倒入到我们另一个栈中,栈顶即为我们所需的队首元素 

代码展示:

typedef struct {
    ST s1;
    ST s2;
} MyQueue;


MyQueue* myQueueCreate() {
    MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));

    STInit(&(obj->s1));
    STInit(&(obj->s2));

    return obj;
}

void myQueuePush(MyQueue* obj, int x) {
    STPush(&(obj->s1),x);
}

int myQueuePop(MyQueue* obj) {
    int front = myQueuePeek(obj);
    STPop(&(obj->s2));
    return front;
}

int myQueuePeek(MyQueue* obj) {
    if(STEmpty(&(obj->s2)))
    {
        //倒数据
        while(!STEmpty(&(obj->s1)))
        {
            int top = STTop(&(obj->s1));
            STPush(&(obj->s2),top);
            STPop(&(obj->s1));
        }
    }
    return STTop(&(obj->s2));
}

bool myQueueEmpty(MyQueue* obj) {
    return STEmpty(&(obj->s1)) && STEmpty(&(obj->s2));
}

void myQueueFree(MyQueue* obj) {
    STDestory(&(obj->s1));
    STDestory(&(obj->s2));

    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);
*/

4. 设计循环队列

OJ链接--622. 设计循环队列 - 力扣(LeetCode)

题目描述:

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

提示:

  • 所有的值都在 0 至 1000 的范围内;
  • 操作数将在 1 至 1000 的范围内;
  • 请不要使用内置的队列库。

思路分析:

构建循环队列的结构

这里我使用了动态数组去实现循环队列,因为这里循环队列的空间是固定的,所以在循环队列的结构体中加入int k,代表循环队列的长度

typedef struct {
    int* a;
    int head;
    int tail;
    int k;
    
} MyCircularQueue;

 

这里我们的队头指针head指向指向队头元素,而我们的tail指向我们的队尾元素的下一位,如下图所示: 

 

但是这样就会出现一个问题:怎么判断我们的循环队列是否为空或者为满呢 

 

 

可以看到我们这样设计是无法判断队列是满还是空,所以在循环队列中我们需要额外加入一个空间,用来判断队列是否已满,如下图所示: 

 

当tail指向额外的空间时,我们只需判断(tail + 1) % (k + 1) == head  即可

循环队列初始化:

MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));

    //多开一个解决假溢出的问题
    obj->a = (int*)malloc(sizeof(int)* (k+1));
    obj->head = 0;
    obj->tail = 0;
    obj->k = k;

    return obj;
}

 循环队列的功能实现

  • MyCircularQueue(k): 构造器,设置队列长度为 k 。
  • Front: 从队首获取元素。如果队列为空,返回 -1 。

进行判断后,直接返回我们的队头指针指向的元素即可

int myCircularQueueFront(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return -1;
    else
        return obj->a[obj->head];
}
  • Rear: 获取队尾元素。如果队列为空,返回 -1 。

获取队尾元素和队头有点不一样,因为我们的队尾指针指向的是队尾的下一个元素,如图所示;

 

int myCircularQueueRear(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return -1;
    else
        return obj->a[(obj->tail+obj->k) % (obj->k+1)];

}

所以这里需要取模运算, return obj->a[(obj->tail - 1+obj->k + 1) % (obj->k+1)];

简化之后即为我们上述的结果

  • enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
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;
}

插入元素之前应该判断队列是否已满,已满直接返回false,不为满我们直接进行插入操作,操作完之后记得对tail取模,很多宝子们对这里取模不了解,大家可以看下面这个图: 

 

我们让之前那个图Pop一次,在push一次,这时候tail已经指向我们空间之外的位置,需要进行取模运算:tail % (k + 1)  tail重新指向a[0]的位置

  • deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。

删除操作首先也需要判断循环队列是否为空,删除之后,head同样要进行取模运算,如图:

 

bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
        return false;
    ++obj->head;

    obj->head %= (obj->k+1);

    return true;
}
  • isEmpty(): 检查循环队列是否为空。
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return obj->head == obj->tail;
}

这个在思路中详细说过,只需判断head==tail 

  • isFull(): 检查循环队列是否已满。
bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return (obj->tail+1) % (obj->k+1) == obj->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(sizeof(int)* (k+1));
    obj->head = 0;
    obj->tail = 0;
    obj->k = k;

    return obj;
}

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return obj->head == obj->tail;
}

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return (obj->tail+1) % (obj->k+1) == obj->head;
}

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 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->a[(obj->tail+obj->k) % (obj->k+1)];

}



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);
*/

相关推荐

  1. LeetCode-232. 实现队列 设计 队列

    2024-05-16 06:04:05       46 阅读
  2. 实现队列

    2024-05-16 06:04:05       39 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-16 06:04:05       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-16 06:04:05       100 阅读
  3. 在Django里面运行非项目文件

    2024-05-16 06:04:05       82 阅读
  4. Python语言-面向对象

    2024-05-16 06:04:05       91 阅读

热门阅读

  1. 2.go语言初始(二)

    2024-05-16 06:04:05       27 阅读
  2. 金卡后打开

    2024-05-16 06:04:05       29 阅读
  3. ROS2体系框架

    2024-05-16 06:04:05       35 阅读
  4. 第10章:新建MDK工程-寄存器版

    2024-05-16 06:04:05       34 阅读