链表应用 II
链表应用 II
应用 2:Leetcode 25. K 个一组翻转链表
题目
输入:\(head = [1,2,3,4,5]\), \(k = 2\)
输出:\([2,1,4,3,5]\)
分析
这里,我们以前面题目中的用例,来说明算法的步骤。
为了避免讨论边界条件,这里,我们使用一个 \(dummy\) 节点指向 \(head\)。
首先,使用三个指针用于遍历主链表时记录子链表在主链表中的位置:
-
\(P_0\) :记录子链表的首节点的前一个节点;
-
\(P_1\) 记录子链表的首节点;
-
\(tail\) 记录子链表的尾节点。
然后,我们遍历主链表,当tail向后移动k步之后,tail所指的元素就是子链表的尾节点,如下图所示:
此时,\(P_1\) 记录子链表的首节点 \(1\),\(tail\) 指向待倒序的子链表的尾节点 \(2\),同时,用 \(post\) 指针记录子链表的下一个节点:
然后,我们对子链表进行倒序,我们定义一个两个指针:
-
\(P_3\) 记录子链表的当前节点的前一个节点;
-
\(P_4\) 记录子链表的当前节点;
对子链表进行倒序操作:
子链表倒序的终止条件是 \(P_3\) 移动到子链表的 \(tail\) 节点;
倒序完成后,重新将 \(P_1\) 指向倒序后的子链表的首节点 \(2\),\(tail\) 指向倒序后的子链表的尾节点 \(1\),如下图所示:
将前驱指针 \(P_0\) 所指向的节点的 \(next\) 指针重新指向新的子链表首节点:
将子链表的尾结点的 \(next\) 指针重新指向主链表后驱节点 \(post\):
重新指针 \(P_0\) 指向下一个子链表的前驱节点(即当前子链表的尾结点),将 \(P_1\) 指向下一个子链表的首节点:
因此,我们可以总结出算法步骤如下:
-
使用一个指针 \(tail\),先让它指向 \(dummy\) 节点,并用它来遍历主链表,使用如下四个指针,记录子链表在主链表中的位置:
-
\(P_0\):前驱指针,用于记录子链表首节点的前一个节点;
-
\(P_1\):记录子链表的首节点;
-
\(tail\):记录子链表的尾节点;
-
\(post\):后驱指针,用于记录子链表尾节点的后一个节点;
-
-
指针 \(tail\) 每移动 \(k\) 步:
-
对 \(P_1\) 至 \(tail\) 所在的子链表进行倒序操作;
-
然后,重新将 \(tail\) 指向倒序后的子链表尾节点,将 \(head\) 指向倒序后的子链表首节点;
-
将倒序后的子链表重新接入主链表,即:
-
将前驱节点 \(P_0\) 的 \(next\) 指针指向的节点指向倒序后的子链表头节点 \(P1\);
-
将倒序后的子链表尾节点 \(tail\) 的 \(next\) 指针指向的节点指向后驱节点 \(post\);
-
-
重新将 \(P_0\) 指向当前子链表的尾节点 \(tail\),即下一个子链表的前驱节点;
-
重新将 \(P_1\) 指向当前子链表的尾节点 \(tail\) 的下一个节点,即下一个子链表的头节点;
-
-
若指针 \(tail\) 移动的步数小于 \(k\) 步,则直接返回;
代码实现
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
int n = listLength(head);
if (n <= 1) {
return head;
}
ListNode dummy = new ListNode(-1);
dummy.next = head;
// 记录子链表的前驱节点
ListNode subListPrev = dummy;
ListNode subListHead = head;
while (subListHead != null) {
// 从前驱节点开始计算子链表的长度
ListNode subListTail = subListPrev;
// 查看剩余部分的长度是否大于等于 k
for (int count = 0; count < k; count++) {
subListTail = subListTail.next;
if (subListTail == null) {
return dummy.next;
}
}
// 记录子链表的后驱节点
ListNode subListPost = subListTail.next;
// 子链表倒序
ListNode [] nodes = reverse(subListHead, subListTail);
subListHead = nodes[0];
subListTail = nodes[1];
// 子链表的前驱节点指向子链表的头部
subListPrev.next = subListHead;
// 子链表的尾节点指向子链表的后驱节点
subListTail.next = subListPost;
// 更新前驱节点的位置
subListPrev = subListTail;
// 更新子链表的头节点的位置
subListHead = subListTail.next;
}
return dummy.next;
}
private int listLength(ListNode head) {
int length = 0;
while (head != null) {
length++;
head = head.next;
}
return length;
}
private ListNode [] reverse(ListNode start, ListNode end) {
ListNode p1 = null, p2 = start;
ListNode temp;
while (p1 != end) {
temp = p2.next;
p2.next = p1;
p1 = p2;
p2 = temp;
}
return new ListNode[]{end, start};
}
}
应用2:Leetcode 234. 回文链表
题目
分析
方法一:数组
思路:
-
将链表的值复制到数组中;
-
使用双指针判断是否是回文。
方法二:递归
略。
方法三:快慢指针
算法个步骤:
-
找到前半部分链表的尾节点;
-
反转后半部分链表;
-
判断两个子链表是否回文;
-
恢复后面一个子链表;
-
返回结果。
代码实现
方法一
class Solution:
def isPalindrome(self, head: Optional[ListNode]) -> bool:
array = list()
current = head
while current:
array.append(current.val)
current = current.next
return array == array[::-1]
复杂度:
-
时间复杂度:\(O(n)\)
-
空间复杂度:\(O(n)\)
方法二
class Solution:
def __init__(self):
self.front_pointer = None
def isPalindrome(self, head: Optional[ListNode]) -> bool:
self.front_pointer = head
return self.check(head)
def check(self, head):
if head:
if not self.check(head.next):
return False
if head.val != self.front_pointer.val:
return False
self.front_pointer = self.front_pointer.next
return True
复杂度:
-
时间复杂度:\(O(n)\)
-
空间复杂度:\(O(n)\)
方法三
class Solution {
public boolean isPalindrome(ListNode head) {
ListNode preNode = getMidOfList(head);
ListNode tailNode = reverseList(preNode.next);
boolean result = true;
ListNode p1 = head, p2 = tailNode;
while (p1 != null && p2 != null) {
if (p1.val != p2.val) {
result = false;
break;
}
p1 = p1.next;
p2 = p2.next;
}
preNode.next = reverseList(tailNode);
return result;
}
private ListNode reverseList(ListNode head) {
if (head == null) {
return null;
}
ListNode p1 = null, p2 = head;
ListNode temp = null;
while (p2 != null) {
temp = p2.next;
p2.next = p1;
p1 = p2;
p2 = temp;
}
return p1;
}
private ListNode getMidOfList(ListNode head) {
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}
复杂度:
-
时间复杂度:\(O(n)\)
-
空间复杂度:\(O(1)\)
应用3:Leetcode 143. 重排链表
题目
给定一个单链表 L 的头节点 head ,单链表 L 表示为:
\(L_0 → L_1 → \cdots → L_{n - 1} → L_n\)
请将其重新排列后变为:
$L_0 → L_n → L_1 → L_{n - 1} → L_2 → L_{n - 2} → \cdots $
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:
输入:head = [1,2,3,4,5]
输出:[1,5,2,4,3]
分析
假设链表长度为 \(n\),比较朴素的思路是,从 \(\frac{n}{2}\) 处,把链表从中点,分割为两个子链表,将后半部分的子链表倒序,然后再按照题目要求重新拼接为一个新链表即可。
其中,寻找链表中点的方法,可以使用快慢指针的方式查找,当快指针到达链表末尾,慢指针刚好到达链表左半段的最后一个节点。
注意,如果链表长度为奇数时,前半段会多一个节点,只需要将其连接到新链表的末尾即可。
代码实现
class Solution {
public void reorderList(ListNode head) {
ListNode mid = getMidNode(head);
// 将链表分成两个子链表
ListNode tailNode = reverse(mid.next);
mid.next = null;
// 将两个子链表按要求合并
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode p = dummy;
ListNode p1 = head, p2 = tailNode;
while (p1 != null && p2 != null) {
p.next = p1;
p = p.next;
p1 = p1.next;
p.next = p2;
p = p.next;
p2 = p2.next;
}
// 如果节点为奇数,需要将剩余节点添加到末尾
if (p1 != null) {
p.next = p1;
}
}
private ListNode getMidNode(ListNode head) {
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
private ListNode reverse(ListNode head) {
ListNode p1 = null, p2 = head;
ListNode temp;
while (p2 != null) {
temp = p2.next;
p2.next = p1;
p1 = p2;
p2 = temp;
}
return p1;
}
}