【Leetcode_Hot100】链表
链表
160. 相交链表
方法一:模拟
依次判断两节点是否相同即可
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode nodeA = headA;
ListNode nodeB = headB;
int lenA = 0, lenB = 0;
while(nodeA.next != null) {
lenA++;
nodeA = nodeA.next;
}
while(nodeB.next != null) {
lenB++;
nodeB = nodeB.next;
}
int diffLen = lenA>=lenB ? lenA-lenB : lenB-lenA;
// 保证 A链表 比 B链表 长
if(lenA < lenB) {
ListNode temp = headA;
headA = headB;
headB = temp;
}
while(diffLen-- > 0) {
headA = headA.next;
}
while(headA != null) {
if(headA == headB) {
return headA;
}
headA = headA.next;
headB = headB.next;
}
return null;
}
}
方法二:快慢指针,消除步长差
- 假设链表A为
***** x #####
,链表B为**** x #####
,相交点为x
,相交部分为x #####
;则链表A中各部分的长度为a(*)+a(1)+a(#)
,链表B中各部分的长度为b(*)+b(1)+b(#)
,由上可知a(*)!=b(*)
,但a(#)=b(#)
- 使用pA指针先遍历A再遍历B,到相交点的长度为:
a(*)+a(1)+a(#) + b(*)+b(1)
- 使用pB指针先遍历B再遍历A,到相交点的长度为:
b(*)+b(1)+b(#) + a(*)+a(1)
- 那么
a(*)+a(1)+a(#) + b(*)+b(1)
与b(*)+b(1)+b(#) + a(*)+a(1)
的关系是怎样的?- 已知
a(*)!=b(*)
,但a(#)=b(#)
- 则,
a(*)+a(1)+a(#) + b(*)+b(1) = a(*)+a(1)+b(#) + b(*)+b(1) = a(*)+a(1)+ b(#)+ b(*)+b(1) = b(*)+b(1)+b(#) + a(*)+a(1)
- 已知
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null) {
return null;
}
ListNode pA = headA, pB = headB;
while(pA != pB) {
// 当pA==null时,说明遍历到了A的末尾,从B开始遍历
pA = pA==null ? headB : pA.next;
// 当pB==null时,说明遍历到了B的末尾,从A开始遍历
pB = pB==null ? headA : pB.next;
}
return pA;
}
}
206. 反转链表
方法一:迭代
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre=null, cur=head, temp=null;
while(cur != null){
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
}
方法二:递归
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode pre, ListNode cur) {
if(cur == null) {
return pre;
}
ListNode temp = null;
temp = cur.next;
cur.next = pre;
return reverse(cur, temp);
}
}
234. 回文链表
方法一:快慢指针
/**
* 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) {
if(head == null) {
return true;
}
// 快慢指针,快指针移动到末尾时,慢指针所指位置是中间节点
ListNode fast = head, slow = head;
while(fast != null) {
fast = fast.next;
if(fast != null) {
fast = fast.next;
}
slow = slow.next;
}
// 翻转后半段的链表
ListNode node = reverse(slow);
// 比较 经过反转后的后半段链表 和 前半段的链表
while(node != null) {
if(head.val != node.val) {
return false;
}
head = head.next;
node = node.next;
}
return true;
}
// 反转链表
private ListNode reverse(ListNode cur) {
ListNode pre = null, post = null;
while(cur != null) {
post = cur.next;
cur.next = pre;
pre = cur;
cur = post;
}
return pre;
}
}
141. 环形链表
方法一:快慢指针
利用快慢指针判断当前链表中是否含有换,如果两个指针相遇,则说明链表中有环
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast = head, slow = head;
// 排除只含有一个元素,且无环的情况,例如例三head=[1]
if(head == null || head.next == null){
return false;
}
// 快慢指针,快指针移两格,慢指针移一格,两指针相遇则说明存在环
while(fast != null) {
fast = fast.next;
if(fast != null) {
fast = fast.next;
}
slow = slow.next;
// 放在移动后判断,因为一开始指针指向同一位置
if(fast == slow) {
return true;
}
}
return false;
}
}
142. 环形链表 II
方法一:快慢指针
- 判断是否存在环(快慢指针在环中相遇)
- 判断入环的位置,由上图,
- 快指针走过的位置为
a+n*(b+c) + b
,n为走过的环的圈数;慢指针走过的距离为a+b
; - 快指针每次移动两步,慢指针每次移动一步
- 则有:
a+n*(b+c) + b = 2*(a+b)
,化简:a = (n-1)*(b+c) + c
- 因此,一个指针从head开始走a步到入环点,另一个指针在环中走
n-1
圈+相遇点到入环点的距离;两者相等
- 快指针走过的位置为
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head, slow = head;
while(fast!=null && fast.next!=null) {
fast = fast.next.next;
slow = slow.next;
// 快慢指针相遇,存在环
if(fast == slow) {
// 寻找环中的第一个节点
ListNode index1 = head;
ListNode index2 = fast;
while(index1 != index2) {
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}
return null;
}
}
21. 合并两个有序链表
方法一:迭代
- 定义变量:
temp
用于记录已合并链表的最大元素,list1
和list2
指向两个未合并链表的头元素,head
记录已合并的连链表头元素 - 依次比较
list1
和list2
所指向元素的值,将temp
指向两者中的较小一个,并更新指针temp
以及list1 / list2
- 当
list1 / list2
遍历到结尾时,将另一链表的剩余元素插入即可
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1==null) return list2;
if(list2==null) return list1;
ListNode temp = null;
if(list1.val < list2.val) {
temp = list1;
list1 = list1.next;
} else {
temp = list2;
list2 = list2.next;
}
ListNode head = temp;
// System.out.println(list1.val + "," + list2.val);
// 指针变化条件:两指针所指位置均不为空
while(!(list1==null || list2==null)) {
if(list1.val < list2.val) {
temp.next = list1;
list1 = list1.next;
} else {
temp.next = list2;
list2 = list2.next;
}
temp = temp.next;
}
// 其中一个指针所指位置为空,则将另一链表的剩余分布添加到当前链表中即可
if(list1==null) {
temp.next = list2;
} else {
temp.next = list1;
}
return head;
}
}
方法二:迭代(化简)
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 设置fakehead,返回值为fakehead.next,不用关心temp究竟指向list1还是list2
ListNode fakehead = new ListNode(-1);
ListNode temp = fakehead;
// 指针变化条件:两指针所指位置均不为空
while(!(list1==null || list2==null)) {
if(list1.val < list2.val) {
temp.next = list1;
list1 = list1.next;
} else {
temp.next = list2;
list2 = list2.next;
}
temp = temp.next;
}
// 其中一个指针所指位置为空,则将另一链表的剩余分布添加到当前链表中即可
temp.next = list1==null ? list2 : list1;
return fakehead.next;
}
}
2. 两数相加
方法一:模拟
- 题目要求,将两个链表对应的元素相加
sum
,并将其个位sum%10
存到一个新的ListNode
中,同时应该注意进位carry=sum/10
- 初始变量,使用
head
和tail
记录新链表的首尾元素,插入第一个元素时更新首尾指针;之后,一直更新尾指针。 - 最后,计算完成后,记得判断最后一次计算是否有进位,如有则将该数据存入链表
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = null, tail = null;
// 进位
int carry = 0;
while(l1 != null || l2 != null) {
int n1 = l1!=null ? l1.val : 0;
int n2 = l2!=null ? l2.val : 0;
int sum = n1 + n2 + carry;
// 处理首尾节点
if(head == null) {
// 头节点,记录首元素
head = tail = new ListNode(sum % 10);
} else {
// 剩余节点使用tail标记,并更新tail指针
tail.next = new ListNode(sum % 10);
tail = tail.next;
}
carry = sum / 10;
if(l1 != null) l1 = l1.next;
if(l2 != null) l2 = l2.next;
}
// 更新最后一个进位元素,如果最高位存在进位,则需要单另创建一个节点更新该值,如示例三
if(carry > 0) {
tail.next = new ListNode(carry);
}
return head;
}
}
方法二:模拟
- 与前述方法类似,只是用fakehead标记头节点,采用temp指针更新元素
- 注意:
while(l1!=null || l2!=null)
与while(!(l1==null || l1==null))
的区别
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode fakehead = new ListNode(-1);
ListNode temp = fakehead;
int add = 0;
while(l1!=null || l2!=null) {
int n1 = l1!=null ? l1.val : 0;
int n2 = l2!=null ? l2.val : 0;
int sum = n1 + n2 + add;
temp.next = new ListNode(sum % 10);
temp = temp.next;
add = sum / 10;
if(l1 != null) l1 = l1.next;
if(l2 != null) l2 = l2.next;
}
if(add > 0) {
temp.next = new ListNode(add);
}
return fakehead.next;
}
}
19. 删除链表的倒数第 N 个结点
方法一:双指针
采用快慢指针的方法,快指针给指向null
时,慢指针
所指元素即为删除元素
- 创建
fakehead
,对于删除头结点的情况不用分类讨论 - 注意
right
和left
的遍历位置,left
需要停到待删除位置的前一个位置,不然无法通过left.next = left.next.next
修改指针指向
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 创建fakehead,对于删除头结点的情况不用分类讨论
ListNode fakehead = new ListNode(-1);
fakehead.next = head;
ListNode left = fakehead, right = fakehead;
for(int i=0; i<n; i++) {
right = right.next;
}
while(right.next != null) {
right = right.next;
left = left.next;
}
left.next = left.next.next;
return fakehead.next;
}
}
24. 两两交换链表中的节点
方法一:模拟,双指针
使用firstNode
和secondNode
记录待交换的两个元素,cur
为待交换元素的前一个元素,依次模拟指针的变化即可
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode fakehead = new ListNode(-1);
fakehead.next = head;
ListNode cur = fakehead;
ListNode firstNode = null, secondNode = null;
while(cur.next!=null && cur.next.next!=null) {
firstNode = cur.next;
secondNode = cur.next.next;
cur.next = secondNode;
firstNode.next = secondNode.next;
secondNode.next = firstNode;
cur = firstNode;
}
return fakehead.next;
}
}
25. K 个一组翻转链表
方法一:模拟
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode fakehead = new ListNode(-1);
fakehead.next = head;
ListNode pre = fakehead;
while(head != null) {
ListNode tail = pre;
// 剩余元素不足k个,不进行翻转,将结果直接返回即可
for(int i=0; i<k; i++) {
tail = tail.next;
if(tail == null) {
return fakehead.next;
}
}
// newhead 为新一组元素首节点
ListNode newhead = tail.next;
ListNode[] reverse = myReverse(head, tail);
head = reverse[0];
tail = reverse[1];
pre.next = head;
tail.next = newhead;
pre = tail;
head = pre.next;
}
return fakehead.next;
}
private ListNode[] myReverse(ListNode head, ListNode tail) {
ListNode prev = tail.next;
ListNode p = head;
while(prev != tail) {
ListNode newhead = p.next;
p.next = prev;
prev = p;
p = newhead;
}
return new ListNode[]{tail, head};
}
}
138. 随机链表的复制
方法一:迭代,哈希表
两次遍历,第一次遍历构建节点并复制val
;第二次遍历构建random
指针
https://leetcode.cn/problems/copy-list-with-random-pointer/description/comments/2247988
class Solution {
public Node copyRandomList(Node head) {
Node fakehead = new Node(-1);
Node p = head, q = fakehead;
HashMap<Node, Node> map = new HashMap<>();
// 第一次遍历,新建链表节点,并将p中的val值复制到新结点中
while(p != null) {
// q为新创建的节点,将p所指节点放入q中
q.next = new Node(p.val);
q = q.next;
map.put(p, q);
p = p.next;
}
// 第二次遍历,建立random的映射:p,q指向逻辑上的同一节点,复制random指针(从map中根据p.random取出节点,并作为q.random)
p = head;
// 注意此处:q=fakehead.next,不应为q=head;因为此处的q是应该指向新的链表fakehead.next;而head为旧的链表
q = fakehead.next;
while(p != null) {
if(p.random != null) {
q.random = map.get(p.random);
}
p = p.next;
q = q.next;
}
return fakehead.next;
}
}
148. 排序链表
方法一:遍历+替换
遍历一遍链表中的元素值,并记录在ArrayList
中;之后利用Collections.sort(list)
对当前ArrayList
中的数字排序;最后,遍历数组元素,将元素值进行替换
注意,使用该思路实现并不符合题意!!!
class Solution {
public ListNode sortList(ListNode head) {
ListNode fakehead = new ListNode(-1);
fakehead.next = head;
List<Integer> list = new ArrayList<>();
while(head != null) {
list.add(head.val);
head = head.next;
}
Collections.sort(list);
head = fakehead.next;
for(int temp : list) {
head.val = temp;
head = head.next;
}
return fakehead.next;
}
}
方法二:冒泡排序
暴力解【超时】
冒泡排序在链表上的时间复杂度为$O(n^2)$,因为需要多次遍历链表,每次遍历需要比较链表中的所有相邻节点。
class Solution {
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
boolean isSorted = false;
// 外层循环:重复冒泡过程直到链表排序完成
while (!isSorted) {
isSorted = true; // 假设本次遍历是有序的
ListNode current = head;
// 内层循环:冒泡
while (current.next != null) {
if (current.val > current.next.val) {
// 交换相邻节点的值
int temp = current.val;
current.val = current.next.val;
current.next.val = temp;
isSorted = false; // 有交换发生,说明仍然无序
}
current = current.next;
}
}
return head;
}
}
23. 合并 K 个升序链表
方法一:两两合并
依次将前一链表合并到后后一链表中,并将后一链表的头节点记录下来;重复上述过程,即可完成链表的合并;最后,返回值因为ListNode[]
中的最后一个元素,即为最后一个链表的头节点。
链表两两合并,可参考上述21. 合并两个有序链表
class Solution {
// 合并K个链表
public ListNode mergeKLists(ListNode[] lists) {
int len = lists.length;
if(len == 0) return null;
if(len == 1) return lists[0];
for(int i=0; i<len-1; i++) {
// 合并两链表,将前一链表的结果合并到后一链表中
lists[i+1] = mergeList(lists[i], lists[i+1]);
}
// 最后合并后的结果为最后一个链表的头节点
return lists[len-1];
}
// 合并两个链表
private ListNode mergeList(ListNode list1, ListNode list2) {
ListNode fakehead = new ListNode(-1);
ListNode temp = fakehead;
while(list1!=null && list2!=null) {
if(list1.val < list2.val) {
temp.next = list1;
list1 = list1.next;
} else {
temp.next = list2;
list2 = list2.next;
}
temp = temp.next;
}
temp.next = list1==null ? list2 : list1;
return fakehead.next;
}
}
方法二:分治法
将一个ListNode[]
数组一分为二,合并前半部分和右半部分;依次递归执行
mid = (left + right) >> 1
为(left+right)/2
,对于left
和right
较大的情况,可防止数据溢出。也可写为left+(right-left)/2
class Solution {
// 合并K个链表
public ListNode mergeKLists(ListNode[] lists) {
int len = lists.length;
if(len == 0) return null;
if(len == 1) return lists[0];
return merge(lists, 0, len-1);
}
private ListNode merge(ListNode[] lists, int left, int right) {
if(left == right) return lists[left];
if(left > right) return null;
int mid = (left + right) >> 1;
// 分治法 合并
return mergeList(merge(lists, left, mid), merge(lists, mid+1, right));
}
// 合并两个链表【和前述方法中的两两合并相同】
private ListNode mergeList(ListNode list1, ListNode list2) {
ListNode fakehead = new ListNode(-1);
ListNode temp = fakehead;
while(list1!=null && list2!=null) {
if(list1.val < list2.val) {
temp.next = list1;
list1 = list1.next;
} else {
temp.next = list2;
list2 = list2.next;
}
temp = temp.next;
}
temp.next = list1==null ? list2 : list1;
return fakehead.next;
}
}
方法三:优先队列
【待补充】
146. LRU 缓存
方法一:双向链表 + 哈希表
- 定义四个函数
addToHaed, removeNode, moveToHead, removeTail
用于处理双端队列中节点移动情况(指针变化情况) - 设置
fakehead
以及faketail
来初始化双向链表 - 使用HashMap
(cache)
记录当前链表中存在的元素
class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {};
public DLinkedNode(int _key, int _value) {
key = _key;
value = _value;
}
}
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int size;
private int capacity;
private DLinkedNode fakehead, faketail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
fakehead = new DLinkedNode();
faketail = new DLinkedNode();
fakehead.next = faketail;
faketail.next = fakehead;
}
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) {
DLinkedNode newNode = new DLinkedNode(key, value);
cache.put(key, newNode);
addToHaed(newNode);
size++;
// 当前元素值超过了设置容量,将双端列表的最后一个元素(最久为访问)删除
if(size > capacity) {
DLinkedNode tail = removeTail();
cache.remove(tail.key);
size--;
}
} else {
// 访问了当前元素,将该元素移动到双端列表首位,表示最近访问
node.value = value;
moveToHead(node);
}
}
// 头插法
private void addToHaed(DLinkedNode node) {
node.prev = fakehead;
node.next = fakehead.next;
fakehead.next.prev = node;
fakehead.next = node;
}
// 移除节点,只需要将节点的前后两个指针分别指向前后元素即可
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 将节点移动到头元素,删除-插入
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHaed(node);
}
// 移除节点
private DLinkedNode removeTail() {
DLinkedNode res = faketail.prev;
removeNode(res);
return res;
}
}
/**
* 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);
*/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?