首页
使用图解的方式来解决链表的算法问题

这周在刷链表的算法时,发现对于这类问题用图解的方式来解决十分简单。通常我们的草图画完了,代码也就随之写出来了,不用绞尽脑汁去想p.next = new Node(); p = p.next这么绕的代码。

下面我们通过两道算法来说明一下图解的方式有多便利。

数组转链表

首先是一个比较简单的数组转链表的问题。代码如下:

//声明一个单链表结点
class ListNode {
  constructor(val) {
    this.val = val;
    this.next = null;
  }
}

/**
 * 数组转链表
 * @param {Array} arr 
 * @return {ListNode} 
 */
export function array2List(arr) {
  if (arr.length === 0) {
    return null;
  }
  const head = new ListNode(null);
  let i = 0, curr = head;
  for (i; i < arr.length; i++) {
    curr.next = new ListNode(arr[i]);
    curr = curr.next;
  }
  return head.next;
}

代码比较简单,但发现很多小伙伴对于下面这几行代码直接去看的话不是很明白。

curr = head
curr.next = new ListNode(arr[i]);
curr = curr.next;

我们自己实现的ListNode在 JavaScript 中实际上是一个引用类型,通过画出堆栈图,其实就很容易明白了。关于JavaScript的值类型与引用类型这里就不再赘述了。

image_1ec45gpbh1a9310lj13v0dhl1s2q9.png-115.6kB

下面我们来逐一分析这个图解。

首先,STEP 0 做的事情就是下面这两句代码

const head = new ListNode(null);
let curr = head;

由于ListNode是引用类型,它存放在堆内存中。headcurr保存的是在栈内存中的一个引用,实际上指向的都是堆内存中的ListNode

STEP 1就是第一次循环做的事情。循环体里就两句代码,也是最核心的部分。

curr.next = new ListNode(arr[i]);
curr = curr.next;

我们通过图解可以看到,new ListNode(arr[i])在堆内存中又生成了一个新的ListNode,我们用ListNode1来表示。堆内存中原来的ListNodenext属性发生了变化,被指向为ListNode1。然后我们把栈内存中的curr也指向为ListNode1

其实通过这一步我们就已经知道整个代码的目的了。curr被当成一个指针,一直指向每次循环新生成的ListNode。下一次循环的时候,也就是STEP 2,可以发现此时curr指向为ListNode1curr.next也就是ListNode1.next指向为新生成的ListNode2

在整个循环中,可以发现唯一不变的就是head了,它一直指向的都是最初的ListNode,变化的是ListNode.next,循环结束后,这个链就串起来了。

最后通过head.next就得到我们想要的链表了。

同理,用这种方法去解答类似 Leetcode两数相加 的问题,可以把它的难度由中等降为简单。

奇偶链表

下一个例子是奇偶链表,这道题在 leetcode 中是一道中等难度的题目,通过画图的方式会发现其实十分简单。

题目描述如下:

image_1ec4eehs514lbbg87ua1sbl3bi9.png-53.6kB

这道题的代码也是十分简单的,但是直接看代码感觉是一脸懵逼。

const oddEvenList = (head) => {
  if (head == null) {
    return null
  }
  let odd = head, even = head.next, evenHead = even;
  while (even != null && even.next != null) {
    odd.next = even.next
    odd = odd.next
    even.next = odd.next
    even = even.next
  }
  odd.next = evenHead
  return head
};

下面,我们通过图解的方式来解析这道题的代码。

首先,我们准备好测试用例。根据链表的长度,我们可以写出三个测试用例。

# 空链表
null
# 链表节点个数为奇数
1->2->3->4->5->NULL
# 链表节点个数为偶数
1->2->3->4->5->6->NULL

得到测试用例后,我们先可以把异常情况处理了,就是空链表的情况。

if (head == null) {
    return null
}

然后,分别画出链表节点个数为奇数和偶数情况下的过程。

链表节点个数为奇数
image_1ec418tlqtv41pfmuos4tqtub13.png-68kB

链表节点个数为偶数
image_1ec417qknejh1ku6vo1lj11secm.png-64.3kB

画完图了,我们再根据图解来写代码就很好写了。

首先是声明奇偶链表的头尾节点。也就是图1和图2的STEP 0

let odd = head, even = head.next, evenHead = even;

然后我们看while循环的结束条件,图1的STEP 2,当even == null就结束了;图2的STEP 2,当even.next == null就结束了。所以综合起来,也就是下面的代码:

while (even != null && even.next != null) {
}

评论区很多人对官方解法的这一步不是很明确,为什么循环的结束条件是这个?用图解的方式是不是就很明确了?

我们再来看循环的结构体代码。

odd.next = even.next
odd = odd.next
even.next = odd.next
even = even.next

我们可以看STEP 0STEP 1的变化,就是把节点2拿到了偶数链表,奇数链表的节点1指向节点3。我们分别操作这两个链表的指针。

首先奇数链表把odd.next指向even.next,也就是1->3,然后指针后移一位:odd = odd.next

odd.next = even.next
odd = odd.next

奇数链表的操作完了,我们再来操作偶数链表。我们需要完成2->4,也就是even.next = odd.next
因为上一步操作奇数链表后odd已经指向了3。同样指针后移一位:even = even.next

even.next = odd.next
even = even.next

STEP 1STEP 2也是在重复这个过程。最后我们看FINAL这一步,把偶数链表接到奇数链表的后面。也就是5->2

odd.next = evenHead

最后返回head节点,就是改变之后的链表了。

后记

Leetcode 的官方题解有一句话我十分赞同:解决链表问题最好的办法是在脑中或者纸上把链表画出来

在脑中去模拟的话比较困难,那我们就在纸上用草图画一画吧。很多类似的算法问题都迎刃而解。