JavaScript版:数据结构之“链表”

897 阅读7分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

image.png

1. 链表(linkedList)简介

1.1 链表是什么?

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

image.png

1.2 数组 VS 链表

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

  • 都是有序结构
  • 链表:查询慢O(n),新增和删除块0(1)
  • 数组:查询块O(1),新增和删除块0(n)

1.3 JS中的链表

  • JavaScript中没有链表
  • 可以用Object模拟链表

1.4 代码部分

创建、遍历、插入、删除

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;

/** 生成的结构如下
{
    "val":"a",
    "next":{
        "val":"b",
        "next": { 
            "val":"c",
            "next": {
                "val":"d"
            }
        }
    }
}
*/


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

// 链表插入e
const e = { val: 'e' };
c.next = e;
e.next = d;

// 链表删除e
c.next = d;

2. LeetCode: 237.删除链表中的节点

delete-node-in-a-linked-list

leetcode-cn.com/problems/delete-node-in-a-linked-list/

image.png

2.1 解题思路

  • 无法直接获取被删除节点的上个节点
  • 将被删除节点转移到下个节点

2.2 解题步骤

  • 将被删除节点的值改为下一个节点的值
  • 删除下一个节点
/**
 * 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;
};

3. LeetCode: 206.反转链表

reverse-linked-list

leetcode-cn.com/problems/re…

image.png

3.1 解题思路

  • 反转两个节点:将n+1的next指向n
输入: ...-> n -> n+1 ->...
输出: ...-> n+1 -> n ->...
  • 反转多个节点:双指针遍历链表,重复上述操作
输入: 1 -> 2 -> 3 -> 4 -> 5 -> NULL
输出: 5 -> 4 -> 3 -> 2 -> 1 -> NULL

3.2 解题步骤

  • 双指针一前一后遍历链表
  • 反转双指针
/**
 * 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;
    let p2 = null;
    while(p1){
        const tmp = p1.next;
        p1.next = p2;
        p2 = p1;
        p1 = tmp;
    }
    return p2
};

4. LeetCode: 2.两数相加

add-two-numbers

leetcode-cn.com/problems/ad…

image.png

4.2 解题思路

  • 数学题,模拟相加操作
  • 需要遍历链表
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出: 7 -> 0 -> 8
原因: 345 + 465 = 807

4.3 解题步骤

  • 新建一个空链表
  • 遍历被相加的两个链表,模拟相加操作,将个位数追加到新链表上,将十位数留下一位去相加
/**
 * 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;
};

5. LeetCode: 83. 删除排序链表中的重复元素

remove-duplicates-from-sorted-list

leetcode-cn.com/problems/re…

image.png

5.1 解题思路

  • 因为链表是有序的,所以重复元素是相邻的
  • 遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素值。
输入:1 -> 1 -> 2
输出: 1 -> 2

5.2 解题步骤

  • 遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素值
  • 遍历结束,返回原链表的头部
/**
 * 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
};

6. LeetCode:141. 环形链表

linked-list-cycle

leetcode-cn.com/problems/li…

image.png

6.1 解题思路

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

6.2 解题步骤

  • 用一快一慢两个指针遍历链表,
    • 如果指针能够相逢,就返回true
    • 如果指针不能够相逢,就返回fasle
/**
 * 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;
};

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

7.1 原型链简介

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

7.2 原型链长啥样?

  • 对象
    • obj -> Object.prototype -> null
  • 函数
    • func -> Function.prototype -> Object.prototype -> null
  • 数组
    • arr -> Array.prototype -> Object.prototype -> null

从上发现,除了对象这个类型,其他对象的原型链,对象先指向自己的原型对象,然后再指向Object的原型对象,比如String、Number都是成立的

7.3 Coding part

  • 对象的原型链 image.png obj.__proto__ === Object.prototype
  • 函数的原型链 image.png func.__proto__.__proto__ === Object.prototype

7.4 原型链知识点

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

image.png

image.png

  • 如果在A对象上没有找到X属性(x 指任何一个属性值),那么会沿着原型链找到X属性 image.png

7.5 面试题一:instanceof 的原型,并用代码实现

面试题分析

  • 知识点:如果A沿着原型链能找到B.prototype,那么 A instanceof Btrue
  • 解法:遍历A的原型链,如果找到B.prototype 返回true,否则返回fasle
// 判断 A 是否是 B 的示例
const instanceOf = (A, B) => {
    let p = A;
    while(p){
        if(p === B.prototype){
            return true
        }
        p = p.__proto__;
    }
    return false
}

console.log('instanceOf([], Array__', instanceOf([], Array))
console.log('instanceOf({}, Object__', instanceOf({}, Object))
console.log('instanceOf(1, Number__', instanceOf(1, Number))

运行结果: image.png

7.6 面试题二

var foo = {},
    F = function () {};

Object.prototype.a = 'value a';
Function.prototype.b = 'value b';

console.log('foo.a__', foo.a)
console.log('foo.b__', foo.b)

console.log('F.a__', F.a)
console.log('F.b__', F.b)
  • 知识点:如果在A对象上没有找到X属性(x 指任何一个属性值),那么会沿着原型链找到X属性
  • 解法:明确foo 和 F 变量的原型链,沿着原型链找 a 属性 和 b 属性

image.png

8. 前端与链表:

8.1 使用链表指针获取 JSON 的节点值

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

const path = ['a', 'b', 'c'];

// 使用链表指针获取 JSON 的节点值 方式

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

8.2 react 的fiber 使用了链表

发现 react 的fiber 使用了链表,
在组件渲染的过程中 可以停顿下来,先做高优先级任务,然后在回来执行。

组件是树结构,中间是不能停的,需要遍历、递归、循环,如果停了上下文就没有了,需要重新开始。

把树变成链表就可以停了。。。

9.链表和数组那个实现队列更快?

9.1 分析

  • 数组是连续存储,push 很快,shift 很慢
  • 链表是非连续存储,ada 和 delete 都很快,(但查找很慢)
  • 结论:链表实现队列更快

9.2 链表实现队列 思路

  • 单向链表,单要同时记录head 和 tail
  • 要从tail 入队,否则出队 时 tail不好定位
  • length 要实时记录,不可遍历链表获取

9.3 链表实现队列 代码

/**
 * @description 用链表实现队列
 */

interface IListNode {
    value: number
    next: IListNode | null
}

export class MyQueue {
    private head: IListNode | null = null
    private tail: IListNode | null = null
    private len = 0

    /**
     * 入队,在 tail 位置
     * @param n number
     */
    add(n: number) {
        const newNode: IListNode = {
            value: n,
            next: null,
        }

        // 处理 head
        if (this.head == null) {
            this.head = newNode
        }

        // 处理 tail
        const tailNode = this.tail
        if (tailNode) {
            tailNode.next = newNode
        }
        this.tail = newNode

        // 记录长度
        this.len++
    }

    /**
     * 出队,在 head 位置
     */
    delete(): number | null {
        const headNode = this.head
        if (headNode == null) return null
        if (this.len <= 0) return null

        // 取值
        const value = headNode.value

        // 处理 head
        this.head = headNode.next

        // 记录长度
        this.len--

        return value
    }

    get length(): number {
        // length 要单独存储,不能遍历链表来获取(否则时间复杂度太高 O(n))
        return this.len
    }
}

9.4 性能分析

  • 空间复杂度都是O(n)
  • add 时间复杂度: 链表O(1)、数组O(1)
  • delete 时间复杂度:链表O(1)、数组O(n)

划重点

  • 数据结构的选择,要比算法优化更重要
  • 要有时间复杂度的敏感性,如length 不能遍历查找

10. 链表-总结

10.1 技术要点

  • 链表里的元素存储不是连续的,之间通过next连接
  • JavaScript 中没有链表,但可以用Object 模拟链表
  • 链表常用操作: 修改链表、遍历链表
  • JS中的原型链也是一个链表
  • 使用链表指针可以获取JSON的节点的值