代码随想录算法训练营第四天|24.两两交换链表中的节点、19.删除链表倒数第n个节点、面试题 02.07链表相交、142.环形链表Ⅱ
一. 继续链表相关算法题
24.两两交换链表中的节点
经过前期锻炼现在现在看到题目首先能够想到双指针处理了,开心
双指针
- 时间复杂度O(n)
- 空间复杂度O(1)
- 定义一前一后两个指针交换指向指针,然后指针前移两位;
- 然后记录处理后的尾端节点链接下轮交换过来的节点
- 就这样循环处理直至两个指针有空指针
- 需要注意的是第一次交换要记录头节点后续交换,用尾端节点链接新节点
class Solution {
public ListNode swapPairs(ListNode head) {
// 如果链表为空或者链表长度为1直接返回空--这里新建一个虚拟头节点的话就不用这一步判断了
if (head==null|| head.next == null){
return head;
}
//双指针每次两个指针向前移动两位,直至出现指针为空终止
ListNode pre = head;
ListNode next = head;
//这里重新定义一个新链表记录头节点
ListNode newHead = null;
//这里定义一个新链表记录尾部节点,后续每次交换都是把上次交换的尾部节点链接前移节点
ListNode tail = null;
//循环终止条件:直至出现指针为空终止
while (pre != null && pre.next!=null){
next = pre.next;
//交换两个指针
pre.next = next.next;
next.next = pre;
//记录新链表的头节点
if (newHead==null){
newHead = next;
}else {
tail.next=next; //上次循环的尾部节点与交换过来的新节点链接
}
tail=pre; //尾部节点后移
pre = pre.next; //指针后移进入下轮交换循环
}
return newHead;
}
}
卡哥版本迭代
- 定义一个虚拟头链表: 原:[1,2,3]—>现:[0,1,2,3]
- 当前节点指向下下节点:[0,2][1][3]
- 下下节点指向下一节点[0,2,1][3]
- 下一节点指向下下下节点[0,2,1,3]
- 然后指针前移
- 从上面可以看出要有两个缓存变量记录下节点下下下节点
class Solution {
public ListNode swapPairs(ListNode head) {
//这里重新定义一个新虚拟头节点链表
ListNode newHead = new ListNode(0);
newHead.next = head;
//当前指针
ListNode current = newHead;
ListNode next = null;
ListNode nextNextNext = null;
//循环终止条件:直至出现指针为空终止
while (current.next != null && current.next.next!=null){
//先记录下节点,下下节点
next = current.next;
nextNextNext = current.next.next.next;
// 步骤一
current.next = current.next.next;
// 步骤二
current.next.next= next;
// 步骤三
next.next= nextNextNext;
//指针前移
current = current.next.next;
}
return newHead.next;
}
}
递归法(递归还是想不太明白先把代码贴出来后续研究)
class Solution {
public ListNode swapPairs(ListNode head) {
// base case 退出提交
if(head == null || head.next == null) return head;
// 获取当前节点的下一个节点
ListNode next = head.next;
// 进行递归
ListNode newNode = swapPairs(next.next);
// 这里进行交换
next.next = head;
head.next = newNode;
return next;
}
}
19.删除链表倒数第n个节点
最简单的是计算链表长度再计算出要删除节点位置处理
数组字典存储链表节点通过数组下标定位删除
- 时间复杂度O(n)
- 空间复杂度O(n)
题目中明确了链表长度不大于30,自己首先想到以空间换时间的方法:定义一个节点数组长度为31存储链表的每一个节点,这样算出要删除节点的前一节点的下下标即可删除,代码如下
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
//定义一个虚拟头节点,免除删除头节点的多余处理
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
//使用数组存储下标
ListNode cur = dummyHead;
//题目中说节点数小于等于30 加上虚拟头就是31
ListNode[] nodes = new ListNode[31];
//将链表节点存在数组中
int i= 0;
while (cur!= null){
nodes[i++] = cur;
cur = cur.next;
}
//最后一把没有进入循环i就是链表长度(包括虚拟头)
nodes[i-n-1].next = nodes[i-n-1].next.next;
return dummyHead.next;
}
}
快慢双指针
- 时间复杂度O(n)
- 空间复杂度O(1)
定义快慢双指针,快指针先多走n+1步然后同步前移,这样待快指针到达链表末尾慢指针就是要删除节点的上一个节点
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
//定义一个虚拟头节点,免除删除头节点的多余处理
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
//使用数组存储下标
ListNode slow = dummyHead;
ListNode fast = dummyHead;
//快指针先走n+1步
n=n+1;
while (n-->0){
fast = fast.next;
}
//同步走直到链表末尾
while (fast!=null){
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;
return dummyHead.next;
}
}
递归倒退n法(这个还没看懂先把到吗贴出来后续研究)
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
let newHead: ListNode | null = new ListNode(0, head);
let cnt = 0;
function recur(node) {
if (node === null) return;
recur(node.next);
cnt++;
if (cnt === n + 1) {
node.next = node.next.next;
}
}
recur(newHead);
return newHead.next;
};
面试题 02.07链表相交
- 先计算出两个链表各自的长度,并计算出长度差
- 较长的链表先走长度差步,然后两个链表同时移动
- 直到第一次出现节点相等即为链表的首个交点
- 时间复杂度O(m+n)~O(n)
- 空间复杂度O(1)
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//计算两个链表长度,长链表先前移长出的步数然后同时移动找出第一个相等的节点(注:链表相交的话交点的对象也就是内存地址相等而不是对象的值相等)
//计算长度
ListNode cur = headA;
int lengthA = 0;
while (cur != null) {
cur = cur.next;
lengthA++;
}
cur = headB;
int lengthB = 0;
while (cur != null){
cur = cur.next;
lengthB++;
}
//长链表先走
//保证A是最长的
if (lengthB>lengthA){
ListNode tempNode = headA;
headA = headB;
headB = tempNode;
int temLength = lengthA;
lengthA = lengthB;
lengthB = temLength;
}
int gap = lengthA-lengthB;
ListNode curA = headA;
while (gap-->0){
curA = curA.next;
}
ListNode curB = headB;
//找到第一个下一节点不等的节点
while (curA!=null){
if (curA == curB){
ListNode tempNode = headA;
headA = headB;
headB = tempNode;
return curA;
}
curA = curA.next;
curB = curB.next;
}
ListNode tempNode = headA;
headA = headB;
headB = tempNode;
return null;
}
}
142.环形链表Ⅱ
使用快慢指针:快指针每次走两步,慢指针每次走一步如果有环的话两者一定会相遇;相遇点到环起点的长度一定是头到环起点的长度,所以同时从相遇点和起点开始两者相遇就是环起点
- 时间复杂度O(n)
- 空间复杂度O(1)
public class Solution {
public ListNode detectCycle(ListNode head) {
//快慢指针:快指针每次走两步,慢指针每次走一步如果有环的话两者一定会相遇;相遇点到环起点的长度一定是头到环起点的长度,所以同时从相遇点和起点开始两者相遇就是环起点
//找相遇点
ListNode meetNode = findMeetNode(head);
if (meetNode == null) {
return meetNode;
}
//找到环起点
ListNode circleNode = head;
while (circleNode != meetNode){
circleNode = circleNode.next;
meetNode = meetNode.next;
}
return circleNode;
}
public ListNode findMeetNode(ListNode head){
ListNode nodeSlow= head;
ListNode nodeFast = head;
while (nodeFast != null && nodeFast.next != null ){
nodeFast = nodeFast.next.next;
nodeSlow = nodeSlow.next;
if (nodeSlow==nodeFast){
return nodeFast;
}
}
return null;
}
}
为什么相遇时相遇节点与头节点同时移动相遇时就是环入口?
- 首先假设头节点到环入口节点长度为x,环入口到相遇点为y,相遇点到环入口为z;
- 那么相遇时快慢指针的行程为:fast =x+y+n(y+z) ;slow = x+y;
- 由于快指针行程是慢指针两倍所以 x+y+n(y+z) = 2(x+y); 销项可得:x = (n-1)y+nz
- 假设如果走一圈两指针就相遇的话可得 x=z
为什么相遇时慢指针还没走一圈?
- 首先慢指针进环时快指针一定进环了,其次最坏的情况下就算两者同时在环入口出发那么快指针走两圈慢指针走一圈就相遇了;
- 如果快指针提前入环的话肯定是少于一圈就相遇了