JavaScript数据结构与算法——链表

77 阅读7分钟

链表

  • 多个元素组成的列表
  • 元素存储不连续,用 next 指针连在一起 image.png

数组 vs 链表

  • 数组:增删非首尾元素时往往需要移动元素
  • 链表:增删非首尾元素,不需要移动元素,只需要更改 next 的指向即可

JS 中实现链表

  • JavaScript 中没有链表
  • 可以用 Object 模拟链表
const a = { val: 'a' };
const b = { val: 'b' };
const c = { val: 'c' };
const d = { val: 'd' };
a.next = b;
b.next = c;
c.next = d;

// 遍历链表
let p = a;
while(p) {
  console.log(p.val);
  p = p.next;
}

// 插入
const e = { val: 'e' };
b.next = e;
e.next = c;

// 删除
b.next = c;

LeetCode:237 删除链表中的节点

有一个单链表的 head,我们想删除它其中的一个节点 node

给你一个需要删除的节点 node 。你将 无法访问 第一个节点 head

链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。

删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是:

  • 给定节点的值不应该存在于链表中。
  • 链表中的节点数应该减少 1。
  • node 前面的所有值顺序相同。
  • node 后面的所有值顺序相同。

自定义测试:

  • 对于输入,你应该提供整个链表 head 和要给出的节点 node。node 不应该是链表的最后一个节点,而应该是链表中的一个实际节点。
  • 我们将构建链表,并将节点传递给你的函数。
  • 输出将是调用你函数后的整个链表。

示例1:

image.png

输入:head = [4,5,1,9], node = 5

输出:[4,1,9]

解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9

示例2:

image.png

输入:head = [4,5,1,9], node = 1

输出:[4,5,9]

解释:指定链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9

思路:

  • 单链表只能获取到下一个节点(无法获取上一个节点,即不能直接删除)
  • 将下一个节点的值赋值给指定删除节点
  • 并将指定节点的 next 指向下一个节点的 next
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} node
 * @return {void} Do not return anything, modify node in-place instead.
 */
var deleteNode = function(node) {
    node.val = node.next.val;
    node.next = node.next.next;
};

时间复杂度:O(1)

空间复杂度:O(1)

LeetCode:206 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例1:

image.png

输入:head = [1,2,3,4,5]

输出:[5,4,3,2,1]

示例 2:

image.png

输入:head = [1,2]

输出:[2,1]

示例 3:

输入:head = []

输出:[]

思路:

  • 反转两个节点:将 n+1next 指向 n

image.png

(n + 1).next = n;
  • 反转多个节点:双指针遍历链表,重复上述操作

image.png

解题步骤:

  • 双指针一前一后遍历链表
let p1 = 1,
    p2 = null;
while(p1) {
    console.log(p1.val, p2 && p2.val);
    p2 = p1;
    p1 = p1.next;   
}
// p1 p2
// 1  null
// 2  1
// 3  2
// 4  3
// 5  4
  • 反转双指针
p1.next = p2;

题解:

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    let p1 = head,
        p2 = null;
    while(p1) {
        const temp = p1.next;
        p1.next = p2;
        p2 = p1;
        p1 = temp;
    }
    return p2;
};

时间复杂度:O(n)

空间复杂度:O(1)

LeetCode:2 两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:

image.png

输入:l1 = [2,4,3], l2 = [5,6,4]

输出:[7,0,8]

解释:342 + 465 = 807.

示例 2:

输入:l1 = [0], l2 = [0]

输出:[0]

示例 3:

输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]

输出:[8,9,9,9,0,0,0,1]

解题思路:

  • 模拟相加操作
  • 需要遍历链表

解题步骤:

  • 新建一个空链表
  • 遍历被相加的两个链表,模拟相加操作,将 个位数 追加到新链表上,将十位数留到下一位去相加
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
    const l3 = new ListNode(0);
    let p1 = l1;
    let p2 = l2;
    let p3 = l3;
    let carry = 0;
    while(p1 || p2) {
        const v1 = p1 ? p1.val : 0;
        const v2 = p2 ? p2.val : 0;
        const val = v1 + v2 + carry;
        carry = Math.floor(val / 10);
        p3.next = new ListNode(val % 10);
        if(p1) p1 = p1.next;
        if(p2) p2 = p2.next;
        p3 = p3.next;
    }
    // 进位
    if(carry) {
        p3.next = new ListNode(carry);
    }
    return l3.next;
};

时间复杂度:O(n)

空间复杂度:O(n)

拓展:

  • floor(arg): 返回参数不大于 arg 的最大整数(向下取整)
  • ceil(arg): 返回参数不大于 arg + 1 的最大整数(向上取整)

LeetCode:83 删除链表中的重复元素

给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。

示例 1:

image.png

输入:head = [1,1,2]

输出:[1,2]

示例 2:

image.png

输入:head = [1,1,2,3,3]

输出:[1,2,3]

解题思路:

  • 因为输入的链表是有序的,所以重复元素一定相邻
  • 遍历链表,如果发现当前元素和下个元素相同,就删除下个元素

解题步骤:

  • 遍历链表,如果发现当前元素和下个元素相同,就删除下个元素
  • 遍历结束后,返回原链表
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    let p = head;
    while(p && p.next) {
        if(p.val === p.next.val){
            p.next = p.next.next;
        }else {
            p = p.next;
        }
    }
    return head;
};

时间复杂度:O(n)

空间复杂度:O(1)

LeetCode:141 环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false

示例 1:

image.png

输入:head = [3,2,0,-4], pos = 1

输出:true

解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

image.png

输入:head = [1,2], pos = 0

输出:true

解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

image.png

输入:head = [1], pos = -1

输出:false

解释:链表中没有环。

解题思路:

  • 两个人在圆形操场上的起点同时起跑,速度快的人一定会超过速度慢的人一圈
  • 用一快一慢两个指针遍历链表,如果指针能够相逢,那么链表就有圈

解题步骤:

  • 用一快一慢两个指针遍历链表,如果指针能够相逢,就返回 true
  • 遍历结束后,还没有相逢就返回 false
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    let p1 = head;
    let p2 = head;
    while(p1 && p2 && p2.next) {
        p1 = p1.next;
        p2 = p2.next.next;
        if(p1 === p2) {
            return true;
        }
    }
    return false;
};

时间复杂度:O(n)

空间复杂度:O(1)

前端与链表:JS 中的原型链

1. 原型链概念

  • 原型链的本质是链表
  • 原型链上的节点是各种原型对象,比如 Function.prototypeObject.prototype ...
  • 原型链通过 __proto__ 属性连接各种原型对象

2. 原型链示例:

  • obj -> Object.prototype -> null
  • func -> Function.prototype -> Object.prototype -> null
  • arr -> Array.prototype -> Object.prototype -> null
const obj = {};
Object.prototype.x = 'x';

const func = () => {};
Function.prototype.y = 'y';

const arr = [];

3. 原型链结论:

  • 如果 A 沿着原型链能找到 B.prototype,那么 A instanceof B 为 true

    上述示例中:

    • obj instanceof Object: true
    • func instanceof Function: true
    • func instanceof Object: true
    • arr instanceof Array: true
    • arr instanceof Object: true
  • 如果 A 对象上没有找到 x 属性,那么会沿着原型链找 x 属性

    上述示例中:

    • obj.x: 'x'
    • func.x: 'x'
    • func.y: 'y'

面试题一:

简述 instanceof 的原理,并用代码实现

解法:

  • 遍历 A 的原型链,如果找到 B.prototype,返回 true,否则返回 false
const instanceOf = (A, B) => {
  let p = A;
  while(p) {
    if(p === B.prototype) {
      return true
    }
    p = p.__proto__;
  }
  return false
}

面试题二:

var foo = {},
    F = function(){};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';

console.log(foo.a); // value a
console.log(foo.b); // undefined

console.log(F.a); // value a
console.log(F.b); // value b

前端与链表:使用链表指针获取 JSON 的节点值

const json = {
    a: { b: { c: 1 } },
    d: { e: 2 },
};

// const path = ['a', 'b', 'c'] // p: 1
// const path = ['a', 'b'] // p: { c: 1 }
const path = ['d', 'e'] // p: 2

let p = json;
path.forEach(k => {
    p = p[k];
})
console.log(p);