1.删除链表的倒数第N个结点
题目: 给你一个链表,删除链表的倒数第n个结点,并且返回链表的头结点。
示例:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
方法思路
1、哑结点(Dummy Node):引入哑结点作为头节点的前驱,统一处理删除头节点的情况。
2、快指针先行:快指针先移动 n 步,确保快慢指针之间的间隔为 n。
3、同步移动:快慢指针同时移动,直到快指针到达链表末尾。此时慢指针指向待删除节点的前驱。
4、删除节点:修改慢指针的 next 指针,跳过待删除节点。
代码实现:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode fast = dummy, slow = dummy;
// 快指针先移动n步
for (int i = 0; i < n; i++) {
fast = fast.next;
}
// 同步移动,直到快指针到达末尾
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
// 删除倒数第n个节点
slow.next = slow.next.next;
return dummy.next;
}
}
2. 反转链表
题目:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
方法思路:迭代法
1、初始化指针:定义前驱指针 prev(初始为 null)和当前指针 curr(初始为头节点 head)。
2、遍历链表:每次循环中,保存当前节点的下一个节点,将当前节点的 next 指针指向前驱节点,然后更新前驱和当前指针。
3、终止条件:当当前指针 curr 为 null 时,遍历结束,此时 prev 指向反转后的新头节点。
代码实现:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next; // 保存下一个节点
curr.next = prev; // 反转指针
prev = curr; // 前驱指针后移
curr = nextTemp; // 当前指针后移
}
return prev; // 返回新头节点
}
}
3.合并两个有序链表
题目:将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
方法思路:
1、处理特殊情况:若其中一个链表为空,直接返回另一个链表。
2、使用哑结点(Dummy Node):简化头节点的处理,避免复杂的条件判断。
3、迭代比较:同时遍历两个链表,选择较小值的节点连接到新链表,并移动相应指针。
4、连接剩余节点:当其中一个链表遍历完成后,将另一个链表的剩余部分直接接入新链表。
代码实现:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1==null){
return l2;
}
if(l2==null){
return l1;
}
ListNode result = new ListNode(0);
if(l1.val<l2.val){
result =l1;
result.next =mergeTwoLists(l1.next,l2);
return result;
}
result =l2;
result.next =mergeTwoLists(l1,l2.next);
return result;
}
}
4. 环形链表
题目:给你一个链表的头节点head ,判断链表中是否有环。
示例:
要判断一个链表是否有环,可以使用快慢指针的方法。快指针每次移动两步,慢指针每次移动一步。如果链表中有环,快指针最终会追上慢指针,两者相遇;如果快指针到达链表末尾(遇到null),则链表无环。
方法思路:
1、初始化指针: 快指针fast和慢指针slow都指向头节点。
2、循环遍历链表: 只要快指针可以继续移动两步(即fast.next和fast.next.next存在),就继续移动指针。
3、移动指针: 每次循环中,慢指针移动一步,快指针移动两步。
4、检查相遇: 如果快慢指针相遇,说明有环,返回true;否则,当快指针无法继续移动时,返回false。
代码实现:
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) {
returnfalse;
}
ListNode slow = head;
ListNode fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
returntrue;
}
}
returnfalse;
}
}
// 定义链表节点
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
5. 排序链表
题目:给你链表的头结点 head ,请将其按升序排列并返回 排序后的链表 。
示例:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
方法思路:
1、快慢指针分割链表:用 fast=slow.next 确保偶数节点时左半部分略短,避免死循环
2、递归排序子链表:left = sortList(head), right = sortList(tmp)
3、合并有序链表:经典的链表归并操作
代码实现:
class Solution {
public ListNode sortList(ListNode head) {
// 递归终止条件:空节点或单节点
if (head == null || head.next == null)
return head;
// 快慢指针找中点(关键点:fast从head.next开始)
ListNode fast = head.next, slow = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 分割链表为左右两部分
ListNode tmp = slow.next;
slow.next = null; // 切断链表
// 递归排序子链表
ListNode left = sortList(head);
ListNode right = sortList(tmp);
// 合并两个有序链表
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
while (left != null && right != null) {
if (left.val < right.val) {
cur.next = left;
left = left.next;
} else {
cur.next = right;
right = right.next;
}
cur = cur.next;
}
cur.next = (left != null) ? left : right;
return dummy.next;
}
}
6. 回文链表
题目: 给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false。
输入:head = [1,2,2,1]
输出:true
要判断一个单链表是否为回文链表,可以采用反转后半部分链表并与前半部分比较的方法,这样可以在O(n)时间复杂度和O(1)空间复杂度内解决问题。
方法思路:
1、找到中间节点:使用快慢指针法找到链表的中间节点。快指针每次移动两步,慢指针每次移动一步,当快指针到达链表末尾时,慢指针正好位于中间位置。
2、反转后半部分链表:从中间节点的下一个节点开始反转后半部分链表。
3、比较前后部分:将前半部分链表和反转后的后半部分链表逐一比较,如果所有节点值相同,则为回文链表。
代码实现:
class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
returntrue;
}
// 找到前半部分的尾节点
ListNode firstHalfEnd = endOfFirstHalf(head);
// 反转后半部分链表
ListNode secondHalfStart = reverseList(firstHalfEnd.next);
// 判断是否是回文
ListNode p1 = head;
ListNode p2 = secondHalfStart;
boolean result = true;
while (result && p2 != null) {
if (p1.val != p2.val) {
result = false;
}
p1 = p1.next;
p2 = p2.next;
}
// 恢复链表结构(可选)
firstHalfEnd.next = reverseList(secondHalfStart);
return result;
}
// 使用快慢指针找到前半部分的尾节点
private ListNode endOfFirstHalf(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast.next != null && fast.next.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
// 反转链表
private ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
}
7. k个一组反转
题目:给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
示例:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
要解决每k个节点一组翻转链表的问题,我们可以采用迭代的方法,通过分段处理链表,确保每组k个节点被正确翻转,最后将各组连接起来。
方法思路
1、分段检查:使用辅助函数检查当前剩余链表是否有足够的k个节点。
2、翻转每组节点:对于每组k个节点,使用标准的链表翻转方法进行翻转。
3、连接各组:将翻转后的子链表正确连接到已处理的部分,并继续处理后续节点。
代码实现:
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prev = dummy;
ListNode curr = head;
while (curr != null) {
ListNode start = curr;
ListNode end = getKthNode(start, k);
if (end == null) {
// 剩余节点不足k个,无需翻转
break;
}
ListNode nextGroupHead = end.next;
end.next = null; // 断开当前组与后续链表的连接
// 翻转当前组
ListNode newHead = reverse(start);
// 将前驱节点连接到新头部
prev.next = newHead;
// 当前组的尾部(原start)连接到下一组的头部
start.next = nextGroupHead;
// 更新前驱节点为当前组的尾部
prev = start;
// 当前指针移动到下一组的头部
curr = nextGroupHead;
}
return dummy.next;
}
// 辅助函数:找到从节点node开始的第k个节点
private ListNode getKthNode(ListNode node, int k) {
while (node != null && k > 1) {
node = node.next;
k--;
}
return node;
}
// 辅助函数:翻转链表
private ListNode reverse(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
}
8. LRU缓存
题目:请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
示例:
输入
[“LRUCache”, “put”, “put”, “get”, “put”, “get”, “put”, “get”, “get”, “get”]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
方法思路:
代码实现:
import java.util.HashMap;
import java.util.Map;
class LRUCache {
// 双向链表节点定义
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}
private Map<Integer, DLinkedNode> cache = new HashMap<>(); // 哈希表用于快速查找
private int capacity; // 缓存容量
private int size; // 当前缓存大小
private DLinkedNode head, tail; // 虚拟头尾节点简化边界操作
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 初始化虚拟头尾节点并相互连接
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
/**
* 获取键对应的值,若不存在返回-1
* 并将该节点移至头部表示最近使用
*/
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
moveToHead(node); // 访问后移至头部
return node.value;
}
/**
* 插入或更新键值对
* 若键已存在则更新值并移至头部
* 若键不存在则创建新节点,插入头部并检查容量
*/
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node != null) { // 存在则更新并移至头部
node.value = value;
moveToHead(node);
} else { // 不存在则创建新节点
DLinkedNode newNode = new DLinkedNode(key, value);
cache.put(key, newNode); // 加入哈希表
addToHead(newNode); // 插入链表头部
size++;
if (size > capacity) { // 超过容量则移除尾部节点
DLinkedNode tailNode = removeTail();
cache.remove(tailNode.key); // 同步删除哈希表项
size--;
}
}
}
// 将节点添加到链表头部
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
// 从链表中移除节点
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 将节点移至头部:先移除再插入头部
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
// 移除尾部节点(最久未使用)
private DLinkedNode removeTail() {
DLinkedNode tailNode = tail.prev; // 尾部节点是虚拟尾节点的前驱
removeNode(tailNode);
return tailNode;
}
}