基本概念
链表(Linked List)是一种线性数据结构,由一系列节点(Node)组成,每个节点包含两部分:存储的数据(data)和指向下一个节点的指针(next)。链表在内存中不需要连续的存储空间,而是通过指针动态地连接各个节点。
单链表
单链表是最简单的链表形式,每个节点只包含一个指向下一个节点的指针。
首先定义节点类型:
class Node {
constructor(data) {
this.data = data; // 存储数据
this.next = null; // 指向下一个节点的指针
}
}
接下来根据上面的节点类型定义一个链表,包含基本的操作方法:
class LinkedList {
constructor() {
this.head = null; // 链表的头节点
}
// 在链表末尾插入节点
append(data) {
const newNode = new Node(data);
if (this.head === null) {
this.head = newNode;
} else {
let current = this.head;
while (current.next !== null) {
current = current.next;
}
current.next = newNode;
}
}
// 在链表头部插入节点
prepend(data) {
const newNode = new Node(data);
newNode.next = this.head;
this.head = newNode;
}
// 删除链表中第一个匹配的节点
delete(data) {
if (this.head === null) return;
if (this.head.data === data) {
this.head = this.head.next;
return;
}
let current = this.head;
while (current.next !== null) {
if (current.next.data === data) {
current.next = current.next.next;
return;
}
current = current.next;
}
}
// 查找链表中是否存在某个数据
contains(data) {
let current = this.head;
while (current !== null) {
if (current.data === data) {
return true;
}
current = current.next;
}
return false;
}
// 打印链表中的所有数据
print() {
let current = this.head;
let result = '';
while (current !== null) {
result += current.data + ' -> ';
current = current.next;
}
console.log(result + 'null');
}
}
移除链表元素
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
var removeElements = function(head, val) {
// 创建一个虚拟头节点,简化删除头节点的逻辑
let dummy = new ListNode(0); // 虚拟头节点
dummy.next = head;
let current = dummy;
while (current.next !== null) {
if (current.next.val === val) {
// 删除当前节点的下一个节点
current.next = current.next.next;
} else {
// 否则,移动到下一个节点
current = current.next;
}
}
return dummy.next; // 返回链表的头节点
};
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val);
this.next = (next === undefined ? null : next);
}
这个也算比较简单,设置一个虚拟头节点,开始遍历链表,然后就判断当前节点和val相等就移除节点,移除也比较简单就是current.next = current.next.next
翻转链表
题意:反转一个单链表。
示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
这个题虽然是简单难度的,但是没写出来,
var reverseList = function(head) {
if(!head || !head.next) return head;
let temp = null, pre = null, cur = head;
while(cur) {
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
// temp = cur = null;
return pre;
};
这里使用的是双指针的解法,也比较好理解。
核心逻辑就是这几步:
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
这个题没任何思路,直接看题解
var swapPairs = function (head) {
let ret = new ListNode(0, head), temp = ret;
while (temp.next && temp.next.next) {
let cur = temp.next.next, pre = temp.next;
pre.next = cur.next;
cur.next = pre;
temp.next = cur;
temp = pre;
}
return ret.next;
};
两两一组,翻转链表
删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
这个题第一想法就是获取链表的长度,然后用长度减去n,遍历时开始计数,当遍历到这个差值是直接返回出去。
var removeNthFromEnd = function(head, n) {
let len = 0;
let current = head;
// 计算链表的长度
while (current) {
len++;
current = current.next;
}
// 计算要删除的节点的位置
let targetIndex = len - n;
// 创建一个虚拟头节点,方便处理删除头节点的情况
let dummy = new ListNode(0);
dummy.next = head;
current = dummy;
// 遍历到要删除的节点的前一个节点
for (let i = 0; i < targetIndex; i++) {
current = current.next;
}
// 删除节点
current.next = current.next.next;
return dummy.next;
};
看了题解,有个更巧妙的做法,使用快慢指针,首先将快指针移动至n,然后双指针同步移动,当快指针到最后一个时,慢指针就移动到将要删除的节点了,然后删除节点即可slow.next = slow.next.next
var removeNthFromEnd = function (head, n) {
// 创建哨兵节点,简化解题逻辑
let dummyHead = new ListNode(0, head);
let fast = dummyHead;
let slow = dummyHead;
while (n--) fast = fast.next;
while (fast.next !== null) {
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;
return dummyHead.next;
};
链表相交
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:
这个题竟然是简单级别的,自己真菜。
首先想了下,没思路,看了下力扣上的提示,说可以先判断链表是否相交,判断相交到是很简单,首先遍历第一个链表,拿到最后一个节点,然后再遍历第二个节点,判断前一个最后的节点是不是在第二个的的链表上就可以了,顺着这个思路,来个暴力破解,两层for循环,判断前一个节点是不是在第二个节点上,如果在那就是相交
var getIntersectionNode = function(headA, headB) {
let listA = headA
let listB = headB
while(listA){
listB = headB;
while(listB){
if(listA == listB){
return listA
}
listB = listB.next
}
listA = listA.next
}
return null
};
也能通过测试。
这里有个关键点,在遍历listB之前,需要重置下listB = headB,不然会listA遍历一个节点,listB直接退出了。
看了题解,发现一个更好的解法,首先计算两个链表的长度,找到差值,然后判断长链表减去差值 和 短链表是否一样,如果不一样就开始同步遍历,然后判断是否相等,这个方法确实巧妙,时间复杂度只有o(n)
var getIntersectionNode = function(headA, headB) {
if (!headA || !headB) return null;
// 计算链表 A 的长度
let lenA = 0, lenB = 0;
let currentA = headA, currentB = headB;
while (currentA) {
lenA++;
currentA = currentA.next;
}
while (currentB) {
lenB++;
currentB = currentB.next;
}
// 重置指针
currentA = headA;
currentB = headB;
// 让较长的链表先移动差值步
if (lenA > lenB) {
for (let i = 0; i < lenA - lenB; i++) {
currentA = currentA.next;
}
} else {
for (let i = 0; i < lenB - lenA; i++) {
currentB = currentB.next;
}
}
// 同时移动两个指针,直到找到交点
while (currentA && currentB) {
if (currentA === currentB) {
return currentA;
}
currentA = currentA.next;
currentB = currentB.next;
}
return null;
};
环形链表
题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
这个题第一想法就是创建一个hash表,然后开始遍历链表,挨个向hash表中塞值,如果是环形链表,一定会在hash表中有重复的
var detectCycle = function(head) {
if (!head) return null;
const map = new Map();
let current = head;
let index = 0;
while (current) {
if (map.has(current)) {
return current; // 找到环的起点
}
map.set(current, index); // 存储节点和索引
current = current.next;
index++;
}
return null; // 无环
};
更优的解法,快慢指针
var detectCycle = function(head) {
if (!head || !head.next) return null;
let slow = head;
let fast = head;
// 第一步:检测是否有环
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) break; // 相遇点
}
// 无环
if (slow !== fast) return null;
// 第二步:找到环的起点
slow = head;
while (slow !== fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
};
看下这个图片就知道了,画的很形象
首先定义一个快指针,每次遍历两个,定义一个慢指针,每次遍历一个,第一次相遇时循环体中,然后一个指针在相遇点开始遍历,一个指针从头开始遍历,下次相遇就是环的起点了。