场景: 我们有100个对象需要存储, 最简单的方式是直接使用Array
var store = [100]*T{}
// 存储
store[0] = OBJ
// 变量
for i := range store
我不知道需要存多少个, 可能100 也可能200 或者更多喃?
使用切片:
var store = []*T{}
// 追加
store = append(store, *T)
// 访问
for i := range store
如果有人要插队, 比如我要在 index2 和 index3 中间插入一个元素?
使用切片来处理插入
var store = []*T{OJB1, OJB2, OBJ3}
// 此时我需要把OJB4 插入到 OJB2 和 OBJ3直接
// 1. 获取之前的和或者之后的
newStore = store = []*T{}
// 拷贝第一部分
newStore = append(newStore, store[:2])
// 2. 添加插入的元素
newStore = append(newStore, OBJ4)
// 3. 拷贝之后的
newStore = append(newStore, store[2:])
// 当然你可以简洁的写成这样
newStore = append(store[:2]..., OBJ4, store[2:]...)
我们可以看到 我们插入的位置,会导致后面的数据都往后移动一位, 假设我们有1亿条数据, 我要插入到第一个, 可以想象这个效率会有多低(如果你的场景是从一头那数据,那么栈更高效)
同理删除也一样, 那我们如何改进?
思考:
由于slice底层使用数组存储元素, 受限于数组的内存结构(连续内存空间):
A + SIZE --> B + SIZE --> C + SIZE ---> D ....
那如果我们插入一个元素理论上应该是啥样的最直接, 比如插入F 到 AB之间: 修改A指向 F, 然后F 指向 B
A ---> B --> C --> D
A + P-> F +P -> B C D
我们使用一个指向来描述前面一个元素和后面一个元素的关系, 这就是链表
链表是一种数据结构,和数组不同,链表并不需要一块连续的内存空间,它通过「指针」将一组零散的内存块串联起来使用
在底层结构上,单向链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点”(Node)。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。如下图所示,我们把这个记录下个结点地址的指针叫作后继指针 next
基础版的功能很简单:
测试用例
func TestListBasic(t *testing.T) {
l := list.NewIntList(1)
l.AddNode(list.NewIntNode(2))
l.AddNode(list.NewIntNode(3))
l.AddNode(list.NewIntNode(4))
l.Traverse(func(n *list.Node){
...
})
}
接下来我们开始设计我们链表:
func NewIntNode(v int) *Node {
return &Node{Value: v}
}
// 定义节点
type Node struct {
// 存储你需要存储的数据
Value interface{}
// 下一跳的指向
Next *Node
}
func NewIntList(headValue int) *List {
// 链表的头
head := &Node{Value: headValue}
return &List{
head: head,
}
}
type List struct {
head *Node
}
func (l *List) AddNode(n *Node) {
// 我需要找到尾节点
next := l.head
for next.Next != nil {
next = next.Next
}
// 修改为节点
next.Next = n
}
func (l *List) Traverse(fn func(n *Node)) {
n := l.head
for n.Next != nil {
fn(n)
n = n.Next
}
fn(n)
fmt.Println()
}
到次我们基础版功能实现完成:
1 --> 2 --> 3 --> 4
但是我们的链表还不支持插入和删除操作
完善版的需求1:
func (l *List) InsertAfter(after, current *Node) error {
return nil
}
func (l *List) Remove(current *Node) error {
return nil
}
func PrintNode(n *list.Node) {
if n.Next != nil {
fmt.Printf("%d --> ", n.Value)
} else {
fmt.Printf("%d", n.Value)
}
}
func TestListRich(t *testing.T) {
l := list.NewIntList(1)
n2, n3, n4 := list.NewIntNode(2), list.NewIntNode(3), list.NewIntNode(4)
l.AddNode(n2)
l.AddNode(n3)
l.AddNode(n4)
l.Traverse(PrintNode)
// 测试插入
l.InsertAfter(n2, list.NewIntNode(20))
l.Traverse(PrintNode)
}
这个简单了,我的亲, 掐指一算就出来了(5分钟)
func (l *List) InsertAfter(after, current *Node) error {
// 假设我们已经插入,他数据结构应该是啥样的
// after --> current --> after_next
// 保存下之前的after next
afterNext := after.Next
// 插入,修改指向
after.Next = current
current.Next = afterNext
return nil
}
由于单向链表的情况下(单方向的), 要获取之前Node 需要遍历整个List, 非常的低效, 所以以下功能并没有在单向链表中实现:
要高效解决这个问题,我们需要一个Previos指针, 直接知道前一个Node的信息, 而不是遍历
单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。而双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点
从上图中可以看出来,双向链表相比于单向链表需要额外增加一个空间来保存前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然多了一个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性
接下来我们改造单向链表为双向链表
// 定义节点
type Node struct {
// 存储你需要存储的数据
Value interface{}
// 下一跳的指向
Next *Node
// 上一跳
Prev *Node
}
补充我们需要实现的功能:
func (l *List) InsertBefore(before, current *Node) error {
return nil
}
func (l *List) Remove(current *Node) error {
return nil
}
添加测试用例
func TestListWithPre(t *testing.T) {
l := list.NewIntList(1)
n2, n3, n4 := list.NewIntNode(2), list.NewIntNode(3), list.NewIntNode(4)
l.AddNode(n2)
l.AddNode(n3)
l.AddNode(n4)
l.Traverse(PrintNode)
// 测试插入
l.InsertAfter(n2, list.NewIntNode(20))
l.Traverse(PrintNode)
// 测试删除
l.Remove(n3)
l.Traverse(PrintNode)
}
这个简单了,我的亲, 掐指一算就出来了(5分钟)
这样行不行?
func (l *List) InsertBefore(before, current *Node) error {
// 假设我们已经插入,他数据结构应该是啥样的
// before --> current --> before_next
// 保存下之前的before next
beforeNext := before.Prev
// 插入,修改指向
before.Next = current
current.Next = beforeNext
return nil
}
func (l *List) Remove(current *Node) error {
// before --> current --> before_next
prev := current.Prev
prev.Next = current.Next
return nil
}
我们忘了添加元素时,我还需要出来prev指针的指向:
func (l *List) AddNode(n *Node) {
...
// 补充Previos指针
n.Prev = next
}
func (l *List) InsertAfter(after, current *Node) error {
...
// 补充Previos指针
// after <-- current <-- after_next
current.Prev = after
afterNext.Prev = current
return nil
}
func (l *List) InsertBefore(before, current *Node) error {
...
// 补充Previos指针
// before <-- current <-- before_next
current.Prev = before
beforeNext.Prev = current
return nil
}
再次测试我们的功能可以正常了
1 --> 2 --> 3 --> 4
1 --> 2 --> 20 --> 3 --> 4
1 --> 2 --> 20 --> 4
其实就是将头节点的前趋指针指向尾节点,将尾节点的后驱指针指向头节点
他也是一个环了(Ring), 可以正向转和反向转, 这个生活中的场景就很多了
让我们把双向变成循环:
// 变身成环
func (l *List) ChangeToRing() {
// 我需要找到尾节点
next := l.head
for next.Next != nil {
next = next.Next
}
head, tail := l.head, next
// head --> tail
head.Prev = tail
// head <-- tail
tail.Next = head
}
func (l *List) Traverse(fn func(n *Node)) {
loopCount := 1
n := l.head
for n.Next != nil {
// 最多循环5伦
if loopCount > 5 {
return
}
fn(n)
n = n.Next
if n == l.head {
loopCount++
}
}
fn(n)
fmt.Println()
}
测试
func TestListRing(t *testing.T) {
l := list.NewIntList(1)
n2, n3, n4 := list.NewIntNode(2), list.NewIntNode(3), list.NewIntNode(4)
l.AddNode(n2)
l.AddNode(n3)
l.AddNode(n4)
l.Traverse(PrintNode)
// 测试插入
l.ChangeToRing()
l.Traverse(PrintNode)
fmt.Println()
l.InsertAfter(n3, list.NewIntNode(100))
l.Traverse(PrintNode)
}
结果:
1 --> 2 --> 3 --> 4
1 --> 2 --> 3 --> 4 --> 1 --> 2 --> 3 --> 4 --> 1 --> 2 --> 3 --> 4 --> 1 --> 2 --> 3 --> 4 --> 1 --> 2 --> 3 --> 4 -->
1 --> 2 --> 3 --> 100 --> 4 --> 1 --> 2 --> 3 --> 100 --> 4 --> 1 --> 2 --> 3 --> 100 --> 4 --> 1 --> 2 --> 3 --> 100 --> 4 --> 1 --> 2 --> 3 --> 100 --> 4 -->
至于轮播同学们可以自己调整Value数据结构及可
数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读
数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,我觉得这也是它与数组最大的区别
总体而言, 对于很多数据来讲选择原则大致如下:
我们的链表还有很多不完善的地方, 比如
但是原理里面应该都会了, 生产使用请使用标准库
go 的标准库 contianer/list 就是完整的双向链表实现, 如果是循环链表可以使用container/ring
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。