该用 *ring.ring 仅当窗口固定、头尾增删且不 resize;它不支持随机访问、len() 为 o(n),存数值会丢失下标致无法判断过期,内存和性能均劣于切片+双指针。

什么时候该用 *ring.Ring 而不是切片或队列
滑动窗口类问题(比如求固定长度子数组最大值、字符频次统计、流式数据去重)常被误以为必须手写双端队列。但 Go 标准库的 *ring.Ring 本质是循环链表,不支持随机访问、不能直接索引、也没有内置长度缓存——它只适合「头尾增删 + 遍历一圈」这种极简场景。
如果你需要频繁 ring.Len() 或者反复从中间取值,立刻放弃;*ring.Ring 的 Len() 是 O(n) 的遍历计数,不是 O(1)。
- ✅ 适合:窗口大小固定、只在头尾做 pop/push、且生命周期内 ring 不 resize
- ❌ 不适合:需要
ring[i]、要动态扩容、依赖len(ring)做条件判断 - ⚠️ 注意:
ring.Next()和ring.Prev()返回的是指针,不是值;赋值前务必检查是否为nil
ring.Do() 遍历时修改元素容易 panic
ring.Do() 是唯一安全遍历方式,但它内部会调用你传入的函数,而这个函数里如果调用了 ring.Move()、ring.Unlink() 或直接改 ring.Value,就可能破坏遍历状态——尤其当 ring 元素少于 2 个时,Next() 可能绕回自身,导致无限循环或空指针解引用。
- 别在
Do回调里调用ring.Link()或ring.Unlink() - 若需边遍历边更新值,直接改
r.Value即可(前提是类型可寻址) - 想删某个节点?先跳出
Do,用ring.Unlink(1)配合手动定位,或者换用切片+双指针
实现滑动窗口最小值:为什么不用 *ring.Ring 存数值而要存索引
标准单调队列解法要求维护一个「递增的下标序列」,靠下标间接访问原数组并判断窗口过期。如果用 *ring.Ring 直接存数值,就丢失了位置信息,无法判断当前队首是否还在窗口内。
立即学习“go语言免费学习笔记(深入)”;
正确做法是让 ring 存 int 类型的下标,每次新元素进来时:
- 从尾部弹出所有对应值 ≥ 当前值的下标(维持单调递增)
- 把当前下标
PushBack - 从头部弹出所有 ≤
i - k的下标(窗口左边界移动) - 此时
ring.Next().Value就是窗口最小值对应下标
示例关键片段:
for i := range nums {
// 清理过期下标
for r.Len() > 0 && r.Value.(int) <= i-k {
r = r.Unlink(1).Next()
}
// 维持单调性
for r.Len() > 0 && nums[r.Prev().Value.(int)] >= nums[i] {
r = r.Prev().Unlink(1)
}
r = r.PushBack(i)
}性能和内存开销比切片高不少
*ring.Ring 每个节点都是堆上分配的结构体(含两个指针 + interface{}),哪怕只存一个 int,也要额外 24 字节(64 位系统)。而滑动窗口常用切片方案(如 []int + 两个 int 索引)全程栈操作,无 GC 压力。
- ring 构建成本高:初始化
ring.New(n)会创建 n 个节点,不是懒加载 - GC 友好度差:大量短生命周期 ring 容易触发高频清扫
- 真实项目中,除非你在写底层 ring 缓冲区抽象,否则滑动窗口优先选切片+双指针
真正容易被忽略的是:ring 的「循环」特性在滑动窗口里几乎没被用到——我们只关心头尾,不关心它能不能绕回来。所谓「循环链表」在这里是个冗余能力。










