# 👉 链表基础

链表和数组相似,它们都是有序的列表、都是线性结构(有且仅有一个前驱、有且仅有一个后继)。

不同点:链表中的数据单位称为结点,结点和结点的分布在内存中是离散(相对于数组的“连续”来说)的。

数组在内存中最为关键的一个特征,就是它一般对应一段位于自己上界和下界之间的、一段连续的内存空间。元素与元素之间,紧紧相连。数组中的元素是连续的,每个元素的内存地址可以根据其索引距离数组头部的距离来计算出来。因此对数组来说,每一个元素都可以通过数组的索引下标直接定位。

而链表中的每个结点则允许散落在内存中各个空间,结点需要通过指针域和后继结点创造关联。
链表中的每一个结点的结构都包括了两部分的内容:数据域和指针域。JS 中的链表,是以嵌套的对象的形式来实现的:

{
  // 数据域
  value: 1,

  // 指针域
  next: {
    value: 2,
    next: { ... }
  }
}

数据结构种类很多,但是底层存储无非数组或者链表,二者的优缺点如下:

  • 数组
    由于是紧凑连续存储,可以随机访问,通过索引快速找到对应元素,而且相对节约存储空间。但正是因为连续存储,内存空间必须一次性分配够,所以说数组如果要扩容,需要重新分配一块更大的空间,再把数据全部复制过去,时间复杂度是 O(n) ;而且你如果想在数组中间进行叉如何删除,每次必须搬移后面的所有数据以保持连续,时间复杂度是 O(n) 。

  • 链表
    因为元素不连续,而是靠指针指向下一个元素的位置,所以不存在数组的扩容问题;如果知道某一元素的前驱和后驱,操作指针即可删除该元素或者插入新元素,时间复杂度 O(1) 。但是正因为存储空间不连续,你无法根据一个索引算出对应元素的位置,所以不能随机访问;而且由于每个元素必须存储指向前后元素的指针,会消耗更多的存储空间。

数组遍历框架,典型的线性迭代结构:

function traverse(list) {
    for (let i = 0; i < list.length; i++) {
        // 迭代访问 list[i]
    }
}

链表遍历框架,兼具迭代和递归结构:

// 基本的单链表节点
ListNode = {
  value: number
  next: ListNode | null
}

function traverse(node) {
  for(let n = node; n !== null; n = n.next) {
    // 迭代访问
  }
}

function recursive(node) {
  // 递归访问
  node && recursive(node.next)
}

二叉树遍历框架,典型的非线性递归遍历结构:

TreeNode = {
  value: number
  left: TreeNode | null
  right: TreeNode | null
}

function recursive(node) {
  // 递归访问
  node?.left && recursive(node.left)
  node?.right && recursive(node.right)
}

参考文章:
学习算法和刷题的框架思维 (opens new window)