单链表是什么
已经熟悉单链表概念的读者可以跳至单链表的实现或习题。
链表和数组很像,它们都是线性的存储结构,它们的不同点是数组是用一段连续的内存地址来存储数据,这也是为什么数组可以通过下标来直接访问的原因。
链表是相对松散的线性结构,也就是说链表中每个节点的物理地址可能不是连续的。
数组读取数据的时间复杂度是O(1),而链表则是O(n),因为要访问链表的某个节点必须从头开始遍历。
链表中的一个节点可以分为两部分:指针域和数据域。
指针域是用来指向下一个节点的位置,而数据域则是用来存储这个节点表示的数据。
链表常用场景
- 操作系统内的动态内存分配 (一种实现方式)
- LRU 缓存淘汰算法
- …
单链表的实现
单链表的实现方式主要有两种,通过对象引用的方式和数组方式
1.对象形式
class ListNode {
val: any
next: ListNode | null
constructor(val: any, next: ListNode) {
this.val = val === undefined ? 0 : val
this.next = next === undefined ? null : next
}
}
function checkChain(head: ListNode) {
let result = ''
while (head) {
result += head.val + '->'
head = head.next
}
console.log(result)
}
// main
const head = new ListNode(0, null)
const node1 = new ListNode(1, null)
const node2 = new ListNode(2, null)
const node3 = new ListNode(3, null)
head.next = node1
node1.next = node2
node2.next = node3
checkChain(head) // 0->1->2->3->
2.数组形式
// @ts-ignore
const data: number[] = new Array(10).fill(0)
// @ts-ignore
const next: number[] = new Array(10).fill(0)
const add = (lastPointer: number, curPointer: number, val: number) => {
next[lastPointer] = curPointer
data[curPointer] = val
}
// main
//* 定义头节点
const head = 3
data[3] = 0
//* 添加后续节点
add(3, 5, 1)
add(5, 2, 2)
add(2, 7, 3)
add(7, 9, 100)
//* 遍历链表
let p = head
let result = ''
while (p !== 0) {
result += `${data[p]}->`
p = next[p]
}
console.log(result) // 0->1->2->3->100->
习题
以下是在LeetCode上有关单链表的习题,有条件的读者可以边看边做。
只要是不看答案就写不出来的都不算是掌握了。
===================================
leetCode 141 环形链表
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false 。
解题思路:
使用快慢指针,慢指针每次走一步,快指针每次走两步,如果有环,两个指针迟早会指向同一个节点。
由于每次快指针都比慢指针多走一步,所以当两指针都进入环时,每走一次二者的距离都少1。
function hasCycle(head: ListNode | null): boolean {
if (!head || !head.next) return false
let slow = head, fast = head
do {
slow = slow.next
fast = fast.next.next
} while (slow !== fast && fast && fast.next)
return !!fast && !!fast.next
};
===================================
leetCode 142 环形链表2
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
解题思路:
和上题类似,定义快慢指针,先找到二者相遇的节点
接着先给出一个结论,此时让慢指针回到链头,慢指针和快指针距离环的起点的距离是相等的
接下来解释这个结论是怎么得出的
结合上图所给条件,又知快指针的步长是慢指针的两倍,所以
2
(
a
+
b
)
=
a
+
b
+
n
(
b
+
c
)
a
=
(
n
−
1
)
b
+
n
c
a
=
(
n
−
1
)
(
b
+
c
)
+
c
∵
(
n
−
1
)
为
圈
数
,
直
接
忽
略
不
计
∴
a
=
c
2(a+b)=a+b+n(b+c)\\ a=(n-1)b+nc\\ a=(n-1)(b+c)+c\\ \because (n-1)为圈数,直接忽略不计\\ \therefore a = c
2(a+b)=a+b+n(b+c)a=(n−1)b+nca=(n−1)(b+c)+c∵(n−1)为圈数,直接忽略不计∴a=c
有了以上结论之后解题步骤就清晰了
- 找到快慢指针的相遇点(和上题一样的做法)
- 让慢指针回到链头
- 让快慢指针以每次单位1的步长前进,直到二者相遇为止
function detectCycle(head: ListNode | null): ListNode | null {
if (!head || !head.next) return null
//* 1.先找到相遇点
let slow = head, fast = head
do {
slow = slow.next
fast = fast.next.next
} while (slow !== fast && fast && fast.next)
if (!fast || !fast.next) return null
//* 2.让慢指针回到链头
slow = head
//* 3.使两指针步长都为1,当两指针相遇时即为链环的起点
while (slow !== fast) {
slow = slow.next
fast = fast.next
}
return fast
};
===================================
leetCode 202 快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 true ;不是,则返回 false
输入:19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
输入:n = 2
输出:false
解题思路:
其实快乐数就是一个判断链表是否有闭环的问题,当有闭环时说明这个数不是快乐数,否则就是。
所以解题思路和 leetCode 141 题是一样的。
function isHappy(n: number): boolean {
let slow = n, fast = n
do {
slow = getNext(slow)
fast = getNext(getNext(fast))
} while (slow !== fast && fast !== 1)
return fast === 1
}
function getNext(num: number) {
return String(num).split('').reduce((sum, cur) => sum + Math.pow(Number(cur), 2), 0)
}
当然,如果笔者使用的是js的话,getNext()
可以写作
function getNext(num) {
return String(num).split('').reduce((sum, cur) => sum + cur * cur, 0)
}
因为两个操作数进行 * 操作的时候会将二者隐式类型转换为数字类型
===================================
leetCode 206 反转链表
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
解题思路:
本题有两个解法:三指针和递归
先说三指针:
- 定义三个指针,pre cur next
- 初始 pre 指向 null, cur 指向头节点,next指向头节点的下一节点
- 以 cur 为参考开始遍历整个链表,每次遍历操作如下
- cur 指向 pre
- pre 移动到 cur 所处节点
- cur 移动到 next 所处节点
- next 移动到 next 的下一节点
function reverseList(head: ListNode | null): ListNode | null {
if (!head || !head.next) return head
let pre = null, cur = head, next = head.next
while (cur) {
cur.next = pre
pre = cur
cur = next
if (next) next = next.next
}
return pre
};
递归:
递归的真谛就是甩锅,自己做不到的事就交给别人做,每次(每个作用域)只解决一个具体的问题
在本题中,每次作用域要做的事情就是将下一节点指向自己,最后返回反转后的头节点
以笔者的文字功底,很难将递归描述清楚,所以接下来我会先给出代码,再把图画出,至于能不能看懂,就要看你的悟性了
function reverseList(head: ListNode | null): ListNode | null {
if (head === null || head.next === null) return head
const tail = head.next, p = reverseList(head.next)
head.next = tail.next
tail.next = head
return p
}
===================================
leetCode 92 反转链表II
反转从位置m到n的链表。请使用一次遍历完成反转。
1 ≤ m ≤ n ≤ 链表长度
输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL
思路分析:
由于本题可能会对链头进行操作,因此需要插入虚拟头节点
- 插入虚拟头节点
- 令指针P指向第一个待反转节点的前一位
- 利用上题的方式进行反转链表,注意次数为 n - m + 1
- 将指针P指向反转链表后的头节点
function reverseBetween(head: ListNode | null, left: number, right: number): ListNode | null {
if (!head || !head.next) return head
const virtualHead = new ListNode(null, head)
let p = virtualHead
const cnt = right - left + 1
// 使p指针移动到第一个待反转节点的前一个节点
while (--left) {
p = p.next
}
// 开始反转节点,并使p的next指向反转后链表的头节点
p.next = reverseN(p.next, cnt)
return virtualHead.next
};
function reverseN(node: ListNode | null, cnt: number): ListNode | null {
if (cnt === 1) return node
const tail = node.next
const p = reverseN(node.next, cnt - 1)
node.next = tail.next
tail.next = node
return p
}
===================================
leetCode 25 K个一组反转链表
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
进阶:
- 你可以设计一个只使用常数额外空间的算法来解决此问题吗?
- 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
输入:head = [1,2,3,4,5], k = 1
输出:[1,2,3,4,5]
输入:head = [1], k = 1
输出:[1]
解题思路:
- 原链表插入虚拟头节点,并定义p、q两个指针
p 指向待操作组的头节点的前一位
q 指向待操作组的头节点 - 每次在反转链表前先让q指针判断待操作组数量够不够n个
- 够,执行反转链表操作
- 不够,返回传入头节点
- 当进行了链表反转操作后,q指针 指向了反转后链表的尾节点,此时让 p指针 移动到 q指针 的位置,q指针移向下一位
function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
if (head === null || head.next === null) return head
const virtualHead = new ListNode(0, head)
let p = virtualHead, q = head
while ((p.next = reverseN(q, k)) !== q) { // 该条件成立说明执行了反转操作
p = q
q = q.next
}
return virtualHead.next
};
function reverseN(node: ListNode, n: number): ListNode {
let p = node
const cnt = n
while (--n && p) p = p.next
if (p === null) return node // 不足n个
return _reverseN(node, cnt)
}
function _reverseN(node: ListNode, n: number): ListNode {
if (n === 1) return node
const tail = node.next,
p = _reverseN(node.next, n - 1)
node.next = tail.next
tail.next = node
return p
}
===================================
leetCode 61 旋转链表
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
输入: 1->2->3->4->5->NULL, k = 2
输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL
向右旋转 2 步: 4->5->1->2->3->NULL
输入: 0->1->2->NULL, k = 4
输出: 2->0->1->NULL
解释:
向右旋转 1 步: 2->0->1->NULL
向右旋转 2 步: 1->2->0->NULL
向右旋转 3 步: 0->1->2->NULL
向右旋转 4 步: 2->0->1->NULL
解题思路:
- 定义 指针p 指向头节点,遍历到尾节点,然后使尾节点指向头节点形成闭环
- 令链表长度为L,使 指针p 走 L-(k % L) 步
- 此时,指针p 所指位置为新链表的尾节点,所指的后一位为新链表的头节点
循环右移时指针移动步数: L - (k % L)
循环左移时指针移动步数: k % L (模仿上图自行推断)
function rotateRight(head: ListNode | null, k: number): ListNode | null {
if (head === null || head.next === null || !k) return head
let p = head, length = 1
while (p.next !== null) {
p = p.next
length++
}
p.next = head
let cnt = length - (k % length)
while (cnt--) p = p.next
const newHead = p.next
p.next = null
return newHead
};
===================================
leetCode 24 两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
输入:head = [1,2,3,4]
输出:[2,1,4,3]
输入:head = []
输出:[]
输入:head = [1]
输出:[1]
解题思路:
这题就是上述 leetCode 25 的一个特殊案例,在那道题中令 k = 2 即可得到这道题的答案。
具体过程就不再复述了,不清楚的同学可以回到上面重新学习。
function swapPairs(head: ListNode | null): ListNode | null {
if (!head || !head.next) return head
const virtualHead = new ListNode(null, head)
let p = virtualHead, q = head
while ((p.next = reverseN(q, 2)) !== q) {
p = q
q = q.next
}
return virtualHead.next
};
function reverseN (node: ListNode, n: number): ListNode {
const cnt = n
let p = node
while (--n && p) {
p = p.next
}
if (!p) return node
return _reverseN(node, cnt)
}
function _reverseN (node: ListNode, cnt: number): ListNode {
if (cnt === 1) return node
const tail = node.next
const p = _reverseN(node.next, cnt - 1)
node.next = tail.next
tail.next = node
return p
}
===================================
leetCode 19 删除链表的倒数第n个节点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
输入:head = [1], n = 1
输出:[]
输入:head = [1,2], n = 1
输出:[1]
解题思路:
链表要删除第n个节点,就要先找到第 n - 1 个节点在哪
使用双指针法就能很快解出本题
- 往原链表添加虚拟头节点 (因为有可能会变更头节点)
- 定义慢指针指向虚拟头节点,快指针指向头节点
- 快指针往后走n步
- 快慢指针同时向后走,直到快指针到尾节点
- 此时慢指针就到被删除节点的前一个节点了
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
if (!head) return null
const virtualHead = new ListNode(null, head)
let slow = virtualHead, quick = head
while (--n && quick.next) {
quick = quick.next
}
while (quick.next) {
slow = slow.next
quick = quick.next
}
slow.next = slow.next.next
return virtualHead.next
};
===================================
leetCode 83 删除排序链表中的重复元素
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
输入: 1->1->2
输出: 1->2
输入: 1->1->2->3->3
输出: 1->2->3
解题思路:
—— 快慢指针
- 定义快慢指针同时指向头节点
- 使快指针一直往后走,直到与慢指针代表的值不同
- 使 慢指针的next 指向 快指针
- 使慢指针移动到快指针的位置
- 重复以上步骤直到慢指针到尾节点
function deleteDuplicates(head: ListNode | null): ListNode | null {
if (!head) return null
let slow = head, fast = head
while (slow) {
while (fast && slow.val === fast.val) {
fast = fast.next
}
slow.next = fast
slow = fast
}
return head
};
—— 单指针
- 定义一个指针指向链头,遍历到链尾
- 每次遍历的时候判断下一节点的val是否和当前节点的相等,如果不等使 当前节点的next 指向下下个节点
function deleteDuplicates(head: ListNode | null): ListNode | null {
if (head === null) return null
let p = head
while (p.next) {
if (p.val === p.next.val) {
p.next = p.next.next
continue
}
p = p.next
}
return head
};
===================================
leetCode 82 删除排序链表中的重复元素
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。
输入: 1->2->3->3->4->4->5
输出: 1->2->5
输入: 1->1->1->2->3
输出: 2->3
解题思路:
- 因为这题又有可能操作头节点,所以需要插入虚拟头节点
- 剩下的解题思路和上题基本相似,但是这题只能用双指针,因为删除节点必须要找到被删除节点的前一个节点,在本题的情况下只能用双指针才更加方便
function deleteDuplicates(head: ListNode | null): ListNode | null {
if (head === null || head.next === null) return head
const virtualHead = new ListNode(null, head)
let p = virtualHead, q = p
while (p.next) {
if (p.next.next && p.next.next.val === p.next.val) {
q = p.next.next
while (q && q.val === p.next.val) q = q.next
p.next = q
continue
}
p = p.next
}
return virtualHead.next
};
结语
创作不易,如果本文对你有帮助,请点一个免费的赞鼓励一下。如果本文有错误或者你有更好的想法,欢迎评论区评论~