是这样的,偶然看到AlgoCasts这个视频讲解算法的网站,它的试看资源里唯一一个简单的算法题,是leetcode的141题环形链表。视频所讲的解法应该是比较好理解的,但是本身就链表这个概念的理解,以及实际的应用场景,我简单google了一下javaScript 链表这个关键字,Emm...,说实话,我没理解。我又搜了一下链表的应用场景,Emm...,告辞。所以,假期刷算法的第一天,应该不能叫刷,叫抄吧,没理解,简单记录下概念和实现,卷吧都卷吧。
概念
大致说下对我看到的概念的一些感性理解吧,不是准确的定义,表述可能不是很准确。
- 链表是由一个个节点组成的,每个节点有一个
val和一个指针next,next指向下一个节点。 - 链表有头
head和尾tail,单向链表tail指向null - 链表分为单向链表、双向链表和循环链表
下面是来自灵魂画手的草图:
再来说下和数组的区别:
- 数组在内存中是连续存储,链表是随机存储
- 数组插入和删除的效率比较低,链表的插入删除效率比较高
- 随机读取数组某个索引的值效率很高,因为连续存储,内存地址是固定的。链表的数据查找效率就很低,需要从头开始遍历。
实现
class Node {
constructor (value) {
this.value = value
this.next = null
}
}
class LinkedList {
constructor () {
this.head = null
this.tail = this.head
this.length = 0
}
append (value) {
const newNode = new Node(value)
if (!this.head) {
this.head = newNode
this.tail = newNode
} else {
this.tail.next = newNode
this.tail = newNode
}
this.length++
}
prepend (value) {
const node = new Node(value)
node.next = this.head
this.head = node
this.length++
}
insert (value, index) {
if (index >= this.length) {
this.append(value)
}
const node = new Node(value)
const { prevNode, nextNode } = thisg.getPrevNextNodes(index)
prevNode.next = node
node.next = nextNode
this.length++
}
lookup (index) {
let counter = 0;
let currentNode = this.head;
while(counter < index){
currentNode = currentNode.next;
counter++;
}
return currentNode;
}
remove (index) {
let {previousNode,currentNode} = this.getNodes(index)
previousNode.next = currentNode.next
this.length--
}
reverse () {
let previousNode = null
let currentNode = this.head
while(currentNode !== null) {
let nextNode = currentNode.next
currentNode.next = previousNode
previousNode = currentNode
currentNode = nextNode
}
this.head = previousNode
}
}
回到算法题
说回题目,同样把几种解法都抄下来:
set法
遍历链表,创建一个临时的空set(用空数组也行),每遍历一个节点,就往set里塞一个,然后判断有没有重复的,有重复的说明链表有环。感觉和数组去重里的一个解法的思路类似。
const hasCycle = function(head) {
// set
const set = new Set()
let cur = head
while (cur) {
if (set.has(cur)) return true
set.add(cur)
cur = cur.next
}
return false
}
快慢指针法
把两个指针想象成两个在跑步的人,一个人的速度是另一个的2倍,如果是直线跑,两人永远都追不上,但如果绕着操场跑,快的肯定能追到慢的。这个方法没有用到额外的变量,所以空间复杂度是O(1),也是几种解法里最优的。
const hasCycle = function(head) {
if(head === null || head.next === null) {
return false;
}
let slow = head;
let fast = head.next;
while (slow) {
if(slow === fast) {
return true
}
slow = slow?.next || null;
fast = fast?.next?.next || null;
}
return false;
};
打标签法
const hasCycle = function(head) {
while (head) {
if (head.tag) {
return true;
}
head.tag = true;
head = head.next;
}
return false;
}
骚操作,利用javascript语言特性
const hasCycle = function(head) {
try {
JSON.stringify(head);
return false;
} catch {
return true;
}
}
由于循环列表存在对象循环引用,而JSON.stringify在面对循环引用的对象时会报错,所以利用这个API的特性,也算是一种解法吧,因为提交也通过了。
很好,今天就抄到这吧,溜了。