JavaScript 实现链表(单向)


线性表的一大结构:链式存储结构的一些知识点总结归纳。并使用 JavaScript 来实现简单的单链表。循环链表以及双向链表是在单链表的基础上扩展。

code resource

1. 什么是链表。

来看一下 LeetCode 上对链表的描述:

链表(Linked List)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,
而是在每一个节点里存到下一个节点的指针(Pointer)。

链表

由于不必须按顺序存储,链表在插入的时候可以达到 O(1)O(1) 的复杂度,比另一种线性表 —— 顺序表
快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n)O(n) 的时间,而顺序表相应的时间复杂
度分别是 O(log\ n)O(log n) 和 O(1)O(1)。

使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,
实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,
空间开销比较大。

在计算机科学中,链表作为一种基础的数据结构可以用来生成其它类型的数据结构。链表通常由一连串节
点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置
的链接(links)。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆
体或磁盘上顺序,数据的访问往往要在不同的排列顺序中转换。而链表是一种自我指示数据类型,因为它包
含指向另一个相同类型的数据的指针(链接)。

链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,
双向链表以及循环链表。

链表通常可以衍生出循环链表,静态链表,双链表等。对于链表使用,需要注意头结点的使用。

以上大概就是对链表的一个说明,那用代码来展示一个链表的话是什么样子呢?
我们先从一个简单的单向链表尝试做一下:

2. 链表的表现形式

const linkedList = {
    head: {
        value: 0,
        next: {
            value: 1,
            next: {
                value: 2,
                next: null
            }
        }
    }
};

与图片上展示的链表结构相同:每个节点都有一个 value 属性,展示当前节点的值,以及一个指向下一个节点(保存指向下一个节点的指针)的属性 next。这构成了一个节点。多个节点构成了链表。同时也可以看到在链表的头部,使用 head 来表示,在链表的末端节点 next 指向为 null

既然知道了链表的展现形式,那就尝试使用代码将链表生成:

3. 链表的生成

// 节点
class LinkedNode {
    value: any 
    next: any
    constructor(nodeValue) {
        this.value = nodeValue;
        this.next = null
    }
}
// 链表
class LinkedList {
    constructor() {
        // 生成链表头部,链表头部的数据域中可以不存储任何信息
        // 也可以存储链表长度等信息
        // 这里为了区分,使头节点的数据为 'head'
        this.head = new LinkedNode("head")
    }
    private head: LinkedNode
}

4. 链表的操作

链表也应该支持查询 / 插入 / 删除 等操作,先来实现链表的查询:

4.1 链表的查询

查找一个元素是否在链表中是链表操作中比较常用的操作:

    /**
     * @author Frank Wang
     * @method
     * @name find
     * @description 查询链表中是否有符合条件的节点
     * @param nodeValue {any} 所要查询的 node 节点的 value 值
     * @return {LinkedNode}
     * @example 创建例子。
     * @public
     */
    find(nodeValue: any): LinkedNode {
        let curNode: LinkedNode = this.head

        while (curNode.value != nodeValue) {
            if (!curNode.next) {
                // 如果到链表尾部还没有循环到符合条件的节点
                // 则返回 value 值为 error 对象的节点
                return new LinkedNode(new Error("无此节点"))
            }
            curNode = curNode.next
        }
        return curNode
    }

主要是一个内部的循环,遍历整个链表,判断当前节点的 value 值是否与要查找的值相同。
既然有了查找,那我们向链表中插入某一节点就有思路了:

4.2 向链表中插入节点

向传入的节点的后方插入节点

    /**
     * @author Frank Wang
     * @method
     * @name insert
     * @description 向链表中插入节点,第二个参数为空的话,向尾部插入
     * @param value 向链表中插入的节点的 value
     * @param nodeValue 已经存在的 node 节点的 value
     * @return {LinkedNode}
     * @example 创建例子。
     * @public
     */
    insert(value: any, nodeValue: any = null):LinkedNode {
        let newNode: LinkedNode = new LinkedNode(value)
        let curNode: LinkedNode

        if (!nodeValue) {
            // append 的方法
            // 查找倒数第二个节点
            curNode = this.head
            while (curNode.next !== null) {
                curNode = curNode.next
            }
            curNode.next = newNode
            newNode.next = null
        } else {
            curNode = this.find(nodeValue)
            newNode.next = curNode.next
            curNode.next = newNode
        }

        return this.head
    }

插入的方法,接受两个参数:一个是新的 node 节点的 value,另一个是可选的,已经存在的 node 节点
的 value,如果只传入一个参数,则往链表尾部插入。函数体首先是根据传入的 value 创建一个 node
实例,以及找到需要插入的node节点。然后将当前节点的 next 赋值给新的节点实例,在将新节点,
赋值给当前节点的 next 值。尤其注意这两步的操作,是不能调换的,可以想像为什么。

4.3 展示链表

插入链表之后我们肯定是想知道链表的当前情况,或者是我们在某个时刻想要查看链表的情况,那我们就需要
一个方法来展示链表:

    /**
     * @author Frank Wang
     * @method
     * @name console
     * @description 将链表所有的值在控制台打印出来
     * @param 
     * @return {void}
     * @example 创建例子。
     * @public
     */
    console(): void {
        let currNode: LinkedNode = this.head;
        while (currNode.next) {
            console.log(currNode.next.value);
            currNode = currNode.next;
        }
    }

这个是打印出除了头节点的其他所有节点的 value 值。如果只是单纯的获取节点对象,那么可以使用:

    show() {
        return this.head
    }

同样的我们还会遇到删除某一节点的需求:

4.4 删除链表中某一节点

在解决这个需求之前,我们可以先来分析一下,如何删除一个链表中的节点,就是将此节点的上一节点的
next 指向当前节点的 next。

所以如果我们要删除某一节点,那么我们就需要知道当前节点的上一个节点

    private findPrev(value: any) {
        let curNode: LinkedNode = this.head
        while (curNode.next !== null && curNode.next.value !== value) {
            curNode = curNode.next
        }
        return curNode
    }
    /**
     * @author Frank Wang
     * @method
     * @name delete
     * @description 删除链表中的节点
     * @param {nodeValue} 需要删除的节点的 value
     * @return {LinkedNode}
     * @example 创建例子。
     * @public
     */
    delete(nodeValue: any) {
        const err = "[object Error]"
        const showType = Object.prototype.toString
        let curNode = this.find(nodeValue)
        if (showType.call(curNode) === err) {
            return curNode
        }
        const prevNode = this.findPrev(nodeValue);
        if (prevNode.next !== null) {
            prevNode.next = prevNode.next.next;
        }
        return this.head;

    }

解释一下上面的逻辑:首先是进行判断,对错误条件尽早返回。然后是使用内部方法 findPrev ,查找
所要删除节点的上一节点: prevNode
然后就是简单的 next 指针的改变。

4.5 单向链表的总结

单链表结构和顺序存储结构做对比:

  • 存储分配方式
    • 顺序存储结构用一段连续的存储单元一次存储线性表的数据元素。
    • 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
  • 时间性能
    • 查找
      • 顺序存储结构O(1)
      • 单链表O(n)
    • 插入和删除
      • 顺序存储结构需要平均移动表长一半的元素,时间为 O(n)
      • 单链表再找出某位置的指针后,插入和删除时间仅为O(1)
  • 空间性能
    • 顺序存储结构需要预分配存储空间,分大了,浪费。分小了,易发生上溢。
    • 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。

5. 循环链表

将单链表中终端节点的指针端由空指针改为指向头节点,就使整个单链表形成了一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。

6. 双向链表

双向链表是在单链表的每个节点中,再设置一个指向其前驱节点的指针域。
所以在双向链表中的节点都有两个指针域,一个指向直接后继,另一个指向直接前驱。

6.1 插入

如果需要将 s 节点,插入到 p 节点和 p.next 节点之中,那这个插入操作的顺序是啥?

  • 把 p 赋值给 s 的前驱
  • 把 p.next 赋值给 s 的后继节点
  • 把 s 赋值给 p.next 的前驱
  • 把 s 赋值给 p 的后继

7. 总结

线性表
顺序存储结构 链式存储结构
单链表静态链表循环链表双向链表

这是链表属于线性表的两大结构之一:链式存储结构。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值