什么是链表结构?
是内存内部的存储方式.
链表是一种思维逻辑结构。
正常链表是已一个空地址(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;
};
虚拟头节点:用来方便操作的。若待反转区域包含了头节点,此时一开始是站在虚拟头节点的位置的。
虚头主要用在链表头地址有可能改变的时候。