链表 (List) 及其经典问题

481 阅读4分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

什么是链表

  • 链表的每个节点至少包含两部分:数据域 与 指针域,并且每个节点,通过指针域的值,形成的一个线性结构
    • 指针域的指在不同编程语言中有不同的概念,如 C 语言中的 地址,js/python/java 中的 引用,数组的下标,也叫相对地址,都可以作为指针域
  • 举个例子,生活中常见的火车,就是一节车厢连着一节车厢,这其实也是链表的一种实现
  • 链表的链式结构,导致其不需要开辟连续的存储区域,即可存储链表中的所有元素,这一点是区别于数组的
  • 链表查找节点的复杂度为O(n),插入/删除节点的复杂度为 O(1)

链表还是数组

  • 提到链表,一定会想到数组,那我们在程序实现时,该如何抉择呢?

  • 首先我们看一下我们相比更加熟悉的数组有什么特点

    • 数组的创建往往意味着一片连续的存储区域的开辟,然后也有可能导致存储空间的浪费
    • 由于数组有索引(下标)的存在,我们在查找某一个元素时是非常搞高效的
    • 但是数组是连续空间的存储方式,导致我们在数组中间插入或者删除一个元素时,就需要挪动其它元素,来保证操作后的数组元素在位置上依然存在连续性,所以在数组中删除和插入元素的操作,效率相对较低
  • 而链表虽然不适合快速的定位数据,但是它比较适合动态的插入和删除数据的应用场景

    • 我们在链表中插入一个元素
      • 只需要将插入位置元素的前一个元素的指针指向插入的新元素
      • 然后将新元素的指针指向当前位置的原元素
    • 同样的,我们在链表中删除一个元素
      • 只需要将需要删除的元素的,前一个位置的元素指针,指向删除的元素的下一个位置的元素
      • 然后将删除元素指向下一个元素的指针给断开即可
  • 所以根据上面两种数据结构的优劣势分析,我们在程序实现中

    • 需要频繁的进行删除和插入操作,可以优先考虑使用链表结构
    • 需要频繁的读取元素,则应该优先考虑使用数组结构

链表的实现方式

  1. 用构造函数创建对象的方式,比较好理解
const createListNode1 = () => {
  function ListNode(value) {
    this.value = value;
    this.next = null;
  }

  const head = new ListNode(1);
  head.next = new ListNode(2);
  head.next.next = new ListNode(3);
  head.next.next.next = new ListNode(4);

  let current = head;
  let listNodeValue = [];
  while (current !== null) {
    listNodeValue.push(current.value);
    current = current.next;
  }
  console.log(listNodeValue.join("->"));
};
createListNode1(); // 1->2->3->4
  1. 创建 2 个数组,一个当作数据域,一个当作指针域,也能创建链表,不好理解,但很神奇
const createListNode2 = () => {
  let next = []; // 指针域
  let data = []; // 数据域

  // 在 index 的位置,添加一个节点 node,其值为 value
  const addNode = (index, node, value) => {
    // 将插入的节点的指针指向 当前插入位置的 下一个节点
    next[node] = next[index]; // 这行代码是防止不按顺序插入节点
    next[index] = node;
    data[node] = value;
  };

  const head = 3; // 定义头节点 3,其值为 0
  data[3] = 0;

  addNode(3, 5, 1); // 在节点 3 的后面添加节点 5,其值为 1
  addNode(5, 2, 2);
  addNode(2, 7, 3);
  addNode(7, 9, 4); // 现在的链表为 0->1->2->3->4

  addNode(5, 6, 5); // 不按顺序插入节点,0->1->5->2->3->4

  let node = head;
  let listNodeValue = [];
  while (node) {
    listNodeValue.push(data[node]);
    node = next[node];
  }
  console.log(listNodeValue.join("->"));
};
createListNode2(); // 0->1->5->2->3->4

链表的用场景

操作系统内的动态内存分配

  • 用于操作系统内的动态内存分配,管理剩余的内存空间
  • 如下图所示,一块 4G 的内存,被 malloc 申请 1G 内存之后,剩下的内存区在底层就是通过链表去维护的 image.png

LRU 缓存淘汰算法

  • 实际的实现不是简单的链表,而是哈希链表
  • 这里简单理解缓存中存储的数据,是通过普通链表维护的
    • 假设缓存总共只能存 3 个数据,当要存第 4 个数据时,系统会将最先插入的数据 1 删除,然后在数据 3 后面添加一个数据 4

image.png

最后

  • 链表的分享就到这里了,欢迎大家在评论区里面讨论自己的理解 👏。
  • 如果觉得文章写的不错的话,希望大家不要吝惜点赞,大家的鼓励是我分享的最大动力 🥰