文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

硬核复刻 Redis 底层双向链表核心实现

2024-11-28 14:17

关注

1.构建双向链表基架

redis中双向链表的节点都是由如下3个元素构成:

所以笔者对于双向链表节点的结构体的定义也按照这套定义复刻:

// Definition of the listNode structure for a doubly linked list
type listNode struct {
 //Node pointing to the previous node of the current node.
 prev *listNode
 //Node pointing to the successor node of the current node.
 next *listNode
 //Record information about the value stored in the current node.
 value *interface{}
}

因为是双向链表,这意味着链表可以从前或者从后进行链表操作,所以双向链表就必须具备如下3个构成部分:

于是我们基于这个思路,再次给出链表的结构体定义:

type list struct {
 //Points to the first node of the doubly linked list
 head *listNode
 //points to the last node of the linked list.
 tail *listNode
 //Record the current length of the doubly linked list
 len int64
}

了解了基础的结构定义,我们就可以编写双向链表初始化的函数listCreate,和redis初始化步骤基本一致,笔者同样是按照:结构体内存空间分配、头尾指针初始化、长度设置为0,然后返回这个双向链表结构体指针的步骤进行操作:

func listCreate() *list {
 //Allocate memory space for the doubly linked list
 var l *list
 l = new(list)
 
 //Initialize the head and tail pointers.
 l.head = nil
 l.tail = nil
 
 //Initialize the length to 0, indicating that the current linked list has no nodes
 l.len = 0
 
 return l
}

实现节点头插和尾后追加

此时,我们就可以实现mini-redis中双向链表的第一个操作——头插法,该操作就是将新插入的节点作为链表的头节点,该操作的步骤比较明确:

完成这些操作之后,维护一下链表长度信息:

基于上述思路笔者给出对应的实现,和原生redis的函数和入参基本一致,传入需要操作的链表和value值之后,将value封装为节点,结合上述的思路将其设置为链表头节点:

func listAddNodeHead(l *list, value *interface{}) *list {
 //Allocate memory for a new node and set its value.
 var node *listNode
 node = new(listNode)
 node.value = value
 //If the length is 0, then both the head and tail pointers point to the new node.
 if l.len == 0 {
  l.head = node
  l.tail = node
 } else {
  //Make the original head node the successor node of the new node, node.
  node.prev = nil
  node.next = l.head
  l.head.prev = node
  l.head = node
 }
 //Maintain the information about the length of the linked list.
 l.len++
 return l
}

与之同理的还有尾插法,无论入参和操作步骤基本一致,唯一区别就是将节点追加到链表末端作为尾节点,读者可以参考笔者的的实现和注释了解操作细节:

func listAddNodeTail(l *list, value *interface{}) *list {
 //Allocate memory for a new node and set its value.
 var node *listNode
 node = new(listNode)
 node.value = value
 //If the length is 0, then both the head and tail pointers point to the new node.
 if l.len == 0 {
  l.head = node
  l.tail = node
 } else {
  //Append the newly added node after the tail node to become the new tail node.
  node.prev = l.tail
  node.next = nil
  l.tail.next = node
  l.tail = node
 }
 //Maintain the information about the length of the linked list.
 l.len++
 return l

}

基于索引定位节点

双向链表支持基于索引的方式查询,例如我们希望查询索引2节点的值,传入index为2,双向链表就会基于索引2这个值跳越两次来到目标节点并返回:

假如我们传入负数,例如负数2,按照语义就是返回倒数第2个节点,双向链表会按照公式(-index)-1得到值1,然后从尾节点跳1步找到目标节点并返回:

对此我们给出相应的源码实现,整体思路和上述说明一致,读者可参考源码和注释了解细节:

func listIndex(l *list, index int64) *listNode {
 var n *listNode
 //"If less than 0, calculate the index value as a positive number n,
 //then continuously jump to the node pointed to by prev based on this positive number n.
 if index < 0 {
  index = (-index) - 1
  n = l.tail

  for index > 0 && n != nil {
   n = n.prev
   index--
  }
 } else {
  //Conversely, walk n steps from the front and reach the target node via next, then return.
  n = l.head
  for index > 0 && n != nil {
   n = n.next
   index--
  }
 }

 return n
}

指定位置插入

双向链表支持在指定元素的前面或者后面插入元素,我们以元素后插入为例,双向链表会将新节点追加到原有节点后面并维护前驱后继指针的信息,插入到指定节点的前方也是同理:

唯一需要注意的就是如果新节点追加到尾节点后面,我们需要将tail指向新节点。追加到头节点同理,我们需要将head指针指向新节点:

对此我们给出listInsertNode的源码实现,读者可参阅思路并结合注释了解实现细节:

func listInsertNode(l *list, old_node *listNode, value *interface{}, after bool) *list {
 //Allocate memory for a new node and set its value.
 var node *listNode
 node = new(listNode)
 node.value = value
 //If after is true, insert the new node after the old node.
 if after {
  node.prev = old_node
  node.next = old_node.next
  //If the old node was originally the tail node, after the modification,
  //make the node the new tail node.
  if l.tail == old_node {
   l.tail = node
  }
 } else {
  //Add the new node before the old node.
  node.next = old_node
  node.prev = old_node.prev
  //If the original node is the head, then set the new node as the head
  if l.head == old_node {
   l.head = node

  }
 }
 //If the node's predecessor node is not empty, then point the predecessor to the node.
 if node.prev != nil {
  node.prev.next = node
 }
 //If the node's successor node is not empty, make this successor point to the node.
 if node.next != nil {
  node.next.prev = node
 }
 //Maintain the information about the length of the linked list.
 l.len++
 return l

}

双向链表节点删除

节点删除则比较简单,传入要被删除的节点指针,让被删除节点d的前驱节点指向d的后继节点,同时让d的后继指向d的前驱:

唯一需要注意的就是以下两种情况:

最后我们断掉被删除节点的前后继指针指向,让go语言垃圾回收自动帮我们完成节点删除即可,这里我们也给出相应的源码实现:

func listDelNode(l *list, node *listNode) {
 //If the predecessor node is not empty,
 //then the predecessor node's next points to the successor node of the node being deleted
 if node.prev != nil {
  node.prev.next = node.next
 } else {
  //If the deleted node is the head node, set the head to point to the next node.
  l.head = node.next
 }

 //If next is not empty, then let next point to the node before the deleted node
 if node.next != nil {
  node.next.prev = node.prev
 } else {
  //If the deleted node is the tail node, make 
  //the node before the deleted node the new tail node.
  l.tail = node.prev
 }
 //help gc
 node.prev = nil
 node.next = nil

 l.len--

}
来源:写代码的SharkChili内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯