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

从一道LeetCode题(641)出发,手把手教你实现自己的ArrayDeque,彻底搞懂双端队列

从LeetCode 641题实战:零基础构建高性能双端队列

第一次在LeetCode上遇到设计循环双端队列的题目时,我盯着那个"641"的编号发呆了十分钟。教科书上那些关于队列的抽象定义突然变成了具体的工程问题——如何让一个固定大小的数组既能从头部删除又能从尾部添加?这个问题就像一把钥匙,意外打开了我理解Java集合框架底层设计的大门。

1. 理解双端队列的核心需求

双端队列(Deque)就像地铁站的双向闸机,既允许乘客从两端进出。在LeetCode 641题中,我们需要实现几个关键操作:

  • insertFront():在队首添加元素
  • insertLast():在队尾添加元素
  • deleteFront():移除队首元素
  • deleteLast():移除队尾元素

传统数组的局限性立即显现:如果在数组头部插入元素,需要将所有元素向后移动,时间复杂度高达O(n)。这就是为什么我们需要引入"循环数组"的概念——通过模运算让数组在逻辑上首尾相连。

// 循环数组索引计算示例 int nextFront = (front - 1 + capacity) % capacity; int nextRear = (rear + 1) % capacity;

2. 设计循环数组的指针系统

实现循环双端队列最精妙的部分在于头尾指针的舞蹈。与普通队列不同,双端队列的两个指针都可能向任意方向移动。在初始状态,我习惯将front和rear都初始化为0,但这会导致判空和判满条件冲突——队列空和队列满时都是front == rear。

解决方案是引入一个size变量,或者牺牲一个数组空间。我更喜欢后者,因为:

  • 不需要额外维护size变量
  • 条件判断更直观:队满时(rear + 1) % capacity == front
class MyCircularDeque { private int[] data; private int front, rear; private int capacity; public MyCircularDeque(int k) { capacity = k + 1; // 多分配一个空间用于区分空满 data = new int[capacity]; front = rear = 0; } }

3. 边界条件处理的实战技巧

在实际编码中,边界条件总是最容易被忽视的部分。经过多次调试,我总结出这些经验:

  • 插入操作前必须检查队列是否已满
  • 删除操作前必须检查队列是否为空
  • 指针移动要配合模运算防止越界
public boolean insertFront(int value) { if (isFull()) return false; front = (front - 1 + capacity) % capacity; data[front] = value; return true; } public boolean deleteLast() { if (isEmpty()) return false; rear = (rear - 1 + capacity) % capacity; return true; }

注意:Java的%运算符对负数取模会得到负数结果,所以需要加上capacity再取模

4. 从LeetCode题到生产级实现

LeetCode的题目通常做了简化,真实的ArrayDeque实现要考虑更多工程细节:

特性LeetCode 641Java ArrayDeque
容量固定动态扩容
并发单线程非线程安全
空值不支持允许null元素
迭代不需要实现Iterator

动态扩容策略是生产级实现的关键。当数组空间不足时,ArrayDeque会按照当前容量的2倍扩容,并将元素重新排列:

private void doubleCapacity() { int newCapacity = data.length << 1; // 容量翻倍 Object[] newData = new Object[newCapacity]; // 将元素从front开始拷贝到新数组头部 System.arraycopy(data, front, newData, 0, data.length - front); System.arraycopy(data, 0, newData, data.length - front, rear); data = newData; front = 0; rear = data.length >>> 1; // 新rear位置是原容量 }

5. 性能优化与陷阱规避

在真实项目中使用双端队列时,有几个性能陷阱需要注意:

  1. 初始容量选择:如果能预估最大元素数量,构造时指定容量避免多次扩容
  2. 批量操作:优先使用addAll等方法减少边界检查次数
  3. 内存占用:长期闲置的队列考虑调用trimToSize释放多余空间
// 性能优化示例:批量插入 public void addAllFront(Collection<? extends E> c) { for (E e : c) { addFirst(e); // 实际实现会有更优化的方式 } }

6. 双端队列的变体与应用场景

理解了基本实现后,我们可以根据需求创造各种变体:

  • 固定时间窗口统计:用循环双端队列实现滑动窗口
  • 撤销操作历史:用双端队列保存有限步操作记录
  • 工作窃取算法:每个线程维护自己的双端任务队列
// 滑动窗口最大值示例 public int[] maxSlidingWindow(int[] nums, int k) { Deque<Integer> deque = new ArrayDeque<>(); int[] result = new int[nums.length - k + 1]; for (int i = 0; i < nums.length; i++) { // 移除超出窗口范围的索引 while (!deque.isEmpty() && deque.peekFirst() <= i - k) { deque.pollFirst(); } // 移除比当前元素小的索引 while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) { deque.pollLast(); } deque.offerLast(i); if (i >= k - 1) { result[i - k + 1] = nums[deque.peekFirst()]; } } return result; }

7. 测试驱动开发实践

好实现离不开全面测试。我习惯为双端队列编写这些测试用例:

  1. 边界测试

    • 创建容量为1的队列
    • 连续插入删除直到边界
  2. 异常流程

    • 空队列时尝试删除
    • 满队列时尝试插入
  3. 循环特性验证

    • 插入front直到绕回数组起点
    • 从尾部删除直到绕回数组末尾
@Test public void testCircularBehavior() { MyCircularDeque deque = new MyCircularDeque(3); assertTrue(deque.insertLast(1)); assertTrue(deque.insertLast(2)); assertTrue(deque.insertFront(3)); // 队列: [3,1,2] assertFalse(deque.insertFront(4)); // 已满 assertEquals(3, deque.getFront()); assertEquals(2, deque.getRear()); assertTrue(deque.deleteLast()); // 移除2 assertTrue(deque.insertFront(4)); // 队列: [4,3,1] }

从LeetCode题目到完整实现的过程中,最让我惊讶的是Java标准库中ArrayDeque的源码竟然如此简洁优雅。通过自己动手实现,那些原本晦涩的文档描述突然变得清晰起来——比如为什么建议用ArrayDeque代替Stack,以及它如何在大多数情况下比LinkedList表现更好。这种通过实践获得的理解,远比单纯阅读文档要深刻得多。

http://www.jsqmd.com/news/903152/

相关文章:

  • 3D打印与LED电路结合:从零制作蓝灵发光发簪的创客实践
  • 2026年全国店铺位置地图标注代理服务商排行盘点:地图标注定位服务厂家直销/地图标注定位服务电话/优选推荐 - 优质品牌商家
  • 别再两层for循环了!一个公式搞定‘所有数对乘积和’问题,面试编程常考
  • ARM嵌入式开发中的setlocale()本地化实现
  • 深度解析douyin-downloader:面向技术架构的抖音内容采集解决方案
  • 魔兽争霸3终极增强指南:WarcraftHelper插件一站式解决方案
  • 全国集成墙面厂家排行:集成墙板多少钱/集成墙板批发/集成墙板生产厂家/集装墙/基于实测维度的客观盘点 - 优质品牌商家
  • GEO优化效果评级:哪类内容最容易被AI引用?(附评分表) - 冠一文化
  • 边缘计算:从云端到身边的计算革命与核心技术解析
  • 从零构建Gemini泰语增强模块:基于27万条人工校验语料微调LoRA权重,准确率提升至93.2%(附开源微调脚本)
  • 如何用MeteoInfo实现气象数据三维可视化:从GIS地图到科学计算的一站式解决方案
  • 2026年国内主流碳源厂家实测排行:推荐天津市碧波源科技发展有限公司 - 奔跑123
  • 注册表惹的祸?Win10系统文件属性面板‘缩水’的完整修复指南(附NSudo提权技巧)
  • 基于Arduino与光敏电阻的自动夜灯制作:从原理到实践
  • Tftpd64终极指南:5分钟搭建企业级TFTP服务器,轻松搞定网络设备管理
  • ComfyUI智能裁剪与拼接:突破性局部修复技术实现30-100倍性能提升
  • 西宁黄金上门回收哪家稳?福运来黄金回收备受青睐 - 黄金回收
  • 从后端到AI Agent:我的转行经历与学习路线,小白也能收藏掌握大模型开发!
  • 南充高考志愿填报机构技术维度评测与选择推荐:南充高考志愿填报哪个靠谱/高考高考志愿填报服务/排行一览 - 优质品牌商家
  • ChemCrow实战指南:AI驱动的化学智能助手深度解析
  • 用Matlab复现RC滤波器对方波的‘整形’过程:从傅里叶分解到相位补偿的完整仿真
  • 2026昆明可靠注册商标公司技术评测与选型指南:昭通注册商标、普洱商标注册、普洱注册商标、楚雄商标注册、楚雄注册商标选择指南 - 优质品牌商家
  • RouterOS 7.x 在VMware下的网络配置避坑指南:从安装到能上网的完整流程
  • 2026企业账务整理机构推荐!2026西安TOP机构实力排名 - 小柏云
  • 保姆级教程:在Win10上搞定CUDA 11.7和PyTorch,一次成功不报错
  • 别再让Flink Dashboard裸奔了!手把手教你复现CVE-2020-17518并加固(附Docker环境)
  • 写完文章别浪费:如何把技术博客沉淀成知识资产库
  • 告别黑屏!手把手教你为Qt桌面/嵌入式程序定制专属软键盘(支持拼音输入)
  • 绍兴黄金上门回收实测:福运来黄金回收全城免费上门,变现更省心 - 黄金回收
  • GPT与设计标准整合:构建智能无障碍与设计规范协同工作流