【GoLang】3、基于虚拟头尾节点快速实现双向链表
1、背景
鸽了很久很久的一个东西了…现在有时间了,还是来把它补齐一下。
双向链表,在 C++ 中经常使用到:
- [C++系列] 54. list的介绍及使用
习题练手:
- 146. LRU 缓存
Github 代码仓库,欢迎 start!:
- go-blog list
2、代码实现
双向链表(Doubly Linked List)实现说明
基于 Go 语言实现了一个带有虚拟头结点和虚拟尾结点的双向链表(Doubly Linked List),旨在简化边界条件处理并提高代码可读性与健壮性。
- 使用虚拟头结点(dummyHead)和虚拟尾结点(dummyTail),避免边界情况下的空指针判断。
- 所有插入、删除操作均统一处理,无需额外判断是否为头尾节点。
- 提供正向和反向遍历接口,返回切片便于测试和调试。
- 插入节点提取公共方法,简化代码。
- 提供完整的单元测试,保证代码质量。
2.1、v1 版本(未提取公共插入代码)
package v1import ("fmt"
)// ListNode 链表节点数据结构定义
type ListNode struct {prev, next *ListNode // 前驱节点、后继节点val any // 数据
}// List 双向链表数据结构定义
type List struct {head, tail *ListNode // 头节点、尾节点len int // 链表长度
}// Next ListNode Next 获取下一个节点,如果不存在,则返回nil
func (l *ListNode) Next() *ListNode {if l != nil && l.next != nil {return l.next}return nil
}// Prev ListNode Prev 获取前一个节点,如果不存在,则返回nil
func (l *ListNode) Prev() *ListNode {if l != nil && l.prev != nil {return l.prev}return nil
}// NewList List 创建一个链表
// 使用虚拟结点写法,创建虚拟头结点、尾结点,避免后续各类边界判断
func NewList() *List {dummyHead := &ListNode{val: nil}dummyTail := &ListNode{val: nil}dummyHead.next = dummyTaildummyTail.prev = dummyHeadlist := &List{head: dummyHead,tail: dummyTail,len: 0,}return list
}// PushBack 尾插
func (l *List) PushBack(v any) {node := &ListNode{val: v,}node.next = l.tailnode.prev = l.tail.prevl.tail.prev.next = nodel.tail.prev = nodel.len++
}// PushFront 头插
func (l *List) PushFront(v any) {node := &ListNode{val: v,}node.prev = l.headnode.next = l.head.nextl.head.next.prev = nodel.head.next = nodel.len++
}// InsertAfter 在 at 节点后面插入节点
// at 不存在,则直接插入末尾
func (l *List) InsertAfter(at *ListNode, v any) {if at == nil {l.PushBack(v)return}node := &ListNode{val: v,}node.prev = atnode.next = at.nextnode.prev.next = nodenode.next.prev = nodel.len++
}// InsertBefore 在 at 节点前面插入节点
// at 不存在则直接插入开头
func (l *List) InsertBefore(at *ListNode, v any) {if at == nil {l.PushFront(v)return}node := &ListNode{val: v,}node.next = atnode.prev = at.prevat.prev.next = nodeat.prev = nodel.len++
}// Find 查找节点
// 仅返回第一个匹配的节点
func (l *List) Find(v any) *ListNode {// 跳过虚拟头结点、跳过虚拟尾结点for node := l.head.next; node.Next() != nil; node = node.next {if node.val == v {return node}}return nil
}// Remove 删除节点
func (l *List) Remove(v any) {node := l.Find(v)if node != nil {node.prev.next = node.nextnode.next.prev = node.prevnode.next = nilnode.prev = nill.len--}
}// GetLen 获取长度
func (l *List) GetLen() int {return l.len
}// ForEach 正向遍历
func (l *List) ForEach() []any {res := make([]any, 0)for node := l.head.next; node.Next() != nil; node = node.next {res = append(res, node.val)}return res
}// ForReverse 反向遍历
func (l *List) ForReverse() []any {res := make([]any, 0)for node := l.tail.prev; node.Prev() != nil; node = node.prev {res = append(res, node.val)}return res
}// Print 打印
func (l *List) Print() {res := make([]any, 0)// 跳过虚拟头结点、跳过虚拟尾结点for node := l.head.next; node.Next() != nil; node = node.next {res = append(res, node.val)}fmt.Println(res)
}
2.2、v2 版本(提取公共插入代码)
package v2import ("fmt"
)// ListNode 链表节点数据结构定义
type ListNode struct {prev, next *ListNode // 前驱节点、后继节点val any // 数据
}// List 双向链表数据结构定义
type List struct {head, tail *ListNode // 头节点、尾节点len int // 链表长度
}// Next ListNode Next 获取下一个节点,如果不存在,则返回nil
func (l *ListNode) Next() *ListNode {if l != nil && l.next != nil {return l.next}return nil
}// Prev ListNode Prev 获取前一个节点,如果不存在,则返回nil
func (l *ListNode) Prev() *ListNode {if l != nil && l.prev != nil {return l.prev}return nil
}// NewList List 创建一个链表
// 使用虚拟结点写法,创建虚拟头结点、尾结点,避免后续各类边界判断
func NewList() *List {dummyHead := &ListNode{val: nil}dummyTail := &ListNode{val: nil}dummyHead.next = dummyTaildummyTail.prev = dummyHeadlist := &List{head: dummyHead,tail: dummyTail,len: 0,}return list
}// insert 插入的通用方法
func (l *List) insert(prev, next *ListNode, v any) {node := &ListNode{val: v, prev: prev, next: next}prev.next = nodenext.prev = nodel.len++
}// PushBack 尾插
func (l *List) PushBack(v any) {l.insert(l.tail.prev, l.tail, v)
}// PushFront 头插
func (l *List) PushFront(v any) {l.insert(l.head, l.head.next, v)
}// InsertAfter 在 at 节点后面插入节点
// at 不存在,则直接插入末尾
func (l *List) InsertAfter(at *ListNode, v any) {if at == nil {l.PushBack(v)return}l.insert(at, at.next, v)
}// InsertBefore 在 at 节点前面插入节点
// at 不存在则直接插入开头
func (l *List) InsertBefore(at *ListNode, v any) {if at == nil {l.PushFront(v)return}l.insert(at.prev, at, v)
}// Find 查找节点
// 仅返回第一个匹配的节点
func (l *List) Find(v any) *ListNode {// 跳过虚拟头结点、跳过虚拟尾结点for node := l.head.next; node != l.tail; node = node.next {if node.val == v {return node}}return nil
}// Remove 删除节点
func (l *List) Remove(v any) {node := l.Find(v)if node != nil {node.prev.next = node.nextnode.next.prev = node.prevnode.next = nilnode.prev = nill.len--}
}// GetLen 获取长度
func (l *List) GetLen() int {return l.len
}// ForEach 正向遍历
func (l *List) ForEach() []any {res := make([]any, 0)for node := l.head.next; node != l.tail; node = node.next {res = append(res, node.val)}return res
}// ForReverse 反向遍历
func (l *List) ForReverse() []any {res := make([]any, 0)for node := l.tail.prev; node != l.head; node = node.prev {res = append(res, node.val)}return res
}// Print 打印
func (l *List) Print() {res := make([]any, 0)// 跳过虚拟头结点、跳过虚拟尾结点for node := l.head.next; node != l.tail; node = node.next {res = append(res, node.val)}fmt.Println(res)
}
3、总结
v2 版本确实是简单易写不易错,且引入了 头尾 的虚拟节点后,在 头插、尾插、头删、尾删 等边界情况下都需要再特殊考虑了。
同时值得一提的是,Go 官方库在一开始其实就支持 list这个数据结构了,且代码写的也十分精简,优雅。
几乎是在 v2 版本上的进一步升级,十分值得学习和使用!