什么是链表
链表:由一组零散的内存块透过指针连接而成,每一个块中必须包含当前节点内容以及后继指针。 常见的链表类型有单链表、双链表以及循环链表。
单链表
- 每个节点存储有当前数据和下一个节点的内存地址
- 最后一个节点next为
null
单链表的基本组成:
- 生成新节点
- 获取链表
- 查询是否存在指定节点
- 新增节点
- 插入节点到指定节点前
- 移除节点
- 节点大小
基本结构代码
class LinkedList {
constructor() {
// 头节点
this.head = null
// 链表长度
this.length = 0
}
// 生成新节点
createNode(val) {
return {
element: val,
next: null
}
}
// 获取链表
getList() {
return this.head
}
// 查询节点
search(element) { }
// 新增节点
append(element) { }
// 插入节点到指定位置
insert(position, element) { }
// 移除节点
remove(element) { }
isEmpty() {
return this.length === 0
}
size() {
return this.length
}
}
新增方法append
新增分为两种情况
- 链表为空时
- 链表不为空时
思路
- 链表为空时,将
head
指向新节点,链表长度加一 - 链表不为空时,遍历节点,直到最后一个节点,将最后一个节点的
next
指向新节点,链表长度加一
代码实现
// 新增节点
append(element) {
const node = this.createNode(element)
// 空链表,head指向新节点
if(this.isEmpty()) {
this.head = node
} else {
let p = this.head
// 寻找最后一个节点
while(p.next) {
p = p.next
}
p.next = node
}
this.length +=1
}
使用
const list = new LinkedList()
list.append('233')
list.append('666')
查询链表中是否存在指定值search
思路
- 链表为空,直接返回false
- 链表不为空,遍历链表
- 判断节点值是否等于指定值,如果等于,返回
true
;否则移动到下一个节点,继续查询
- 判断节点值是否等于指定值,如果等于,返回
- 查询完整个列表,都没有找到,返回
false
代码实现
// 查询节点
search(element) {
// 空链表
if(this.isEmpty()) {
return false
}
// 非空链表
let p = this.head
while(p) {
// 查询到,返回true
if(p.element === element) {
return true
}
p = p.next
}
// 查询整个链表,没有找到
return false
}
指定位置插入节点insert
position
为0
时,插入节点的next
指向head
,head
指向插入节点,更新链表长度position
满足0 < position <= 链表长度
条件时,遍历节点- 直到遍历节点索引和
position
相等,插入节点的next
指向索引对应的节点,索引前一个节点的next
指向插入节点, 更新链表长度
- 直到遍历节点索引和
代码实现
// 插入节点到指定位置
insert(position, element) {
const node = this.createNode(element)
if(position === 0) {
node.next = this.head
this.head = node
this.length +=1
}
if(position > 0 && position <= this.size()) {
let prev = this.head
let cur = this.head
const size = this.size()
for (let i = 0; i <= size; i++) {
if(i === position) {
node.next = cur
prev.next = node
this.length +=1
break;
}
// 指针后移
prev = cur
cur = cur.next
}
}
}
移除节点remove
情况一:
情况二:
- 空链表,直接返回
- 链表不为空,
- 如果移除第一个节点,把
head
指向head.next
,更新链表长度,结束函数 - 不是移除第一个节点,遍历节点
- 判断当前节点值与移除值是否相等,如果相等,前一个节点的
next
指向当前节点的next
- 判断当前节点值与移除值是否相等,如果相等,前一个节点的
- 如果移除第一个节点,把
代码实现
// 移除节点
remove(element) {
if(this.isEmpty()) {
return
}
// 如果移除第一个节点
if(this.head.element === element) {
this.head = this.head.next
this.length--
return
}
// 指向前一个节点
let prev = this.head
let cur = this.head
let isFind = false
while(cur && !isFind) {
// 判断是否查询到指定值
if(cur.element === element) {
isFind = true
// 前一个节点next指向当前节点的next,跳过当前节点,达到移除效果
const next = cur.next
prev.next = next
// 更新链表长度
this.length--
} else {
prev = cur
cur = cur.next
}
}
}
完整单链表代码
class LinkedList {
constructor() {
// 头节点
this.head = null
// 链表长度
this.length = 0
}
// 生成新节点
createNode(val) {
return {
element: val,
next: null
}
}
// 获取链表
getList() {
return this.head
}
// 查询节点
search(element) {
// 空链表
if(this.isEmpty()) {
return false
}
// 非空链表
let p = this.head
while(p) {
// 查询到,返回true
if(p.element === element) {
return true
}
p = p.next
}
// 查询整个链表,没有找到
return false
}
// 新增节点
append(element) {
const node = this.createNode(element)
// 空链表,head指向新节点
if(this.isEmpty()) {
this.head = node
} else {
let p = this.head
// 寻找最后一个节点
while(p.next) {
p = p.next
}
p.next = node
}
this.length +=1
}
// 插入节点到指定位置
insert(position, element) {
const node = this.createNode(element)
if(position === 0) {
node.next = this.head
this.head = node
this.length +=1
}
if(position > 0 && position <= this.size()) {
let prev = this.head
let cur = this.head
const size = this.size()
for (let i = 0; i <= size; i++) {
if(i === position) {
node.next = cur
prev.next = node
this.length +=1
break;
}
// 指针后移
prev = cur
cur = cur.next
}
}
}
// 移除节点
remove(element) {
if(this.isEmpty()) {
return
}
// 如果移除第一个节点
if(this.head.element === element) {
this.head = this.head.next
this.length--
return
}
// 指向前一个节点
let prev = this.head
let cur = this.head
let isFind = false
while(cur && !isFind) {
// 判断是否查询到指定值
if(cur.element === element) {
isFind = true
// 前一个节点next指向当前节点的next,跳过当前节点,达到移除效果
const next = cur.next
prev.next = next
// 更新链表长度
this.length--
} else {
prev = cur
cur = cur.next
}
}
}
isEmpty() {
return this.length === 0
}
size() {
return this.length
}
print() {
if(!this.isEmpty()) {
const arr = []
let p = this.head
while(p) {
arr.push(p.element)
p = p.next
}
console.log(arr)
}
}
}
查找:从头节点开始查找,时间复杂度为 O(n)
插入或删除:在某一节点后插入或删除一个节点(后继节点)的时间复杂度为 O(1)
双向链表
双链表中的节点有两个指针,前驱指针和后继指针,新增了一个尾节点指针tail
,永远指向最后一个节点
基本结构
// 双向链表
class DoublyLinkedList {
constructor() {
// 头节点
this.head = null
// 尾节点
this.tail = null
// 链表长度
this.length = 0
}
// 生成新节点
createNode(val) {
return {
element: val, // 节点值
prev: null, // 前驱指针
next: null // 后继指针
}
}
// 获取链表
getList() { return this.head }
// 查询节点
search(element) {}
// 新增节点
append(element) {}
// 插入节点到指定位置
insert(position, element) {}
// 移除节点
remove(element) {}
isEmpty() { return this.length === 0 }
size() { return this.length }
}
新增节点到尾部append
链表为空时
链表不为空
思路
- 创建新节点
- 判断链表是否为空,如果为空,把
head
指向新节点,tail
也指向新节点 - 链表不为空,通过
tail
取出尾部节点,尾节点的next
指向新节点
, 最后把tail
指向新节点
- 链表长度加一
代码实现
// 新增节点
append(element) {
const node = this.createNode(element)
// 空链表
if(!this.head) {
this.head = node
this.tail = node
} else {
// 取出尾部节点
// 添加到新节点到链表中
const cur = this.tail
cur.next = node
node.prev = cur
// 更新tail指向
this.tail = node
}
this.length++
}
指定位置插入节点insert
在头部插入
在尾部插入
中间任意位置插入
插入位置position
合法范围为:0 <= position <= 链表长度
思路
- 判断
position
是否为合法范围,如果不是,直接返回false
,插入失败- 插入位置有效
position=0
,头部位置插入,新节点next
指向head.next
,再把head.prev
指向新节点,最后head
指向新节点,链表长度加一,返回插入成功position=链表长度
,尾部位置插入,新节点prev
指向尾部节点,尾部节点next
指向新节点,tail
指向新节点position=k
,中间任意位置插入,遍历链表- 直到节点索引等于k,
新节点.next
指向cur
节点,cur.prev
指向新节点,再让新节点.prev
指向前一个节点,前一个节点prev
指向新节点,最后返回插入成功
- 直到节点索引等于k,
- 插入位置有效
代码实现
// 插入节点到指定位置
insert(position, element) {
const node = this.createNode(element)
// 判断position合法性
if(position >= 0 && position <= this.length) {
// 插入头部
if(position === 0) {
const cur = this.head
node.next = cur
cur.prev = node
this.head = node
} else if(position === this.length) {
// 插入尾部
const cur = this.tail
node.prev = cur
cur.next = node
this.tail = node
} else {
// 插入中间任意位置
let cur = this.head
let prev = this.head
const size = this.size()
for (let i = 0; i < size; i++) {
if(position === i) {
node.next = cur
cur.prev = node
node.prev = prev
prev.next = node
break
} else {
prev = cur
cur = cur.next
}
}
}
this.length++
return true
}
return false
}
移除指定位置的节点remove
思路
- 链表为空,直接返回null
- 链表不为空,判断
position
是否符合合法范围:position >= 0 && position < 链表长度
- 在合法范围内,遍历链表,直到指定位置链表,移除节点:
position=0
, 记录头节点,head
指向头结点的next
position=数组长度-1
,记录尾节点,取出尾节点的前驱节点,前驱节点next
指向null
,tail
指向前驱节点- 找到索引等于
position
的节点,记录被移除的节点,取出移除节点的后继节点
,后继节点.prev
指向prev节点
,prev节点
的next
指向后继节点 - 链表长度减少一,返回被移除的结点
- 不在合法范围内,返回
null
代码实现
// 移除指定位置的节点
removeAt(position) {
// 空链表
if(this.length === 0) {
return null
}
// 被移除的节点
let removeNode = null
if(position >=0 && position < this.length) {
// 移除头节点
if(position === 0) {
const removeNode = this.head
this.head = removeNode.next
} else if(position === this.length-1) {
// 移除尾节点
const removeNode = this.tail
const prev = removeNode.prev
prev.next = null
this.tail = prev
} else {
// 移除中间位置节点
let cur = this.head
let prev = this.head
let length = this.length
for (let i = 0; i < length; i++) {
if(position === i) {
removeNode = cur
const nextNode = removeNode.next
nextNode.prev = prev
prev.next = nextNode
break
} else {
prev = cur
cur = cur.next
}
}
}
this.length--
return removeNode
}
// 没有找到要移除的元素
return null
}
循环单链表
循环单链表是一种特殊的单链表,它和单链表的唯一区别是:单链表的尾节点指向的是 NULL,而循环单链表的尾节点指向的是头节点,这就形成了一个首尾相连的环:
合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
思路
- 从链表开头开始,比较
l1.val
与l2.val
大小- 如果
l1.val
小于等于l2.val
,此时最小值就是l1.val
,第二小值在l1.next
和l2
中,进行递归查找 - 如果
l1.val
大于l2.val
,此时最小值就是l2.val
,第二小值在l2.next
和l1.val
中,进行递归查找 - 直到递归到
l1
l2
均为null
- 当递归到任意链表为
null
,直接将next
指向另一条链表,不需要继续递归
- 如果
代码实现
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
const mergeTwoLists = function(l1, l2) {
// 节点为null,直接返回另外一条链表
if(!l1) {
return l2
}
if(!l2) {
return l1
}
if(l1.val <= l2.val) {
// 最小值为l1,继续递归找第二小的值,在l1.next和l2中查找
l1.next = mergeTwoLists(l1.next, l2)
// 返回链表
return l1
} else {
// 最小值为l2,继续递归找第二小的值,在l2.next和l1中查找
l2.next = mergeTwoLists(l2.next, l1)
return l2
}
}
判断链表是否有环
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false
思路
- 定义两个指针,一快一满。慢指针每次只移动一步,而快指针每次移动两步。
- 初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
代码实现
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
const hasCycle = function(head) {
// 形成环,至少需要2个节点
if(head === null || head.next === null) {
return false
}
// 初始设置快慢指针
let slow = head
let fast = head.next
while(slow !== fast) {
// 没有环
if(fast === null || fast.next === null) {
return false
}
slow = slow.next
// 快指针每次移动两个位置
fast = fast.next.next
}
return true
}
反转链表
给定单链表的头节点 head
,请反转链表,并返回反转后的链表的头节点。
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶: 你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
迭代法
思路: 遍历链表,每个节点的后继指针指向它的前一个节点
代码实现
/**
* @param {ListNode} head
* @return {ListNode}
*/
const reverseList = function(head) {
// 如果是空链表或者只有一个节点
if(!head || !head.next) {
return head
}
let prev = null
let cur = head
while(cur) {
// 临时存储curr的后继节点
const nextNode = cur.next
// 反转后继指针
cur.next = prev
// 移动到下一个节点
prev = cur
cur = nextNode
}
// 更新head为最后一个节点
head = prev
return head
}
链表的中间结点
给定一个带有头结点 head
的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例1
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例2
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
思路
- 使用快慢指针,慢指针每次走一步,快指针每次走两步,当快指针到达尾部时,慢指针刚好到中间
代码实现
/**
* @param {ListNode} head
* @return {ListNode}
*/
const middleNode = function(head) {
// 快慢指针
let slow = head
let fast = head
// 当快指针到达终点时,慢指针刚好在中间
while(fast && fast.next) {
// 慢指针每次走一步
// 快指针走两步
slow = slow.next
fast = fast.next.next
}
return slow
}
删除链表的倒数第n个结点
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
示例:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例2
输入:head = [1], n = 1
输出:[]
说明: 给定的 n 保证是有效的。
方式一:数组法
- 遍历一次链表,把每个结点放入数组中
- 获取数组长度,通过长度可以计算出
删除节点
的前驱节点索引为length-n-1
,删除节点
的索引为length-n
, 再把前驱节点的next
指向删除节点
的next
,最后返回head
- 如果
length
等于n
,是移除头节点
代码实现
/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/
const removeNthFromEnd = function(head, n) {
// 定义栈存储节点
let stack = []
let p = head
while(p) {
stack.push(p)
p = p.next
}
const length = stack.length
// 移除头节点
if(n === length) {
head = head.next
return head
}
// 移除其它位置的,取出移除节点的前驱节点,前驱节点指向移除节点的后继
let prevNode = stack[length-n-1]
let curNode = stack[length-n]
prevNode.next = curNode.next
return head
}
方式二:双指针法
- 定义
first
和second
指针 second
指针先走n
步- 判断second是否等于null,如果等于,说明是移除头节点,直接返回
first.next
- 移除头节点之后的节点
first
和second
指针同时移动,每次移动一个位置,直到second.next
为null
,说明second
指向尾节点, 此时first
指向移除节点的前一个节点first.next
指向first.next.next
,完成节点移除- 返回
head
移除头节点
移除头节点之后的节点
代码实现
/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/
const removeNthFromEnd = function(head, n) {
let first = head
let second = head
// 第2个指针先走n步
while(n > 0) {
second = second.next
n--
}
// 移除头节点
if(second === null) {
head = first.next
return head
}
// 移除头节点之后的节点
// 两个指针一起移动,直到second到最后一个节点为止
while(second.next) {
first = first.next
second = second.next
}
// 前驱节点指向后继的后继
first.next = first.next.next
return head
}
相交链表
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例2
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
思路
- 准备两个指针
pA
和pB
,分别指向两链表头节点headA
,headB
- 假设链表
A
长度为m
,链表B
长度为n
,「两链表的公共尾部」的节点数量为c
- 同步遍历链表
A
和B
- 如果指针
pA
先遍历完链表A
, 把指向更改为headB
,遍历链表B
, 如果发生重合,一共走过步数为
m+(n-c)
- 如果指针
pB
先遍历完链表B
, 把指向更改为headA
,遍历链表A
, 如果发生重合,一共走过步数为
n+(m-c)
- 如果指针
pA
和pB
相交,那么有以下公式成立
m+(n-c) = n+(m-c)
发生重合情况时有两种情况:
pA
和pB
指向同一个节点,c>0
pA
和pB
都为null,c=0
代码实现
/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
const getIntersectionNode = function(headA, headB) {
let pA = headA
let pB = headB
// 如果相交,结束遍历,如果不相交,在遍历两个链表后,最后都是null
while(pA !== pB) {
// 没有遍历完,取出下一个节点; 否则移动到另外一个链表头继续遍历
pA = pA === null ? headB : pA.next
pB = pB === null ? headA : pB.next
}
return pA
}
图示
- 初始状态
- 同步遍历链表A和B,其中一条较短链表遍历完,移动指针到另外一条继续遍历,如下
pB
遍历完链表B
,转向链表A
的头部开始
pA
遍历完链表A
,转向链表B
的头部开始
- 最后在同一个节点相遇