文章

剑指Offer JavaScript-链表专题

剑指Offer JavaScript-链表专题

从尾到头打印链表

1. 题目描述

输入一个链表,从尾到头打印链表每个节点的值。

2. 解题思路

可以从头到尾遍历一遍链表,将节点放入栈中,然后依次取出打印(后入先出)。

优化就是借助“递归”,先向下查找再打印输出,也可实现这种“后入先出”。可以类比二叉树的后序遍历。

3. 代码

用 JS 实现了简单实现了链表这种数据结构,这不是重点。

重点在printFromTailToHead函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Node {
    /**
     * 节点构造函数
     * @param {*} value
     * @param {Node} next
     */
    constructor(value, next) {
        this.value = value;
        this.next = next;
    }
}

class List {
    constructor() {
        this.head = new Node(null, null);
    }

    /**
     * 从0开始计算,找到包括head在内的位于index的节点
     * @param {Number} index
     */
    find(index) {
        let current = this.head;
        for (let i = 0; i < index; ++i) {
            current = current.next;
        }
        return current;
    }

    /**
     * 向index位置插入元素
     * @param {*} value
     * @param {Number} index
     */
    insert(value, index) {
        const prev = this.find(index);
        const next = new Node(value, prev.next);
        prev.next = next;
    }
}

/**
 * 逆序打印链表
 * @param {Node} node
 */
function printFromTailToHead(node) {
    if (node.next) {
        printFromTailToHead(node.next);
    }
    node.value && console.log(node.value);
}

/**
 * 以下是测试代码
 */
let list = new List();
list.insert("a", 0);
list.insert("b", 1);
list.insert("c", 2);

printFromTailToHead(list.head);

快速删除链表节点

1. 题目描述

给定单向链表的头指针和一个结点指针,定义一个函数在 $O(1)$ 时间删除该结点。

2. 思路描述

正常的做法肯定是在 $O(N)$ 时间内删除节点。而这么过分的要求,显然是通过“重新赋值”才能做到。

比如要删除节点 a,那么就将 a.next 的 value 和 next 赋值给节点 a,然后删除 a.next。

表面“看起来”像是删除了节点 a,其实是将其后节点的信息转移到了它自己身上。

除此之外,对于最后一个节点,还是要退化成 $O(N)$ 的复杂度。而整体分析一下复杂度:

\[O(T) = (O(N) + O(1) * (n - 1)) / n = O(1)\]

3. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Node {
    /**
     * 节点构造函数
     * @param {*} value
     * @param {Node} next
     */
    constructor(value, next) {
        this.value = value;
        this.next = next;
    }
}

/**
 *
 * @param {Node} head
 * @param {Node} toDelete
 */
function deleteNode(head, toDelete) {
    if (head === toDelete || !toDelete || !head) {
        return;
    }

    let nextNode = toDelete.next;

    if (!nextNode) {
        // 尾节点
        let node = head;
        while (node.next !== toDelete) {
            node = node.next;
        }
        node.next = null;
        toDelete = null;
    } else {
        toDelete.value = nextNode.value;
        toDelete.next = nextNode.next;
        nextNode = null;
    }
}

/**
 * 测试代码
 */

let node3 = new Node(3, null),
    node2 = new Node(2, node3),
    node1 = new Node(1, node2),
    head = new Node(null, node1);

deleteNode(head, node2);
let node = head.next;
while (node) {
    console.log(node.value);
    node = node.next;
}

链表倒数第k节点

1. 题目描述

输入一个单链表,输出该链表中倒数第 k 个结点。

2. 思路描述

思路一:从头到尾遍历一遍,统计长度length。再从头遍历,直到length - k个节点停止,这就是倒数第 k 个节点。

思路二:只需要遍历一遍。准备 2 个指针ab均指向第一个节点,a先移动k个位置;然后ab一起向后移动,此时两个只指针的位置差为k;直到a移动到尾结点停止,此时b指向的节点就是倒数第 k 个节点。

3. 代码实现

下面是“思路二”的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
 * 节点定义
 */
class Node {
    constructor(value, next) {
        this.value = value;
        this.next = next;
    }
}

/**
 * 寻找倒数第k个节点
 * @param {Node} head 初始节点
 * @param {Number} k 顺序(倒数)
 */
function findKthFromTail(head, k) {
    if (!head || k <= 0) {
        return null;
    }

    let a = head,
        b = head;

    for (let i = 0; i < k; ++i) {
        a = a.next;
        if (!a) {
            return null;
        }
    }

    while (a) {
        b = b.next;
        a = a.next;
    }

    return b;
}

/**
 * 以下是测试代码, 分别输出倒数第2、3和5个节点
 */

let node3 = new Node(3, null),
    node2 = new Node(2, node3),
    node1 = new Node(1, node2),
    head = new Node(0, node1);

console.log(findKthFromTail(head, 2));
console.log(findKthFromTail(head, 3));
console.log(findKthFromTail(head, 5));

反转链表

1. 题目描述

定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。

2. 思路描述

思路一:经典的“链表头插法”,时间复杂度是 $O(N)$,但是空间复杂度也是 $O(N)$

思路二:链表原地操作,时间复杂度是 $O(N)$,但是空间复杂度只有 $O(1)$。

  1. 保存当前节点node的上一个节点pre
  2. 节点nodenext指向pre
  3. 分别将prenode向后移动 1 个位置
    • 如果node为 null,链表翻转完毕,此时pre指向新的头节点,返回即可
    • 否则,回到第 1 步继续执行

3. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
 * 节点定义
 */
class Node {
    constructor(value, next) {
        this.value = value;
        this.next = next;
    }
}

/**
 * 翻转链表
 * @param {Node} head 未翻转链表的头节点
 * @return {Node} 翻转链表后的头节点
 */
function reverseList(head) {
    let node = head,
        pre = null;

    while (node) {
        let next = node.next;

        node.next = pre;

        pre = node;
        node = next;
    }

    return pre;
}

/**
 * 以下是测试代码, 分别输出倒数第2、3和5个节点
 */

let node3 = new Node(3, null),
    node2 = new Node(2, node3),
    node1 = new Node(1, node2),
    head = new Node(0, node1);

let newHead = reverseList(head);
while (newHead) {
    console.log(newHead);
    newHead = newHead.next;
}

合并两个有序链表

1. 题目描述

输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按照递增排序的。

2. 思路分析

准备一个指针node,假设指向两个链表的中节点的指针分别是:p1p2

  1. 比较p1p2value大小
    • 如果,p1.value 小于 p2.value, node.next 指向 p1, p1 向后移动
    • 否则,node.next 指向 p2, p2 向后移动
  2. 重复第 1 步,直到其中一个链表遍历完
  3. 跳出循环,将 node.next 指向未遍历完的链表的剩余部分

整个过程的时间复杂度是 O(N), 空间复杂度是 O(1)

3. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
 * 节点定义
 */
class Node {
    constructor(value = null, next = null) {
        this.value = value;
        this.next = next;
    }
}

/**
 * 合并2个有序单链表成为1个新的有序单链表
 * @param {Node} p1
 * @param {Node} p2
 */
function merge(p1, p2) {
    if (!p1) {
        return p2;
    } else if (!p2) {
        return p1;
    }

    let head = new Node(),
        node = head;

    while (p1 && p2) {
        if (p1.value < p2.value) {
            node.next = p1;
            p1 = p1.next;
        } else {
            node.next = p2;
            p2 = p2.next;
        }

        node = node.next;
    }

    if (!p1) {
        node.next = p2;
    }

    if (!p2) {
        node.next = p1;
    }

    return head.next;
}

/**
 * 以下是测试代码
 */

let list1 = new Node(1, new Node(3, new Node(5, new Node(7, null))));
let list2 = new Node(2, new Node(4, new Node(6, new Node(8, null))));

let head = merge(list1, list2);
while (head) {
    console.log(head.value);
    head = head.next;
}

复杂链表的复制

1. 题目描述

请实现函数ComplexListNode *Clone(ComplexListNode* pHead),复制一个复杂链表。在复杂链表中,每个结点除了有一个 next 指针指向下一个结点外,还有一个 sibling 指向链表中的任意结点或者 NULL。

2. 思路分析

按照正常的思路,首先从头到尾遍历链表,拷贝每个节点的 value 和 next 指针。然后从头再次遍历,第二次遍历的目的在于拷贝每个节点的 sibling 指针。

然而即使找到原节点的 sibling 指针,还是得为了找到复制节点对应的 sibling 指针而再遍历一遍。那么对于 n 个要寻找 sibling 指针的节点,复杂度就是 O(N*N)。

显然,为了降低复杂度,必须从第二次遍历着手。这里采用的方法是,在第一次遍历的时候,把 (原节点, 复制节点) 作为映射保存在表中。那么第二次遍历的时候,就能在 O(1) 的复杂度下立即找到原链上 sibling 指针在复制链上对应的映射。

3. 代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Node {
    constructor(value, next = null, sibling = null) {
        this.value = value;
        this.next = next;
        this.sibling = sibling;
    }
}

/**
 * 复制复杂链表
 * @param {Node} first
 */
function cloneNodes(first) {
    if (!first) {
        return null;
    }

    const map = new Map();

    let copyFirst = new Node(first.value),
        node = first.next, // 被copy链的当前节点
        last = copyFirst; // copy链的当前节点, 此节点相对于被copy链短位移少1位

    map.set(first, copyFirst);

    while (node) {
        last.next = new Node(node.value);
        last = last.next;
        map.set(node, last);
        node = node.next;
    }

    // 第二次遍历, 迁移sibling
    node = first;
    while (node) {
        map.get(node) && (map.get(node).sibling = map.get(node.sibling));
        node = node.next;
    }

    return copyFirst;
}

/**
 * 测试代码
 */
const node1 = new Node("a"),
    node2 = new Node("b"),
    node3 = new Node("c"),
    node4 = new Node("d");

node1.next = node2;
node2.next = node3;
node3.next = node4;

node1.sibling = node3;
node4.sibling = node2;

let copyNode = cloneNodes(node1);
while (copyNode) {
    console.log(copyNode);
    copyNode = copyNode.next;
}

两个链表中的第一个公共节点

1. 题目描述

输入两个链表,找出它们的第一个公共结点。

2.1 思路一:栈实现

在第一个公共节点前的节点都是不相同的,因此只要倒序遍历两个链表,找出最后一个出现的相同节点即可。

因为链表不能倒序遍历,所以借助栈实现。

2.2 思路二:快慢指针

假设链表 A 长度大于链表 B 长度,它们的长度差为 diff。

让 A 的指针先移动 diff 的位移,然后 A 和 B 的指针再同时向后移动,每次比较节点,选出第一个出现的相同节点。

3. 代码实现

为了方便,先简单实现节点数据结构:

1
2
3
4
5
6
class Node {
    constructor(value, next) {
        this.value = value;
        this.next = next;
    }
}

3.1 思路一:栈实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * 思路一:利用栈实现
 *
 * @param {Node} list1
 * @param {Node} list2
 */
function method1(list1, list2) {
    const stack1 = [],
        stack2 = [];

    let node = list1;
    while (node) {
        stack1.push(node);
        node = node.next;
    }

    node = list2;
    while (node) {
        stack2.push(node);
        node = node.next;
    }

    node = null;
    while (stack1.length && stack2.length) {
        let top1 = stack1.pop(),
            top2 = stack2.pop();
        if (top1 === top2) {
            node = top1;
        } else {
            break;
        }
    }

    return node;
}

3.2 思路二:快慢指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
 * 思路二:快慢指针
 *
 * @param {Node} list1
 * @param {Node} list2
 */
function method2(list1, list2) {
    let length1 = 0,
        length2 = 0;

    let node = list1;
    while (node) {
        ++length1;
        node = node.next;
    }

    node = list2;
    while (node) {
        ++length2;
        node = node.next;
    }

    let diff = Math.abs(length1 - length2),
        longList = null,
        shortList = null;
    if (length1 > length2) {
        longList = list1;
        shortList = list2;
    } else {
        longList = list2;
        shortList = list1;
    }

    while (diff > 0) {
        longList = longList.next;
        --diff;
    }

    while (longList && shortList) {
        if (longList === shortList) {
            return longList;
        }
        longList = longList.next;
        shortList = shortList.next;
    }

    return null;
}

3.3 测试代码

1
2
3
4
5
6
const node4th = new Node(4);
const node3th = new Node(3, node4th);
const list1 = new Node(1, new Node(2, new Node(3, node3th)));
const list2 = new Node(5, new Node(6, node3th));

console.log(method2(list1, list2));
本文由作者按照 CC BY 4.0 进行授权