• 数据结构与算法 -- 链表


    一、单链表

            链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。这个记录下个结点地址的指针叫作后继指针 next。习惯性地把第一个结点叫作头结点,把最后一个结点叫作尾结点。其中,头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址 NULL,表示这是链表上最后一个结点。

                       

             在链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的。所以,在链表中插入和删除一个数据是非常快速的。针对链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以对应的时间复杂度是 O(1)。

             链表要想随机访问第 k 个元素,就没有数组那么高效了。因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点。随机访问的时间复杂度为O(n)。

    二、循环链表

             循环链表是一种特殊的单链表。实际上,循环链表也很简单。它跟单链表唯一的区别就在尾结点。单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。

                    

             循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。比如著名的约瑟夫问题。

    三、双向链表

            双向链表,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。

                 

            删除给定指针指向的结点,删除某个结点 q 需要知道其前驱结点,而单链表并不支持直接获取前驱结点,所以,为了找到前驱结点,我们还是要从头结点开始遍历链表,时间复杂度为O(n);双向链表中的结点已经保存了前驱结点的指针,不需要像单链表那样遍历,双向链表只需要在 O(1) 的时间复杂度内就搞定了。

             用空间换时间的设计思想。当内存空间充足的时候,如果我们更加追求代码的执行速度,我们就可以选择空间复杂度相对较高、但时间复杂度相对很低的算法或者数据结构。相反,如果内存比较紧缺,比如代码跑在手机或者单片机上,这个时候,就要反过来用时间换空间的设计思路。

     四、双向循环链表

                 

    五、链表VS数组

            数组需要一块连续的内存空间来存储,对内存的要求比较高。如果我们申请一个 100MB 大小的数组,当内存中没有连续的、足够大的存储空间时,即便内存的剩余总可用空间大于 100MB,仍然会申请失败。链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用,所以如果我们申请的是 100MB 大小的链表,根本不会有问题。

             数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容。

            如果你的代码对内存的使用非常苛刻,那数组就更适合你。因为链表中的每个结点都需要消耗额外的存储空间去存储一份指向下一个结点的指针,所以内存消耗会翻倍。而且,对链表进行频繁的插入、删除操作,还会导致频繁的内存申请和释放,容易造成内存碎片。

    六、链表代码注意事项

    1、理解指针或引用的含义

             将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。

     2、警惕指针丢失和内存泄漏

            插入结点时,一定要注意操作的顺序;  删除链表结点时,也一定要记得手动释放内存空间。

    3、 利用哨兵简化实现难度

    4、 重点留意边界条件处理

             如果链表为空时,代码是否能正常工作?

             如果链表只包含一个结点时,代码是否能正常工作?

             如果链表只包含两个结点时,代码是否能正常工作?

             代码逻辑在处理头结点和尾结点的时候,是否能正常工作?

    七、常见链表算法题

    1、实现单链表、循环链表、双向链表,支持增删操作

    1. package main
    2. import "fmt"
    3. //节点结构体
    4. type Node struct {
    5. data int
    6. next *Node
    7. }
    8. //链表类
    9. type List struct {
    10. head *Node
    11. length int
    12. }
    13. //创建节点
    14. func CreateNode(data int) *Node{
    15. return &Node{data, nil}
    16. }
    17. //创建链表
    18. func NewList() *List{
    19. return &List{
    20. &Node{0,nil},
    21. 0,
    22. }
    23. }
    24. //打印链表
    25. func (l *List)DispList(){
    26. pre := l.head.next
    27. for i := 1;i <= l.length;i++{
    28. fmt.Printf("%d ", pre.data)
    29. pre = pre.next
    30. }
    31. fmt.Printf("\n")
    32. }
    33. //求链表长度
    34. func (l *List)ListLength()int{
    35. return l.length
    36. }
    37. //获得索引值
    38. func (l *List)GetElem(index int) int {
    39. pre := l.head
    40. var j int
    41. for j < index && pre != nil {
    42. j++
    43. pre = pre.next
    44. }
    45. if pre == nil{
    46. return -1
    47. } else{
    48. return pre.data
    49. }
    50. }
    51. //获得索引值
    52. func (l *List)LocateElem(data int) int{
    53. pre := l.head
    54. var i int = 0
    55. for pre != nil && pre.data != data{
    56. pre = pre.next
    57. i++
    58. }
    59. if pre == nil{
    60. return -1
    61. }else{
    62. return i
    63. }
    64. }
    65. //插入节点
    66. func (l *List)ListInsert(i, data int)bool{
    67. if i < 1{
    68. return false
    69. }
    70. pre := l.head
    71. var j int = 0
    72. for j < i-1 && pre != nil{
    73. j++
    74. pre = pre.next
    75. }
    76. if pre == nil {
    77. return false
    78. }else{
    79. s := CreateNode(data)
    80. s.next = pre.next
    81. pre.next = s
    82. return true
    83. }
    84. }
    85. //删除节点
    86. func (l *List)ListDelete(i int)bool{
    87. if i < 1 {
    88. return false
    89. }
    90. pre := l.head
    91. j := 0
    92. for j < i-1 && pre != nil{
    93. j++
    94. pre = pre.next
    95. }
    96. if pre == nil{
    97. return false
    98. }else{
    99. q := pre.next
    100. if q == nil{
    101. return false
    102. }
    103. pre.next = q.next
    104. return true
    105. }
    106. }
    107. //判断链表是否为空
    108. func (l *List)IsEmpty()bool{
    109. if l.head.next == nil{
    110. return true
    111. }
    112. return false
    113. }
    114. //创建链表
    115. func (l *List)CreateList(arr []int, n int)bool{
    116. r := l.head
    117. for i := 0;i < n;i++{
    118. s := CreateNode(arr[i])
    119. r.next = s
    120. r = s
    121. }
    122. r.next = nil
    123. l.length = n
    124. return true
    125. }
    126. //查找最后一个值为x的节点序号
    127. func (l *List)FindLastValue(x int)int{
    128. pre := l.head.next
    129. i := 0
    130. j := 0
    131. for pre != nil{
    132. i++
    133. if pre.data == x{
    134. pre = pre.next
    135. j = i
    136. }
    137. }
    138. return j
    139. }
    140. func main(){
    141. arr := []int{1,2,3,4,5}
    142. list := NewList()
    143. list.CreateList(arr, 5)
    144. fmt.Printf("list length = %d\n", list.ListLength())
    145. list.DispList()
    146. fmt.Printf("%d\n", list.GetElem(2))
    147. fmt.Printf("%d\n", list.LocateElem(4))
    148. list.ListInsert(2,8)
    149. list.DispList()
    150. list.ListDelete(2)
    151. list.DispList()
    152. fmt.Printf("%d\n", list.FindLastValue(4))
    153. }
    1. package main
    2. import "fmt"
    3. type DNode struct {
    4. data int
    5. next *DNode
    6. prev *DNode
    7. }
    8. type DoubleLinkList struct {
    9. length int
    10. head *DNode
    11. tail *DNode
    12. }
    13. func CreateDNode(data int) *DNode{
    14. return &DNode{
    15. data,
    16. nil,
    17. nil,
    18. }
    19. }
    20. func CreateLinkList() *DoubleLinkList{
    21. return &DoubleLinkList{
    22. 0,
    23. &DNode{
    24. 0,
    25. nil,
    26. nil,
    27. },
    28. &DNode{
    29. 0,
    30. nil,
    31. nil,
    32. },
    33. }
    34. }
    35. //初始化链表
    36. func (l *DoubleLinkList)InitList(){
    37. l.length = 0
    38. l.head = nil
    39. l.tail = nil
    40. }
    41. //获得链表长度
    42. func (l *DoubleLinkList)GetListLength()int{
    43. return l.length
    44. }
    45. //正向打印链表
    46. func (l *DoubleLinkList)PrintList(){
    47. pre := l.head.next
    48. for i:= 0;i
    49. fmt.Printf("%d ", pre.data)
    50. pre = pre.next
    51. }
    52. fmt.Printf("\n")
    53. }
    54. //反向打印链表
    55. func (l *DoubleLinkList)PrintListReverse(){
    56. pre := l.tail.prev
    57. for i:= 0;i
    58. fmt.Printf("%d ", pre.data)
    59. pre = pre.prev
    60. }
    61. fmt.Printf("\n")
    62. }
    63. //尾插法创建链表
    64. func (l *DoubleLinkList)CreateDoubleLinkList(arr []int, n int){
    65. p := l.head
    66. for i := 0;i < n;i++{
    67. r := CreateDNode(arr[i])
    68. p.next = r
    69. r.prev = p
    70. p = r
    71. }
    72. p.next = l.tail
    73. l.tail.prev = p
    74. l.length = n
    75. }
    76. //获得链表中的值的索引值
    77. func (l *DoubleLinkList)LocateElem(data int)int{
    78. pre := l.head.next
    79. indexOfData := 0
    80. for pre != l.tail && pre.data != data{
    81. pre = pre.next
    82. indexOfData++
    83. }
    84. if pre == l.tail{
    85. return -1
    86. }else{
    87. return indexOfData
    88. }
    89. }
    90. //取得索引值为index的节点的值
    91. func (l *DoubleLinkList)GetElem(index int)int{
    92. i := 0
    93. pre := l.head.next
    94. for pre != nil && i < index{
    95. pre = pre.next
    96. i++
    97. }
    98. if pre == l.tail{
    99. return -1
    100. }else{
    101. return pre.data
    102. }
    103. }
    104. //插入节点
    105. func (l *DoubleLinkList)InsertList(index ,data int)bool{
    106. if index < 1 || index >l.length{
    107. return false
    108. }
    109. s := CreateDNode(data)
    110. p := l.head.next
    111. for i := 0;i < index;i++{
    112. p = p.next
    113. i++
    114. }
    115. s.next = p.next
    116. s.next.prev = s
    117. p.next = s
    118. s.prev = p
    119. l.length++
    120. return true
    121. }
    122. //删除节点
    123. func (l *DoubleLinkList)DeleteList(index int)bool{
    124. if index < 1 || index >l.length{
    125. return false
    126. }
    127. p := l.head.next
    128. for i := 0;i < index;i++{
    129. p = p.next
    130. i++
    131. }
    132. s := p.next
    133. p.next = s.next
    134. s.next.prev = p
    135. p = nil
    136. l.length--
    137. return true
    138. }
    139. func main(){
    140. arr := []int{1, 2, 3, 4, 5, 6, 7, 8}
    141. list := CreateLinkList()
    142. list.CreateDoubleLinkList(arr, 8)
    143. fmt.Printf("%d\n", list.GetListLength())
    144. list.PrintList()
    145. list.PrintListReverse()
    146. fmt.Printf("%d\n", list.LocateElem(6))
    147. fmt.Printf("%d\n", list.GetElem(3))
    148. list.InsertList(2,19)
    149. list.PrintList()
    150. list.DeleteList(2)
    151. list.PrintList()
    152. }

     

    2、单链表反转

    3、 链表中环的检测

    4、两个有序的链表合并

    5、 删除链表倒数第 n 个结点

    6、求链表的中间结点

    7、LRU缓存淘汰算法 

    8、约瑟夫问题 

    声明:本文参考极客时间《数据结构与算法之美》

  • 相关阅读:
    5.5线程同步机制类封装及线程池实现
    虚拟机安装openEuler/MobaXterm工具登录系统
    社交媒体数据恢复:Facebook
    ChatGPT 在机器学习中的应用
    Vue el-table 重置按钮设计模板
    【机器学习】无监督学习中的协同过滤算法(推荐系统)
    HCIE Datacom考试-MAC 地址基础及漂移技术汇总!
    MongoDB基础之查询文档操作
    【 java 面向对象】多态性以及 instanceof 关键字的使用
    【Linux】使用ntpdate同步
  • 原文地址:https://blog.csdn.net/u012967763/article/details/126452961