160. 相交链表
思路:
- 计算两个链表的长度: 遍历两个链表,计算它们的长度 LAL_ALA 和 LBL_BLB。
- 对齐链表的起始位置: 如果链表A较长,则向前移动指向链表A头部的指针,使得两个链表的剩余部分具有相同的长度。反之亦然。
- 寻找相交节点: 同时遍历两个链表,比较每一对节点。如果找到相同的节点,则返回该节点。如果到达任一链表的末尾,则返回
null
。
// Definition for singly-linked list.
public class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int lenA = getLength(headA), lenB = getLength(headB);
// 对齐链表的起始位置
while (lenA > lenB) {
headA = headA.next;
lenA--;
}
while (lenB > lenA) {
headB = headB.next;
lenB--;
}
// 寻找相交节点
while (headA != headB) {
headA = headA.next;
headB = headB.next;
}
return headA; // 如果没有相交,headA和headB都将为null
}
private int getLength(ListNode head) {
int length = 0;
while (head != null) {
length++;
head = head.next;
}
return length;
}
}
21.合并两个有序链表(两个链表对齐问题)
思路:
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
public class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 创建一个虚拟头节点,方便后续操作
ListNode dummy = new ListNode(0);
// 使用当前节点指针追踪新链表的最后一个节点
ListNode current = dummy;
// 当两个链表都不为空时,进行合并操作
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
current.next = list1;
list1 = list1.next;
} else {
current.next = list2;
list2 = list2.next;
}
current = current.next;
}
//不用while循环
// 如果其中一个链表不为空,则直接将其剩余部分连接到新链表的末尾
if (list1 != null) {
current.next = list1;
} else {
current.next = list2;
}
return dummy.next; // 返回新链表的头节点
}
}
使用双指针
206. 反转链表(当前指针与pre前指针)**********
思路:
1. 双指针法:
一个指针指向当前节点,另一个指针指向当前节点的前结点。
- 初始化: 使用两个指针,一个指向当前节点(
current
),另一个指向前一个节点(prev
)。 - 迭代: 遍历链表,将当前节点的
next
指针指向前一个节点,然后将current
和prev
前进一步。 - 返回: 当到达链表末尾时,返回
prev
指针。
// Definition for singly-linked list.
public class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
public class Solution {
public ListNode reverseList(ListNode head) {
//注意prev初始化为null,而且没有prev.next = head
ListNode prev = null;
ListNode current = head;
while (current != null) {
ListNode nextTemp = current.next;
current.next = prev;
prev = current;
current = nextTemp;
}
return prev;
}
}
2. 递归法:
1. 基本情况
如果链表为空或只有一个节点,我们不需要做任何事情,直接返回头节点。
2. 递归调用
对于更长的链表,我们将问题分解为两部分:头节点和剩余部分。
在上面的示例中,头节点是 1
,剩余部分是链表 2 -> 3 -> 4 -> 5
。
我们递归调用 reverseList(head.next)
来反转剩余部分。这将返回新的头节点 5
,并且剩余部分现在是:
5→4→3→2
3. 链接头节点
现在,我们要将原始头节点 1
添加到新链表的末尾。为此,我们需要执行以下步骤:
- 设置
head.next.next = head
,即将节点2
的next
指针指向节点1
。 - 将
head.next
设置为null
,以便节点1
成为新链表的最后一个节点。
4. 返回新链表的头节点
返回新链表的头节点,即节点 5
。
public class Solution {
public ListNode reverseList(ListNode head) {
// 基本情况: 链表为空或只有一个元素
if (head == null || head.next == null) return head;
// 递归步骤: 反转尾部
ListNode reversedRest = reverseList(head.next);
// 链接: 将原始头节点添加到新链表的末尾
head.next.next = head;
head.next = null;
// 返回新链表的头节点
return reversedRest;
}
}
234. 回文链表(快指针fast=fast.next.next, 满指针slow = slow.next) ********
思路:
1. 找到中点
首先,我们需要找到链表的中点。我们可以使用快慢指针的技术来实现。快指针每次移动两个节点,慢指针每次移动一个节点。当快指针到达链表的末尾时,慢指针将指向链表的中点。
2. 反转后半部分
然后,我们反转链表的后半部分。我们可以使用迭代或递归方法来实现。
3. 比较前半部分和反转的后半部分
现在,我们有两个链表:原始链表的前半部分和反转的后半部分。我们逐个比较这两个部分的节点值。如果所有值都相同,则链表是回文的。
class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null) return true;
// 找到中点
ListNode slow = head, fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 反转后半部分
ListNode prev = null, curr = slow.next;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
// 比较前半部分和反转的后半部分
ListNode p1 = head, p2 = prev;
while (p2 != null) {
if (p1.val != p2.val) return false;
p1 = p1.next;
p2 = p2.next;
}
return true;
}
}
注意:
在找中点的while循环中,使用的是fast.next&&fast.next.next != null. 这时返回的slow节点当长度为奇数时,正好是中点,为偶数时,是中间的前一个点。
而在下面的快慢指针问题中,在while循环中要使用fast != null && fast.next !=null来判断fast是否来到了尾部。
如果使用fast != null && fast.next !=null来判断fast是否来到了尾部。那么slow返回的中点为 中间点的后一个节点。这个要注意。
问题:
什么情况下是.next,什么情况需要.next.next:
在这段代码中,使用了两个指针 `fast` 和 `slow` 来寻找链表的中点。`fast` 指针以两倍速度移动,而 `slow` 指针以正常速度移动。当 `fast` 指针到达链表末尾时,`slow` 指针将位于链表的中点。
检查 `fast.next != null && fast.next.next != null` 的目的是确保 `fast` 指针和 `fast.next` 都不为 `null`,这样就可以安全地访问 `fast.next.next`。
- 当链表长度为奇数时,`fast.next.next` 将首先变为 `null`,所以循环将在 `fast` 指针到达倒数第二个节点时停止。此时,`slow` 指针将指向中间节点。
- 当链表长度为偶数时,`fast.next` 将首先变为 `null`,所以循环将在 `fast` 指针到达最后一个节点时停止。此时,`slow` 指针将指向中间的第一个节点。
这种方法允许你在不知道链表长度的情况下找到中点,无论链表长度是奇数还是偶数。如果你只检查 `fast.next != null`,则在链表长度为奇数时可能会出现空指针异常,因为你可能会尝试访问 `null` 节点的 `next` 属性。
所以说需要node.next.next的时候就需要进行node.next && node.next.next != null 的判断。因为会有奇数偶数长度的区别。
141. 环形链表(快慢指针)*********
思路:
- 创建两个指针,一个慢指针每次移动一步,一个快指针每次移动两步。
- 如果链表中没有环,快指针将首先到达链表的末尾。
- 如果链表中有环,快慢指针最终将在环内的某个位置相遇。
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head; // 慢指针每次移动一步
//注意fast是head.next而不是head了!!!!!!!!!!
ListNode fast = head.next; // 快指针每次移动两步
while (fast != null && fast.next != null) {
if (slow == fast) { // 如果快慢指针相遇,则存在环
return true;
}
slow = slow.next;
fast = fast.next.next;
}
return false; // 如果快指针到达链表末尾,说明没有环
}
}
142.环形链表2(两次快慢指针)********
思路:
- 使用快慢指针找到环内的一个相遇点。如果没有环,返回 null。
- 使用两个速度相同的指针,一个从链表头部开始,另一个从相遇点开始,当它们相遇时就是环的起点。
这种方法的时间复杂度为 O(n)O(n)O(n),空间复杂度为 O(1)O(1)O(1),其中 nnn 是链表的长度。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null || head.next == null){
return null;
}
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(fast == slow){
break;
}
}
//注意!要记得判断fast.next也不为null。
if(fast == null || fast.next==null){
return null;
}
ListNode pre1 = head;
ListNode pre2 = slow;
while(pre1 != pre2){
pre1 = pre1.next;
pre2 = pre2.next;
}
return pre1;
}
}
使用dummy节点
创建dummy节点,将dummy.next = head
19. 删除链表的倒数第n个节点******************
思路1:
1. 获得链表的长度,用长度-n-1得到要删除的前一个结点
2.设置dummy节点,指向head头节点。从dummy节点开始往后移动。
代码:
注意: 一定要用哑节点来。next 指向head。这样才会考虑到删除头节点的情况。
/**
* 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 removeNthFromEnd(ListNode head, int n) {
if (head == null || head.next == null) {
return null;
}
//注意要从prev节点开始,而不是head节点
ListNode prev = new ListNode(0,head);
prev.next = head;
int length = 0;
ListNode cur = prev;
while (head != null) {
head = head.next;
length ++;
}
int num = length - n - 1;
while(num >= 0 && cur != null) {
cur = cur.next;
//一定注意,别忘减去!!!!!!!!!!!!!!!!!!!!
num --;
}
cur.next = cur.next.next;
return prev.next;
}
}
思路2:
使用两个指针,并保持它们之间的间隔为 n,当第一个指针到达链表的末尾时,第二个指针将指向倒数第 n 个节点的前一个节点。
以下是解决此问题的步骤:
- 初始化两个指针:创建两个指针
first
和second
,并将它们都设置为链表的头节点。 - 移动第一个指针:将
first
指针向前移动 n 个节点。现在,first
和second
之间的间隔是 n。 - 同时移动两个指针:同时移动
first
和second
指针,直到first
指针到达链表的末尾。 - 删除倒数第 n 个节点:现在,
second
指针指向倒数第 n 个节点的前一个节点。更新second
的next
指针以跳过倒数第 n 个节点,即second.next = second.next.next
。 - 返回链表的头节点:返回链表的头节点以完成任务。
代码:
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 removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode first = dummy;
ListNode second = dummy;
// Advances first pointer so that the gap between first and second is n nodes apart
for (int i = 1; i <= n + 1; i++) {
first = first.next;
}
// Move first to the end, maintaining the gap
while (first != null) {
first = first.next;
second = second.next;
}
// Removes the nth node from the end
second.next = second.next.next;
return dummy.next;
}
}
24. 两两交换链表中的节点(三指针)*****************
思路:
首先,我们创建一个哑节点 dummy
并让其 next
指向 head
。这样做主要是为了处理如果链表的头两个节点需要交换的情况,使用哑节点可以简化代码。
接下来,我们使用三个指针:
prev
:指向当前要交换的一对节点的前一个节点p1
:指向当前要交换的第一个节点p2
:指向当前要交换的第二个节点
我们使用一个 while
循环来遍历链表。在每次迭代中,我们都会交换一对节点。
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; }
}
public class Solution {
public ListNode swapPairs(ListNode head) {
// 创建哑节点,并将其 next 指向 head
ListNode dummy = new ListNode(0);
dummy.next = head;
// prev 初始化为哑节点
ListNode prev = dummy;
ListNode current = head;
//注意还要current.next != null !!!!!!!!!!!!!!!!!!!!!
while (current!= null && current.next != null) {
// 初始化 p1 和 p2
ListNode p1 = current;
ListNode p2 = current.next;
//顺序是prev,p1,p2 !!!!!!!!!!!!
// Step 1: 将 prev 的 next 指向 p2
prev.next = p2;
// Step 2: 更新 p1 的 next 指向 p2 的 next
p1.next = p2.next;
// Step 3: 更新 p2 的 next 指向 p1,完成交换
p2.next = p1;
// 更新 prev 和 head 指针,准备下一轮交换
prev = p1;
current= p1.next;
}
// 最终链表的头是 dummy.next
return dummy.next;
}
}
25.k个一组翻转链表***********
- 计算链表长度:首先遍历一遍链表,计算出链表的总长度,以判断是否有足够的节点进行翻转。
- 分组翻转:对于链表的每一段长度为
k
的部分,我们进行翻转操作。翻转的同时,需要处理好每个子链表翻转后的头尾节点与原链表的连接。 - 处理剩余节点:如果链表的长度不是
k
的整数倍,那么链表的最后几个节点将保持原有顺序。
1. 定义dummy节点,dumm.next = head.
2. 使用两个指针, pre和end. 注意pre和end开始时指向同一个位置(dummy). pre代表要翻转的前一个节点, end表示要翻转链表的最后一个节点. 等到while循环中再通过for循环去找到end.
3. 找到当前要翻转链表的头节点(start = prev.next) , 与下一段要翻转的头节点(next = end.next). 存起来备用。并且注意要将当前要反转的链表与后面截断(end.next = null).
4. 然后将需要翻转的链表进行翻转操作. 并返回翻转后的头节点。然后将翻转后的节点与原链表做连接(pre.next = tail; start.next = next;)
5. 更新pre和end,让两者指向同一个地方 start .
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
// 创建一个哑节点,它的next指向head,便于处理边界情况
ListNode dummy = new ListNode(0);
dummy.next = head;
// 初始化pre和end,它们都指向dummy。pre表示要翻转链表部分的前一个节点,end表示要翻转链表部分的最后一个节点
ListNode pre = dummy, end = dummy;
while (end.next != null) {
// 移动end,使其指向当前要翻转的子链表的最后一个节点
for (int i = 0; i < k && end != null; i++) end = end.next;
if (end == null) break; // 如果end为null,说明剩余节点不足k个,不进行翻转
// 断开要翻转的链表部分,并保存下一段的起点
ListNode start = pre.next;
ListNode next = end.next;
//将要翻转的部分与原链表后面断开
end.next = null;
// 翻转当前链表部分,并将翻转后的链表连接回原链表中
pre.next = reverse(start);
// 将翻转部分的尾节点连接到下一段的起点
start.next = next;
// 重新调整pre和end,准备翻转下一段链表, pre和end一开始都先指向同一个地方.
pre = start;
end = pre;
}
return dummy.next;
}
// 辅助函数:翻转链表,并返回翻转后的头节点
private ListNode reverse(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
}
链表的原理
链表的深拷贝
138. 随机链表的复制
思路:
-
创建哈希表: 创建一个哈希表来存储原始链表节点和新链表节点的映射关系。
-
第一遍遍历: 在第一遍遍历中,为每个原始链表节点创建一个新的节点,并将其添加到哈希表中。
-
第二遍遍历: 在第二遍遍历中,设置每个新节点的
next
和random
指针。通过哈希表,我们可以在O(1)时间内找到对应的新节点。
代码:
import java.util.*;
// 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 {
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
// 创建哈希表
Map<Node, Node> map = new HashMap<>();
// 第一遍遍历,创建新节点并存储到哈希表
Node current = head;
while (current != null) {
map.put(current, new Node(current.val));
current = current.next;
}
// 重置current指针
current = head;
// 第二遍遍历,设置next和random指针
while (current != null) {
map.get(current).next = map.get(current.next);
map.get(current).random = map.get(current.random);
current = current.next;
}
// 返回新链表的头节点
return map.get(head);
}
}
问题:
1. Node current = head;这个是拷贝的head的地址,还是再创建的对象?
在这行代码 Node current = head;
中,current
变量是获得了 head
变量的引用,它们现在都指向原始链表的头节点。它没有创建一个新的对象,而是拷贝了 head
的引用(或地址)。因此,current
和 head
两个变量现在都指向同一个对象。
在Java中,对象是通过引用来操作的。当你将一个对象赋值给另一个变量时,你实际上是在复制对象的引用,而不是创建一个新的对象。这意味着 current
和 head
都指向原始链表的头节点,它们共享同一块内存地址。任何通过 current
变量对对象所做的更改也会影响通过 head
变量看到的对象,反之亦然。
在随后的代码中,你可以看到,通过遍历 current
变量,我们能够遍历整个原始链表,而不会影响 head
变量的值。head
变量仍然指向原始链表的头节点,这样我们可以在第二次遍历时再次使用它。
2. 为什么要用两次遍历
主要还是random的存在
遍历两次是为了确保在设置新节点的next
和random
指针时,所有的新节点都已经被创建并且可以被访问。这里是具体的解释:
-
第一次遍历:
- 在第一次遍历中,目的是创建原链表中每个节点的副本,并将原节点和新节点的映射关系保存在哈希表中。
- 通过这样做,我们确保了每个原始节点都有一个对应的新节点,并且我们可以通过哈希表快速访问到新节点。
-
第二次遍历:
- 在第二次遍历中,我们利用哈希表中的映射关系来设置新节点的
next
和random
指针。 - 由于
random
指针可以指向链表中的任何节点,我们需要确保所有的新节点都已经被创建,以便我们可以正确地设置random
指针。这也是为什么我们需要在第一次遍历中创建所有新节点,并在第二次遍历中设置指针。
- 在第二次遍历中,我们利用哈希表中的映射关系来设置新节点的
如果我们试图在单次遍历中完成上述两个任务,可能会遇到一些问题。例如,当我们试图设置一个新节点的random
指针时,random
指针指向的新节点可能还没有被创建。这将导致我们无法正确设置random
指针。因此,通过分两次遍历,我们可以避免这种情况,确保所有的指针都被正确设置。
链表的特性(O1时间添加删除)
146. LRU缓存
思路:
需要O1时间查找,所以需要使用HashMap
需要O1时间增加,删除和移动,所以需要使用双向链表
维护两个虚拟节点,头head和尾tail。与head连接的节点是新加入或新使用的,与tail相连的节点是超出存储范围需要删除的节点。
具体流程可以参考leecode的解题思路。
class LRUCache {
int capacity;
int size;
LinkedNode head;
LinkedNode tail;
Map<Integer, LinkedNode> map;
public LRUCache(int capacity) {
this.capacity = capacity;
this.size = 0;
this.map = new HashMap<>();
this.head = new LinkedNode(0,0);
this.tail = new LinkedNode(0,0);
head.next = tail;
tail.prev = head;
}
public int get(int key) {
LinkedNode node = map.get(key);
//check node is null, if not , get the node value and add to head;
if (node == null) {
return -1;
} else{
removeNode(node);
addToHead(node);
return node.val;
}
}
public void put(int key, int value) {
LinkedNode node = map.get(key);
// The key exited, change the value, move to head
if (node != null) {
node.val = value;
removeNode(node);
addToHead(node);
} else {
//The key didnot exit, add the node to the head and change map
LinkedNode newNode = new LinkedNode(key, value);
addToHead(newNode);
map.put(key, newNode);
size ++;
//whether the capacity is overflow
if (size > capacity) {
LinkedNode tail = removeTail();
map.remove(tail.key);
size --;
}
}
}
public void removeNode(LinkedNode node) {
node.next.prev = node.prev;
node.prev.next = node.next;
}
public void addToHead(LinkedNode node) {
node.next = head.next;
head.next.prev = node;
head.next = node;
node. prev = head;
}
public LinkedNode removeTail() {
LinkedNode node = tail.prev;
removeNode(node);
return node;
}
}
class LinkedNode {
int val;
int key;
LinkedNode prev;
LinkedNode next;
public LinkedNode(int key, int val) {
this.val = val;
this.key = key;
}
}
/**
* 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);
*/
在 `put` 方法中,你需要做的是添加一个新的键值对到缓存中,或者如果键已经存在则更新现有键的值。同时,如果添加新的键值对导致缓存超过其容量,你需要删除最久未使用的键值对。以下是 `put` 方法的逻辑步骤及代码实现:
1. **查找键**:
- 首先,你需要检查指定的键是否已经存在于缓存中。你可以通过查询哈希表来完成这个任务。
Node node = map.get(key);
2. **键存在的情况**:
- 如果键已经存在,你应该更新节点的值,然后将节点移动到双向链表的头部,表示该节点最近被访问过。
if (node != null) {
node.value = value;
moveToHead(node);
3. **键不存在的情况**:
- 如果键不存在,你需要创建一个新的节点,并将其添加到哈希表和双向链表的头部。
- 你还需要检查当前缓存的大小是否已经超过了容量。如果超过了容量,你应该删除双向链表尾部的节点,并从哈希表中删除相应的键。
} else {
node = new Node(key, value);
map.put(key, node);
addToHead(node);
size++;
if (size > capacity) {
Node tail = removeTail();
map.remove(tail.key);
size--;
}
}
4. **辅助方法的调用**:
- `moveToHead`,`addToHead` 和 `removeTail` 是辅助方法,用于管理双向链表的节点。
- `moveToHead` 方法将一个现有的节点移动到链表的头部。
- `addToHead` 方法将一个新的节点添加到链表的头部。
- `removeTail` 方法删除链表尾部的节点,并返回被删除的节点,以便你可以从哈希表中删除相应的键。
整合上述步骤,以下是 `put` 方法的完整代码:
public void put(int key, int value) {
Node node = map.get(key);
if (node != null) {
node.value = value;
moveToHead(node);
} else {
node = new Node(key, value);
map.put(key, node);
addToHead(node);
size++;
if (size > capacity) {
Node tail = removeTail();
map.remove(tail.key);
size--;
}
}
}
这样,`put` 方法能够根据指定的键和值更新缓存,确保缓存的大小不超过其容量,并保持最近最少使用的缓存替换策略。
这几个辅助方法是为了管理双向链表中的节点而设计的。它们可以帮助简化 `put` 和 `get` 方法的逻辑,并使代码更容易理解和维护。下面是每个方法的详细解释:
1. **`moveToHead` 方法**:
- 这个方法用于将一个已存在的节点移动到链表的头部。
- 它首先调用 `removeNode` 方法从链表中删除该节点,然后调用 `addToHead` 方法将该节点添加到链表的头部。
private void moveToHead(Node node) {
removeNode(node);
addToHead(node);
}
2. **`addToHead` 方法**:
- 这个方法用于将一个新的节点添加到链表的头部。
- 它首先更新新节点的 `next` 引用为当前头节点的 `next` 引用,然后更新当前头节点的 `next` 节点的 `prev` 引用为新节点。
- 接下来,它更新当前头节点的 `next` 引用为新节点,最后更新新节点的 `prev` 引用为当前头节点。
private void addToHead(Node node) {
node.next = head.next;
head.next.prev = node;
head.next = node;
node.prev = head;
}
3. **`removeNode` 方法**:
- 这个方法用于从链表中删除一个指定的节点。
- 它通过更新指定节点的前一个节点的 `next` 引用和指定节点的下一个节点的 `prev` 引用来完成删除操作。
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
4. **`removeTail` 方法**:
- 这个方法用于删除链表尾部的节点,并返回被删除的节点。
- 它首先获取当前尾节点的前一个节点(即最久未使用的节点),然后调用 `removeNode` 方法从链表中删除该节点。
private Node removeTail() {
Node node = tail.prev;
removeNode(node);
return node;
}
通过这几个辅助方法,你可以轻松地在双向链表中添加、删除和移动节点。这些操作是实现 LRU 缓存 `put` 和 `get` 方法的基础,同时也保证了代码的清晰和高效。
链表排序(分治算法,归并排序)
148. 排序链表
要解决此问题,一种有效的方法是采用分治策略,如归并排序。由于链表不能随机访问元素,我们不能使用快速排序,但归并排序在链表上效果很好。以下是解决方案的步骤:
1. **基本情况**:如果链表为空或只有一个元素,那么它已经是排序的,我们可以直接返回链表头。
```java
if (head == null || head.next == null) {
return head;
}
```
2. **分割链表**:使用快慢指针技巧找到链表的中间节点并分割链表。
```java
ListNode slow = head, fast = head, prev = null;
while (fast != null && fast.next != null) {
prev = slow;
slow = slow.next;
fast = fast.next.next;
}
prev.next = null; // 将链表从中间分成两部分
```
3. **递归排序**:递归地对两个子链表进行排序。
```java
ListNode left = sortList(head);
ListNode right = sortList(slow);
```
4. **合并**:将两个已排序的链表合并为一个已排序的链表。
```java
return merge(left, right);
```
其中,`merge`是一个辅助函数,用于合并两个已排序的链表。
```java
private ListNode merge(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode();
ListNode current = dummy;while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
current.next = l1;
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
current = current.next;
}if (l1 != null) {
current.next = l1;
} else {
current.next = l2;
}return dummy.next;
}
```
现在我们将所有这些放在一起。
class Solution {
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode slow = head, fast = head, prev = null;
while (fast != null && fast.next != null) {
prev = slow;
slow = slow.next;
fast = fast.next.next;
}
这里注意要将链表断开
prev.next = null;
ListNode left = sortList(head);
ListNode right = sortList(slow);
return merge(left, right);
}
private ListNode merge(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode();
ListNode current = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
current.next = l1;
这里注意别忘了移动l1,和l2
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
别忘了移动current
current = current.next;
}
注意这里不用while循环
if (l1 != null) {
current.next = l1;
} else {
current.next = l2;
}
return dummy.next;
}
}
这个解决方案的时间复杂度是 O(n log n),其中 n 是链表的长度,空间复杂度是 O(log n),由于递归调用的堆栈深度。
23.合并K个升序链表(分治算法)
这个问题的解法使用了分治策略,分而治之是一种将问题分解为若干个更小的、相互独立的子问题的方法,然后递归解决子问题,并将子问题的解合并为原问题的解。在这个问题中,我们将合并 k 个链表的问题分解为合并 2 个链表的问题。
下面我将逐步解释这个解决方案的每个部分:
1. **基本情况**:
首先,我们检查输入数组是否为空或长度为0,如果是的话,就直接返回 `null`。如果数组的长度为1,就直接返回那个链表。
if (lists == null || lists.length == 0) {
return null;
}
if (lists.length == 1) {
return lists[0];
}
2. **合并两个链表**:
我们创建一个辅助方法 `mergeTwoLists`,它能合并两个已排序的链表。这个方法通过比较两个链表的头节点的值,确定新链表的头节点,然后递归地合并剩下的节点。
好的,我会为你详细解释 `mergeTwoLists` 方法的工作原理。
这个方法的目的是将两个已排序的链表 `l1` 和 `l2` 合并为一个新的排序链表。它采用递归的方式来完成这个任务。
以下是 `mergeTwoLists` 方法的步骤:
2.1. **基本情况**:
- 如果 `l1` 是 `null`,则返回 `l2`,因为没有更多的节点需要合并。
- 如果 `l2` 是 `null`,则返回 `l1`,因为没有更多的节点需要合并。
if (l1 == null) return l2;
if (l2 == null) return l1;
2.2. **比较头节点的值**:
- 比较 `l1` 和 `l2` 的头节点的值,以确定哪个节点应该是新链表的头节点。如果 `l1` 的头节点的值小于 `l2` 的头节点的值,那么 `l1` 的头节点应该是新链表的头节点。
if (l1.val < l2.val) {
2.3. **递归调用**:
- 现在,我们知道了新链表的头节点,下一步是确定新链表的下一个节点。为了做到这一点,我们递归地调用 `mergeTwoLists` 方法,传入 `l1` 的下一个节点和 `l2`。这将返回新链表的剩余部分,我们将这个剩余部分连接到 `l1` 的头节点上。
l1.next = mergeTwoLists(l1.next, l2);
return l1;
- 同样,如果 `l2` 的头节点的值小于或等于 `l1` 的头节点的值,我们会做相反的事情:将 `l2` 的头节点设置为新链表的头节点,并递归地合并 `l1` 和 `l2` 的下一个节点。
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
整个 `mergeTwoLists` 方法的代码如下:
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
通过这种方式,`mergeTwoLists` 方法能够将两个排序的链表合并为一个新的排序链表。递归调用使得代码简洁明了,而递归的基本情况确保了递归最终会终止,并且在每个递归级别上,我们都会将两个链表中的一个节点添加到结果链表中。
3. **分治合并**:
在 `mergeKLists` 方法中,我们将输入的链表数组分为两半,然后递归地对每一半执行合并操作,最后再将两个结果合并。
public ListNode mergeKLists(ListNode[] lists, int start, int end) {
if (start == end) {
return lists[start];
}
int mid = start + (end - start) / 2;
ListNode l1 = mergeKLists(lists, start, mid);
ListNode l2 = mergeKLists(lists, mid + 1, end);
return mergeTwoLists(l1, l2);
}
在分治合并阶段,我们采用分治策略将合并多个有序链表的问题分解为更小的子问题。我们将原始的链表数组分为两个更小的数组,然后递归地对每个子数组进行合并,最终将两个子数组的合并结果再合并成一个完整的有序链表。这种方法利用了递归和分治策略,逐步缩小问题的规模,直到可以直接解决为止。
以下是`mergeKLists`方法的详细解释:
3.1. **递归基本情况**:
- 如果`start`等于`end`,表示只有一个链表,直接返回这个链表即可。
if (start == end) {
return lists[start];
}
3.2. **计算中间索引**:
- 使用`(start + end) / 2`或`start + (end - start) / 2`计算中间索引,将原始数组分为两个子数组。
int mid = start + (end - start) / 2;
3.3. **递归调用**:
- 递归地调用`mergeKLists`方法,对左半边和右半边的链表数组进行合并。
ListNode l1 = mergeKLists(lists, start, mid);
ListNode l2 = mergeKLists(lists, mid + 1, end);
3.4. **合并两个结果**:
- 使用`mergeTwoLists`方法将两个递归结果合并成一个有序链表,并返回。
return mergeTwoLists(l1, l2);
整个`mergeKLists`方法代码如下:
public ListNode mergeKLists(ListNode[] lists, int start, int end) {
if (start == end) {
return lists[start];
}
int mid = start + (end - start) / 2;
ListNode l1 = mergeKLists(lists, start, mid);
ListNode l2 = mergeKLists(lists, mid + 1, end);
return mergeTwoLists(l1, l2);
}
在每个递归调用中,我们都将问题的规模减半,直到只剩下一个链表为止。然后,我们从底层开始,将小的有序链表两两合并成更大的有序链表,直到最终得到一个完整的有序链表。通过这种方式,分治策略帮助我们以递归的方式将多个有序链表合并成一个有序链表。
4. **调用分治函数**:
在主方法 `mergeKLists` 中,我们调用上述分治函数,传入链表数组及其开始和结束索引。
public ListNode mergeKLists(ListNode[] lists) {
return mergeKLists(lists, 0, lists.length - 1);
}
将所有这些放在一起,就得到了解决方案。通过递归地将合并 k 个链表的问题分解为合并 2 个链表的问题,并利用 `mergeTwoLists` 方法来合并两个链表,我们能够将 k 个已排序的链表合并为一个单一的、已排序的链表。
完整代码:
/**
* 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 mergeKLists(ListNode[] lists) {
注意一定要考虑到这两种情况,分别对应[] 和[[]]的情况
if (lists == null || lists.length == 0) {
return null;
}
return mergeKLists(lists, 0, lists.length - 1);
}
public ListNode mergeKLists(ListNode[] lists, int start, int end) {
if (start == end) {
return lists[start];
}
int mid = start + (end - start) / 2;
ListNode l1 = mergeKLists(lists, start, mid);
ListNode l2 = mergeKLists(lists, mid + 1, end);
return mergeTwoLists(l1, l2);
}
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}