zhuanyongxigua/blog

如何一气呵成细节较多的《K 个一组翻转链表》

Opened this issue · 0 comments

这道题由于是每一组 k 个节点都需要进行反转,所以你需要先搞清楚剩下的节点中是否有 k 这么多个节点,这不可能在一个 n(节点数量)次的遍历中就完全解决问题。你需要知道这一次的将要到达的结尾,这一次的开头和上一次的结尾。这就是你需要维护的状态

第一次完全通过写出来的东西,肉眼可见的狼狈,为了解决问题,除了上诉的必要状态之外,我还加了好几个其他的状态:

var reverseKGroup = function(head, k) {
  if (head === null || k === 0) {
    return head
  }
  let tail = head
  let num = 0
  let numStart = head
  let numTail = head
  let isReverse = false
  let reverseNum = 0
  let lastTail = head
  let curHead = null
  while (tail !== null || isReverse) {
    if (isReverse) {
      if (num > 1) {
        if (num === k) {
          curHead = numStart
        }
        let nextHead = numStart.next
        numStart.next = numTail
        numTail = numStart
        numStart = nextHead
        num--
      }
      if (num === 1) {
        if (reverseNum !== 0 && lastTail !== null) {
          lastTail.next = numStart
        }
        lastTail = curHead
        numStart.next = numTail
        reverseNum++
        isReverse = false
        num = 0
        if (reverseNum === 1) {
          head = numStart
        }
        numStart = tail
      }
    } else {
      num++
      tail = tail.next
      if (num === k) {
        numTail = tail
        isReverse = true
      }
    }
  }
  return head
}

过多的状态造成了维护状态很痛苦,这样就会出现很多很讨厌的细节,很难快速的写对,没有清晰的思路,这将造成下次写的时候还是一样的不会写,需要从头思考。

人类解决这种问题的较为通用的方法,就是分类,在代码中就是拆分模块。想一想,这里有什么是可以拆分的?显而易见,就是反转 k 个之内的链表。我们就可以针对这个局部的需求来单独封装一个函数。

这个函数的参数和返回值应该如何设计呢?head 一定是必须的,这个函数必须要知道从哪儿开始。不过这一个参数显然不够,还需要知道 tail 或者是 k。选择后者,好处是可以尝试递归。这里我们选择前者,更容易理解。对于返回值,可有可无。这里我们选择没有返回值,穿进去的 head 就会变成 tail,tail 会变成 head。

写一个在知道 head 和 tail 时情况下,反转这一段链表的函数:

function reverseGroup (head, tail) {
  if (head === tail) return
  // 修改了 tail 这个变量的指向,真是的 tail 节点依然在那里
  tail.next = null
  tail = null
  while (head !== null) {
    const nextHead= head.next
    head.next = tail
    tail = head
    head = nextHead
  }
}

主函数好了一点点,经常变化的状态由八个变成了四个,但是还是不够好,状态还是偏多,但是思路对比之前已经清晰了很多:

var reverseKGroup = function(head, k) {
  if (
    head === null
    || head.next === null
    || k <= 1
  ) return head

  const protectedHead = {
    value: -1,
    next: head
  }
  let num = 0
  let paramHead = head
  let paramTail = protectedHead
  let lastTail = paramTail
  while (paramTail !== null) {
    if (num < k) {
      num++
      paramTail = paramTail.next
    } else {
      const nextParamHead = paramTail.next
      reverseGroup(paramHead, paramTail)
      lastTail.next = paramTail
      lastTail = paramHead
      paramHead = nextParamHead
      paramTail = nextParamHead
      num = 1
    }
  }
  if (num <= k) {
    lastTail.next = paramHead
  }
  return protectedHead.next
}

这里的四个状态,除了在文章开头我们提出的必须的三个之外之外,还多了一个 num,用来判断当前剩下的节点数量,是否有 k 个。从代码从可以看到,这个状态非常的麻烦,维护它的地方很多,也很散乱。所以我们下一步就可以考虑把它抽离。

它是用来判断剩下节点数量是否满足要求的,那我们可不可以写一个函数,当满足 k 个节点的时候,返回这一组的 tail 节点,不满足的时候返回 null?

function getTail (head, k) {
  while (k > 1 && head !== null) {
    head = head.next
    k--
  }
  return head
}

再优化一下,还有三个需要维护的状态:

var reverseKGroup = function(head, k) {
  if (
    head === null
    || head.next === null
    || k <= 1
  ) return head
  const protectedHead = {
    value: -1,
    next: head
  }

  let paramHead = head
  let paramTail = protectedHead
  let lastTail = paramTail
  while (paramTail !== null) {
    let paramTail = getEnd(paramHead, k)
    if (paramTail === null) {
      lastTail.next = paramHead
      break
    }
    const nextParamHead = paramTail.next
    reverseGroup(paramHead, paramTail)
    lastTail.next = resultTail
    lastTail = resultHead
    paramHead = nextParamHead
    paramTail = nextParamHead
  }
  return protectedHead.next
}

优化到这里,基本上已经达到文章开头提到的最简化的状态了。可如果你追求简洁了,依然可以继续优化。

仔细观察,while 中的判断条件是 paraTail,也就是上一次的结尾,可在 while 里面,如果发现 paramTail 为 null,就可以直接 break 了。所以在剩余的节点数量不足 k 个的时候,我们是不需要在循环的外面适用这个变量的。可如果剩余的节点数量刚好等于 k 个怎么办?可以用 paramHead 来判断。同时也可以直接用 head 来代替 paramHead。如果题目要求你知道初始的 head,那可以用一个 const 声明的常量来存一下初始的 head,这题并不需要。

最终的版本,只有一个需要一直维护的状态,到了这里,你甚至都不再需要刻意的背什么东西,就可以顺着思路写下去了:

var reverseKGroup = function(head, k) {
  if (
    head === null
    || head.next === null
    || k <= 1
  ) return head
  const protectedHead = {
    value: -1,
    next: head
  }

  let lastTail = protectedHead
  while (head !== null) {
    let curTail = getEnd(head, k)
    if (curTail === null) {
      lastTail.next = head
      break
    }
    const nextHead = curTail.next
    reverseGroup(head, curTail)
    lastTail.next = curTail
    lastTail = head
    head = nextHead
  }
  return protectedHead.next
}

完整的代码:

export function reverseGroup (head, tail) {
  if (head === tail) return
  tail.next = null
  tail = null
  while (head !== null) {
    const nextHead = head.next
    head.next = tail
    tail = head
    head = nextHead
  }
}

function getEnd (head, k) {
  while (k > 1 && head !== null) {
    head = head.next
    k--
  }
  return head
}

var reverseKGroup = function(head, k) {
  // 这种就是白给的,看一眼就记住了
  if (
    head === null
    || head.next === null
    || k <= 1
  ) return head

  const protectedHead = {
    value: -1,
    next: head
  }
  let lastTail = protectedHead
  while (head !== null) {
    let curTail = getEnd(head, k)
    if (curTail === null) {
      lastTail.next = head
      break
    }
    const nextHead = curTail.next
    reverseGroup(head, curTail)
    lastTail.next = curTail
    lastTail = head
    head = nextHead
  }
  return protectedHead.next
}