Bootstrap

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

Example002

题目

假设以带头结点的循环链表表示队列,并且只设一个指针指向队尾结点,但不设头指针,请写出相应的入队列和出队列的算法。

分析

注意,这里的不设头指针的意思是不设定队头指针。

我们设 rear 为带头结点的循环链队的尾指针,但 rear 却不是指向循环链队的头结点,而是指向循环链队的队尾结点。

本题是链队基本操作的扩展,知道尾指针后,要实现元素入队,则直接用链表的插入操作即可。要实现出队操作,则需要根据尾指针找出头结点和开始结点,然后进行删除。要注意的是,尾指针应始终指向终端结点,并且当删除结点后队列为空时,必须特殊处理

注意:在解题时不必考虑结点的结构体,其实这里只需要用到单链表结点 LNode 就可以了,即只需要一个 next 指针就可以了,下面之所以使用 CLNode 结点并添加一个 rear 指针,是为了便于测试方法而写出来的。实际解题只需要在参数列表中传入队尾指针 rear 即可,不需要去考虑如何它是如何来的。如果下面的测试代码用的是单链表结点 LNode`,那么如果想要传入队尾指针作为参数,那么需要遍历单链表找到队尾结点,然后作为参数传入,当然这些都不在解题的考虑范围内。之所以解释这一段,也是因为我们不能只写具体算法,还要写能够测试写出来算法是否有效的其他代码。

图解

有点不太好理解,其实画个图就很容易看明白了,就是链表的插入和删除操作而已。

  • 循环链队如图

在这里插入图片描述

  • 循环链队元素入队如图

在这里插入图片描述

  • 循环链队元素出队如图

在这里插入图片描述

C实现

核心代码:

/**
 * 将元素入队
 * @param rear 循环链式队列的尾指针,指向链式队列的尾结点
 * @param ele 待入队的元素
 */
void enQueue(CLNode **rear, int ele) {
    // 1.创建新结点
    // 1.1 为新结点分配存储空间
    CLNode *newNode = (CLNode *) malloc(sizeof(CLNode));
    // 1.2 为新结点指定数据域
    newNode->data = ele;
    // 1.2 为新结点指定指针域,新结点的指针域初始都指向 null
    newNode->next = NULL;

    // 2.将新结点入队
    // 2.1 将新结点的 next 指针指向原队尾结点的后继结点。即将新结点与原队列头结点连接起来
    // 实际上 rear 指向队尾结点;rear->next 由于是循环链式队列,所以指向的是头结点
    newNode->next = (*rear)->next;
    // 2.2 将原队尾结点的 next 指针指向新结点。即将原队列尾与新结点连接起来,才构成一个循环链
    (*rear)->next = newNode;
    // 2.3 将队尾指针指向新结点,即此时新结点成为了队列的队尾结点
    (*rear) = newNode;
}

/**
 * 将元素出队
 * @param rear 循环链式队列的尾指针,指向链式队列的尾结点
 * @param ele 保存出队的元素
 * @return 如果出队成功则返回 1,否则返回 0 表示出队失败
 */
int deQueue(CLNode **rear, int *ele) {
    // 0.参数校验,如果是空队列,则返回 0 表示不能出队。注意循环链表的判空条件
    if ((*rear)->next == *rear) {
        return 0;
    }

    // 其中 rear 指向队尾结点;rear->next 由于是循环链式队列,所以指向的是头结点
    // 局部变量,头结点
    CLNode *headNode = (*rear)->next;
    // 局部变量,开始结点。而头结点的后继结点就是开始结点
    CLNode *startNode = headNode->next;

    // 1.用 ele 保存开始结点的数据域,因为队列是从队头出队,所以就是出队开始结点
    *ele = startNode->data;
    // 2.删除队头结点。即将队头结点的 next 指针指向原开始结点的后继结点
    headNode->next = startNode->next;
    // 如果元素出队后队列为空,则需要特殊处理
    if (startNode==*rear){
        *rear=(*rear)->next;
    }
    // 3.释放被删结点的空间
    free(startNode);
    // 4.返回 1 表示出队成功
    return 1;
}

完整代码:

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

/**
 * 链式循环队列结点结构体定义
 */
typedef struct CLNode {
    /**
     * 结点数据域,存储结点的数据值
     */
    int data;
    /**
     * 结点指针域,存储当前结点的后继结点的地址
     */
    struct CLNode *next;
    /**
     * 结点指针域,即队尾指针,指向队尾结点,存储队列尾结点的地址
     */
    struct CLNode *rear;
} CLNode;

/**
 * 初始化队列
 * @param queue 未初始化的队列
 */
void init(CLNode **queue) {
    // 创建头结点,queue 即为链式队列的头结点,为头结点分配空间
    *queue = (CLNode *) malloc(sizeof(CLNode));
    // 将链式队列头结点的 next 和 rear 指针都指向自身,因为是循环的,并且是空队列
    (*queue)->next = *queue;
    (*queue)->rear = *queue;
}

/**
 * 将元素入队
 * @param rear 循环链式队列的尾指针,指向链式队列的尾结点
 * @param ele 待入队的元素
 */
void enQueue(CLNode **rear, int ele) {
    // 1.创建新结点
    // 1.1 为新结点分配存储空间
    CLNode *newNode = (CLNode *) malloc(sizeof(CLNode));
    // 1.2 为新结点指定数据域
    newNode->data = ele;
    // 1.2 为新结点指定指针域,新结点的指针域初始都指向 null
    newNode->next = NULL;

    // 2.将新结点入队
    // 2.1 将新结点的 next 指针指向原队尾结点的后继结点。即将新结点与原队列头结点连接起来
    // 实际上 rear 指向队尾结点;rear->next 由于是循环链式队列,所以指向的是头结点
    newNode->next = (*rear)->next;
    // 2.2 将原队尾结点的 next 指针指向新结点。即将原队列尾与新结点连接起来,才构成一个循环链
    (*rear)->next = newNode;
    // 2.3 将队尾指针指向新结点,即此时新结点成为了队列的队尾结点
    (*rear) = newNode;
}

/**
 * 将元素出队
 * @param rear 循环链式队列的尾指针,指向链式队列的尾结点
 * @param ele 保存出队的元素
 * @return 如果出队成功则返回 1,否则返回 0 表示出队失败
 */
int deQueue(CLNode **rear, int *ele) {
    // 0.参数校验,如果是空队列,则返回 0 表示不能出队。注意循环链表的判空条件
    if ((*rear)->next == *rear) {
        return 0;
    }

    // 其中 rear 指向队尾结点;rear->next 由于是循环链式队列,所以指向的是头结点
    // 局部变量,头结点
    CLNode *headNode = (*rear)->next;
    // 局部变量,开始结点。而头结点的后继结点就是开始结点
    CLNode *startNode = headNode->next;

    // 1.用 ele 保存开始结点的数据域,因为队列是从队头出队,所以就是出队开始结点
    *ele = startNode->data;
    // 2.删除队头结点。即将队头结点的 next 指针指向原开始结点的后继结点
    headNode->next = startNode->next;
    // 如果元素出队后队列为空,则需要特殊处理
    if (startNode==*rear){
        *rear=(*rear)->next;
    }
    // 3.释放被删结点的空间
    free(startNode);
    // 4.返回 1 表示出队成功
    return 1;
}

/**
 * 打印循环链式队列
 * @param queue 队列
 */
void print(CLNode *queue) {
    printf("[");
    CLNode *node = queue->next;
    while (node != queue) {
        printf("%d", node->data);
        if (node->next != queue) {
            printf(", ");
        }
        node = node->next;
    }
    printf("]\n");
}

int main() {
    // 声明队列
    CLNode *queue;

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

    // 将元素入队
    printf("\n将元素入队:\n");
    // 这里传入的参数可能有点难以理解,因为 enQueue 方法的第一个参数是双指针,所以传入的是指针变量的地址
    // queue是一个指针变量;&queue就是指针变量的地址,而queue->rear是为了获取队列的队尾指针,将队尾指针作为参数传入而非在函数内获取,是为了更直观了解函数的功能
    enQueue(&(queue->rear), 11);
    print(queue);
    enQueue(&(queue->rear), 22);
    print(queue);
    enQueue(&(queue->rear), 33);
    print(queue);
    enQueue(&(queue->rear), 44);
    print(queue);
    enQueue(&(queue->rear), 55);
    print(queue);
    enQueue(&(queue->rear), 66);
    print(queue);

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

执行结果:

初始化队列:
[]

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

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

上面是纯 C 语言实现,而通常数据结构题目中会使用 & 来引用,而这属于 C++ 语言范畴,但这样更便于理解函数的功能,而非陷入 C 语言语法中忽略了算法的本质。但使用了 & 语法的代码在纯 C 语言编译器是无法通过编译的,可以使用 Dev-C++ 来执行代码。所以代码如下:

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

/**
 * 链式循环队列结点结构体定义
 */
typedef struct CLNode {
    /**
     * 结点数据域,存储结点的数据值
     */
    int data;
    /**
     * 结点指针域,存储当前结点的后继结点的地址
     */
    struct CLNode *next;
    /**
     * 结点指针域,即队尾指针,指向队尾结点,存储队列尾结点的地址
     */
    struct CLNode *rear;
} CLNode;

/**
 * 初始化队列
 * @param queue 未初始化的队列
 */
void init(CLNode *&queue) {
    // 创建头结点,queue 即为链式队列的头结点,为头结点分配空间
    queue = (CLNode *) malloc(sizeof(CLNode));
    // 将链式队列头结点的 next 和 rear 指针都指向自身,因为是循环的,并且是空队列
    queue->next = queue;
    queue->rear = queue;
}

/**
 * 将元素入队
 * @param rear 循环链式队列的尾指针,指向链式队列的尾结点
 * @param ele 待入队的元素
 */
void enQueue(CLNode *&rear, int ele) {
    // 1.创建新结点
    // 1.1 为新结点分配存储空间
    CLNode *newNode = (CLNode *) malloc(sizeof(CLNode));
    // 1.2 为新结点指定数据域
    newNode->data = ele;
    // 1.2 为新结点指定指针域,新结点的指针域初始都指向 null
    newNode->next = NULL;

    // 2.将新结点入队
    // 2.1 将新结点的 next 指针指向原队尾结点的后继结点。即将新结点与原队列头结点连接起来
    // 实际上 rear 指向队尾结点;rear->next 由于是循环链式队列,所以指向的是头结点
    newNode->next = rear->next;
    // 2.2 将原队尾结点的 next 指针指向新结点。即将原队列尾与新结点连接起来,才构成一个循环链
    rear->next = newNode;
    // 2.3 将队尾指针指向新结点,即此时新结点成为了队列的队尾结点
    rear = newNode;
}

/**
 * 将元素出队
 * @param rear 循环链式队列的尾指针,指向链式队列的尾结点
 * @param ele 保存出队的元素
 * @return 如果出队成功则返回 1,否则返回 0 表示出队失败
 */
int deQueue(CLNode *&rear, int *ele) {
    // 0.参数校验,如果是空队列,则返回 0 表示不能出队。注意循环链表的判空条件
    if (rear->next == rear) {
        return 0;
    }

    // 其中 rear 指向队尾结点;rear->next 由于是循环链式队列,所以指向的是头结点
    // 局部变量,头结点
    CLNode *headNode = rear->next;
    // 局部变量,开始结点。而头结点的后继结点就是开始结点
    CLNode *startNode = headNode->next;

    // 1.用 ele 保存开始结点的数据域,因为队列是从队头出队,所以就是出队开始结点
    *ele = startNode->data;
    // 2.删除队头结点。即将队头结点的 next 指针指向原开始结点的后继结点
    headNode->next = startNode->next;
    // 如果元素出队后队列为空,则需要特殊处理
    if (startNode==*rear){
        *rear=(*rear)->next;
    }
    // 3.释放被删结点的空间
    free(startNode);
    // 4.返回 1 表示出队成功
    return 1;
}

/**
 * 打印循环链式队列
 * @param queue 队列
 */
void print(CLNode *queue) {
    printf("[");
    CLNode *node = queue->next;
    while (node != queue) {
        printf("%d", node->data);
        if (node->next != queue) {
            printf(", ");
        }
        node = node->next;
    }
    printf("]\n");
}

int main() {
    // 声明队列
    CLNode *queue;

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

    // 将元素入队
    printf("\n将元素入队:\n");
    // 这里传入的参数可能有点难以理解,因为 enQueue 方法的第一个参数是双指针,所以传入的是指针变量的地址
    // queue是一个指针变量;&queue就是指针变量的地址,而queue->rear是为了获取队列的队尾指针,将队尾指针作为参数传入而非在函数内获取,是为了更直观了解函数的功能
    enQueue(queue->rear, 11);
    print(queue);
    enQueue(queue->rear, 22);
    print(queue);
    enQueue(queue->rear, 33);
    print(queue);
    enQueue(queue->rear, 44);
    print(queue);
    enQueue(queue->rear, 55);
    print(queue);
    enQueue(queue->rear, 66);
    print(queue);

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

Java实现

核心代码:

    /**
     * 将元素入队
     *
     * @param ele 待入队的元素
     */
    public void enQueue(int ele) {
        // 1.创建新结点
        // 1.1 为新结点分配存储空间
        CLNode newNode = new CLNode();
        // 1.2 为新结点指定数据域
        newNode.data = ele;
        // 1.2 为新结点指定指针域,新结点的指针域初始都指向 null
        newNode.next = null;

        // 2.将新结点入队
        // 2.1 将新结点的 next 指针指向原队尾结点的后继结点。即将新结点与原队列头结点连接起来
        // 实际上 queue.rear 指向队尾结点;queue.rear.next 由于是循环链式队列,所以指向的是头结点
        newNode.next = queue.rear.next;
        // 2.2 将原队尾结点的 next 指针指向新结点。即将原队列尾与新结点连接起来,才构成一个循环链
        queue.rear.next = newNode;
        // 2.3 将队尾指针指向新结点,即此时新结点成为了队列的队尾结点
        queue.rear = newNode;
    }

    /**
     * 将元素出队
     *
     * @return 出队的元素
     * @throws Exception 如果队列为空则抛出该异常
     */
    public int deQueue() throws Exception {
        // 0.参数校验,如果是空队列,则抛出异常表示不能出队。注意循环链表的判空条件
        if (queue.rear == queue) {
            throw new Exception("空队列无法出队!");
        }

        // 其中 queue 表示头指针,指向队列的头结点,queue.rear 表示队尾指针,指向队尾结点;queue.rear.next 由于是循环链式队列,所以指向的是头结点
        // 局部变量,头结点
        CLNode headNode = queue.rear.next;
        // 局部变量,开始结点。而头结点的后继结点就是开始结点
        CLNode startNode = headNode.next;

        // 1.保存开始结点的数据域,因为队列是从队头出队,所以就是出队开始结点
        int data = startNode.data;
        // 2.删除队头结点。即将队头结点的 next 指针指向原开始结点的后继结点
        headNode.next = startNode.next;
        // 如果元素出队后队列为空,则需要特殊处理
        if (startNode == queue.rear) {
            queue.rear = queue.rear.next;
        }
        // 3.释放被删结点的空间
        startNode.next = null;
        startNode = null;
        // 4.返回出队元素
        return data;
    }

完整代码:

public class Queue {
    /**
     * 声明链式循环队列,queue 即链式循环队列的头结点
     */
    private CLNode queue;

    /**
     * 初始化队列
     */
    public void init() {
        // 初始化链式队列
        queue = new CLNode();
        // 将链式队列头结点的 next 和 rear 指针都指向自身,因为是循环的,并且是空队列
        queue.next = queue;
        queue.rear = queue;
    }

    /**
     * 将元素入队
     *
     * @param ele 待入队的元素
     */
    public void enQueue(int ele) {
        // 1.创建新结点
        // 1.1 为新结点分配存储空间
        CLNode newNode = new CLNode();
        // 1.2 为新结点指定数据域
        newNode.data = ele;
        // 1.2 为新结点指定指针域,新结点的指针域初始都指向 null
        newNode.next = null;

        // 2.将新结点入队
        // 2.1 将新结点的 next 指针指向原队尾结点的后继结点。即将新结点与原队列头结点连接起来
        // 实际上 queue.rear 指向队尾结点;queue.rear.next 由于是循环链式队列,所以指向的是头结点
        newNode.next = queue.rear.next;
        // 2.2 将原队尾结点的 next 指针指向新结点。即将原队列尾与新结点连接起来,才构成一个循环链
        queue.rear.next = newNode;
        // 2.3 将队尾指针指向新结点,即此时新结点成为了队列的队尾结点
        queue.rear = newNode;
    }

    /**
     * 将元素出队
     *
     * @return 出队的元素
     * @throws Exception 如果队列为空则抛出该异常
     */
    public int deQueue() throws Exception {
        // 0.参数校验,如果是空队列,则抛出异常表示不能出队。注意循环链表的判空条件
        if (queue.rear == queue) {
            throw new Exception("空队列无法出队!");
        }

        // 其中 queue 表示头指针,指向队列的头结点,queue.rear 表示队尾指针,指向队尾结点;queue.rear.next 由于是循环链式队列,所以指向的是头结点
        // 局部变量,头结点
        CLNode headNode = queue.rear.next;
        // 局部变量,开始结点。而头结点的后继结点就是开始结点
        CLNode startNode = headNode.next;

        // 1.保存开始结点的数据域,因为队列是从队头出队,所以就是出队开始结点
        int data = startNode.data;
        // 2.删除队头结点。即将队头结点的 next 指针指向原开始结点的后继结点
        headNode.next = startNode.next;
       	// 如果元素出队后队列为空,则需要特殊处理
        if (startNode == queue.rear) {
            queue.rear = queue.rear.next;
        }
        // 3.释放被删结点的空间
        startNode.next = null;
        startNode = null;
        // 4.返回出队元素
        return data;
    }

    /**
     * 打印循环链式队列
     */
    public void print() {
        System.out.print("[");
        CLNode node = queue.next;
        while (node != queue) {
            System.out.print(node.data);
            if (node.next != queue) {
                System.out.print(", ");
            }
            node = node.next;
        }
        System.out.print("]\n");
    }
}

/**
 * 链式循环队列结点定义
 */
class CLNode {
    /**
     * 结点数据域,存储结点的数据值
     */
    int data;
    /**
     * 结点指针域,存储当前结点的后继结点的地址
     */
    CLNode next;
    /**
     * 结点指针域,即队尾指针,指向队尾结点,存储队列尾结点的地址
     */
    CLNode rear;
}

测试代码:

public class Test {
    public static void main(String[] args) throws Exception {
        Queue queue = new Queue();

        // 队列初始化
        System.out.println("队列初始化:");
        queue.init();
        queue.print();

        // 将元素入队
        System.out.println("\n将元素入队:");
        queue.enQueue(11);
        queue.print();
        queue.enQueue(22);
        queue.print();
        queue.enQueue(33);
        queue.print();
        queue.enQueue(44);
        queue.print();
        queue.enQueue(55);
        queue.print();
        queue.enQueue(66);
        queue.print();

        // 将元素出队
        System.out.println("\n将元素出队:");
        int ele;
        ele = queue.deQueue();
        System.out.println("出队元素:" + ele);
        queue.print();
        ele = queue.deQueue();
        System.out.println("出队元素:" + ele);
        queue.print();
        ele = queue.deQueue();
        System.out.println("出队元素:" + ele);
        queue.print();

    }
}

执行结果:

队列初始化:
[]

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

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