队列练习之Example006-设计队列要求入队时增加队列空间,出队后出队元素所占用空间可重复使用,以保持队列空间只增不减,并且要求入队操作和出队操作的时间复杂度都为O

Posted 二木成林

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了队列练习之Example006-设计队列要求入队时增加队列空间,出队后出队元素所占用空间可重复使用,以保持队列空间只增不减,并且要求入队操作和出队操作的时间复杂度都为O相关的知识,希望对你有一定的参考价值。

Example006

题目

请设计一个队列,要求满足:

  • ①初始时队列为空;
  • ②入队时,允许增加队列占用空间;
  • ③出队后,出队元素所占用的空间可重复使用,即整个队列所占用的空间只增不减;
  • ④入队操作和出队操作的时间复杂度始终保持为O(1)。

请回答下列问题:

  • 1)该队列是应选择链式存储结构,还是应选择顺序存储结构?
  • 2)画出队列的初始状态,并给出判断队空和队满的条件。
  • 3)画出第一个元素入队后的队列状态。
  • 4)给出入队操作和出队操作的基本过程。

分析

本题很难,毕竟是 408 统考真题,也没有要求写代码。但如果通过画图那么还是很容易理解到如何出队和入队的。题解如下:

1)该队列是应选择链式存储结构,还是应选择顺序存储结构?

  • 要求①很容易满足,因为初始化队列都是空的;
  • 对于要求②,可以使用链式存储,每次入队新元素时便创建新结点并分配空间,这样也增加了队列空间。而顺序存储空间是一开始就分配好的,队列的占用空间无法随着入队操作而增加,所以需要选择链式存储结构。
  • 对于要求③,要求出队元素的空间可以重复使用,即即使链队结点出队后,也不要释放空间,以前的链表删除操作都会释放掉被删结点的空间。我们可以这样操作:对出队后的结点不真正释放(即不调用 free 函数释放空间),而是用队头指针指向新的队头结点,原队头结点仍然保留在链式队列中,但队头指针却不指向它了;而有新元素入队时,有空余结点则无需创建新结点开辟新空间,直接赋值到队尾后的第一个空结点即可,然后用队尾指针指向新的队尾结点。这就要求设计成一个首尾相接的循环单链表,即链式循环队列。
  • 对于要求④,由于有队头指针和队尾指针,所以链式循环队列的入队操作和出队操作的时间复杂度都是 O(1)

2)画出队列的初始状态,并给出判断队空和队满的条件。

该链式循环队列的实现可以参考顺序循环队列,不同之处在于链式循环队列可以动态地增加空间,出队的结点也可以循环利用,入队时空间不够也可以动态增加。同样,链式循环队列,也需要区分队满和队空的情况,这里参考顺序循环队列牺牲一个单元来判断,即在链式循环队列中有一个结点是不存储任何数据的,仅用来判断链式循环队列是队满还是队空。初始化时,创建一个只有空闲结点(初始时就必须有这个空闲结点,用来判断队空和队满的空闲结点)循环单链表,队头指针 front 和队尾指针 rear 均指向空闲结点,如图所示:

  • 队空的判定条件:queue->front==queue->rear
  • 队满的判定条件:queue->front==queue->rear->next

注:

  • 初始时,无论是队空的判定还是队满的判定条件都能通过,所以初始时既是队空也是队满。
  • 链式循环队列不存在队满的。由于在出队后,结点空间不会释放掉,所以会一直存在,那么队满的判定条件就会返回 0,表示可以直接利用空余结点来存储新元素。如果再入队,那么就会把空余结点也给用完,那时候就存在队满了,即没有空余结点就是队满状态。如果只是一直入队那么一直都是队满状态。
  • 上图只是初始化图,是不太容易看得出到底是队空还是队满的,可以看看下图:


3)画出第一个元素入队后的队列状态。

插入第一个元素后的状态如图所示:

注:有些书上是将新结点放在空闲结点(即图中的紫色结点)前面,我在这里面踩了一些坑,这样的做法是很不容易插入新结点的,代码不太好写,所以不推荐。如下图所示:

4)给出入队操作和出队操作的基本过程。

如果是3)中第一张图的作法,那么操作的基本过程如下:

// 入队操作
if(queue->front==queue->rear->next) // 队满
    创建新结点 newNode 并为其分配空间;
    newNode->next=queue->rear->next;
    queue->rear->next=newNode;
    queue->rear=newNode;
 else // 队未满
    queue->rear=queue->rear->next;
    queue->rear->data=ele;


// 出队操作
if(queue->front==queue->rear) // 队空
    则出队失败,返回;
 else // 队非空
    queue->front=queue->front->next;
    ele=queue->front->data;

如果是3)中第二张图的作法,那么操作的基本过程如下:

// 入队操作
if(queue->front==queue->rear->next) //队满
    则在 rear 后面插入一个新的空闲结点;
入队元素保存到 rear 所指结点中;
queue->rear=queue->rear->next;
返回;

// 出队操作
if(queue->front==queue->rear) //队空
    则出队失败然后返回;
取 front 所指结点中的元素 ele;
queue->front=queue->front->next;
返回 ele;

注:如果是3)中第一张图的作法,则入队多个元素后如图所示:

注:如果是3)中第二张图的作法,则入队多个元素后如图所示:

注:之所以要指出这种区别,是为了不陷入某些参考书中,选择自己能理解的解法即可。本题后面采用的是3)中第一张图的做法。

图解

  • 初始化链式循环队列

  • 队满与队未满状态

  • 队空与队非空

  • 入队(在队已满情况下)

  • 出队

  • 入队(在队未满情况下)

C实现

核心代码:

/**
 * 将元素入队
 * @param queue 链队列
 * @param ele 待入队的元素
 */
void enQueue(LinkedQueue **queue, int ele) 
    // 如果链式队列已满,则创建新结点进行插入
    if (isFull(*queue)) 
        // 创建新结点
        QNode *newNode = (QNode *) malloc(sizeof(QNode));
        newNode->data = ele;
        newNode->next = NULL;
        // 将新结点插入
        newNode->next = (*queue)->rear->next;
        (*queue)->rear->next = newNode;
        (*queue)->rear = newNode;
    
        // 如果链式队列未满,则直接利用已有结点的空间,而不必创建新结点
    else 
        (*queue)->rear = (*queue)->rear->next;
        (*queue)->rear->data = ele;
    


/**
 * 将元素出队
 * @param queue 链队列
 * @param ele 用来保存出队元素
 * @return 如果链队列为空则不能出队则返回 0 表示出队失败;否则返回 1 表示出队成功
 */
int deQueue(LinkedQueue **queue, int *ele) 
    if (isEmpty(*queue)) 
        return 0;
     else 
        (*queue)->front = (*queue)->front->next;
        *ele = (*queue)->front->data;
        return 1;
    

完整代码:

#include<stdio.h>
#include<stdlib.h>

/**
 * 链队列中的结点结构体定义
 */
typedef struct QNode 
    /**
     * 结点数据域,存储链队列中结点的数据
     */
    int data;
    /**
     * 结点指针域,存储当前结点的后继结点的地址
     */
    struct QNode *next;
 QNode;

/**
 * 链队列结构体定义
 */
typedef struct 
    /**
     * 存储链队列的队头结点的地址,即指向队头结点
     */
    QNode *front;
    /**
     * 存储链队列的队尾结点的地址,即指向队尾结点
     */
    QNode *rear;
 LinkedQueue;

/**
 * 初始化链队列
 * @param queue 未初始化的链队列
 */
void init(LinkedQueue **queue) 
    // 其实 queue 就相当于链队列的头结点,不过它有两个指针,分别存储队头结点和队尾结点的地址
    // 为链队列头结点分配存储空间
    *queue = (LinkedQueue *) malloc(sizeof(LinkedQueue));
    // 将队头指针和队尾指针都指向 NULL,表示空队列
    (*queue)->front = NULL;
    (*queue)->rear = NULL;
    // 创建一个结点用于来判断链式循环队列是否队满或队空
    QNode *node = (QNode *) malloc(sizeof(QNode));
    node->next = node;
    // 将队列的 front 和 rear 都指向 node 结点
    (*queue)->front = node;
    (*queue)->rear = node;


/**
 * 判断链队列是否为空
 * @param queue 链队列
 * @return 如果链队列为空则返回 1,否则返回 0 表示非空
 */
int isEmpty(LinkedQueue *queue) 
    if (queue->front == queue->rear) 
        return 1;
     else 
        return 0;
    


/**
 * 判断链队列是否已满
 * @param queue 链队列
 * @return 如果队满则返回 1,否则返回 0
 */
int isFull(LinkedQueue *queue) 
    if (queue->front == queue->rear->next) 
        return 1;
     else 
        return 0;
    


/**
 * 将元素入队
 * @param queue 链队列
 * @param ele 待入队的元素
 */
void enQueue(LinkedQueue **queue, int ele) 
    // 如果链式队列已满,则创建新结点进行插入
    if (isFull(*queue)) 
        // 创建新结点
        QNode *newNode = (QNode *) malloc(sizeof(QNode));
        newNode->data = ele;
        newNode->next = NULL;
        // 将新结点插入
        newNode->next = (*queue)->rear->next;
        (*queue)->rear->next = newNode;
        (*queue)->rear = newNode;
    
        // 如果链式队列未满,则直接利用已有结点的空间,而不必创建新结点
    else 
        (*queue)->rear = (*queue)->rear->next;
        (*queue)->rear->data = ele;
    


/**
 * 将元素出队
 * @param queue 链队列
 * @param ele 用来保存出队元素
 * @return 如果链队列为空则不能出队则返回 0 表示出队失败;否则返回 1 表示出队成功
 */
int deQueue(LinkedQueue **queue, int *ele) 
    if (isEmpty(*queue)) 
        return 0;
     else 
        (*queue)->front = (*queue)->front->next;
        *ele = (*queue)->front->data;
        return 1;
    


/**
 * 获取链队列中的结点个数
 * @param queue 链队列
 * @return 结点个数
 */
int size(LinkedQueue *queue) 
    // 变量,记录链队列结点个数
    int len = 0;
    // 变量,结点链队列的队头结点,相当于单链表的开始结点
    QNode *node = queue->front->next;
    // 扫描链队列,即遍历单链表,统计结点个数
    while (node != queue->front) 
        len++;
        node = node->next;
    
    return len;


/**
 * 获取链队列的队头结点数据值
 * @param queue 链队列
 * @param ele 用来保存队头结点数据值
 * @return 如果链队列为空则返回 0 表示获取失败,否则返回 1 表示获取成功
 */
int getFront(LinkedQueue *queue, int *ele) 
    // 0.参数校验,如果链队列为空,则表示不能获取队头元素
    if (isEmpty(queue)) 
        return 0;
    
    // 1.用 ele 保存队头结点的数据值,即队头指针所指向的后继结点的数据域
    *ele = queue->front->next->data;
    return 1;


/**
 * 获取链队列的队尾结点数据值
 * @param queue 链队列
 * @param ele 用来保存队尾结点数据值
 * @return 如果链队列为空则返回 0 表示获取失败,否则返回 1 表示获取成功
 */
int getRear(LinkedQueue *queue, int *ele) 
    // 0.参数校验,如果链队列为空,则表示不能获取队尾元素
    if (isEmpty(queue)) 
        return 0;
    
    // 1.用 ele 保存队尾结点的数据值,即队尾指针所指向的结点
    *ele = queue->rear->data;
    return 1;


/**
 * 打印链队列的所有结点值
 * @param queue 链队列
 */
void print(LinkedQueue *queue) 
    printf("[");
    QNode *frontNode = queue->front;
    QNode *node = frontNode->next;
    // 对于队满和队非满两种情况
    if (isFull(queue)) 
        while (node != frontNode) 
            printf("%d", node->data);
            if (node->next != frontNode) 
                printf(", ");
            
            node = node->next;
        
     else 
        while (node != queue->rear) 
            printf("%d", node->data);
            if (node->next != queue->rear) 
                printf(", ");
            
            node = node->next;
        
        printf(", %d", queue->rear->data);
    
    printf("]\\n");



int main() 
    // 声明链队列
    LinkedQueue *queue;

    // 初始化链队列
    printf("\\n初始化链队列:\\n");
    init(&queue);
    print(queue);

    // 队列是否为空
    printf("\\n队列是否为空:\\n");
    int empty;
    empty = isEmpty(queue);
    printf("%d\\n", empty);

    // 将元素入队
    printf("\\n将元素入队:\\n");
    enQueue(&queue, 11);
    print(queue);
    enQueue(&queue, 22);
    print(queue);
    enQueue(&queue, 33);
    print(queue);
    enQueue(&queue, 44);
    print(queue);

    // 队列是否已满
    printf("\\n队列是否已满:\\n");
    int full;
    full = isFull(queue);
    printf("%d\\n", full);

    // 将元素出队
    printf("\\n将元素出队:\\n");
    int ele;
    deQueue(&queue, &ele);
    printf("出队元素:%d\\n", ele);
    print(queue);
    deQueue(&queue, &ele);
    printf("出队元素:%d\\n", ele);
    print(queue);

    // 队列是否已满
    printf("\\n队列是否已满:\\n");
    full = isFull(queue);
    printf("%d\\n", full);

    // 再将元素入队
    printf("\\n将元素入队:\\n");
    enQueue(&queue, 55);
    print(queue);
    enQueue(&queue, 66);
    print(queue);

    // 队列是否已满
    printf("\\n队列是否已满:\\n");
    full = isFull(queue);
    printf("%d\\n", full);

    // 队头元素
    printf("\\n队头元素:\\n");
    int front;
    getFront(queue, &front);
    printf("%d\\n", front);

    // 队尾元素
    printf("\\n队尾元素:\\n");
    int rear;
    getRear(queue, &rear);
    printf("%d\\n", rear);

    // 队列元素个数
    printf("\\n队列元素个数:\\n");
    int len;
    len = size(queue);
    printf("%d\\n", len);

执行结果:

初始化链队列:
[]

队列是否为空:
1

将元素入队:
[11]
[11, 22]
[11, 22, 33]
[11, 22, 33, 44]

队列是否已满:
1

将元素出队:
出队元素:11
[22, 33, 44]
出队元素:22
[33, 44]

队列是否已满:
0

将元素入队:
[33, 44, 55]
[33, 44, 55, 66]

队列是否已满:
1

队头元素:
33

队尾元素:
66

队列元素个数:
4

Java实现

略。

以上是关于队列练习之Example006-设计队列要求入队时增加队列空间,出队后出队元素所占用空间可重复使用,以保持队列空间只增不减,并且要求入队操作和出队操作的时间复杂度都为O的主要内容,如果未能解决你的问题,请参考以下文章

队列练习之Example002-用带头结点的循环链表表示队列,并且只设一个指针指向队尾结点,实现对应的入队列和出队列的算法

队列练习之Example004-设计一个循环队列,用 front 和 rear 分别作为队头和队尾指针,另外用一个标志 tag 表示队列是空还是不空

队列练习之Example005-Q 是一个队列,S 是一个空栈,实现将队列中的元素逆置的算法

队列练习之Example003-如果允许在循环队列的两端都可以进行插入和删除操作,分别写出从队尾删除和从队头插入的算法

栈练习之Example006-判定给定的由 I 和 O 组成的入栈和出栈组成的操作序列是否合法

物联网架构成长之路(27)-Docker练习之Zookeeper安装