【03/16】力扣押题推荐(反转链表、删除链表的倒数第N节点、K个一组翻转链表)_数据结构

 ​​剑指 Offer 24. 反转链表​​

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
限制:
0 <= 节点个数 <= 5000

思考:面试经常手写的一道题,可以用双指针交换两个结点的指向;可以用递归不断转换两个结点的指向(重复子问题)

题解:

public ListNode reverseList(ListNode head) {
// 双指针法
ListNode cur = head, pre = null;
while(cur != null) {
ListNode tmp = cur.next; // 暂存后继节点 cur.next
cur.next = pre; // 修改 next 引用指向
pre = cur; // pre 暂存 cur
cur = tmp; // cur 访问下一节点
}
return pre;
}

public ListNode reverseList(ListNode head) {
// 递归法
if(head == null || head.next == null){ // 递归终止条件
return head;
}
ListNode newHead = reverseList(head.next); // 执行递归
head.next.next = head; // 将当前节点的下一个 指向自己
head.next = null; // 将自己的后继去掉
return newHead;
}

​​19. 删除链表的倒数第 N 个结点​​

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

【03/16】力扣押题推荐(反转链表、删除链表的倒数第N节点、K个一组翻转链表)_数据结构_02

示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
提示:
链表中结点的数目为 sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz

思考:我们可以遍历一遍链表,找到对应位置删除;快指针先走n步,当快指针为null时,删除慢指针的位置

题解:

public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head); // 保留的头节点
int length = getLength(head); // 获取链表长度
ListNode cur = dummy;
for (int i = 1; i < length - n + 1; ++i) { // 遍历到 L-n+1的位置
cur = cur.next;
}
cur.next = cur.next.next; // 删除节点
ListNode ans = dummy.next; // 返回保留的头节点
return ans;
}
public int getLength(ListNode head) { /*遍历一遍,计算链表长度*/
int length = 0;
while (head != null) {
++length;
head = head.next;
}
return length;
}
// 快指针先走n步,当快指针为null时,删除慢指针的位置
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head); // 保留的头节点
ListNode first = head; // 快指针
ListNode second = dummy; // 慢指针
for (int i = 0; i < n; ++i) // 快指针先走的n的位置
first = first.next;
while (first != null) { // 同时向后移动
first = first.next;
second = second.next;
}
second.next = second.next.next; // 删除节点
ListNode ans = dummy.next; // 返回保留的头节点
return ans;
}

 ​​25. K 个一组翻转链表​​

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

进阶:
你可以设计一个只使用常数额外空间的算法来解决此问题吗?
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:

【03/16】力扣押题推荐(反转链表、删除链表的倒数第N节点、K个一组翻转链表)_算法_03

输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
示例 2:
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
示例 3:
输入:head = [1,2,3,4,5], k = 1
输出:[1,2,3,4,5]
示例 4:
输入:head = [1], k = 1
输出:[1]
提示:
列表中节点的数量在范围 sz 内
1 <= sz <= 5000
0 <= Node.val <= 1000
1 <= k <= sz

思考:使用递归法,将K个一组的节点,转化为反转链表;还可以用双指针遍历,找到需要交换的链表反转

题解:

public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || head.next == null) { // 递归终止条件
return head;
}
// 找到
ListNode tail = head;
for (int i = 0; i < k; i++) {
//剩余数量小于k的话,则不需要反转。
if (tail == null) {
return head;
}
tail = tail.next;
}
// 反转前 k 个元素
ListNode newHead = reverse(head, tail);
// 下一轮的开始的地方就是tail
head.next = reverseKGroup(tail, k);
return newHead;
}

/*
左闭又开区间
*/
/**
* @param head 整个未反转的链表
* @param tail 本轮不需要反转的链表
* @return
*/
private ListNode reverse(ListNode head, ListNode tail) {
ListNode pre = null;
ListNode next = null;

// 对其进行翻转。并返回翻转后的头结点
while (head != tail) {
// 每次都将当前节点的指针反转

// 报错为反转的节点
next = head.next;

// 待反转的节点指向pre(pre会向后移动的)
head.next = pre;
// pre重新指向当前头结点
pre = head;

// head向后移动到未反转的节点
head = next;
}
return pre;
}
/**
* 时间复杂度O(n),空间复杂度O(1) 双指针遍历 ,假设;head=[1,2,3,4,5] ,k=2
*
* @param head 链表实例头结点
* @param k 翻转的K数
* @return
*/
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || head.next == null) {
return head;
}

// 创建一个空节点[0]
ListNode dummy = new ListNode(0);
// 将新创建的空结点 指向head节点[0 -> 1,2,3,4,5],此时dummy=[0,1,2,3,4,5]
dummy.next = head;

// 设置两个记录指针,此时pre=[0,1,2,3,4,5],此时end=[0,1,2,3,4,5]
ListNode pre = dummy;
ListNode end = dummy;

// 遍历链表
while (end.next != null) {
// 这里设计的很巧妙,遍历k次的end,使end最终等于需要交换的节点,此时end=[2,3,4,5]
for (int i = 0; i < k && end != null; i++) {
end = end.next;
}
// end如果为null,就跳出链表的遍历,证明已经把所有需要的节点交换完毕
if (end == null) {
break;
}

// 这是创建两个交换指针 ,start=[1,2,3,4,5] next=[3,4,5]
// 记录要翻转链表的头节点
ListNode start = pre.next;
// 记录下end.next,方便后面链接链表
ListNode next = end.next;
// 将end下一个指向null,目的是断开链表,end=[2,null ...]同时start=[1,2,null ...] ,就是end=[2],start=[1,2],
end.next = null;

// 拿到start需要反转的链表开始反转,此时执行完反转方法 pre=[0,2,1],dummy=[0,2,1],head=[1],start=[1],end=[2,1] , next=[3,4,5]
pre.next = reverse(start);

// 翻转后头节点变到最后。通过.next把断开的链表重新链接。start=[1,3,4,5],此时pre=[0,2,1,3,4,5],dummy=[0,2,1,3,4,5],end=[2,1,3,4,5] , next=[3,4,5]
start.next = next;

// 将pre换成下次要翻转的链表的头结点的上一个节点。即start pre=[1,3,4,5]
pre = start;

// 翻转结束,将end置为下次要翻转的链表的头结点的上一个节点。即start end=[1,3,4,5] , 其余同理(遍历第二遍dummy=[0,2,1,4,3,5],pre=[3,5]end=[3,5])
end = pre;
}
// 返回链表头结点
return dummy.next;
}

/**
* 反转链表,此时的head是需要反转的链表
*/
private ListNode reverse(ListNode head) {
//单链表为空或只有一个节点,直接返回原单链表
if (head == null || head.next == null) {
return head;
}

// 创造当前节点的前一个节点
ListNode pre = null;
// 当前节点指针,此时curr就是[1,2]
ListNode curr = head;

// 遍历当前节点的链表
while (curr != null) {
// 记录指向下一个节点,保存当前节点后面的链表,next=[2] ; 第二次循环next=null
ListNode next = curr.next;
// 将curr当前节点next域指向前一个节点pre,此时curr=[1],pre=null,head=[1] ; 第二次循环next=null,curr=[2,1]
curr.next = pre;
// pre指针向后移动,此时pre只想当前节点 pre=[1] ; 第二次循环pre=[2,1]
pre = curr;
// cur指针向后移动。下一个节点变成当前节点 curr=[2],pre=[1],head=[1] ; 第二次循环curr = null,pre=[2,1],head=[1]
curr = next;
}
// 返回已交换链表的头结点
return pre;
}