链表
什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点是又两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表分为单链表,双链表,循环链表
class Node {
constructor(data) {
this.data = data; // 节点的数据域
// 节点的指针域 单链表中用不上prev前指针
this.prev = null;
this.next = null; // 节点的指针域
}
}
链表和数组的区别:
- 数组在内存中是连续分布的,但是链表在内存中不是连续分布的
- 链表是通过指针域中指针链接在内存中的各个节点
- 所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
类型 | 插入/删除时间复杂度 | 查询 | 适用场景 |
---|---|---|---|
数组 | O(n) | O(1) | 数据量固定,频繁查询,较少增删 |
链表 | O(1) | O(n) | 数据量步固定,频繁增删,较少查询 |
链表的操作
- 删除节点,比如p->q->s,如果我们想要删除q节点,我们只用将p节点的next指针指向s节点即可。(此时q节点依然存在于内存中,所以需要内存释放。)
- 增加节点,比如p->q->s,如果我们想要增加节点t在p和q中间,只需要将p的next指针指向t,t的next指针指向q,就完成了添加操作
- 可以看到,链接的增加节点和删除节点操作时间复杂度都是O(1)。
- 查询节点,链接只支持从头到尾进行查找,不像数组那样,找到第五个数直接array[4]。因此查询时间复杂度为O(n)
1、移除链表元素
- leetCode203移除链表元素
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
迭代的方法删除链表中所有节点值等于特定值的节点。
用temp 表示当前节点。
- 如果temp 的下一个节点不为空且下一个节点的节点值等于给定的val,则需要删除下一个节点。删除下一个节点可以通过以下做法实现:
Temp.next=temp.next.next
-
如果temp 的下一个节点的节点值不等于给定的 \textit{val}val,则保留下一个节点,将temp 移动到下一个节点即可。
-
当temp 的下一个节点为空时,链表遍历结束,此时所有节点值等于 val 的节点都被删除。
具体实现方面,由于链表的头节点head 有可能需要被删除,因此创建哑节点 dummyHead,令dummyHead.next=head,初始化temp=dummyHead,然后遍历链表进行删除操作。最终返回dummyHead.next 即为删除操作后的头节点。
var removeElements = function(head, val) {
const dummyHead = new ListNode(0);
dummyHead.next = head;
let temp = dummyHead;
while (temp.next !== null) {
if (temp.next.val == val) {
temp.next = temp.next.next;
} else {
temp = temp.next;
}
}
return dummyHead.next;
};
2. 设计链表
- leetCode707 设计链表
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); //链表变为1-> 2-> 3
linkedList.get(1); //返回2
linkedList.deleteAtIndex(1); //现在链表是1-> 3
linkedList.get(1); //返回3
class MyLinkedList {
constructor () {
this.data = null
}
// 获取链表中第 index 个节点的值。如果索引无效,则返回-1
get (index) {
let curr = this.data
for (let i = 0; i < index; i++) {
if (!curr) break
curr = curr.next
}
return curr ? curr.val : -1
}
// 在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点
addAtHead (val) {
this.data = { val, next: this.data }
}
// 将值为 val 的节点追加到链表的最后一个元素
addAtTail (val) {
let curr = this.data
if (!curr) return this.data = { val, next: null }
while (curr.next) {
curr = curr.next
}
curr.next = { val, next: null }
}
// 在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点
addAtIndex (index, val) {
if (index <= 0) return this.addAtHead(val)
let curr = this.data
index--
while (index && curr.next) {
index--
curr = curr.next
}
if (index > 0) return
let tmp = curr.next
curr.next = { val, next: tmp }
}
// 如果索引 index 有效,则删除链表中的第 index 个节点
deleteAtIndex (index) {
let curr = this.data, prev = null
if (!curr) return
if (index === 0) return this.data = curr.next
while (index && curr.next) {
index--
prev = curr
curr = curr.next
}
if (index > 0) return
prev.next = curr.next
}
}
3. 翻转链表
- leetCode206 反转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
var reverseList = function(head) {
let prev = null,
curr = head;
while (curr !== null) {
[curr.next, prev, curr] = [prev, curr, curr.next];
}
return prev;
};
var reverseList = function (head) {
let prev = null // 尾随cur的prev指针,开始时指向null
let cur = head // 推进指针,开始时指向头结点
while (cur) { // cur指针推进到null节点,则退出循环
let next = cur.next // 暂存cur的下一节点
cur.next = prev // 将cur的next指针指向prev
prev = cur // 将prev更新为cur节点
cur = next // 将cur指针推进一个节点
} // 退出while时,cur指向null,prev指向原链的尾节点
return prev
};
4. 交换链表
-
leetCode24 两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
输入:head = [1,2,3,4]
输出:[2,1,4,3]
const swapPairs = (head) => {
const dummy = new ListNode(0);
dummy.next = head;
let prev = dummy;
while (head && head.next) {
const next = head.next; // 临时保存head.next,因为head.next待会要改变
head.next = next.next;
next.next = head;
prev.next = next;
prev = head; // 指针更新
head = head.next; // 指针更新
}
return dummy.next;
};
5. 删除链表中的倒数第N个节点
- leetCode19 删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
**进阶:**你能尝试使用一趟扫描实现吗?
思路
双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
const removeNthFromEnd = (head, n) => {
// 定义虚拟节点
const dummy = new ListNode(0, head)
// 定义左右指针,都指向虚拟节点
let left = dummy,
right = dummy
// 右指针先走n+1步
while (1 + n--) {
right = right.next
}
// 如果此时右指针到null了,说明删除的是第一个节点
// 直接返回dummy.next.next;
if (!right) return dummy.next.next
// 右指针没到头
while (right) {
// 左右指针一起走
right = right.next
left = left.next
}
// 右指针走到null之后,删除左指针的下一个节点即可
left.next = left.next.next
// 返回虚拟节点的next
return dummy.next
}
6. 链表相交
- leetCode的面试题02.07 链表相交
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
思路
解法1:
两个指针最多走过headA链表长度 + headB链表长度的距离
- 如果相交,会提前相遇在相交节点。此时返回相交节点。
- 如果不相交,则各自走过headA链表长度 + headB链表长度的距离,指向null。此时返回null.
var getIntersectionNode = function(headA, headB) {
var p1 = headA, p2 = headB;
while (p1 != p2) {
p1 = p1 ? p1.next : headB;
p2 = p2 ? p2.next : headA;
}
return p1;
};
解法2:
- 先获取两个链表的长度,
- 然后判定谁是长的链表谁是短的链表,获取长链表和短链表的长度差
- 相差多少则长链表就先走多少步,然后遍历长链表或者遍历短链表都可以
- 因为此时的长链表和短链表长度都一样了,所以遍历谁都可以
- 如果相等则返回链接,
- 如果遍历完毕都还没有找到相等的链接,则直接返回null,说明此时并没有找到相交链接
function getListNodeLength(head) {
if (head == null) return 0
let len = 0
let curr = head
while (curr != null) {
curr = curr.next
len++
}
return len
}
var getIntersectionNode = function(headA, headB) {
let lenA = getListNodeLength(headA)
let lenB = getListNodeLength(headB)
let diff = 0
let longer = headA
let shorter = headB
if (lenA > lenB) {
diff = lenA - lenB
} else {
diff = lenB - lenA
longer = headB
shorter = headA
}
// 相差多少个长度,就让长的链表先多走步
for (let i = 0; i < diff; i++) {
longer = longer.next
}
while (longer != null) {
if (longer == shorter) {
return longer // 返回shorter 或者longer都可以
}
longer = longer.next
shorter = shorter.next
}
return null
}
环形链表
- leetCode142 环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
思路
首先需要判断是否有环
-
可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢
首先第一点: fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。
-
如果有环,就判断环的入口,那么就根据代码随想录即可明白啦。y和z是相等的
-
这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
var detectCycle = function (head) {
let slow = head;
let fast = head;
while (fast) {
if (fast.next == null) { // fast.next走出链表了,说明无环
return null;
}
slow = slow.next; // 慢指针走一步
fast = fast.next.next; // 慢指针走一步
if (slow == fast) { // 首次相遇
fast = head; // 让快指针回到头节点
while (true) { // 开启循环,让快慢指针相遇
if (slow == fast) { // 相遇,在入环处
return slow;
}
slow = slow.next;
fast = fast.next; // 快慢指针都走一步
}
}
}
return null;
};
总结
关于链表的相关几道leetCode题,参考各种大佬的解法,了解了一些相关基础。觉得自己写下来印象会更深刻些