1. 题目描述

设计链表的实现。您可以选择使用单链表双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

示例:

  1. MyLinkedList linkedList = new MyLinkedList();
  2. linkedList.addAtHead(1);
  3. linkedList.addAtTail(3);
  4. linkedList.addAtIndex(1,2); //链表变为1-> 2-> 3
  5. linkedList.get(1); //返回2
  6. linkedList.deleteAtIndex(1); //现在链表是1-> 3
  7. linkedList.get(1); //返回3

提示:

  • 所有val值都在 [1, 1000] 之内。
  • 操作次数将在 [1, 1000] 之内。
  • 请不要使用内置的 LinkedList 库。

    2. 解题思路

    对于这道题目,我们可以在初始化时,给链表前面加一个哑结点,这样,在链表的操作过程中,可以减少很多指针为空的判断。下面来看详细的结题过程。

(1)链表初始化

初始化假头链表,首先需要 new 出一个链表结点,并且让链表的 dummy 和 tail 指针都指向它,代码如下:

  1. var listNode = function(val) {
  2. this.val = val
  3. this.next = null
  4. };
  5. var MyLinkedList = function() {
  6. this.dummy = new listNode()
  7. this.tail = this.dummy
  8. this.length = 0
  9. };

初始化完成后,链表已经有了一个结点,但是此时,整个链表中还没有任何数据。因此,对于一个空链表,就是指已经初始化好的带假头链表。

虽然 head 和 tail 初始化完成之后,都指向null。但是这两者有一个特点,叫“动静结合”:

  • 静:head 指针初始化好以后,永远都是静止的,再也不会动了。
  • 动:tail 指针在链表发生变动的时候,就需要移动调整。

    (2)尾部追加结点

    尾部添加新结点操作只有两步,代码如下:

    1. MyLinkedList.prototype.addAtTail = function(val) {
    2. // 尾部添加一个新结点
    3. this.tail.next = new listNode(val)
    4. // 移动tail指针
    5. this.tail = this.tail.next;
    6. // 链表长度+1
    7. this.length ++
    8. };

    带假头的链表初始化之后,可以保证 tail 指针永远非空,因此,也就可以直接去修改 tail.next 指针,省略掉了关于 tail 指针是否为空的判断。

    (3)头部插入结点

    需要插入的新结点为 p,插入之后,新结点 p 会成为第一个有意义的数据结点。通过以下 3 步可以完成头部插入:

  • 新结点 p.next 指向 dummy.next;

  • dummy.next 指向 p;
  • 如果原来的 tail 指向 dummy,那么将 tail 指向 p。

对应的代码如下:

  1. MyLinkedList.prototype.addAtHead = function(val) {
  2. // 生成一个结点,存放的值为val
  3. const p = new listNode(val)
  4. // 将p.next指向第一个结点
  5. p.next = this.dummy.next;
  6. // dummy.next指向新结点,使之变成第一个结点
  7. this.dummy.next = p;
  8. // 注意动静结合原则,添加结点时,注意修改tail指针。
  9. if (this.tail == this.dummy) {
  10. this.tail = p;
  11. }
  12. // 链表长度+1
  13. this.length ++
  14. };

这段代码有趣的地方在于,当链表为空的时候,它依然是可以工作的。因为虽然链表是空的,但是由于有 dummy 结点的存在,代码并不会遇到空指针。

注意:**如果链表添加了结点,或者删除了结点,一定要记得修改 tail 指针。如果忘了修改,那么就不能正确地获取链表的尾指针,从而错误地访问链表中的数据。

(4)查找结点

在查找索引值为 index(假设 index 从 0 开始)的结点时,你需要注意,大多数情况下,返回指定结点前面的一个结点 prev 更加有用。好处有以下两个方面:

  • 通过 prev.next 就可以访问到想要找到的结点,如果没有找到,那么 prev.next 为 null;
  • 通过 prev 可以方便完成后续操作,比如在 target 前面 insert 一个新结点,或者将 target 结点从链表中移出去。

因此,如果要实现 get 函数,应该先实现一个 getPrevNode 函数:

  1. MyLinkedList.prototype.getPreNode = function(index) {
  2. if (index < 0 || index >= this.length) {
  3. return -1;
  4. }
  5. // 初始化front与back,分别一前一后
  6. let front = this.dummy.next
  7. let back = this.dummy
  8. // 在查找的时候,front与back总是一起走
  9. for (let i = 0; i < index && front != null; i++) {
  10. back = front;
  11. front = front.next;
  12. }
  13. // 把back做为prev并且返回
  14. return back
  15. };

有了假头的帮助,这段查找代码就非常健壮了,可以处理以下 2 种情况:

  • 如果 target 在链表中不存在,此时 prev 返回链表的最后一个结点;
  • 如果为空链表(空链表指只有一个假头的链表),此时 prev 指向 dummy。也就是说,返回的 prev 指针总是有效的。

借助 getPrevNode 函数来实现 get 函数:

  1. MyLinkedList.prototype.get = function(index) {
  2. // 获取链表中第 index 个结点的值。如果索引无效,则返回-1。
  3. // index从0开始
  4. if (index < 0 || index >= this.length) {
  5. return -1;
  6. }
  7. // 因为getPrevNode总是返回有效的结点,所以可以直接取值。
  8. return this.getPreNode(index).next.val
  9. };

(5)插入指定位置之前

插入指定位置的前面,有 4 个需求。

  • 如果 index 大于链表长度,则不会插入结点。
  • 如果 index 等于链表的长度,则该结点将附加到链表的末尾。
  • 如果 index 小于 0,则在头部插入结点。
  • 否则在指定位置前面插入结点。

其中,Case 1~3 较容易处理。可以直接写。重点在于 Case 4。现在已经有了 getPrevNode() 函数,就可以比较容易地写出 Case 4 的代码,思路如下:

  • 使用 getPrevNode() 函数拿到 index 之前的结点 pre;
  • 在 pre 的后面添加一个新结点。

以下是具体的 Case 1~4 的操作过程:

  1. MyLinkedList.prototype.addAtIndex = function(index, val) {
  2. if (index > this.length) {
  3. // Case 1.如果 index 大于链表长度,则不会插入结点。
  4. return;
  5. } else if (index == this.length) {
  6. // Case 2.如果 index 等于链表的长度,则该结点将附加到链表的末尾。
  7. this.addAtTail(val);
  8. } else if (index <= 0) {
  9. // Case 3. 如果index小于0,则在头部插入结点。
  10. this.addAtHead(val);
  11. } else {
  12. // Case 4.
  13. // 得到index之前的结点pre
  14. const pre = this.getPreNode(index);
  15. // 在pre的后面添加新结点
  16. const p = new listNode(val);
  17. p.next = pre.next;
  18. pre.next = p;
  19. // 链表长度+1
  20. this.length++;
  21. }
  22. };

(6)删除节点

删除结点操作是给定要删除的下标 index(下标从 0 开始),删除的情况分 2 种:

  • 如果 index 无效,那么什么也不做;
  • 如果 index 有效,那么将这个结点删除。

上面这 2 种情况中,Case 1 比较容易处理,相对要麻烦一些的是 Case 2。要删除 index 结点,最好是能找到它前面的结点。有了前面的结点,再删除后面的结点就容易多了。不过已经有了 getPrevNode 函数,所以操作起来还是很简单的。

以下是具体的操作过程

  1. MyLinkedList.prototype.deleteAtIndex = function(index) {
  2. // Case 1. 如果index无效,那么什么也不做。
  3. if (index < 0 || index >= this.length) {
  4. return;
  5. }
  6. // Case 2. 删除index结点
  7. // step 1. 找到index前面的结点
  8. const pre = this.getPreNode(index);
  9. // step 2. 如果要删除的是最后一个结点,那么需要更改tail指针
  10. if (this.tail == pre.next) {
  11. this.tail = pre;
  12. }
  13. // step. 3 进行删除操作。并修改链表长度。
  14. pre.next = pre.next.next;
  15. this.length--;
  16. };

3. 代码实现

  1. /**
  2. * Initialize your data structure here.
  3. */
  4. var listNode = function(val) {
  5. this.val = val
  6. this.next = null
  7. };
  8. var MyLinkedList = function() {
  9. this.dummy = new listNode()
  10. this.tail = this.dummy
  11. this.length = 0
  12. };
  13. /**
  14. * Get the value of the index-th node in the linked list. If the index is invalid, return -1.
  15. * @param {number} index
  16. * @return {number}
  17. */
  18. MyLinkedList.prototype.getPreNode = function(index) {
  19. if (index < 0 || index >= this.length) {
  20. return -1;
  21. }
  22. // 初始化front与back,分别一前一后
  23. let front = this.dummy.next
  24. let back = this.dummy
  25. // 在查找的时候,front与back总是一起走
  26. for (let i = 0; i < index && front != null; i++) {
  27. back = front;
  28. front = front.next;
  29. }
  30. // 把back做为prev并且返回
  31. return back
  32. };
  33. MyLinkedList.prototype.get = function(index) {
  34. if (index < 0 || index >= this.length) {
  35. return -1;
  36. }
  37. return this.getPreNode(index).next.val
  38. };
  39. /**
  40. * Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list.
  41. * @param {number} val
  42. * @return {void}
  43. */
  44. MyLinkedList.prototype.addAtHead = function(val) {
  45. // 生成一个结点,存放的值为val
  46. const p = new listNode(val)
  47. // 将p.next指向第一个结点
  48. p.next = this.dummy.next;
  49. // dummy.next指向新结点,使之变成第一个结点
  50. this.dummy.next = p;
  51. // 注意动静结合原则,添加结点时,注意修改tail指针。
  52. if (this.tail == this.dummy) {
  53. this.tail = p;
  54. }
  55. // 链表长度+1
  56. this.length ++
  57. };
  58. /**
  59. * Append a node of value val to the last element of the linked list.
  60. * @param {number} val
  61. * @return {void}
  62. */
  63. MyLinkedList.prototype.addAtTail = function(val) {
  64. // 尾部添加一个新结点
  65. this.tail.next = new listNode(val)
  66. // 移动tail指针
  67. this.tail = this.tail.next;
  68. // 链表长度+1
  69. this.length ++
  70. };
  71. /**
  72. * Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted.
  73. * @param {number} index
  74. * @param {number} val
  75. * @return {void}
  76. */
  77. MyLinkedList.prototype.addAtIndex = function(index, val) {
  78. if (index > this.length) {
  79. // Case 1.如果 index 大于链表长度,则不会插入结点。
  80. return;
  81. } else if (index == this.length) {
  82. // Case 2.如果 index 等于链表的长度,则该结点将附加到链表的末尾。
  83. this.addAtTail(val);
  84. } else if (index <= 0) {
  85. // Case 3. 如果index小于0,则在头部插入结点。
  86. this.addAtHead(val);
  87. } else {
  88. // Case 4.
  89. // 得到index之前的结点pre
  90. const pre = this.getPreNode(index);
  91. // 在pre的后面添加新结点
  92. const p = new listNode(val);
  93. p.next = pre.next;
  94. pre.next = p;
  95. // 链表长度+1
  96. this.length++;
  97. }
  98. };
  99. /**
  100. * Delete the index-th node in the linked list, if the index is valid.
  101. * @param {number} index
  102. * @return {void}
  103. */
  104. MyLinkedList.prototype.deleteAtIndex = function(index) {
  105. // Case 1. 如果index无效,那么什么也不做。
  106. if (index < 0 || index >= this.length) {
  107. return;
  108. }
  109. // Case 2. 删除index结点
  110. // step 1. 找到index前面的结点
  111. const pre = this.getPreNode(index);
  112. // step 2. 如果要删除的是最后一个结点,那么需要更改tail指针
  113. if (this.tail == pre.next) {
  114. this.tail = pre;
  115. }
  116. // step. 3 进行删除操作。并修改链表长度。
  117. pre.next = pre.next.next;
  118. this.length--;
  119. };
  120. /**
  121. * Your MyLinkedList object will be instantiated and called as such:
  122. * var obj = new MyLinkedList()
  123. * var param_1 = obj.get(index)
  124. * obj.addAtHead(val)
  125. * obj.addAtTail(val)
  126. * obj.addAtIndex(index,val)
  127. * obj.deleteAtIndex(index)
  128. */

4. 提交结果

image.png