用 JavaScript 实现链表操做 - 04 Insert Nth Node

TL;DR

插入第 N 个节点。系列目录见 前言和目录javascript

需求

实现一个 insertNth() 方法,在链表的第 N 个索引处插入一个新节点。java

insertNth() 能够当作是 01 Push & Build List 中的 push() 函数的更通用版本。给定一个链表,一个范围在 0..length 内的索引号,和一个数据,这个函数会生成一个新的节点并插入到指定的索引位置,并始终返回链表的头。node

insertNth(1 -> 2 -> 3 -> null, 0, 7) === 7 -> 1 -> 2 -> 3 -> null)
insertNth(1 -> 2 -> 3 -> null, 1, 7) === 1 -> 7 -> 2 -> 3 -> null)
insertNth(1 -> 2 -> 3 -> null, 3, 7) === 1 -> 2 -> 3 -> 7 -> null)

若是索引号超出了链表的长度,函数应该抛出异常。git

实现这个函数容许使用第一个 kata 中的 push 方法。github

递归版本

让咱们先回忆一下 push 函数的用处,指定一个链表的头和一个数据,push 会生成一个新节点并添加到链表的头部,并返回新链表的头。好比:segmentfault

push(null, 23) === 23 -> null
push(1 -> 2 -> null, 23) === 23 -> 1 -> 2 -> null

如今看看 insertNth ,假设函数方法签名是 insertNth(head, index, data) ,那么有两种状况:函数

若是 index === 0 ,则等同于调用 push 。实现为 push(head, data)测试

若是 index !== 0 ,咱们能够把下一个节点当成子链表传入 insertNth ,并让 index 减一。insertNth 的返回值必定是个链表,咱们把它赋值给 head.next 就行。这就是一个递归过程。若是此次递归的 insertNth 完不成任务,它会继续递归到下一个节点,直到 index === 0 的最简单状况,或 head 为空抛出异常(索引过大)。ui

完整代码实现为:code

function insertNth(head, index, data) {
  if (index === 0) return push(head, data)
  if (!head) throw 'invalid argument'
  head.next = insertNth(head.next, index - 1, data)
  return head
}

循环版本

若是能理解递归版本的 head.next = insertNth(...) ,那么循环版本也不难实现。不一样的是,在循环中咱们遍历到 index 的前一个节点,而后用 push 方法生成新节点,并赋值给前一个节点的 next 属性造成一个完整的链表。

完整代码实现以下:

function insertNthV2(head, index, data) {
  if (index === 0) return push(head, data)

  for (let node = head, idx = 0; node; node = node.next, idx++) {
    if (idx + 1 === index) {
      node.next = push(node.next, data)
      return head
    }
  }

  throw 'invalid argument'
}

这里有一个边界状况要注意。由于 insertNth 要求返回新链表的头。根据 index 是否为 0 ,这个新链表的头多是生成的新节点,也可能就是老链表的头 。这点若是写进 for 循环就不可避免有 if/else 的返回值判断。因此咱们把 index === 0 的状况单独拿出来放在函数顶部。这个边界状况并不是没法归入循环中,咱们下面介绍的一个技巧就与此有关。

循环版本 - dummy node

在以前的几个 kata 里,咱们提到循环能够更好的容纳边界状况,由于一些条件判断都能写到 for 的头部中去。但这个例子的边界状况是返回值不一样:

  1. 若是 index === 0 ,返回新节点 。

  2. 若是 index !== 0 ,返回 head 。新节点会被插入 head 以后的某个节点链条中。

如何解决这个问题呢,咱们能够在 head 前面再加入一个节点(数据任意,通常赋值 null)。这个节点称为 dummy 节点。这样一来,无论新节点插入到哪里,dummy.next 均可以引用到修改后的链表。

代码实现以下,注意 return 的不一样。

function insertNthV3(head, index, data) {
  const dummy = push(head, null)

  for (let node = dummy, idx = 0; node; node = node.next, idx++) {
    if (idx === index) {
      node.next = push(node.next, data)
      return dummy.next
    }
  }

  throw 'invalid argument'
}

dummy 节点是不少链表操做的经常使用技巧,虽然在这个 kata 中使用 dummy 节点的代码量并无变少,但这个技巧在后续的一些复杂 kata 中会很是有用。

参考资料

Codewars Kata
GitHub 的代码实现
GitHub 的测试

相关文章
相关标签/搜索