小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
1. 链表(linkedList)简介
1.1 链表是什么?
- 多个元素组成的列表
- 元素存储不联续,用next 指针连在一起
1.2 数组 VS 链表
- 数组: 增删非首尾元素时往往需要移动元素
- 链表:增删非首尾元素,不许要移动元素,只需要更改
next
的指向即可。
- 都是有序结构
- 链表:查询慢O(n),新增和删除块0(1)
- 数组:查询块O(1),新增和删除块0(n)
1.3 JS中的链表
- JavaScript中没有链表
- 可以用Object
模拟
链表
1.4 代码部分
创建、遍历、插入、删除
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;
/** 生成的结构如下
{
"val":"a",
"next":{
"val":"b",
"next": {
"val":"c",
"next": {
"val":"d"
}
}
}
}
*/
// 遍历链表
let p = a;
while (p) {
console.log(p.val);
p = p.next;
}
// 链表插入e
const e = { val: 'e' };
c.next = e;
e.next = d;
// 链表删除e
c.next = d;
2. LeetCode: 237.删除链表中的节点
delete-node-in-a-linked-list
leetcode-cn.com/problems/delete-node-in-a-linked-list/
2.1 解题思路
- 无法直接获取被删除节点的上个节点
- 将被删除节点转移到下个节点
2.2 解题步骤
- 将被删除节点的值改为下一个节点的值
- 删除下一个节点
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} node
* @return {void} Do not return anything, modify node in-place instead.
*/
var deleteNode = function(node) {
node.val = node.next.val;
node.next = node.next.next;
};
3. LeetCode: 206.反转链表
reverse-linked-list
3.1 解题思路
- 反转两个节点:将n+1的next指向n
输入: ...-> n -> n+1 ->...
输出: ...-> n+1 -> n ->...
- 反转多个节点:双指针遍历链表,重复上述操作
输入: 1 -> 2 -> 3 -> 4 -> 5 -> NULL
输出: 5 -> 4 -> 3 -> 2 -> 1 -> NULL
3.2 解题步骤
- 双指针一前一后遍历链表
- 反转双指针
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let p1 = head;
let p2 = null;
while(p1){
const tmp = p1.next;
p1.next = p2;
p2 = p1;
p1 = tmp;
}
return p2
};
4. LeetCode: 2.两数相加
add-two-numbers
4.2 解题思路
- 数学题,模拟相加操作
- 需要遍历链表
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出: 7 -> 0 -> 8
原因: 345 + 465 = 807
4.3 解题步骤
- 新建一个空链表
- 遍历被相加的两个链表,模拟相加操作,将个位数追加到新链表上,将十位数留下一位去相加
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
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;
};
5. LeetCode: 83. 删除排序链表中的重复元素
remove-duplicates-from-sorted-list
5.1 解题思路
- 因为链表是有序的,所以重复元素是相邻的
- 遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素值。
输入:1 -> 1 -> 2
输出: 1 -> 2
5.2 解题步骤
- 遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素值
- 遍历结束,返回原链表的头部
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var deleteDuplicates = function(head) {
let p = head;
while (p && p.next) {
if(p.val === p.next.val){
p.next = p.next.next;
}else {
p = p.next;
}
}
return head
};
6. LeetCode:141. 环形链表
linked-list-cycle
6.1 解题思路
- 两个人在圆形操场上的起点同事起跑,速度快的人一定超过速度慢的人一圈
- 用一块一慢两个指针遍历链表,如果指针能够相逢,那么链表就有圈
6.2 解题步骤
- 用一快一慢两个指针遍历链表,
- 如果指针能够相逢,就返回true
- 如果指针不能够相逢,就返回fasle
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
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;
};
7. 前端与链表:JS 中的原型链
7.1 原型链简介
- 原型链的本质是链表
- 原型链上的节点是各种原型对象,比如:
Function.prototype
、Object.prototype
- 原型链通过
__proto__
(而不是next
)属性链接各种原型对象
7.2 原型链长啥样?
- 对象
obj -> Object.prototype -> null
- 函数
func -> Function.prototype -> Object.prototype -> null
- 数组
arr -> Array.prototype -> Object.prototype -> null
从上发现,除了对象这个类型,其他对象的原型链,对象先指向自己的原型对象,然后再指向Object的原型对象,比如String、Number
都是成立的
7.3 Coding part
- 对象的原型链
obj.__proto__ === Object.prototype
- 函数的原型链
func.__proto__.__proto__ === Object.prototype
7.4 原型链知识点
- 如果
A
沿着原型链能找到B.prototype
,那么A instanceof B
为true
- 如果在
A
对象上没有找到X
属性(x 指任何一个属性值),那么会沿着原型链找到X
属性
7.5 面试题一:instanceof 的原型,并用代码实现
面试题分析
- 知识点:如果
A
沿着原型链能找到B.prototype
,那么A instanceof B
为true
- 解法:遍历
A
的原型链,如果找到B.prototype
返回true
,否则返回fasle
// 判断 A 是否是 B 的示例
const instanceOf = (A, B) => {
let p = A;
while(p){
if(p === B.prototype){
return true
}
p = p.__proto__;
}
return false
}
console.log('instanceOf([], Array__', instanceOf([], Array))
console.log('instanceOf({}, Object__', instanceOf({}, Object))
console.log('instanceOf(1, Number__', instanceOf(1, Number))
运行结果:
7.6 面试题二
var foo = {},
F = function () {};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';
console.log('foo.a__', foo.a)
console.log('foo.b__', foo.b)
console.log('F.a__', F.a)
console.log('F.b__', F.b)
- 知识点:如果在
A
对象上没有找到X
属性(x 指任何一个属性值),那么会沿着原型链找到X
属性 - 解法:明确foo 和 F 变量的原型链,沿着原型链找 a 属性 和 b 属性
8. 前端与链表:
8.1 使用链表指针获取 JSON 的节点值
const json = {
a: { b: { c: 1 } },
d: { e: 2 },
};
const path = ['a', 'b', 'c'];
// 使用链表指针获取 JSON 的节点值 方式
let p = json;
path.forEach((k) => {
p = p[k];
});
8.2 react 的fiber 使用了链表
发现 react 的fiber 使用了链表,
在组件渲染的过程中 可以停顿下来,先做高优先级任务,然后在回来执行。
组件是树结构,中间是不能停的,需要遍历、递归、循环,如果停了上下文就没有了,需要重新开始。
把树变成链表就可以停了。。。
9.链表和数组那个实现队列更快?
9.1 分析
- 数组是连续存储,push 很快,shift 很慢
- 链表是非连续存储,ada 和 delete 都很快,(但查找很慢)
- 结论:链表实现队列更快
9.2 链表实现队列 思路
- 单向链表,单要同时记录head 和 tail
- 要从tail 入队,否则出队 时 tail不好定位
- length 要实时记录,不可遍历链表获取
9.3 链表实现队列 代码
/**
* @description 用链表实现队列
*/
interface IListNode {
value: number
next: IListNode | null
}
export class MyQueue {
private head: IListNode | null = null
private tail: IListNode | null = null
private len = 0
/**
* 入队,在 tail 位置
* @param n number
*/
add(n: number) {
const newNode: IListNode = {
value: n,
next: null,
}
// 处理 head
if (this.head == null) {
this.head = newNode
}
// 处理 tail
const tailNode = this.tail
if (tailNode) {
tailNode.next = newNode
}
this.tail = newNode
// 记录长度
this.len++
}
/**
* 出队,在 head 位置
*/
delete(): number | null {
const headNode = this.head
if (headNode == null) return null
if (this.len <= 0) return null
// 取值
const value = headNode.value
// 处理 head
this.head = headNode.next
// 记录长度
this.len--
return value
}
get length(): number {
// length 要单独存储,不能遍历链表来获取(否则时间复杂度太高 O(n))
return this.len
}
}
9.4 性能分析
- 空间复杂度都是O(n)
- add 时间复杂度: 链表O(1)、数组O(1)
- delete 时间复杂度:链表O(1)、数组O(n)
划重点
- 数据结构的选择,要比算法优化更重要
- 要有时间复杂度的敏感性,如length 不能遍历查找
10. 链表-总结
10.1 技术要点
- 链表里的元素存储不是连续的,之间通过next连接
- JavaScript 中没有链表,但可以用Object 模拟链表
- 链表常用操作: 修改链表、遍历链表
- JS中的原型链也是一个链表
- 使用链表指针可以获取JSON的节点的值