链表
链表是多个元素组成的列表。元素存储不连续,用 next
指针连在一起
数组 VS 链表
-
数组:增删首尾元素时往往需要移动元素
-
链表:增删非首尾元素,不需要移动元素,只需要更改
next
的指向插入一个节点效率很高,只需修改它前面的节点,使其指向新加入的节点
删除一个节点,将待删除的元素
JavaScript 中没有链表,可以使用 Object
模拟链表
const a = { val: 'a' }
const b = { val: 'b' }
const c = { val: 'c' }
const d = { val: 'd' }
a.next = b
b.next = c
c.next = d
// 遍历链表
let p = a
while (p) {
console.log(p.val)
p = p.next
}
// 插入
const e = { val: 'e' }
c.next = e
e.next = d
// 删除
c.next = d
237. 删除链表中的节点
输入输出
输入:head = [4,5,1,9], node = 5
输出:[4,1,9]
输入:head = [4,5,1,9], node = 1
输出:[4,5,9]
输入:head = [1,2,3,4], node = 3
输出:[1,2,4]
输入:head = [0,1], node = 0
输出:[1]
输入:head = [-3,5,-99], node = -3
输出:[5,-99]
题解
- 无法直接获取被删除节点的上个节点(链表的节点只会指向下个节点),将被删除节点转移到下个节点
- 将被删节点的值改为下个节点的值
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
var deleteNode = function (node) {
node.val = node.next.val
node.next = node.next.next
}
复杂度:
- 时间复杂度:
O(1)
- 空间复杂度:
O(1)
206. 反转链表
输入输出
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
输入:head = [1,2]
输出:[2,1]
输入:head = []
输出:[]
迭代
- 反转两个节点:将
n+1
的next
指向n
- 反转多个节点:双指针遍历链表,重复上述操作
图解:
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BlSQTw1n-1643104522630)(https://gitee.com/lilyn/pic/raw/master/jslearn-img/206quan.gif)]
-
在遍历列表时,需要将当前节点(
curr
)的next
指针指向上一个节点(prev
),之后移动prev
和curr
为下一个循环做准备
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/*
prev -> curr
null -> 0 -> 1 -> 2
prev <- curr
null <- 0 <- 1 <- 2
*/
var reverseList = function (head) {
let curr = head
let prev = null
while (curr !== null) {
const tmp = curr.next
// 当前节点的next指向上一个节点(完成逆序)
curr.next = prev
// 为下一个循环做准备
prev = curr
curr = tmp
}
return prev
}
简化版:
var reverseList = function (head) {
let [prev, curr] = [null, head]
while (curr !== null) {
[curr.next, prev, curr] = [prev, curr, curr.next]
}
return prev
}
复杂度:
- 时间复杂度:
O(n)
,n
为链表节点数 - 空间复杂度:
O(1)
递归
var reverseList = function (head) {
if (head == null || head.next == null) return head
let next = head.next // next节点,反转后是最后一个节点
const reverseHead = reverseList(head.next)
head.next = null // 裁减 head
next.next = head // 尾接
return reverseHead
}
复杂度:
- 时间复杂度:
O(n)
,共n-1
层递归 - 空间复杂度:
O(n)
,共n-1
层递归
2. 两数相加
输入输出
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。请你将两个数相加,并以相同形式返回一个表示和的链表
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
输入:l1 = [0], l2 = [0]
输出:[0]
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
题解
- 新建一个空链表
- 遍历被相加的两个链表,模拟相加操作,将 个位数 追加到新链表上,将 十位数 留到下一位去相加
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
var addTwoNumbers = function (l1, l2) {
const l3 = new ListNode(0)
let p1 = l1
let p2 = l2
let p3 = l3
let carry = 0
while (p1 || p2) {
// 有可能链表是一长一短
const v1 = p1 ? p1.val : 0
const v2 = p2 ? p2.val : 0
// 两链表节点相加再进位
const val = v1 + v2 + carry
carry = Math.floor(val / 10)
p3.next = new ListNode(val % 10)
if (p1) p1 = p1.next
if (p2) p2 = p2.next
p3 = p3.next
}
// 判断最后一位节点是否有进位
if (carry) {
p3.next = new ListNode(carry)
}
return l3.next
}
复杂度:
- 时间复杂度:
O(n)
,n
为较长链表的长度 - 空间复杂度:
O(n)
,n
为较长链表的最大值
83. 删除排序链表中的重复元素
输入输出
输入:head = [1,1,2]
输出:[1,2]
输入:head = [1,1,2,3,3]
输出:[1,2,3]
题解
- 因为链表是有序的,所以重复元素一定相邻
- 遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素值
- 遍历结束后,返回原链表的头部
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
var deleteDuplicates = function (head) {
let p = head
while (p) {
if (p.val === p.next.val) {
p.next = p.next.next
} else {
p = p.next
}
}
return head
}
复杂度:
- 时间复杂度:
O(n)
,n
为链表的长度 - 空间复杂度:
O(1)
141. 环形链表
输入输出
给你一个链表的头节点 head
,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1,则在该链表中没有环
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
题解
- 两个人在圆形操场上的起点同时起跑,速度快的人一定会超过速度慢的人一圈
- 用一快一慢两个指针遍历链表,如果指针能够相逢,那么链表就有圈
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
var hasCycle = function (head) {
let p1 = head
let p2 = head
while (p1 && p2 && p2.next) {
p1 = p1.next
p2 = p2.next.next
if (p1 === p2) {
return true
}
}
return false
}
复杂度:
- 时间复杂度:
O(n)
- 空间复杂度:
O(1)
原型链
- 原型链本质是链表
- 原型链上的节点是各种原型对象,比如:
Function.prototype
、Object.prototype
- 原型链通过
__proto__
属性连接各种原型对象
原型链 除了对象其它都是先指向自己类型的原型对象再指向 Object
的原型对象
obj -> Object.prototype -> null
func -> Fuction.prototype -> Object.prototype -> null
arr -> Array.prototype -> Object.prototype -> null
const obj = {}
const func = () => {}
const arr = []
/* watch */
obj.__proto__ === Object.prototype
func.__proto__ === Fuction.prototype
func.__proto__.__proto__ === Object.prototype
arr.__proto__ === Array.prototype
arr.__proto__.__proto__ === Object.prototype
原型链知识点
-
如果
A
沿着原型链找到B.prototype
,那么A instanceof B === true
这个实际调用的是
B[Symbol.hasInstanceof](A)
-
如果
A
对象上没有找到x
属性,那么会沿着原型链找x
属性
面试题分析
instanceof
的原理,并用代码实现
解法:
- 遍历
A
的原型链,如果找到B.prototype
,返回true
,否则返回false
const instanceOf = (obj, Ctor) => {
let p = obj
while (p) {
if (p === Ctor.prototype) return true
p = p.__proto__
}
return false
}
推荐解法:
- 检测数据类型步骤跳过
function instance_of(obj, Ctor) {
// 先检测是否有Symbol.hasInstance这个属性
if (typeof Ctor[Symbol.hasInstance] === 'function') return Ctor[Symbol.hasInstance](obj)
// 最后才会按照原型链进行处理
let prototype = Object.getPrototypeOf(obj)
while (prototype) {
if (prototype === Ctor.prototype) return true
prototype = Object.getPrototypeOf(prototype)
}
return false
}
说出下列输出结果
var foo = {},
F = function () {}
Object.prototype.a = 'value a'
Function.prototype.b = 'value b'
console.log(foo.a) // value a
console.log(foo.b) //
console.log(F.a) // value a
console.log(F.b) // value b
解法:
- 明确
foo
和F
变量的原型链,沿着原型链找a
属性和b
属性
前端与链表
使用链表指针获取 JSON 的节点值
const json = {
a: { b: { c: 1 } },
d: { e: 2 },
}
const path = ['a', 'b', 'c']
let p = json
path.forEach(k => {
p = p[k]
})
console.log(p) // 1
234. 回文链表
输入输出
输入:head = [1,2,2,1]
输出:true
输入:head = [1,2]
输出:false
数组双指针
- 将值赋值到数组中后用双指针法
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
var isPalindrome = function (head) {
const arr = []
while (head !== null) {
arr.push(head.val)
head = head.next
}
for (let i = 0, j = arr.length - 1; i < j; i++, j--) {
if (arr[i] !== arr[j]) return false
}
return true
}
复杂度:
- 时间复杂度:
O(n)
,n
为链表的元素个数 - 空间复杂度:
O(n)
,n
为链表的元素个数
快慢指针
将链表的后半部分反转,然后将前半部分和后半部分进行比较,比较完成后再将链表恢复原样
-
找到前半部分链表的尾节点
可以使用 快慢指针 找到链表的中间:慢指针一次走一步,快指针一次走两步,快慢指针同时出发,当快指针移动到链表的末尾,慢指针恰好到链表的中间
-
反转后半部分链表
-
判断是否回文
var isPalindrome = function (head) {
if (head === null) return true
// 快慢指针,让慢指针指向链表后半部分的开头
let fast = head
let slow = head
// 反转指针
let prev = null
let curr = head
while (fast !== null && fast.next !== null) {
// 快慢指针,获得链表中部
fast = fast.next.next
slow = slow.next
// 反转指针,反转前半部分指针
curr.next = prev
prev = curr
curr = slow
}
/*
如果节点为奇数个
原链表 1->3->5->3->1->null
快指针 1 2 3
慢指针 1 2 3
如果节点为偶数个
原链表 1->3->3->1->null
快指针 1 2 3
慢指针 1 2 3
*/
if (fast !== null) {
slow = slow.next
}
while (prev !== null && slow !== null) {
if (slow.val !== prev.val) return false
slow = slow.next
prev = prev.next
}
return true
}