1) 可无限循环的 UI 组件
- 轮播图 / 走马灯 / 图片预览器:上一张/下一张永远存在,首尾相接,当前结点左右各 O(1) 找到相邻项,删/插也 O(1)。
- 步骤器(Stepper)/ 引导页:到最后一步再“下一步”回到第一步(或反之)。
- 滚轮选择器(Picker) :如 iOS 风格时间/月份滚轮,1→12→1 循环。
2) 键盘可达性(a11y)与焦点管理
- Roving tabindex:一组可聚焦元素(如菜单项、Tag 列表、照片墙),按左右/上下键在集合中循环移动焦点(末项再→回到首项)。
- Tab 环导航:定制化的组件内 tab 次序闭环,避免“走丢”。
3) 播放与展示队列
- 音乐/视频播放列表的“列表循环” :播放到末尾继续从头播,支持在当前曲目前后 O(1) 插入/删除。
- 幻灯片放映:自动播放场景(定时器 tick 指向下一个结点)。
4) 固定容量与“最近记录”
- 撤销/重做历史(定长) :可用环形结构或环形缓冲(ring buffer)保存最近 N 次操作,指针前后移动像在环上游走。
- 日志/通知面板的滑动窗口:保留最近 N 条,老的自动被覆盖(更常见用数组环形缓冲,但链式也可)。
5) 数据虚拟化/无限滚动的“回环模型”
- 日期/时间选择:星期、月份、本地化短列表天然首尾相连。
- 虚拟列表中的“循环”呈现:比如一个可循环的相对定位项(更常见是索引取模,但概念与环一致)。
6) 交互/游戏回合
- 回合制玩家/指针轮转:当前玩家指针顺时针传递,支持中途插入/移除玩家。
7) 缓存策略内部实现
- LRU/LFU 的链表骨架:很多 LRU 实现用双向(有时带哨兵的环形)链表维护访问顺序,移到表头/淘汰表尾都是 O(1)。
什么时候“真的”用环形链表(而不是数组取模)?
- 需要频繁对“当前元素附近”做插入/删除,并保持常数时间,而不是整体移动。
- 节点可能频繁在中间位置移动(比如播放队列中从任意位置移除、插入下一首)。
- 想用“哨兵结点”把边界情况(首/尾)统一掉,逻辑更干净。
纯“循环访问”的场景(轮播、Picker、虚拟列表)在前端通常用数组 + 取模就能高效搞定;JS 引擎对数组非常优化,遍历/索引通常比链表更快。
实现要点与小坑
- 哨兵结点:用一个不存业务数据的
head,令head.next指首,head.prev指尾,能把插入/删除边界简化成统一操作。 - 双向链表更实用:UI 往往需要“上一项/下一项”,用
prev/next都是 O(1)。 - 内存与 GC:链表节点多、对象分散,访问局部性不如连续数组;JS 下要注意意外引用导致的对象“留存”。
- 可达性/可访问性:做循环焦点时记得配合
aria-activedescendant或 roving tabindex 模式,避免与浏览器默认 Tab 顺序冲突。 - 性能权衡:对滚动渲染/动画等热路径,先用数组与索引取模基线实现,量化后再决定是否切链表。
一个极简的环双链 + 哨兵示意(思路)
class Node { constructor(val){ this.val=val; this.prev=this; this.next=this; } }
class Ring {
constructor(){ this.head = new Node(null); } // 哨兵
insertAfter(node, val){
const n = new Node(val);
n.next = node.next; n.prev = node;
node.next.prev = n; node.next = n;
return n;
}
remove(node){
node.prev.next = node.next; node.next.prev = node.prev;
}
first(){ return this.head.next === this.head ? null : this.head.next; }
last(){ return this.head.prev === this.head ? null : this.head.prev; }
}
在轮播中,可把“当前页”持有一个指针 cur,左右切换仅做 cur = cur.next/prev,插入/删除幻灯片不影响其它节点。