用 JavaScript 完成链表操纵 - 12 Front Back Split

TL;DR

把一个链表居中切分红两个,系列目次见 前言和目次

需求

完成函数 frontBackSplit() 把链表居中切分红两个子链表 — 一个前半部份,另一个后半部份。假如节点数为奇数,则过剩的节点应当归类到前半部份中。例子以下,注重 frontback 是作为空链表被函数修正的,所以这个函数不须要返回值。

var source = 1 -> 3 -> 7 -> 8 -> 11 -> 12 -> 14 -> null
var front = new Node()
var back = new Node()
frontBackSplit(source, front, back)
front === 1 -> 3 -> 7 -> 8 -> null
back === 11 -> 12 -> 14 -> null

假如函数的任何一个参数为 null 或许原链表长度小于 2 ,应当抛出非常。

提醒:一个简朴的做法是盘算链表的长度,然后除以 2 得出前半部份的长度,末了支解链表。另一个要领是应用双指针。一个 “慢” 指针每次遍历一个节点,同时一个 ”快“ 指针每次遍历两个节点。当快指针遍历到末端时,慢指针恰好遍历到链表的中段。

这个 kata 重要磨练的是指针操纵,所以解法用不上递归。

解法 1 — 依据长度支解

代码以下:

function frontBackSplit(source, front, back) {
  if (!front || !back || !source || !source.next) throw new Error('invalid arguments')

  const array = []
  for (let node = source; node; node = node.next) array.push(node.data)

  const splitIdx = Math.round(array.length / 2)
  const frontData = array.slice(0, splitIdx)
  const backData = array.slice(splitIdx)

  appendData(front, frontData)
  appendData(back, backData)
}

function appendData(list, array) {
  let node = list
  for (const data of array) {
    if (node.data !== null) {
      node.next = new Node(data)
      node = node.next
    } else {
      node.data = data
    }
  }
}

解法思绪是把链表变成数组,如许轻易盘算长度,也轻易用 slice 要领支解数组。末了用 appendData 把数组转回链表。由于涉及到屡次遍历,这并非一个高效的计划,而且还须要一个数组处置惩罚暂时数据。

解法 2 — 依据长度支解改进版

代码以下:

function frontBackSplitV2(source, front, back) {
  if (!front || !back || !source || !source.next) throw new Error('invalid arguments')

  let len = 0
  for (let node = source; node; node = node.next) len++
  const backIdx = Math.round(len / 2)

  for (let node = source, idx = 0; node; node = node.next, idx++) {
    append(idx < backIdx ? front : back, node.data)
  }
}

// Note that it uses the "tail" property to track the tail of the list.
function append(list, data) {
  if (list.data === null) {
    list.data = data
    list.tail = list
  } else {
    list.tail.next = new Node(data)
    list.tail = list.tail.next
  }
}

这个解法经由过程遍历链表来猎取总长度并算出中心节点的索引,算出长度后再遍历一次链表,然后用 append 要领挑选性地把节点数据到场 frontback 两个链表中去。这个解法不依赖中心数据(数组)。

append 要领有个值得注重的处所。平常状况下把数据插进去链表的末端的空间复杂度是 O(n) ,为了防止这类状况 append 要领为链表加了一个 tail 属性并让它指向尾节点,让空间复杂度变成 O(1) 。

解法 3 — 双指针

代码以下:

function frontBackSplitV3(source, front, back) {
  if (!front || !back || !source || !source.next) throw new Error('invalid arguments')

  let slow = source
  let fast = source

  while (fast) {
    // use append to copy nodes to "front" list because we don't want to mutate the source list.
    append(front, slow.data)
    slow = slow.next
    fast = fast.next && fast.next.next
  }

  // "back" list just need to copy one node and point to the rest.
  back.data = slow.data
  back.next = slow.next
}

思绪在开篇已经有诠释,当快指针遍历到链表末端,慢指针恰好走到链表中部。但怎样修正 frontback 两个链表照样有点技能的。

关于 front 链表,慢指针每次遍历的数据就是它须要的,所以每次遍用时把慢指针的数据 appendfront 链表中就行(第 9 行)。

关于 back 链表,它所需的数据就是慢指针停下的位置到末端。我们不必复制全部链表数据到 back ,只用复制第一个节点的 datanext 即可。这类 复制头结点,共用盈余节点 的技能常常出现在一些 Immutable Data 的操纵中,以省去不必要的复制。这个技能实在也能够用到上一个解法里。

参考资料

Codewars Kata
GitHub 的代码完成
GitHub 的测试

    原文作者:darkbaby123
    原文地址: https://segmentfault.com/a/1190000008243727
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞