当前位置: 首页 > news >正文

数组-环形数组【arr2】

1、什么是环形数组?

环形数组(Circular Array),也称为循环缓冲区或环形队列,是一种在逻辑上将普通数组的“首”和“尾”连接起来形成一个环的数据结构。

想象一下,一个普通的数组就像一条单行道,走到尽头就是终点。而环形数组则像一个环形的跑道,跑到终点后,下一步自然就回到了起点。

:::info

💡 关键理解

环形数组的"环形"特性并不是物理上的,而是逻辑上的。在内存中,数组仍然是线性连续存储的,但我们通过取模运算来实现环形的访问模式。

:::

为什么需要环形数组?
它最主要的应用场景是实现队列。使用普通数组实现队列时,每次出队(删除头部元素)后,为了保持队列的连续性,可能需要移动后续所有元素,这个操作的时间复杂度是 O(n)。而环形数组通过移动指针来代替数据移动,使得入队和出队操作的时间复杂度都能达到 O(1),极大地提高了效率。

2、核心思想:取模运算

环形数组“环”的特性并不是物理上实现的,而是通过一个简单的数学技巧——取模运算 (在编程中通常用 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">%</font> 符号表示) 来在逻辑上实现的。

公式非常简单:<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">下一个位置 = (当前位置 + 1) % 数组容量</font>

让我们来看一个容量为 5 的数组:

  • 索引从 0 开始,正常移动:0 → 1 → 2 → 3 → 4
  • 当指针在索引 4 (末尾) 时,我们想移动到下一个位置:
  • 计算:<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">(4 + 1) % 5</font>
  • 结果:<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">5 % 5 = 0</font>
  • 看!指针神奇地从索引 4 “跳”回了索引 0,形成了一个环。

为什么是取模运算?
取模运算求的是一个数除以另一个数后的余数。当一个数小于除数时,余数就是它本身 (例如
_<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">3 % 5 = 3</font>_)。当一个数等于除数时,余数是 0 (例如 _<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">5 % 5 = 0</font>_)。这个特性完美地契合了我们想要的效果:在数组范围内,索引正常递增;一旦超出范围,就自动归零,回到起点。

3、关键组成部分

一个完整的环形数组需要以下几个关键组件:

  • <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">capacity</font>: 数组的总容量,即数组能存储多少个元素。这个值是固定的。
  • <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head</font>: 头指针,指向队列中的第一个有效元素。当有元素出队时,<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head</font> 指针会移动。
  • <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">tail</font>: 尾指针,指向下一个可插入元素的位置。注意,它指向的是队尾元素的下一个位置,是一个空位。当有新元素入队时,会放在这个位置,然后 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">tail</font> 指针移动。

为什么需要头尾双指针?

简单来说,头尾指针(headtail)是为了以极高的效率(O(1) 时间复杂度)追踪队列的“起点”和“终点”,从而避免了普通数组在实现队列时需要进行的大量数据移动。

让我们从“如果没有头尾指针会怎么样”来思考,就更能理解它们的重要性了。

场景一:使用普通数组实现队列(没有 **head****tail** 指针)

假设我们用一个普通数组 **[A, B, C, D, E]** 来模拟一个队列。

  1. 入队 : 很简单,直接在数组末尾添加元素即可。比如再加入一个 **F**,数组变成 **[A, B, C, D, E, F]**。这个操作很快。
  2. 出队 : 队列的规则是“先进先出”(FIFO),所以我们必须移除第一个元素 **A**
    • 移除 **A** 之后,数组变成了 **[null, B, C, D, E, F]**
    • 问题来了:** 数组的开头出现了一个空位!为了保持队列的紧凑和正确性(我们希望队头永远在索引 **0** 的位置),我们必须将 **B****C****D****E****F**全部向前移动一位**。
    • 移动后的数组是 **[B, C, D, E, F, null]**

这个“全体移动”的操作是性能的瓶颈。如果队列里有成千上万个元素,每一次出队都意味着成千上万次的数据移动。这就是所谓的 O(n) 时间复杂度,效率非常低下。


场景二:使用环形数组(带有 **head****tail** 指针)

现在,我们有了 **head****tail** 指针。它们就像两个智能书签,告诉我们队列的有效数据在哪里。

  • **head**** 指针:永远指向队头元素(下一个要出队的元素)。**
  • **tail**** 指针:永远指向队尾的下一个空位(下一个新元素要插入的位置)。**
  1. 入队 : 在 **tail** 指针的位置放入新元素,然后把 **tail** 指针“向前”移动一位。这个操作只涉及一次写入和一次指针位置的计算,非常快。
  2. 出队 : 取出 **head** 指针指向的元素,然后把 **head** 指针也“向前”移动一位。我们完全不需要移动数组中的任何其他元素! 我们只是改变了 **head** 指针的值,告诉程序:“现在队列的起点在这里了”。

这个“只移动指针”的操作,不管队列里有多少元素,花费的时间都是一样的。这就是 O(1) 时间复杂度,是最高效的方式。

4、核心操作

环形数组最核心的操作就是入队 和出队 。

4.1 入队

当一个新元素需要加入队列时:

  1. 检查队列是否已满。如果满了,则无法添加。
  2. 将新元素放入 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">tail</font> 指针指向的位置。
  3. 更新 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">tail</font> 指针:<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">tail = (tail + 1) % capacity</font>

4.2 出队

当一个元素需要从队列中移除时:

  1. 检查队列是否为空。如果为空,则无元素可取。
  2. 取出 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head</font> 指针指向的元素。
  3. 更新 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head</font> 指针:<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head = (head + 1) % capacity</font>

4.3 状态判断的挑战与解决

一个有趣的问题出现了:我们如何判断队列是“空”还是“满”?

  • 初始状态(空): <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head</font><font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">tail</font> 都指向 0,即 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head == tail</font>
  • 入队一圈后(满): 假设容量为 5,我们依次存入 5 个元素。<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">tail</font> 指针会移动 5 次,经过 0 → 1 → 2 → 3 → 4 → 0。此时,<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">tail</font> 又回到了 0,也出现了 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head == tail</font> 的情况!

这就产生了歧义:<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head == tail</font> 既可以表示队列为空,也可以表示队列为满。为了解决这个问题,通常有两种主流方案:

方案一:使用计数器

引入一个额外的变量 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">count</font> 来记录队列中元素的数量。

  • 入队成功: <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">count++</font>
  • 出队成功: <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">count--</font>
  • 判空: <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">count == 0</font>
  • 判满: <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">count == capacity</font>

优点: 逻辑直观,易于理解,数组空间可以被完全利用。

方案二:牺牲一个存储空间

约定当 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">tail</font> 的下一个位置是 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head</font> 时,队列就为满。

  • 判空: <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head == tail</font>
  • 判满: <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">(tail + 1) % capacity == head</font>

优点: 无需额外变量。

缺点: 数组会浪费一个存储单元,即容量为 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">k</font> 的数组最多只能存 <font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">k-1</font> 个元素。

为什么能解决歧义?
因为我们人为地规定队列满时,
_<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">tail</font>__<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head</font>_ 之间必须有一个空位。这样,_<font style="color:rgb(239, 68, 68);background-color:rgb(243, 244, 246);">head == tail</font>_ 的情况就唯一地代表队列为空。当队列满时,它们永远不会相遇,而是保持一个位置的距离。

5、可视化演示

下面是一个容量为 5 的环形数组(使用计数器方案)。通过输入数字并点击按钮来亲自体验它的工作流程。

juejin

6、Java实现

基于Java(使用计数器方案)实现一个简易的环形数组。

package array;public class CircularQueue {private int capacity;private Object[] queue;private int head;private int tail;private int count;// 构造方法,初始化队列容量public CircularQueue(int capacity) {this.capacity = capacity;this.queue = new Object[capacity];this.head = 0;this.tail = 0;this.count = 0;}// 检查队列是否为空public boolean isEmpty() {return count == 0;}// 检查队列是否已满public boolean isFull() {return count == capacity;}// 向队列中添加一个元素public boolean enqueue(Object item) {if (isFull()) {System.out.println("Error: Queue is full");return false;}// 在尾指针位置放入元素queue[tail] = item;// 使用取模运算更新尾指针,实现环形效果tail = (tail + 1) % capacity;// 元素数量加一count++;return true;}// 从队列中移除一个元素public Object dequeue() {if (isEmpty()) {System.out.println("Error: Queue is empty");return null;}// 从头指针位置取出元素Object item = queue[head];queue[head] = null;  // 可选:将出队位置清空// 使用取模运算更新头指针head = (head + 1) % capacity;// 元素数量减一count--;return item;}// 方便打印队列状态@Overridepublic String toString() {StringBuilder sb = new StringBuilder();sb.append("Queue: [");for (int i = 0; i < capacity; i++) {sb.append(queue[i]);if (i < capacity - 1) {sb.append(", ");}}sb.append("]\n");sb.append("Head: ").append(head).append(", Tail: ").append(tail).append(", Count: ").append(count);return sb.toString();}public static void main(String[] args) {CircularQueue q = new CircularQueue(5);// 插入元素q.enqueue(10);q.enqueue(20);q.enqueue(30);System.out.println(q);// Output:// Queue: [10, 20, 30, null, null]// Head: 0, Tail: 3, Count: 3// 出队System.out.println("Dequeued: " + q.dequeue());  // Dequeued: 10System.out.println(q);// Output:// Queue: [null, 20, 30, null, null]// Head: 1, Tail: 3, Count: 2// 再次添加元素q.enqueue(40);q.enqueue(50);q.enqueue(60);System.out.println(q);// Output:// Queue: [60, 20, 30, 40, 50]// Head: 1, Tail: 1, Count: 5// 再次添加元素 已满 报错q.enqueue(70);  // Error: Queue is full}
}

7、总结

环形数组通过取模运算在逻辑上将线性数组的头尾连接起来,形成一个环。它通过维护 <font style="color:rgb(71, 85, 105);">head</font><font style="color:rgb(71, 85, 105);">tail</font> 两个指针,并巧妙地利用一个空闲单元来区分“满”和“空”状态,从而实现了高效的、固定大小的队列结构。

它的核心优势在于空间复用O(1)的时间复杂度,避免了数据的频繁移动。

http://www.dtcms.com/a/540058.html

相关文章:

  • 打开AI黑箱:SHAP让医疗AI决策更清晰的编程路径
  • 营销型商务网站wordpress html5 主题
  • 知识掘金者:API+Dify工作流,开启「深度思考」的搜索革命
  • 《道德经》第三十八章
  • 企业网站管理系统湖南岚鸿搜狗网站入口
  • 汕头网站推广制作怎么做济南源聚网络公司
  • webrtc代码走读(十)-QOS-Sender Side BWE原理
  • 102-Spring AI Alibaba RAG Pgvector 示例
  • 【刷机分享】解决K20Pro刷入PixelOS后“网络连接”受限问题(附详细ADB命令)
  • Rust 语言入门基础教程:从环境搭建到 Cargo 工具链
  • 【Linux】HTTPS协议
  • node.js 和npm 搭建项目基本流程
  • 【STM32】PWR电源控制
  • 做网页局域网站点配置wordpress仿简书主题
  • 《Linux篇》进程控制——进程创建(写时拷贝)、进程终止(退出码,exit,_exit)
  • 【MATLAB 数据分析学习指南】
  • Android PDF 操作 - AndroidPdfViewer 显示 PDF 异常清单(数据为 null、数据为空、PDF 文件损坏、非 PDF 文件)
  • 界面控件DevExpress WPF v25.2预览 - 模板工具包全新升级
  • 【音视频】H264中的SPS和PPS
  • ThinkPHP6 集成TCP长连接 GatewayWorker
  • TMap的查询
  • SpringCloud--Sleuth 解析
  • 【C++:继承和多态】多态加餐:面试常考——多态的常见问题11问
  • 零基础新手小白快速了解掌握服务集群与自动化运维(十五)Redis模块-哨兵集群
  • 今日Cortex-M3/M4研究总结
  • 2014吉林省赛题解 | CCUT应用OJ题解——Sign in
  • 涿州网站建设推广浙江建筑信息网站
  • 前端性能优化实战指南:从首屏加载到用户体验的全面提升
  • 【OPENGL ES 3.0 学习笔记】第十一天:glDrawArrays和glDrawElements
  • Linux入门1(2/2)