【GoLang】3、基于虚拟头尾节点快速实现双向链表

1、背景

鸽了很久很久的一个东西了…现在有时间了,还是来把它补齐一下。

双向链表,在 C++ 中经常使用到:

习题练手:

Github 代码仓库,欢迎 start!

2、代码实现

双向链表(Doubly Linked List)实现说明

基于 Go 语言实现了一个带有虚拟头结点和虚拟尾结点的双向链表(Doubly Linked List),旨在简化边界条件处理并提高代码可读性与健壮性。

  • 使用虚拟头结点(dummyHead)和虚拟尾结点(dummyTail),避免边界情况下的空指针判断。
  • 所有插入、删除操作均统一处理,无需额外判断是否为头尾节点。
  • 提供正向和反向遍历接口,返回切片便于测试和调试。
  • 插入节点提取公共方法,简化代码。
  • 提供完整的单元测试,保证代码质量。

2.1、v1 版本(未提取公共插入代码)

package v1

import (
	"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 = dummyTail
	dummyTail.prev = dummyHead

	list := &List{
		head: dummyHead,
		tail: dummyTail,
		len:  0,
	}
	return list
}

// PushBack 尾插
func (l *List) PushBack(v any) {
	node := &ListNode{
		val: v,
	}
	node.next = l.tail
	node.prev = l.tail.prev
	l.tail.prev.next = node
	l.tail.prev = node
	l.len++
}

// PushFront 头插
func (l *List) PushFront(v any) {
	node := &ListNode{
		val: v,
	}

	node.prev = l.head
	node.next = l.head.next
	l.head.next.prev = node
	l.head.next = node
	l.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 = at
	node.next = at.next
	node.prev.next = node
	node.next.prev = node
	l.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 = at
	node.prev = at.prev
	at.prev.next = node
	at.prev = node
	l.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.next
		node.next.prev = node.prev
		node.next = nil
		node.prev = nil
		l.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 v2

import (
	"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 = dummyTail
	dummyTail.prev = dummyHead

	list := &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 = node
	next.prev = node
	l.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.next
		node.next.prev = node.prev
		node.next = nil
		node.prev = nil
		l.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 版本上的进一步升级,十分值得学习和使用!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值