头图

🧑‍💻JavaScript数据结构与算法-HowieCong

务必要熟悉JavaScript使用再来学!

一、链表的基本形态

  • 链表和数组都是有序的列表,都是线性结构(有且仅有一个前驱,有且仅有一个后续);不同点在于,链表中,数据单位的名称叫做“结点”,而结点和结点的分布,在内存中都是离散的

1. 数组的“连续”

  • 在内存中最为关键的一个特征,就是对应一段位于自身上界和下界之间的,一段连续的内存空间。元素与元素之间,紧密相连,如下:

image.png

2. 链表的“离散

  • 链表的结点,允许散落在内存空间的各个角落里。内容为1->2->3->4->5的链表,它的内存形态可以散乱如下:

image.png

3. 区别

  • 数组的元素是连续的,每个元素的内存地址可以根据其索引距离数组头部的距离来计算。对数组来说,每一个元素都可以通过数组的索引下标直接定位
  • 链表的元素和元素之间似乎毫无内存上的可分,没有关联时,就创造关联!在链表中,每个结点的结构包括了两部分的内容:数据域和指针域。JS中的链表,是以嵌套对象的形式来实现的:
{
    // 数据源
    val: 1,
    // 指针域,指向下一个结点
    next:{
        val: 2,
        next:{
            val:3,
            next:...
        }
    }
}       
!!!数据域存储的是当前结点所存储的数据值,而指针域则代表下一个结点(后续结点)的引用。用next指针来记录后续结点的引用,每一个结点至少都能知道自己后面的数值是哪位了,原来的相互独立的结点之间从无关系都有关系了!!!主动才有故事啊

image.png

简化来就是如下这样:

image.png

要想访问链表的每一个元素,都得从起点结点开始,逐个访问next,一直访问到目标结点为止,为了保证起点结点是可抵达的,我们还会设定一个head指针来专门指向链表的开始位置:

image.png

二、链表结点的创建

  • 创建链表结点,需要一个构造函数
function ListNode(val){
    this.val = val;
    this.next = null;
}
  • 使用构造函数去创建结点时,传入val(数据域对应的值内容)、指定next(下一个链表结点)
const node = new ListNode(1)
node.next = new ListNode(2)

image.png

三、链表结点的添加

  • 链表的结点间关系是通过next指针来维系的,因此,链表元素的添加和删除操作,本质上是在围绕next指针来做文章
  • 直接在尾部添加相对比较简单方便,改变一个next指针就可以
  • 如果在两个结点间插入一个结点?

    • 链表有时会有头结点,这时即便你是在链表头增加结点,其本质也是“在头结点和第一个结点之间插入一个新结点”。
    • 因此,任意两结点间插入一个新结点这种类型的增加操作,将会是链表基础中的一个关键考点
  • 我们需要变更的是前驱结点和目标结点的next指针指向:如下

    • 插入前:

    image.png

    • 插入后:

    image.png

// 如果目标结点本来不存在,那么记得手动创建
const new ListNode(3)
// 把node3的指针指向node2(node1.next)
node3.next = node1.next
// 把node1的next指针指向node3
node1.next = node3

四、链表结点的删除

  • 把重心放在next指针的调整上
  • 如何把刚才添加进来的node3从现在的链表删除?
  • 删除的标准:在链表的遍历过程中,无法遍历到某个结点的存在,按照这个标准,要想遍历不到node3,我们直接让它的前驱结点node1的next指针跳过它,指向node3的后续即可:

image.png

  • node3成为了一个完全不抵达的结点,它会被JS的垃圾回收器自动回收掉,这个过程如下代码:
node1.next = node3.next
提醒:在涉及链表删除操作的题目中,重点不是定位目标结点,而是定位目标结点的前驱结点。做题时,完全可以只使用一个指针(引用)这个指针用来定位目标结点的前驱结点
// 利用node1可以定位到node3
const target = node1.next
node1.next = target.next

五、链表和数组的辨析

1. JS数组未必是真正的数组

  • 假设数组长度为n,那么因为增加或删除操作导致需要移动的元素数量,就会随着数组长度n的增大而增大,呈现一个线性关系,所以说数组增加或删除操作对应的复杂度为O(n)
  • JavaScript有点特别,JS数组未必是真正的数组!

    • 我们在一个数组中只定义了一种类型的元素,如下
    const arr = [1,2,3,4]
    • 定义了不同类型的元素:
    const arr = ['haha', 1, {a:1}]
    对应的就是一段非连续的内存。JS 数组不再具有数组的特征,其底层使用哈希映射分配内存空间,是由对象链表来实现的

2. 链表高效的增删操作

  • 在链表中,添加和删除操作的复杂度是固定的
  • 不管链表里面的结点个数n有多大,只要我们明确了要插入/删除的目标位置,那么我们需要做的都仅仅是改变目标结点及其前驱/后继结点的指针指向
  • 因此链表增删操作的复杂度是常数级别的复杂度,用大O表示法表示为 O(1)

3. 链表麻烦的访问操作

  • 链表也有一个弊端:当我们试图读取某一个特定的链表结点时,必须遍历整个链表来查找它
  • 比如说我要在一个长度为 n(n>10) 的链表里,定位它的第 10 个结点,如下:
// 记录目标结点的位置 
const index = 10 
// 设一个游标指向链表第一个结点,从第一个结点开始遍历 
let node = head 
// 反复遍历到第10个结点为止 
for(let i=0;i<index&&node;i++) { 
    node = node.next 
}
  • 随着链表长度的增加,我们搜索的范围也会变大、遍历其中任意元素的时间成本自然随之提高。这个变化的趋势呈线性规律,用大O表示法表示为 O(n)
  • 在数组中,我们直接访问索引、可以做到一步到位,这个操作的复杂度会被降级为常数级别(O(1)),如下:
arr[9]

六、总结

链表的插入/删除效率较高,而访问效率较低

数组的访问效率较高,而插入效率较低

可能会作为数据结构选型的依据来单独考察

❓其他

1. 疑问与作者HowieCong声明

  • 如有疑问、出错的知识,请及时点击下方链接添加作者HowieCong的其中一种联系方式或发送邮件到下方邮箱告知作者HowieCong
  • 若想让作者更新哪些方面的技术文章或补充更多知识在这篇文章,请及时点击下方链接添加里面其中一种联系方式或发送邮件到下方邮箱告知作者HowieCong
  • 声明:作者HowieCong目前只是一个前端开发小菜鸟,写文章的初衷只是全面提高自身能力和见识;如果对此篇文章喜欢或能帮助到你,麻烦给作者HowieCong点个关注/给这篇文章点个赞/收藏这篇文章/在评论区留下你的想法吧,欢迎大家来交流!

2. 作者社交媒体/邮箱-HowieCong


HowieCong
2 声望0 粉丝

大前端开发 => AI 小菜鸡!虚心好学!欢迎一起交流!每篇文章尾部有HowieCong联系方式!(Wechat|Instagram|Feishu|Juejin|Segmentfault...)