循环队列简要分析与实现(Java)

目录

开场白

队列应用举例

现实生活

操作系统

计算机网络

从队列到循环队列

队列的定义

队列的不足

循环队列的定义

实现循环队列

1. 准备工作

2. 构造方法

3. 判空 & 判满

4. 入队

5. 出队

6. 获取队首元素

7. 遍历

final. 完整实现代码 & 单元测试

Java实现

C语言实现

实现链队列

总结


开场白

在先前的几篇文章中,我们分别介绍了链表、栈等线性数据结构。除了这二者外,队列也是一种非常重要且经典的线性数据结构。

队列应用举例

现实生活

实际上,队列数据结构或队列思想在生活中比比皆是。我们在商场中购物结束,会来到收银台进行结账,在不同的收银台前都会排一个长长的队伍,尤其是在过年备年货的时候(比如近几天),那真的是人满为患。我们按照先来后到的顺序在收银台前排队,每当有一个顾客购物完毕需要来到收银台进行结账时,会来到待结账顾客队伍的队尾入队(排队);如果一个顾客完成结账,那就会从当前这个顾客队伍的队首出队,然后该干什么干什么。这其实就是经典的先来先服务(First Come First Server, FCFS)。

操作系统

操作系统(Operating System,简称OS)中,队列也是一种非常重要的数据结构,广泛应用于各种操作系统场景中,我们简要介绍队列是如何在操作系统进程调度中扮演角色的。当某个进程(Process)完成创建态而转变成就绪态时,此时就意味着当前进程只要获取到了CPU执行权就可以上处理机运行,操作系统是如何维护这样的进程组呢?操作系统用一个名为就绪队列的队列维护这样的就绪进程,CPU按照一定的调度算法,在就绪队列中选择满足要求的进程让其上处理机运行,此时的进程状态就变为运行态,当进程要等待某个资源(如IO操作)而暂时无法继续执行,操作系统会将其转存到阻塞队列中,并将当前进程的状态修改为阻塞态,转而在就绪队列中去选择下一个满足要求的进程,让其上处理机运行。当阻塞队列中的阻塞态进程等待到了需要的资源,操作系统会让其重新进入就绪队列等待CPU调度。例如当操作系统使用时间片轮转(Round Robin, RR)调度算法进行进程调度时,每次会在就绪队列头部取出一个进程,为其分配一个指定大小的时间片,如果当前任务在时间片内完成,则将其从队列中彻底移除;如果当前任务在时间片结束时还未完成,则将其重新放回到就绪队列的队尾

计算机网络

在计算机网络中,数据包的传输和处理是一个典型的队列应用场景。想象一下,你正在使用电脑浏览网页,每次点击链接或提交表单数据时,数据都会被打包成一个个小的数据包,通过网络传输到服务器。服务器接收到这些数据包后,需要对它们进行处理并返回响应。如果服务器同时收到太多的数据包,而处理能力有限,就会导致数据包积压。如果没有一个合理的机制来管理这些数据包,服务器可能会崩溃,或者某些用户的请求会被延迟甚至丢失。为了解决这个问题,服务器通常会使用队列来管理接收到的数据包。每当一个数据包到达服务器时,它会被放入队列的末尾;而服务器会从队列的头部依次取出数据包进行处理。这种先进先出的机制确保了数据包按照到达的顺序被处理。

从队列到循环队列

需要提前指出的是,循环队列与普通队列是两种不同的队列实现方案。前者是基于环形数组实现的,后者则是基于普通数组实现。但是二者的基本操作和应用场景实际上是相同的,只是前者的空间利用率操作效率是更高的,我们本文通过介绍普通队列,阐述其在某些场景上的缺陷,进而引出循环队列。为了方便起见,后文中提到的所有普通队列我们都简称为队列。

队列的定义

队列(Queue)是一种先进先出(First In First Out, FIFO)的线性表,允许插入元素的一端称为队尾,允许删除的一端称为队首队尾元素就是队列中的最后一个元素,队首元素就是队列中的第一个元素。例如我们给定一个元素序列 { e_{n}},即 { e_{1},e_{2},e_{3},...,e_{n}},如果用队列来维护这个元素序列,那么元素 e_{1} 就是队首元素,元素 e_{n} 就是队尾元素。在队列的顺序存储结构中,为了方便起见,我们定义两个整型变量 head 和 tail 分别表示队首指针与队尾指针,队首指针指向队首元素在队列中的存放下标,队尾指针指向队尾元素在队列中的存放下标 +1。假设我们在队列中依次添加元素 { a,b,c,d,e},示意图如下。

队列的不足

还是上面示意图中的队列,我们假设一个场景:继续向队列中添加元素 { f,g,h,i,j},示意图如下:

然后依次出队 7 个元素,此时队列就变成了如下图所示的结构。

此时队首指针指向当前队列中的队首元素 h,而队尾指针指向当前队列中队尾元素的下标 +1 处,这个位置同时也是数组最后一个空间处。此时已经不能再入队了,如果再入队,tail 指针指向哪里呢?但是从示意图中我们不难发现,数组中还有 7 个空闲的可以存储元素的空间,这种现象我们称为假溢出

循环队列的定义

不难发现,解决假溢出的方法之一就是从头再来。也就是让数组头尾相接构成环形,于是就可以引出循环队列的定义,我们把队列的这种头尾相接的顺序存储结构称为循环队列。需要指出的是,并不是在物理层面让数组头尾相接,而是逻辑上头尾相接,我们可以使用取模实现此需求。

于是继续我们上述的例子,在将元素 k 入队时,可以将元素存储在当前的 tail 位置上,此时的 tail 指针可以转变为指向下标 0 的位置,这样就不会造成指针指向不明的问题了,如下图。

此时,就又可以按照之前的逻辑继续执行入队操作,假设我们让元素序列 { l,m,n,o,p,q} 依次入队,此时的队列结构如下图所示。

此时队列中用于存放元素的数组还未被充分利用,可以再让一个元素入队,假设我们让元素 r 入队,此时队列结构如下图。此时的队首指针与队尾指针指向同一个位置,也就是 head=tail,意味着队列已满。

我们上面说明的所有情况都是已经存储了元素的队列,那么一个空队列如何表示呢?实际上当初始化一个队列的时候,仍然满足 head=tail,这与队列判满的判断条件一致?我们不妨新定义一个变量 size 用来表示队列中存储元素的数目,那么此时的判空判满操作就可以通过比较 size 与数组长度 length 实现了。

实现循环队列

我们定义名为 ArrayQueue 的类用来实现循环队列。

1. 准备工作

为了为实现类提供统一规范,我们首先定义名为 Queue 的接口。

public interface Queue<E> {

    boolean offer(E value);

    E pull();

    E peek();

    boolean isEmpty();

    boolean isFull();
}

让类 ArrayQueue 实现自定义接口 Queue 和可迭代接口 Iterable,队列中存储的元素数据类型是泛型,由用户自己决定存储的元素类型。

public class ArrayQueue<E> implements Queue<E>, Iterable<E>{}

根据前文的介绍,需要在类中提供几个成员变量,分别表示真正存储元素的数组、队首指针、队尾指针以及队列中存储的元素数量。

    private E[] array;
    private int head;
    private int tail;
    private int size;

2. 构造方法

为构造方法传入用来表示队列规模的参数 capacity,届时队列真正存储元素的数组就是根据此参数创建的,我加上了 @SuppressWarnings 镇压注解以忽略编译器提出的警告信息。

    @SuppressWarnings("unchecked")
    public ArrayQueue(int capacity) {
        array = (E[]) new Object[capacity];
    }

3. 判空 & 判满

在执行入队和出队操作之前,对队列满空状态做判断是很有必要的。由于在类中定义了表示队列存储元素数量的成员变量 size,所以判空与判满操作就可以基于对 size 的判断实现。

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public boolean isFull() {
        return size == array.length;
    }

4. 入队

在执行入队操作之前,需要判断队列是否已满,如果已满,则无法将元素入队;前文中我们提到,队尾指针 tail 用来表示当前队列队尾元素在数组中存储的下标 +1,换句话说,如果需要将一个新元素入队,新的元素就存放在数组中的 tail 位置;在执行了入队操作后,需要更新队尾指针 tail,让其指向新队尾元素在数组中的存放下标 +1但存在一种特殊情况,即此时的队尾指针已经指向了数组的最后一个存储位置 array.length-1,此时需要让 tail 指针回到数组的第一个位置,那么执行 (tail + 1) % array.length,此时元素插入成功,更新表示队列中存储元素数量的 size 变量即可。

    @Override
    public boolean offer(E value) {
        if (isFull())
            return false;
        array[tail] = value;
        tail = (tail + 1) % array.length;
        size++;
        return true;
    }

5. 出队

在执行出队操作之前,需要先判断队列此时是否为空,如果队列为空,说明此时队列中没有存放有效元素,返回 null 即可——这也是我们使用泛型的一个原因,不会因为返回值而产生歧义;如果队列此时不为空,返回队首指针指向的元素,并需要按照同样的取模规则,更新队首指针的指向。

    @Override
    public E pull() {
        if (isEmpty())
            return null;
        E value = array[head];
        head = (head + 1) % array.length;
        size--;
        return value;
    }

6. 获取队首元素

同样的,需要先判断队列是否为空,如果队列为空,返回 null 即可;否则返回队首指针指向的元素即可,与出队操作不同的是,此时不需要再做额外的操作。

    @Override
    public E peek() {
        if (isEmpty())
            return null;
        return array[head];
    }

7. 遍历

与之前文章中对栈的遍历的分析类似,此时的遍历方法是一个伪遍历,按照真正的队列特性,只有将队首元素不断出队,不断更新队首指针才能完整得到队列中存储的所有元素,若按照这个思路,遍历一遍队列,队列中存储的元素也就瓦解了。

    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            int index = head;
            @Override
            public boolean hasNext() {
                return index != tail;
            }

            @Override
            public E next() {
                E value = array[index];
                index = (index + 1) % array.length;
                return value;
            }
        };
    }

final. 完整实现代码 & 单元测试

Java实现

/**
 * @Author Arrebol
 * @Date 2025/1/19 10:12
 * @Project datastructure_algorithm
 * @Description:
 */
public class ArrayQueue<E> implements Queue<E>, Iterable<E> {

    private E[] array;
    private int head;
    private int tail;
    private int size;

    @SuppressWarnings("unchecked")
    public ArrayQueue(int capacity) {
        array = (E[]) new Object[capacity];
    }

    @Override
    public boolean offer(E value) {
        if (isFull())
            return false;
        array[tail] = value;
        tail = (tail + 1) % array.length;
        size++;
        return true;
    }

    @Override
    public E pull() {
        if (isEmpty())
            return null;
        E value = array[head];
        head = (head + 1) % array.length;
        size--;
        return value;
    }

    @Override
    public E peek() {
        if (isEmpty())
            return null;
        return array[head];
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public boolean isFull() {
        return size == array.length;
    }

    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            int index = head;
            @Override
            public boolean hasNext() {
                return index != tail;
            }

            @Override
            public E next() {
                E value = array[index];
                index = (index + 1) % array.length;
                return value;
            }
        };
    }
}
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

import java.util.Iterator;

class ArrayQueueTest {

    private ArrayQueue<Integer> queue;

    @BeforeEach
    void setUp() {
        queue = new ArrayQueue<>(5);
    }

    @Test
    void testOfferAndPull() {
        assertTrue(queue.offer(1));
        assertTrue(queue.offer(2));
        assertTrue(queue.offer(3));

        assertEquals(1, queue.pull());
        assertEquals(2, queue.pull());
        assertEquals(3, queue.pull());

        assertNull(queue.pull());
    }

    @Test
    void testPeek() {
        assertTrue(queue.offer(10));
        assertTrue(queue.offer(20));

        assertEquals(10, queue.peek());
        assertEquals(10, queue.pull());
        assertEquals(20, queue.peek());
    }

    @Test
    void testIsEmpty() {
        assertTrue(queue.isEmpty());

        queue.offer(1);
        assertFalse(queue.isEmpty());

        queue.pull();
        assertTrue(queue.isEmpty());
    }

    @Test
    void testIsFull() {
        assertFalse(queue.isFull());

        for (int i = 0; i < 5; i++)
            queue.offer(i);

        assertTrue(queue.isFull());

        assertFalse(queue.offer(6));
    }

    @Test
    void testIterator() {
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);

        Iterator<Integer> iterator = queue.iterator();
        assertTrue(iterator.hasNext());
        assertEquals(1, iterator.next());
        assertEquals(2, iterator.next());
        assertEquals(3, iterator.next());
        assertFalse(iterator.hasNext());

        queue.pull();
        queue.pull();
        queue.pull();
        iterator = queue.iterator();
        assertFalse(iterator.hasNext());
    }

    @Test
    void testCircularBehavior() {
        for (int i = 0; i < 5; i++) {
            queue.offer(i);
        }

        assertEquals(0, queue.pull());
        assertEquals(1, queue.pull());

        assertTrue(queue.offer(5));
        assertTrue(queue.offer(6));

        assertEquals(2, queue.pull());
        assertEquals(3, queue.pull());
        assertEquals(4, queue.pull());
        assertEquals(5, queue.pull());
        assertEquals(6, queue.pull());

        assertTrue(queue.isEmpty());
    }
}

C语言实现

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

typedef struct {
    int *array;
    int head;
    int tail;
    int size;
    int capacity;
} ArrayQueue;

ArrayQueue* createQueue(int capacity) {
    ArrayQueue* queue = (ArrayQueue*)malloc(sizeof(ArrayQueue));
    queue->array = (int*)malloc(capacity * sizeof(int));
    queue->head = 0;
    queue->tail = 0;
    queue->size = 0;
    queue->capacity = capacity;
    return queue;
}

bool offer(ArrayQueue* queue, int value) {
    if (queue->size == queue->capacity)
        return false;
    queue->array[queue->tail] = value;
    queue->tail = (queue->tail + 1) % queue->capacity;
    queue->size++;
    return true;
}

int pull(ArrayQueue* queue) {
    if (queue->size == 0)
        return -1; // Assuming -1 represents NULL in this context
    int value = queue->array[queue->head];
    queue->head = (queue->head + 1) % queue->capacity;
    queue->size--;
    return value;
}

int peek(ArrayQueue* queue) {
    if (queue->size == 0)
        return -1; // Assuming -1 represents NULL in this context
    return queue->array[queue->head];
}

bool isEmpty(ArrayQueue* queue) {
    return queue->size == 0;
}

bool isFull(ArrayQueue* queue) {
    return queue->size == queue->capacity;
}

void freeQueue(ArrayQueue* queue) {
    free(queue->array);
    free(queue);
}

// Iterator implementation
typedef struct {
    ArrayQueue* queue;
    int index;
    int count;
} QueueIterator;

QueueIterator createIterator(ArrayQueue* queue) {
    QueueIterator iterator;
    iterator.queue = queue;
    iterator.index = queue->head;
    iterator.count = 0;
    return iterator;
}

bool hasNext(QueueIterator* iterator) {
    return iterator->count < iterator->queue->size;
}

int next(QueueIterator* iterator) {
    int value = iterator->queue->array[iterator->index];
    iterator->index = (iterator->index + 1) % iterator->queue->capacity;
    iterator->count++;
    return value;
}

// Unit tests
void testOfferAndPull() {
    ArrayQueue* queue = createQueue(5);
    assert(offer(queue, 1));
    assert(offer(queue, 2));
    assert(offer(queue, 3));

    assert(pull(queue) == 1);
    assert(pull(queue) == 2);
    assert(pull(queue) == 3);

    assert(pull(queue) == -1); // Queue is empty

    freeQueue(queue);
    printf("testOfferAndPull passed\n");
}

void testPeek() {
    ArrayQueue* queue = createQueue(5);
    assert(offer(queue, 10));
    assert(offer(queue, 20));

    assert(peek(queue) == 10);
    assert(pull(queue) == 10);
    assert(peek(queue) == 20);

    freeQueue(queue);
    printf("testPeek passed\n");
}

void testIsEmpty() {
    ArrayQueue* queue = createQueue(5);
    assert(isEmpty(queue));

    offer(queue, 1);
    assert(!isEmpty(queue));

    pull(queue);
    assert(isEmpty(queue));

    freeQueue(queue);
    printf("testIsEmpty passed\n");
}

void testIsFull() {
    ArrayQueue* queue = createQueue(5);
    assert(!isFull(queue));

    for (int i = 0; i < 5; i++)
        assert(offer(queue, i));

    assert(isFull(queue));
    assert(!offer(queue, 6)); // Queue is full

    freeQueue(queue);
    printf("testIsFull passed\n");
}

void testIterator() {
    ArrayQueue* queue = createQueue(5);
    offer(queue, 1);
    offer(queue, 2);
    offer(queue, 3);

    QueueIterator iterator = createIterator(queue);
    assert(hasNext(&iterator));
    assert(next(&iterator) == 1);
    assert(next(&iterator) == 2);
    assert(next(&iterator) == 3);
    assert(!hasNext(&iterator));

    pull(queue);
    pull(queue);
    pull(queue);
    iterator = createIterator(queue);
    assert(!hasNext(&iterator));

    freeQueue(queue);
    printf("testIterator passed\n");
}

void testCircularBehavior() {
    ArrayQueue* queue = createQueue(5);
    for (int i = 0; i < 5; i++) {
        assert(offer(queue, i));
    }

    assert(pull(queue) == 0);
    assert(pull(queue) == 1);

    assert(offer(queue, 5));
    assert(offer(queue, 6));

    assert(pull(queue) == 2);
    assert(pull(queue) == 3);
    assert(pull(queue) == 4);
    assert(pull(queue) == 5);
    assert(pull(queue) == 6);

    assert(isEmpty(queue));

    freeQueue(queue);
    printf("testCircularBehavior passed\n");
}

int main() {
    testOfferAndPull();
    testPeek();
    testIsEmpty();
    testIsFull();
    testIterator();
    testCircularBehavior();

    printf("All tests passed!\n");
    return 0;
}

实现链队列

链队列也是一种特殊的链表,需要指出的是,在队列背景下,元素的插入只能从队列尾部插入,元素的删除只能从队列头部删除,这里直接给出代码实现。

import java.util.Iterator;

/**
 * @Author Arrebol
 * @Date 2025/1/19 14:03
 * @Project datastructure_algorithm
 * @Description:
 */
public class LinkedListQueue<E> implements Queue<E>, Iterable<E> {

    private static class QueueNode<E>{
        E data;
        QueueNode<E> next;

        public QueueNode(E data, QueueNode<E> next) {
            this.data = data;
            this.next = next;
        }
    }

    private QueueNode<E> head = new QueueNode<>(null, null);
    private QueueNode<E> tail = head;
    private final int capacity;
    private int size = 0;

    public LinkedListQueue(int capacity) {
        this.capacity = capacity;
    }

    @Override
    public boolean offer(E value) {
        if (isFull())
            return false;
        tail.next = new QueueNode<>(value, null);
        tail = tail.next;
        size++;
        return true;
    }

    @Override
    public E pull() {
        if (isEmpty())
            return null;
        E value = head.next.data;
        head.next = head.next.next;
        size--;
        if (isEmpty())
            tail = head;
        return value;
    }

    @Override
    public E peek() {
        if (isEmpty())
            return null;
        return head.next.data;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public boolean isFull() {
        return size == capacity;
    }

    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            QueueNode<E> indexNode = head.next;
            @Override
            public boolean hasNext() {
                return indexNode != null;
            }

            @Override
            public E next() {
                E data = indexNode.data;;
                indexNode = indexNode.next;
                return data;
            }
        };
    }
}

总结

本文先通过生活中的例子引出队列的概念,分析普通队列的缺陷进而引出循环队列,对循环队列常用操作进行简要分析,最后用代码实现。本文到此结束,后续将对单调队列优先级队列进行简要分析与实现,敬请期待... ...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值