旋转链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode rotateRight(ListNode head, int k) {
// 如果头结点为空,直接返回null
if(head == null){
return null;
}
// 创建一个哑节点,其next指向头结点
ListNode dummy = new ListNode();
dummy.next = head;
// nextNode用于遍历链表
ListNode nextNode = dummy;
// temp用于计算链表的长度
ListNode temp = head;
// 初始化链表长度为1
int len = 1;
// 遍历链表,计算链表长度
while(temp.next != null){
len++;
temp = temp.next;
}
// 重置temp为头结点
temp = head;
// 移动temp到倒数第k+1个节点
for(int i = 0 ; i < len - (k % len) - 1 ; i++){
temp = temp.next;
}
// nextNode的next指向temp的next,即新的头结点
nextNode.next = temp.next;
// temp的next置为null,断开链表
temp.next = null;
// 移动nextNode到新的尾结点
while(nextNode.next != null){
nextNode = nextNode.next;
}
// 将新的尾结点的next指向原来的头结点,形成新的链表
nextNode.next = head;
// 返回新的头结点
return dummy.next;
}
}
移除节点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
// 创建一个虚拟头节点,方便处理删除头节点的情况
ListNode dummyHead = new ListNode();
dummyHead.next = head;
// 初始化双指针
ListNode pre = dummyHead; // pre指向当前节点的前一个节点
ListNode cur = head; // cur指向当前节点
// 遍历链表
while (cur != null) {
// 如果当前节点的值等于要删除的值
if (cur.val == val) {
// 跳过当前节点,直接链接前一个节点到当前节点的下一个节点
pre.next = cur.next;
} else {
// 如果当前节点的值不等于要删除的值,更新 pre 指针为当前节点
pre = cur;
}
// 无论当前节点的值是否被删除,都移动 cur 指针到下一个节点
cur = cur.next;
}
// 返回新的头节点,跳过虚拟头节点
return dummyHead.next;
}
}
82. 删除排序链表中的重复元素 II - 力扣(LeetCode)
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
/**
* 删除排序链表中的重复元素
* @param head 链表的头节点
* @return 删除重复元素后的链表头节点
*/
public ListNode deleteDuplicates(ListNode head) {
// 如果头节点为空,直接返回空
if(head == null){
return head;
}
// 创建一个哑节点(dummy node),它的next指向头节点
// 这样可以简化在循环中删除头节点的复杂性
ListNode dummpHead = new ListNode(-1);
dummpHead.next = head;
// 从哑节点开始遍历链表
ListNode cur = dummpHead;
// 循环直到当前节点的下一个和下下一个节点都为空
while(cur.next != null && cur.next.next != null) {
// 如果当前节点的值和下一个节点的值相同
if(cur.next.val == cur.next.next.val){
int val = cur.next.val;
// 循环删除所有值相同的节点
while(cur.next != null && cur.next.val == val){
// 将当前节点的next指向下一个节点的next,从而删除下一个节点
cur.next = cur.next.next;
}
}else{
// 如果当前节点的值和下一个节点的值不同,移动到下一个节点
cur = cur.next;
}
}
// 返回哑节点的下一个节点,即删除重复元素后的链表头节点
return dummpHead.next;
}
}
设计链表
在链表类中实现这些功能:得到第 index 个节点的值,
在链表的第一个元素之前添加一个值为 val 的节点,
将值为 val 的节点追加到链表的最后一个元素,
在链表中的第 index 个节点之前添加值为 val 的节点,
删除链表中的第 index 个节点
解法:单链表
class ListNode {
int val; // 节点的值
ListNode next; // 指向下一个节点的指针
ListNode() {} // 默认构造函数
ListNode(int val) { // 带值的构造函数
this.val = val;
}
}
class MyLinkedList {
// size 存储链表元素的个数
int size;
// 虚拟头结点,便于处理头节点的插入和删除
ListNode dummyHead;
public MyLinkedList() {
size = 0; // 初始化链表大小为 0
dummyHead = new ListNode(0); // 创建虚拟头结点
}
// 获取链表中第 index 个节点的值,索引从 0 开始
public int get(int index) {
// 检查索引是否有效
if (index < 0 || index >= size) {
return -1; // 返回 -1 表示索引无效
}
ListNode curNode = dummyHead; // 从虚拟头节点开始
// 遍历到目标索引的节点
for (int i = 0; i <= index; i++) {
curNode = curNode.next; // 移动到下一个节点
}
return curNode.val; // 返回目标节点的值
}
// 在链表头部添加一个新节点
public void addAtHead(int val) {
ListNode newNode = new ListNode(val); // 创建新节点
newNode.next = dummyHead.next; // 新节点指向当前头节点
dummyHead.next = newNode; // 虚拟头节点指向新节点
size++; // 更新链表大小
}
// 在链表尾部添加一个新节点
public void addAtTail(int val) {
ListNode curNode = dummyHead; // 从虚拟头节点开始
// 遍历到链表尾部
while (curNode.next != null) {
curNode = curNode.next; // 移动到下一个节点
}
ListNode newNode = new ListNode(val); // 创建新节点
curNode.next = newNode; // 尾节点指向新节点
size++; // 更新链表大小
}
// 在指定索引处添加一个新节点
public void addAtIndex(int index, int val) {
// 如果索引大于链表大小,无法添加
if (index > size) {
return;
}
// 如果索引小于 0,从头部插入
if (index < 0) {
index = 0;
}
ListNode newNode = new ListNode(val); // 创建新节点
ListNode curNode = dummyHead; // 从虚拟头节点开始
// 遍历到目标索引的前一个节点
for (int i = 0; i < index; i++) {
curNode = curNode.next; // 移动到下一个节点
}
// 插入新节点
newNode.next = curNode.next; // 新节点指向当前位置的下一个节点
curNode.next = newNode; // 前一个节点指向新节点
size++; // 更新链表大小
}
// 删除指定索引的节点
public void deleteAtIndex(int index) {
// 检查索引是否有效
if (index < 0 || index >= size) {
return; // 索引无效,不进行任何操作
}
size--; // 先减少链表大小
ListNode curNode = dummyHead; // 从虚拟头节点开始
// 遍历到目标索引的前一个节点
for (int i = 0; i < index; i++) {
curNode = curNode.next; // 移动到下一个节点
}
// 删除目标节点
curNode.next = curNode.next.next; // 前一个节点指向目标节点的下一个节点
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
双指针
合并两个有序链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 临时指针,指向链表1的头节点
ListNode list1Temp = list1;
// 临时指针,指向链表2的头节点
ListNode list2Temp = list2;
// 创建一个哑节点,作为合并后的链表的头节点的前一个节点
ListNode dummyHead = new ListNode(-1, null);
// 临时指针,指向合并后的链表的当前节点
ListNode temp = dummyHead;
// 当链表1和链表2都还有节点时,进行比较和合并
while(list1Temp != null && list2Temp != null){
// 初始化较小值为整数最大值
int small = Integer.MAX_VALUE;
// 如果链表1当前节点的值大于等于链表2当前节点的值
if(list1Temp.val >= list2Temp.val){
// 更新较小值为链表2当前节点的值
small = list2Temp.val;
// 链表2的临时指针后移
list2Temp = list2Temp.next;
}else if(list2Temp.val > list1Temp.val){
// 更新较小值为链表1当前节点的值
small = list1Temp.val;
// 链表1的临时指针后移
list1Temp = list1Temp.next;
}
// 创建一个新节点,值为较小值
ListNode newNode = new ListNode(small);
// 将新节点连接到合并后的链表的当前节点后面
temp.next = newNode;
// 合并后的链表的临时指针后移
temp = temp.next;
}
// 如果链表1还有剩余节点,则将其连接到合并后的链表后面
// 如果链表2还有剩余节点,则将其连接到合并后的链表后面
temp.next = list1Temp == null ? list2Temp : list1Temp;
// 返回合并后的链表的头节点
return dummyHead.next;
}
}
反转链表 (难)
class Solution {
// 反转链表的核心方法,传入链表的头节点 head
public ListNode reverseList(ListNode head) {
// 如果链表为空或链表只有一个节点,则不需要反转,直接返回原链表的头节点
if (head == null || head.next == null) {
return head; // 终止条件:空链表或只有一个节点
}
// 初始化两个指针:pre 用来跟踪已经反转的部分,cur 用来跟踪未反转的部分
ListNode pre = head; // pre 指向当前节点,初始时为链表的头节点
ListNode cur = head.next; // cur 指向下一个节点,初始时为头节点的下一个节点
// 将头节点的 next 设为 null,因为反转后原来的头节点会成为尾节点,尾节点的 next 应该是 null
head.next = null;
// 开始遍历链表,直到 cur 为空(即链表末尾)
while (cur != null) {
ListNode temp = cur.next; // 暂存 cur 的下一个节点,防止链表断开后丢失后续节点
cur.next = pre; // 将当前节点 cur 的 next 指向 pre,从而实现反转
pre = cur; // pre 前进到 cur,表示反转后的部分已经包括当前节点
cur = temp; // cur 前进到下一个节点(即之前暂存的节点),继续反转
}
// 当 cur 为 null 时,pre 指向的是反转后的新头节点,返回该节点
return pre;
}
}
两两交换链表中的节点
// 定义链表节点类
public class ListNode {
int val; // 节点的值
ListNode next; // 指向下一个节点的指针
// 无参构造函数
ListNode() {}
// 带有节点值的构造函数
ListNode(int val) {
this.val = val;
}
// 带有节点值和下一个节点的构造函数
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
// 解决方案类,包含交换链表节点对的方法
class Solution {
public ListNode swapPairs(ListNode head) {
// 定义一个虚拟头节点 dummyHead,用来简化链表头部的操作
ListNode dummyHead = new ListNode(0); // 虚拟头节点,值为0,指向实际链表的头节点
dummyHead.next = head; // 将虚拟头的 next 指向传入的 head,便于处理链表的开头
// 定义临时指针 temp,用来遍历链表。初始指向 dummyHead,方便处理链表头部
ListNode temp = dummyHead;
// node1 和 node2 分别用于指向要交换的两个相邻节点
ListNode node1;
ListNode node2;
// 只要 temp 后面有两个节点存在(即 temp.next 和 temp.next.next 都不为空),就可以继续交换
while (temp.next != null && temp.next.next != null) {
// node1 指向第一对中第一个节点
node1 = temp.next;
// node2 指向第一对中第二个节点
node2 = temp.next.next;
// 开始交换:将 temp 的 next 指向第二个节点(node2)
temp.next = node2;
// 将第一个节点 node1 的 next 指向第二个节点 node2 的下一个节点,即交换后第一个节点应该指向的节点
node1.next = node2.next;
// 将第二个节点 node2 的 next 指向第一个节点 node1,完成交换
node2.next = node1;
// 将 temp 指向 node1,继续处理下一对
temp = node1; // node1 是交换后的第二个节点,所以 temp 移动到这里准备处理下一对节点
}
// 返回新链表的头节点,即 dummyHead 的 next
return dummyHead.next; // dummyHead 是虚拟头节点,实际链表的头节点在 dummyHead.next
}
}
删除链表的倒数第N个节点(难)
19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
// 定义链表节点类
public class ListNode {
int val; // 节点的值
ListNode next; // 指向下一个节点的指针
// 无参构造函数
ListNode() {}
// 带有节点值的构造函数
ListNode(int val) {
this.val = val;
}
// 带有节点值和下一个节点的构造函数
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
class Solution {
// 移除链表中倒数第 n 个节点
public ListNode removeNthFromEnd(ListNode head, int n) {
// 边界检查,如果链表为空,直接返回 null
if (head == null) {
return null;
}
// 定义一个虚拟头节点,dummyHead 用来处理链表头部的特殊情况(例如删除头节点)
ListNode dummyHead = new ListNode(0);
dummyHead.next = head; // dummyHead.next 指向链表头节点
// 定义快指针 fast 和慢指针 slow,都初始化为 dummyHead
ListNode fast = dummyHead;
ListNode slow = dummyHead;
// 快指针 fast 先前进 n+1 步,以便与慢指针 slow 之间的距离为 n
for (int i = 0; i <= n; i++) {
fast = fast.next;
}
// 快慢指针同时向前移动,直到 fast 到达链表末尾
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
// 此时慢指针 slow 指向待删除节点的前一个节点,执行删除操作
slow.next = slow.next.next;
// 返回新的链表头节点(dummyHead.next),此时 dummyHead.next 是链表的头节点
return dummyHead.next;
}
}
训练计划 IV
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
/**
* 合并两个排序的链表。
* @param l1 第一个链表的头节点。
* @param l2 第二个链表的头节点。
* @return 合并后的链表头节点。
*/
public ListNode trainningPlan(ListNode l1, ListNode l2) {
// 创建一个哑节点,作为合并后链表的头节点的前驱节点。
ListNode dummpHead = new ListNode(-1);
// cur指针用于追踪合并后链表的最后一个节点。
ListNode cur = dummpHead;
// 当两个链表都未到达末尾时,进行循环。
while(l1 != null && l2 != null){
// 如果l1的当前节点值小于l2的当前节点值,将l1的当前节点接到cur后面。
if(l1.val < l2.val){
cur.next = l1;
l1 = l1.next;
} else {
// 否则,将l2的当前节点接到cur后面。
cur.next = l2;
l2 = l2.next;
}
// 移动cur指针到下一个节点。
cur = cur.next;
}
// 如果l1还有剩余节点,直接接到cur后面。
// 如果l2还有剩余节点,也直接接到cur后面。
// 因为已经保证了l1和l2中的节点都是有序的,所以直接连接即可。
cur.next = l1 == null ? l2 : l1;
return dummpHead.next;
}
}
删除有序链表中重复的元素-II
import java.util.*;
/*
* 定义一个单链表的节点类 ListNode。
* 每个节点包含一个整数值 val 和一个指向下一个节点的引用 next。
*/
public class ListNode {
int val;
ListNode next = null; // 初始化时,下一个节点为空
public ListNode(int val) {
this.val = val; // 构造函数,用于创建一个新的节点并初始化其值
}
}
public class Solution {
/**
* 删除排序链表中的重复元素。
*
* @param head ListNode类 链表的头节点
* @return ListNode类 删除重复元素后的链表头节点
*/
public ListNode deleteDuplicates(ListNode head) {
// 如果链表为空或只有一个节点,不需要删除重复,直接返回头节点
if (head == null || head.next == null) {
return head;
}
// 使用哑节点(dummy node)简化头节点处理
ListNode dummyHead = new ListNode(-1);
dummyHead.next = head;
// cur 用于遍历链表
ListNode cur = dummyHead;
// 遍历链表,直到最后一个节点的前一个节点
while (cur.next != null && cur.next.next != null) {
// 如果当前节点和下一个节点的值相同,说明发现重复
if (cur.next.val == cur.next.next.val) {
int temp = cur.next.val; // 记录重复的值
// 继续向后遍历,直到值不再相同
while (cur.next != null && temp == cur.next.val) {
cur.next = cur.next.next; // 跳过重复的节点
}
} else {
// 如果当前节点和下一个节点的值不相同,移动到下一个节点
cur = cur.next;
}
}
// 返回新链表的头节点
return dummyHead.next;
}
}
删除有序链表中重复的元素-I
import java.util.*;
/*
* 定义一个单链表的节点类 ListNode。
* 每个节点包含一个整数值 val 和一个指向下一个节点的引用 next。
*/
public class ListNode {
int val;
ListNode next = null; // 初始化时,下一个节点为空
public ListNode(int val) {
this.val = val; // 构造函数,用于创建一个新的节点并初始化其值
}
}
public class Solution {
/**
* 删除排序链表中的所有重复元素,保留第一次出现的元素。
*
* @param head ListNode类 链表的头节点
* @return ListNode类 删除重复元素后的链表头节点
*/
public ListNode deleteDuplicates(ListNode head) {
// 如果链表为空,直接返回 null
if (head == null) {
return null;
}
// 使用哑节点(dummy node)简化头节点处理
ListNode dummyHead = new ListNode(-1);
dummyHead.next = head;
// cur 用于遍历链表,从哑节点开始
ListNode cur = dummyHead;
// 遍历链表,直到最后一个节点
while (cur.next != null) {
// 如果当前节点和下一个节点的值相同,说明发现重复
if (cur.next.val == cur.next.next != null && cur.next.val == cur.next.next.val) {
int temp = cur.next.val; // 记录重复的值
// 继续向后遍历,直到值不再相同
while (cur.next != null && temp == cur.next.val) {
cur.next = cur.next.next; // 跳过重复的节点
}
} else {
// 如果当前节点和下一个节点的值不相同,移动到下一个节点
cur = cur.next;
}
}
// 返回新链表的头节点
return dummyHead.next;
}
}
两数相加
我自己写的
// 定义链表节点类
class ListNode {
int val; // 节点的值
ListNode next; // 指向下一个节点的指针
// 默认构造函数
ListNode() {
}
// 带值的构造函数
ListNode(int val) {
this.val = val;
}
// 带值和指针的构造函数
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
int shouldAddOne = 0; // 进位标志,初始为0
int returnList = 1; // 返回链表的标志,1表示l1,2表示l2
ListNode l1First = l1; // 保存l1的头节点
ListNode l2First = l2; // 保存l2的头节点
ListNode l1End = l1; // 用于找到l1的尾节点
ListNode l2End = l2; // 用于找到l2的尾节点
// 遍历两个链表,直到都遍历完
while (l1 != null || l2 != null) {
if (l1 != null && l2 != null) { // 两个链表都有节点
int temp = l2.val + l1.val + shouldAddOne; // 计算当前位的和
shouldAddOne = temp >= 10 ? 1 : 0; // 更新进位
l2.val = temp % 10; // 更新l2当前节点的值
l1.val = temp % 10; // 更新l1当前节点的值
returnList = 2; // 更新返回链表标志
l1 = l1.next; // 移动到下一个节点
l2 = l2.next; // 移动到下一个节点
} else if (l1 == null && l2 != null) { // l1已遍历完,l2还有节点
int temp = l2.val + shouldAddOne; // 计算当前位的和
shouldAddOne = temp >= 10 ? 1 : 0; // 更新进位
l2.val = temp % 10; // 更新l2当前节点的值
returnList = 2; // 更新返回链表标志
l2 = l2.next; // 移动到下一个节点
} else { // l2已遍历完,l1还有节点
int temp = l1.val + shouldAddOne; // 计算当前位的和
shouldAddOne = temp >= 10 ? 1 : 0; // 更新进位
l1.val = temp % 10; // 更新l1当前节点的值
returnList = 1; // 更新返回链表标志
l1 = l1.next; // 移动到下一个节点
}
}
// 找到l1的尾节点
while (l1End.next != null) {
l1End = l1End.next;
}
// 找到l2的尾节点
while (l2End.next != null) {
l2End = l2End.next;
}
// 如果最后还有进位
if (shouldAddOne == 1) {
if (returnList == 1) { // 如果返回链表是l1
l1End.next = new ListNode(1, null); // 在l1的尾部添加新节点
return l1First; // 返回l1
}
if (returnList == 2) { // 如果返回链表是l2
l2End.next = new ListNode(1, null); // 在l2的尾部添加新节点
return l2First; // 返回l2
}
}
// 返回最终的链表
return returnList == 1 ? l1First : l2First;
}
}
chatgpt写的
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(0); // 创建一个虚拟头节点
ListNode current = dummyHead; // 当前节点指针
int carry = 0; // 进位
while (l1 != null || l2 != null || carry != 0) {
int sum = carry; // 进位加上当前位的和
if (l1 != null) {
sum += l1.val;
l1 = l1.next; // 移动到下一个节点
}
if (l2 != null) {
sum += l2.val;
l2 = l2.next; // 移动到下一个节点
}
carry = sum / 10; // 更新进位
current.next = new ListNode(sum % 10); // 创建新节点并连接
current = current.next; // 移动到下一个节点
}
return dummyHead.next; // 返回结果链表,跳过虚拟头节点
}
}
回文链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
// 创建一个列表,用于存储链表中的节点值
List<Integer> list = new ArrayList<>();
// 创建一个指针,用于遍历链表
ListNode temp = head;
// 遍历链表,将每个节点的值添加到列表中
while(temp != null){
list.add(temp.val); // 将当前节点的值添加到列表
temp = temp.next; // 移动指针到下一个节点
}
// 初始化两个指针,分别指向列表的头部和尾部
int left = 0;
int right = list.size() - 1;
// 使用双指针法检查列表是否为回文
while(left < right){
// 如果左右指针所指向的值不相等,则说明不是回文
if(list.get(left) != list.get(right)){
return false;
}
// 移动指针向中间靠拢
left++;
right--;
}
// 如果所有对应位置的值都相等,则说明是回文
return true;
}
}
面试题 02.04. 分割链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val; // 节点的值
* ListNode next; // 指向下一个节点的引用
* ListNode() {} // 无参构造函数
* ListNode(int val) { this.val = val; } // 带值的构造函数
* ListNode(int val, ListNode next) { this.val = val; this.next = next; } // 带值和下一个节点引用的构造函数
* }
*/
class Solution {
public ListNode partition(ListNode head, int x) {
// 创建两个虚拟头节点,分别用于存储小于x的节点和大于等于x的节点
ListNode beforeHead = new ListNode();
ListNode afterHead = new ListNode();
// 初始化两个指针,分别指向两个虚拟头节点
ListNode before = beforeHead;
ListNode after = afterHead;
// 初始化当前节点指针,指向链表的头节点
ListNode cur = head;
// 遍历整个链表
while(cur != null){
// 如果当前节点的值小于x
if(cur.val < x){
// 将当前节点连接到before链表的末尾
before.next = cur;
// 移动before指针到新添加的节点
before = before.next;
} else {
// 否则,将当前节点连接到after链表的末尾
after.next = cur;
// 移动after指针到新添加的节点
after = after.next;
}
// 移动当前节点指针到下一个节点
cur = cur.next;
}
// 将after链表的末尾置为null,防止形成环
after.next = null;
// 将before链表的末尾连接到after链表的头部(去掉虚拟头节点)
before.next = afterHead.next;
// 返回before链表的头部(去掉虚拟头节点)
return beforeHead.next;
}
}
哈希表
判断是否存在同一个元素
链表相交
LRU 缓存
class LRUCache {
// 自定义双向链表节点类
class MyNode{
int key; // 节点的键
int value; // 节点的值
MyNode pre; // 前驱节点指针
MyNode next; // 后继节点指针
public MyNode(){}
public MyNode(int key, int value){
this.key = key;
this.value = value;
}
}
HashMap<Integer, MyNode> map; // 用于快速查找节点的哈希表
int size; // 当前缓存的大小
int capacity; // 缓存的最大容量
MyNode head, tail; // 双向链表的虚拟头节点和虚拟尾节点
// 构造函数,初始化缓存
public LRUCache(int capacity) {
map = new HashMap<>();
this.size = 0;
this.capacity = capacity;
head = new MyNode(); // 创建虚拟头节点
tail = new MyNode(); // 创建虚拟尾节点
head.next = tail; // 初始化头节点的后继为尾节点
tail.pre = head; // 初始化尾节点的前驱为头节点
}
// 获取缓存中的值
public int get(int key) {
if(!map.containsKey(key)){ // 如果缓存中不存在该键
return -1;
}
moveToHead(key); // 将访问的节点移动到头部,表示最近访问
return map.get(key).value; // 返回节点的值
}
// 向缓存中插入键值对
public void put(int key, int value) {
if(!map.containsKey(key)){ // 如果缓存中不存在该键
MyNode newNode = new MyNode(key, value); // 创建新节点
addToHead(newNode); // 将新节点添加到头部
map.put(key, newNode); // 将新节点加入哈希表
if(size > capacity){ // 如果当前缓存大小超过容量
MyNode node = removeTail(); // 移除尾部节点(最久未访问)
map.remove(node.key); // 从哈希表中移除该节点
}
}else{ // 如果缓存中已存在该键
MyNode oldNode = map.get(key); // 获取旧节点
oldNode.value = value; // 更新节点的值
moveToHead(key); // 将节点移动到头部,表示最近访问
}
}
// 将节点添加到头部
void addToHead(MyNode node) {
node.pre = head; // 设置节点的前驱为头节点
node.next = head.next; // 设置节点的后继为头节点的后继
head.next.pre = node; // 修改头节点后继的前驱为当前节点
head.next = node; // 修改头节点的后继为当前节点
size++; // 缓存大小加1
}
// 将节点移动到头部
void moveToHead(int key){
MyNode node = map.get(key); // 从哈希表中获取节点
removeNode(node); // 移除节点
addToHead(node); // 将节点添加到头部
}
// 移除节点
void removeNode(MyNode node){
size--; // 缓存大小减1
node.pre.next = node.next; // 修改前驱节点的后继为当前节点的后继
node.next.pre = node.pre; // 修改后继节点的前驱为当前节点的前驱
}
// 移除尾部节点(最久未访问)
MyNode removeTail(){
MyNode node = tail.pre; // 获取尾部节点
removeNode(node); // 移除尾部节点
return node; // 返回移除的节点
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
面试题 02.07. 链表相交 - 力扣(LeetCode)
public class Solution {
// 定义一个方法来获取两个链表的交点节点
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 使用一个 HashSet 来存储链表 A 的所有节点
Set<ListNode> hashset = new HashSet<ListNode>();
// 创建一个指针 cur,用于遍历链表 A
ListNode cur = headA;
// 遍历链表 A,直到链表的末尾
while(cur != null){
// 将当前节点添加到 HashSet 中
hashset.add(cur);
// 移动到下一个节点
cur = cur.next;
}
// 重新使用 cur 指针遍历链表 B
cur = headB;
// 遍历链表 B,直到链表的末尾
while(cur != null){
// 如果当前节点在 HashSet 中,说明找到了交点
if(hashset.contains(cur)){
// 返回交点节点
return cur;
}
// 移动到下一个节点
cur = cur.next;
}
// 如果没有交点,返回 null
return null;
}
}
环形链表
public class Solution {
public ListNode detectCycle(ListNode head) {
// 创建一个 HashSet 用来存储访问过的节点
Set<ListNode> hashset = new HashSet<>();
// 初始化当前节点为链表的头节点
ListNode cur = head;
// 循环遍历整个链表
while (cur != null) {
// 如果当前节点已经存在于 HashSet 中,说明链表有环,且该节点就是环的起点
if (hashset.contains(cur)) {
return cur; // 返回环的起点节点
} else {
// 如果当前节点不在 HashSet 中,将其添加到集合中,表示已经访问过该节点
hashset.add(cur);
}
// 移动当前指针到下一个节点
cur = cur.next;
}
// 如果遍历完整个链表都没有找到重复的节点,说明链表无环,返回 null
return null;
}
}
面试题 02.01. 移除重复节点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val; // 节点的值
* ListNode next; // 指向下一个节点的引用
* ListNode() {} // 无参构造函数
* ListNode(int val) { this.val = val; } // 带值的构造函数
* ListNode(int val, ListNode next) { this.val = val; this.next = next; } // 带值和下一个节点引用的构造函数
* }
*/
class Solution {
public ListNode removeDuplicateNodes(ListNode head) {
// 使用HashSet来存储已经遇到的节点值,以便快速查找重复节点
HashSet<Integer> set = new HashSet<>();
// 创建一个虚拟头节点,简化边界条件处理
ListNode dummpHead = new ListNode(-1);
dummpHead.next = head;
// 使用tmp指针遍历链表
ListNode tmp = dummpHead;
while(tmp.next != null){
// 获取当前节点
ListNode cur = tmp.next;
// 如果当前节点的值不在集合中,说明是第一次遇到该值
if(!set.contains(cur.val)){
// 将该值加入集合
set.add(cur.val);
// 移动tmp指针到下一个节点
tmp = tmp.next;
}else{
// 如果当前节点的值已经在集合中,说明是重复节点
// 跳过当前节点,即删除当前节点
tmp.next = tmp.next.next;
}
}
// 返回处理后的链表头节点(虚拟头节点的下一个节点)
return dummpHead.next;
}
}
复制链表
复杂链表的复制
// 定义一个解决方案类,用于解决复制含有随机指针的链表问题。
class Solution {
// 复制一个含有随机指针的链表
public Node copyRandomList(Node head) {
// 如果链表头为null,说明链表为空,直接返回null
if(head == null){
return null;
}
// 使用HashMap来存储原链表节点和复制节点的对应关系
Map<Node, Node> map = new HashMap<>();
// 定义一个指针cur,从头节点开始遍历链表
Node cur = head;
// 第一遍遍历:复制每个节点,并建立原节点和复制节点的对应关系
while(cur != null){
// 为当前节点创建一个值相同的复制节点,并放入map中
map.put(cur, new Node(cur.val));
// 移动到下一个节点
cur = cur.next;
}
// 重置cur指针,从头节点开始第二遍遍历
cur = head;
// 第二遍遍历:设置复制节点的next和random指针
while(cur != null){
// 将当前节点的复制节点的next指针指向当前节点的下一个节点的复制节点
map.get(cur).next = map.get(cur.next);
// 将当前节点的复制节点的random指针指向当前节点的random节点的复制节点
map.get(cur).random = map.get(cur.random);
// 移动到下一个节点
cur = cur.next;
}
// 返回复制链表的头节点
return map.get(head);
}
}
回溯
随机链表的复制
/*
// Definition for a Node.
class Node {
int val; // 节点的值
Node next; // 指向下一个节点的指针
Node random; // 指向任意节点的随机指针
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
Map<Node, Node> map = new HashMap<Node, Node>(); // 使用哈希表来存储原节点和新节点的映射关系
public Node copyRandomList(Node head) {
if(head == null){ // 如果头节点为空,则直接返回null
return null;
}
if(!map.containsKey(head)){ // 如果哈希表中没有当前节点的映射关系
Node newNode = new Node(head.val); // 创建一个新的节点,其值与原节点相同
map.put(head, newNode); // 将原节点和新节点的映射关系存入哈希表
newNode.next = copyRandomList(head.next); // 递归复制下一个节点,并将结果赋给新节点的next指针
newNode.random = copyRandomList(head.random); // 递归复制随机指针指向的节点,并将结果赋给新节点的random指针
}
return map.get(head); // 返回哈希表中存储的当前原节点对应的新节点
}
}
总结
以下针对你列出的各链表题目,逐一总结每题的核心思路和要点(按出现顺序):
61. 旋转链表 (Rotate List)
思路概览:先遍历一遍链表,计算长度 len,并记录尾节点 tail;计算实际需要移动的步数:k = k % len。如果 k == 0,直接返回原链表;从头节点走到第 len - k - 1 个节点,用指针 slow 定位切断点;将切断点后面的节点作为新头,原尾节点指向原头,断开切断点的 next;返回新的头节点。
关键点:必须先求链表长度,再对 k 取模,避免过度旋转;用哑节点或尾节点帮助调整指针,注意边界(空链表或 k%len == 0)。时间复杂度:O(n),空间复杂度:O(1)。
203. 移除链表元素 (Remove Linked List Elements)
思路概览:新建一个哑节点 dummyHead,其 next 指向 head,方便删除头节点;用两个指针 pre(前驱)和 cur(当前)来遍历:若 cur.val == val,则 pre.next = cur.next,跳过 cur;否则 pre = cur;始终 cur = cur.next,继续遍历;遍历结束后,返回 dummyHead.next。
关键点:使用“虚拟头节点”可统一处理删除头节点的情况;删除时只要改变 pre.next,无需额外释放节点(Java 会自动回收)。时间复杂度:O(n),空间复杂度:O(1)。
82. 删除排序链表中的重复元素 II (Remove Duplicates from Sorted List II)
思路概览:同样创建哑节点 dummyHead 指向 head,用指针 cur 从 dummyHead 开始;当 cur.next != null && cur.next.next != null 时,如果发现 cur.next.val == cur.next.next.val,说明出现重复:记住重复的值 dup = cur.next.val,然后不停地将 cur.next 指向 cur.next.next,跳过所有值为 dup 的节点;若没有重复,则 cur = cur.next,继续向前;返回 dummyHead.next。
关键点:一旦发现两连节点值相同,就要“跳过所有等于该值的节点”,而不仅仅是跳过两处;哑节点用来简化删除操作,尤其是头部节点为重复时;时间复杂度:O(n),空间复杂度:O(1)。
707. 设计链表 (Design Linked List)
思路概览:用单链表实现 get(index)、addAtHead(val)、addAtTail(val)、addAtIndex(index, val)、deleteAtIndex(index)。维护:size:当前链表长度;dummyHead:哑节点,其 next 指向实际头节点。各操作关键步骤:get(index):如果 index<0 或 index>=size 返回 -1;否则从 dummyHead.next 遍历 index 步,取出节点值;addAtHead(val):等价于 addAtIndex(0, val);addAtTail(val):从 dummyHead 遍历到末尾,插入新节点,并 size++;addAtIndex(index, val):如果 index > size,不插入;若 index < 0,视作在头部插入;从 dummyHead 遍历到第 index - 1 个位置,将新节点插入;size++;deleteAtIndex(index):若 index<0 或 index>=size,不删除;否则从 dummyHead 遍历到第 index - 1 个位置,cur.next = cur.next.next,size--。
关键点:哑节点:简化头节点插入/删除的边界处理;维护 size:保证 get、addAtIndex、deleteAtIndex 时能快速判断索引有效性;时间复杂度:各操作最坏 O(n)(遍历),空间复杂度:O(1)。
合并两个有序链表 (Merge Two Sorted Lists) —— LeetCode 21 (训练计划 IV 同理)
思路概览:新建哑节点 dummyHead,cur = dummyHead;用指针 l1 和 l2 分别遍历两个有序链表,当且仅当都非空时,比较 l1.val 与 l2.val:较小节点接到 cur.next,对应指针后移;cur = cur.next;循环结束后,将剩余节点(l1 或 l2 的非空部分)直接接到 cur.next;返回 dummyHead.next。
关键点:哑节点 帮助快速返回合并后链表头;循环中对两链表头进行“逐节点比较并拼接”,最后不必再遍历剩余部分;时间复杂度:O(n+m),空间复杂度:O(1)。
206. 反转链表 (Reverse Linked List)
思路概览(迭代):边界:若 head == null || head.next == null,直接返回 head;初始化两个指针:pre = null,cur = head;当 cur != null:nextTemp = cur.next(暂存下一个节点);cur.next = pre(反转指向);pre = cur,cur = nextTemp;循环结束后,pre 即为新头,返回 pre。
关键点:每次断开当前节点与后续的连接后,将其 next 指向前已反转部分;保留一个临时 nextTemp 避免链表断链;时间复杂度:O(n),空间复杂度:O(1)。
24. 两两交换链表中的节点 (Swap Nodes in Pairs)
思路概览:新建哑节点 dummyHead,dummyHead.next = head,prev = dummyHead;只要 prev.next != null && prev.next.next != null:令 first = prev.next,second = prev.next.next;交换步骤:prev.next = second;first.next = second.next;second.next = first;然后 prev = first(跳到已交换对的尾端),继续下一对;返回 dummyHead.next。
关键点:需要一个固定“前驱”prev 帮助定位和拼接;交换时先断开再重连,注意保存 second.next;时间复杂度:O(n),空间复杂度:O(1)。
19. 删除链表的倒数第 N 个结点 (Remove Nth Node From End)
思路概览:新建哑节点 dummyHead,dummyHead.next = head;设置 fast = dummyHead 和 slow = dummyHead;先让 fast 前进 n + 1 步,此时 fast 和 slow 相距 n 个节点;然后同时移动 fast、slow,直到 fast == null;此时 slow.next 即为要删除的「倒数第 n」节点:slow.next = slow.next.next;返回 dummyHead.next。
关键点:通过“快指针先走 n+1 步”,让“慢指针”最终停在待删节点的前驱处;使用哑节点简化当 n == 长度(删除头节点)的情况;时间复杂度:O(n),空间复杂度:O(1)。
删除有序链表中重复的元素-II(牛客题霸实现,与 LeetCode 82 类似)
思路概览:与 LeetCode 82 基本一致:用哑节点 dummy 指向 head,cur = dummy;当 cur.next != null && cur.next.next != null 时,如果发现 cur.next.val == cur.next.next.val:记录该重复值 val,然后不断执行 cur.next = cur.next.next,跳过所有相同节点;否则 cur = cur.next;返回 dummy.next。
关键点:对所有连续相同值节点一次性跳过;哑节点用于处理头部连续重复的情况;时间复杂度:O(n),空间复杂度:O(1)。
删除有序链表中重复的元素-I —— 保留一次出现 (Remove Duplicates from Sorted List I, LeetCode 83)
思路概览:若 head == null,直接返回 null。用指针 cur = head;遍历到链表末尾:当 cur.next != null 且 cur.val == cur.next.val 时:执行 cur.next = cur.next.next,跳过下一个重复节点;否则 cur = cur.next;返回 head。
关键点:只需删除“连续重复”的后一节点,保留第一次出现;无需哑节点,因为不处理头以外的特殊删除;时间复杂度:O(n),空间复杂度:O(1)。
两数相加 (Add Two Numbers, LeetCode 2)
思路概览:新建哑节点 dummyHead,cur = dummyHead,carry = 0;当 l1 != null || l2 != null || carry != 0 时:设 sum = carry;若 l1 != null,则 sum += l1.val、l1 = l1.next;若 l2 != null,则 sum += l2.val、l2 = l2.next;carry = sum / 10;cur.next = new ListNode(sum % 10),cur = cur.next;最后返回 dummyHead.next。
关键点:同时遍历两条链,用 carry 处理进位;循环条件要包含 “carry != 0”,以防最高位还需新加一位;哑节点简化链表拼接。时间复杂度:O(max(m, n)),空间复杂度:O(max(m, n))(返回新链表)。
回文链表 (Palindrome Linked List, LeetCode 234)
思路概览:方法一(借助列表):遍历链表,将所有节点值依次存入 List<Integer> vals;用双指针 i=0, j=vals.size()-1,逐对比 vals.get(i) 与 vals.get(j);若都相等,则继续;否则返回 false。如果全部对比完成,返回 true。方法二(快慢指针 + 反转后半链表):用快慢指针找到链表中点;将后半部分链表原地反转;比较前半链表与反转后半链表对应节点值;最后(可选)恢复链表结构。
关键点:借助 ArrayList 实现最简单,但空间 O(n);优化方案:快慢指针 + 原地反转后半段 --> 空间 O(1);时间复杂度:O(n),空间复杂度:方法一 O(n),方法二 O(1)。
LRU 缓存 (LRU Cache, LeetCode 146)
思路概览:维护一个 双向链表 + 哈希表:双向链表 用于记录访问顺序,链表头是“最近使用”,链表尾是“最久未使用”;哈希表 用于 key -> 节点 的快速访问;操作:get(key):若 key 不存在于哈希表,返回 -1;否则通过哈希表拿到对应节点,将其移动到链表头(表示最近访问),并返回节点的 value;put(key, value):若 key 不存在:创建新节点,插入到链表头,写入哈希表;若此时 size > capacity,移除链表尾节点,并从哈希表中删除该 key;若 key 已存在:更新该节点的 value,并将该节点移动到链表头;链表细节:设计“虚拟头节点”和“虚拟尾节点”,简化插入/删除操作;addToHead(node):把 node 插到 head 之后;removeNode(node):从链表中拔掉 node;moveToHead(node):先 removeNode(node),再 addToHead(node);removeTail():移除链表尾部(tail.pre)并返回该节点。
关键点:哈希表实现 O(1) 的节点定位;双向链表实现 O(1) 的插入/删除;必须同时维护这两种结构才能保证操作均摊 O(1);时间复杂度:O(1) 均摊读写,空间复杂度:O(capacity)。
面试题 02.07. 链表相交 (Intersection of Two Linked Lists, LeetCode 160)
思路概览:哈希法(如示例):遍历链表 A,将所有节点引用(ListNode 对象)加入 HashSet;遍历链表 B,如果某节点已在集合中,说明为交点,直接返回该节点;否则继续;若遍历完 B 仍无交点,返回 null。双指针法(更常见)(不在题述中,但常用):令 pa = headA,pb = headB。同时向前遍历;若 pa == null,则 pa = headB;若 pb == null,则 pb = headA;这样两指针走过的总长度相同,最终要么在交点相遇,要么都为 null。
关键点:哈希法空间复杂度 O(m);双指针法空间 O(1);哈希法实现简单直观,但若输入规模较大且空间敏感,可考虑双指针。时间复杂度:O(m+n),哈希法空间 O(m);双指针法空间 O(1)。
142. 环形链表 II (Linked List Cycle II)
思路概览:哈希法(如示例):遍历时将访问过的节点存入 HashSet;当某个节点即将访问时发现已在集合中,则此节点为链表环的入口,返回它;若遍历完链表无重复,则返回 null。快慢指针法(Floyd 判圈)(常见,未在示例中):先用快慢指针判断是否有环:slow = slow.next,fast = fast.next.next;若相遇,说明有环;否则无环返回 null;如果相遇,在相遇点分别令 p1 = head、p2 = meetingPoint,两指针同步向前(每次都 = .next),再次相遇即为环入口。
关键点:哈希法简单,但空间 O(n);快慢指针法空间 O(1);快慢指针判定有环后,从头节点和相遇点重新同步走,最终交汇即入环点。时间复杂度:O(n),哈希法空间 O(n),快慢指针空间 O(1)。
复制带随机指针的链表 (Copy List with Random Pointer, LeetCode 138)
思路概览:哈希映射法(如示例):第一遍遍历:cur 从 head 开始,遇到每个原节点 node,在 map 中创建一个新节点 newNode = new Node(node.val),并使 map.put(node, newNode);第二遍遍历:重置 cur = head,对于每个原节点 node:map.get(node).next = map.get(node.next);map.get(node).random = map.get(node.random);最后返回 map.get(head)。三步法(无额外映射):复制每个节点并插入原节点后面:原链表 A→B→C 变成 A→A'→B→B'→C→C';设置随机指针:对于每个原节点 node,其复制节点 node.next.random = node.random.next(若 node.random != null);拆分两个链表:将原链表和复制链表拆开,还原各自的 next 指针;最终返回复制链表的头节点。
关键点:哈希映射法简单易懂但空间 O(n);三步法空间 O(1),但链表指针操作需谨慎(先插入,再赋 random,再拆分)。时间复杂度:O(n),哈希法空间 O(n),三步法空间 O(1)。
两数相加(你自己与 ChatGPT 版本对比)
思路概览与要点:核心思路一致:维护 carry 进位;同时遍历 l1 和 l2,相加并产生新节点;对于较短链表,当另一条链表还有节点时,也要加上 carry;最后若 carry == 1,需要额外再加一个新节点。区别点:你自己的实现思路是对原链表就地修改,并通过 returnList 标记哪条链表较长;ChatGPT 的版本通过新建哑节点构造全新返回链表,更加直观、不修改输入;关键点:不要忘记最后处理 carry;哑节点能够统一收尾逻辑;时间复杂度:O(max(m, n)),空间复杂度:如果就地修改,空间 O(1);如果新建,空间 O(max(m, n))。
回文链表(再次总结)已在第 12 条中给出两种常见做法,概念重复,此处不再赘述。
哈希表部分:判断是否存在同一个元素(示例中并未详细展开)主要思路:将遍历到的节点引用(ListNode 对象)存入 HashSet,如果后续再次遇到相同引用,就说明存在。这在检测交点或环时都可借鉴。
链表相交(已在第 14 条概述)
LRU 缓存(已在第 13 条概述)
面试题 02.07. 链表相交(已在第 14 条概述)
142. 环形链表 II(已在第 15 条概述)
复制链表 & 复杂链表复制(已在第 16 条概述)
总结性要点归纳
哑节点(Dummy/虚拟头节点):几乎所有涉及“头节点可能被删除或插入”的题目,都可以通过在最前面添加一个哑节点,来简化边界情况处理。
双指针技巧:“快慢指针”常用于寻找链表中间、判断有无环并定位环入口。“前驱+当前”指针组合常用于删除操作。“同时遍历两链表,或先走 n 步再遍历”是删除倒数第 n、合并有序链表、相交链表等题目的高频思路。
链表原地修改 vs. 新建结果链表:有些算法可以就地修改原链表(例如 “局部删除”),也有一些需要返回一个全新链表(例如 “两数相加”、“复杂链表复制”)。若不想破坏原结构,就需要借助额外节点或哈希映射;若可就地修改,则要格外留意指针断开与重连。
哈希表辅助:用于检测“是否出现过某个节点引用”(判断相交、检测环、复制带随机指针)。空间换时间,常以 O(n) 空间来实现相同的时间复杂度优化。
原地反转链表:反转链表问题只需要两个指针(prev、cur)不断变更 cur.next,并保留 nextTemp = cur.next,逐步推进。在“回文链表”中,也会用到“反转后半链表”的技巧。
链表设计:需要维护长度(size)、哑节点,并用“遍历到第 index - 1 个节点”来插入或删除。所有索引判断都要基于 size 做边界检查。
特殊链表题目思路汇总:旋转链表:先求长、取模、找到新尾、接尾后断、重连。移除元素:双指针或哑节点,一次遍历 O(n)。删除排序重复(I/II):I 只删多余,II 要删全重复段;都离不开“比较 cur.next 与 cur.next.next”。合并有序:指针依次拼接,最后直连剩余。反转:迭代反转每个节点指向。成对交换:用哑节点 + 每次取 first/second,三步拼接。删除倒数第 n:快慢先走定距离,再同速共行。两数相加:并行遍历两链 + carry 进位,哑节点新链。回文链表:可借助数组/栈,或快慢+后半反转。LRU:哈希表 + 双向链表维护访问顺序。链表相交:哈希法或双指针 pa→headB, pb→headA。环形链表 II:哈希法或 Floyd 快慢指针找入口。复制带随机指针:哈希映射两遍填 next/random,或三步法原地插入+拆分。
以上即为各题的核心思路与常见要点,供复习时快速回顾。希望对你理解和攻克链表题有所帮助!