链表

133 阅读3分钟

什么是链表结构?

是内存内部的存储方式.
链表是一种思维逻辑结构。
正常链表是已一个空地址(null)为结尾 可以想象是一串节点串成一串的数据结构。
每个节点至少包含两个部分。一个是【数据区域】;一个是【指针区域(指向下一个节点的地址)】。
链表中的每个节点,通过指针域的值,形成一个线性结构。
只要在数据结构中有指针域就可以串成一个链表结构。(引用)
链表数据结构不适合快速定位数据,适合动态插入和删除数据的应用场景。
单向链表:有一个指针域,只能一直往后走,向着一个方向访问。 双向链表:有两个指针域,一个指向前,一个指向后。可以向前查找,也可以向后查找。

链表的实现方式

js 链表传统实现方式(指针实现)

function Node (data) {
  this.data = data;  // 代表数据
  this.next = null;  // 代表指针
}

// 链表
function main () {
  const head = new Node(1)
  const node1 = new Node(2)
  const node2 = new Node(3)
  const node3 = new Node(4)
  head.next = node1
  node1.next = node2
  node2.next = node3
  // 遍历链表
  var p = head
  let res = ''
  while (p !== null) {
    res += `${p.data}->`
    p = p.next
  }
  return res
}
console.log(main());  // 结果:1->2->3->4->

js 的第二种实现方式(下标实现)

  const data = new Array(10)
  // 指针域 存储是数组下标,相对地址的概念
  const next = new Array(10)

  // 添加节点的函数 在index节点后面添加一个地址为p的节点,值为val
  // index 下标
  // p 节点
  // val 值
  function add(index, p, val) {
    next[index] = p
    data[p] = val
  }

  function main() {
    const head = 3
    data[3] = 0
    add(3, 5, 1)
    add(5, 2, 2)
    add(2, 7, 3)
    add(7, 9, 100)

    // 遍历
    var p = head
    let res = ''
    while (p !== undefined) {
      res += `${data[p]}->`
      p = next[p]
    }
    return res
  }
  console.log(main()) // 0->1->2->3->100-> 

链表的典型应用场景

场景一:操作系统内的动态分配内存
把不同的内存碎片串成了一个链表

场景二:LRU缓存淘汰算法
缓存:对于低速设备的一种有效管理手段。高速设备对于低速设备的一种称呼
内存对于cpu读取数据速度非常快,硬盘对于cpu读取数据速度很慢。将cpu中常用的数据存储到内存中(开辟出来的1GB),内存中的1GB空间对于硬盘来讲就是硬盘的缓存空间。缓存里存的是经常能用到的数据。CPU先到缓存中找,没有再到硬盘中找。
内存中的数据维护是用一种哈希链表的数据结构。
LRU缓存淘汰算法:去缓存中查找数据时,若是查找到了是命中缓存,直接使用,若是没有命中,这会添加到链表中的最后一位,将最前面的一个数据删除(淘汰掉)。

补充知识

环形链表:不是已null作为链表的结尾,而是已链表中的某个节点作为链表。就是有环链表。
链表思维:唯一指向思维

判断链表是否有环:
方法一:借助一个额外的存储区。遍历这个链表,当遍历到新节点时就放到存储区中,判断当前节点是否在存储区中出现过,若出现过证明是环形链表。
方法二:快慢指针。定义两个指针,一个每次向前走两步,一个每次向前走一步。若没有环,两个指针永远不会相遇。若是有环,则两个指针一定会在坏中相遇。

// 141
var hasCycle = function(head) {
  if (head === null || head.next === null) return false;
  let p = head;
  let q = head.next;
  while (p !== q && q && q.next) {
    p = p.next;
    q = q.next.next;
  }
  return p === q;
};

找环的起点:当快慢指针相遇时,将其中一个指针放到链表的起点,两个指针同时向前走,当再次相遇时的那个点就是环的起点

142
var detectCycle = function(head) {
  if (head === null || head.next === null) return null;
  let p = head;
  let q = head;
  do {
    p = p.next;
    q = q.next.next;
  } while (p !== q && q && q.next);
  if (q === null || q.next === null) return null;
  p = head;
  while (p !== q) {
    p = p.next;
    q = q.next;
  }
  return q;
};

虚拟头节点:用来方便操作的。若待反转区域包含了头节点,此时一开始是站在虚拟头节点的位置的。
虚头主要用在链表头地址有可能改变的时候。