链表以head 为头节点,head中不存放任何数据,代表链表的开始
链表也是一种线性表,但是链表中的节点是非连续的,非顺序存储的
链表的顺序是通过指针依次链接链表中的每个节点
链表中有一种双向链表,可以从一个节点查看它的下一个节点,也可以查看它的上一个节点

链表的定义

  1. 链表是线性结构
  2. 链表的存储方式是不连续的,是将零散的内存块串联起来
  3. 每个内存块称为节点Node, 除了存储数据,还需要存储指向下一个内存块的指针
  4. 存储方式,以head为头节点,头节点可以不存放任何数据,它是链表的开始,指向链表中的第一个节点,而每个节点都有一个next的向下引用,指向下一个节点,直到最后一个节点,每个节点由两部分组成,包括data, next

链表中常见的名词

  1. 首元节点,指的是链表中存储第一个数据元素的节点
  2. 头节点,它是在首元节点之前设置的一个节点,其指针指向首元节点
  3. 头指针,指向链表中的第一个节点的指针

使用链表的必要性

  1. 使用链表不需要预先知道大小,去申请内存,它实现动态管理内存
  2. 它的增删O(1), 随机访问O(n)
    1. 链表的增删只需要改变指针指向
    2. 随机访问需要每次都从头开始遍历
  3. 数组的缺点
    1. 需要申请连续空间,连续空间不足会申请失败
    2. 大小固定,空间不足需扩容的情况下需要进行数据复制
  4. 链表的缺点
    1. 占用内存过大,需要保存额外的指针
    2. 增删频繁的情况下,需要频繁的申请和释放内存,容器产生内存碎片
  5. 选择
    1. 访问操作 > 增删操作 - 数组
    2. 访问操作 < 增删操作 - 链表

常见的链表

  1. 单向链表
    1. 每个节点只包含一个后继指针
    2. 单链表有两个特殊的节点,即首节点和尾结点
    3. 性能,插入和删除节点 O(1), 查找复杂度O(n)
  2. 单向循环链表
    1. 尾结点的后继指针指向首节点的单向链表
  3. 双向链表
    1. 每个节点有两个节点,指向前一个节点地址prev和下一个节点 的指针next
    2. 首节点的前驱指针和尾结点的后继指针均指向空地址
    3. 性能特点
      1. 和单链表相比,存储相同的数据,需要消耗更多的存储空间,每个节点需要存储两个指针
      2. 插入、删除操作比单链表效率更高,O(1)级别
        1. 给定数据值删除对应的节点,单链表和双向链表,都需要从头到尾进行遍历,从而找到对应的节点进行删除,时间复杂度为O(n)
        2. 给定节点地址删除节点,要进行操作必须找到前驱节点,单链表需要从头到尾,进行遍历直到p.next = q, 时间复杂度为 O(n), 而双向链表可以直接找到前驱节点,时间复杂度为O(1)
      3. 对于一个有序链表,双向链表的按值查询效率要比单向链表高一些,可逆向查找,节省查找时间
  4. 双向循环链表
    1. 首节点的前驱指针指向尾结点,尾结点的后继指针指向首节点

单链表的类实现

  1. class Node {
  2. constructor(el) {
  3. this.el = el
  4. this.next = null
  5. }
  6. }
  7. class LinkedList {
  8. constructor() {
  9. this.head = null
  10. this.length = 0
  11. }
  12. append(el) {
  13. let node = new Node(el)
  14. if (this.length == 0) {
  15. this.head = node;
  16. } else {
  17. let current = this.head;
  18. while(current.next) {
  19. current = current.next;
  20. }
  21. current.next = node;
  22. }
  23. ++this.length
  24. }
  25. insert(el, pos) {
  26. let current;
  27. let previous;
  28. let node = new Node(el)
  29. let index = 0;
  30. if (pos >= 0 && pos <= this.length) {
  31. if (pos == 0) {
  32. node.next = this.head;
  33. this.head = node;
  34. } else {
  35. let current = this.head;
  36. while(index < pos) {
  37. previous = current;
  38. current = current.next;
  39. index++
  40. }
  41. node.next = current;
  42. previous.next = node;
  43. }
  44. this.length++
  45. }
  46. }
  47. remove(el) {
  48. let pos = this.indexOf(el)
  49. return removeAt(pos)
  50. }
  51. removeAt(pos) {
  52. let previous;
  53. let current = this.head;
  54. let index = 0;
  55. if (pos > - 1 && pos < this.length) {
  56. if (pos == 0) {
  57. this.head = current.next;
  58. } else {
  59. while(index < pos) {
  60. previous = current
  61. current = current.next
  62. index++
  63. }
  64. previous.next = current.next;
  65. --this.length;
  66. return true;
  67. }
  68. }
  69. return false;
  70. }
  71. indexOf(el) {
  72. let index = 0;
  73. let current = this.head;
  74. while(current) {
  75. if (current.el == el) return index;
  76. current = current.next;
  77. index++
  78. }
  79. return index;
  80. }
  81. size() {
  82. return this.length;
  83. }
  84. isEmpty() {
  85. return this.length == 0;
  86. }
  87. }
  1. 判断链表中是否有环
    1. const hasCycle = (head) => {
    2. if (head == null || head.next == null) {
    3. return false;
    4. }
    5. let slow = fast = head;
    6. while(fast != null && fast.next != null) {
    7. fast = fast.next.next;
    8. slow = slow.next;
    9. if (fast == slow) {
    10. return true
    11. }
    12. }
    13. return false;
    14. }

小结

  1. 什么是链表
  2. 链表的优缺点, 增删快,查找慢
  3. 单向和双向链表
  4. 如何查找链表中的环

练习题

查找未知长度链表中倒数第K个元素

方法1

  1. 先遍历链表,获取链表长度
  2. 将链表长度 - k 获取要查找的索引值
  3. 再次遍历数组,获取到该索引位置的值;
  4. 若查找不到,返回-1
  5. 缺点,要遍历两次
  1. /*
  2. 输入: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11;
  3. */
  4. function getLengthOfLinkedList(linkedList) {
  5. let length = 0;
  6. let current = linkedList.head;
  7. if (current == null) return length;
  8. while(current) {
  9. length++;
  10. current = current.next;
  11. }
  12. return length;
  13. }
  14. function searchNode(linkedList, k) {
  15. let index = 0;
  16. let resultIndex = getLengthOfLinkedList(linkedList) - k;
  17. let current = linkedList.head
  18. if (current == null) return -1;
  19. while(current) {
  20. if (index === resultIndex) return current.element;
  21. current = current.next;
  22. index++
  23. }
  24. return -1;
  25. }

方法2

  1. 定义两个指针
  2. 先让指针p1 向前移动 k - 1 步
  3. 遍历链表,如果p1 指向了尾结点,此时p2就是我们要找的节点
  4. 只需要遍历一次就可以查找到元素
  1. /*
  2. 输入: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11;
  3. */
  4. function searchNode(linkedList, k) {
  5. let p1 = linkedList.head
  6. let p2 = linkedList.head
  7. for(let i = 0; i < k - 1; i++) {
  8. p1 = p1.next;
  9. }
  10. while(p1.next) {
  11. p1 = p1.next;
  12. p2 = p2.next;
  13. }
  14. return p2.elment;
  15. }
  16. console.log(searchNode(linkedList, 3)) // 9