今天是第四天,也是学习链表的第二天,主要做了四个题,做个简单的总结:
1.对于链表的操作,尽量创建一个虚拟头结点会简化很多操作;
2.对于链表节点的指针移动,之前一直很迷糊,什么时候该创建临时变量,先移哪个,后移哪个,很容易造成死循环,听了Carl老师的讲解之后真的是茅塞顿开,对于要移动指针的节点,需要将该指针原先指向的节点先暂存下,否则移动操作完成之后,这个原先被指向的节点就找不着了,如果后续还需要对这个节点进行操作的话,就无法完成了。
也就是要把原先那个指向切断,则需要将该指向的对应节点暂存。
理解了这个,对于两两交换链表中的节点这题就非常容易完成了。
24.两两交换链表中的节点
public ListNode swapPairs(ListNode head) {
ListNode dummyHead = new ListNode(0);//建一个虚拟头结点,指向head
dummyHead.next = head;
ListNode curr = dummyHead;
while(curr.next != null && curr.next.next != null) {
//此时的结构是这样
//curr --> 1 --> 2 --> 3 --> 4 --> ...--> null
//在调整curr.next从原来指向1到指向2之前,得先把1暂存下,不然后面2没法找到这个1节点了,因为指向1的指针被指给2了
//同样的,在调整2的next由原来指向3改成指向1之前,必须要把3先暂存下
//不然2.next指向1之后,3就找不到了
ListNode node1 = curr.next; //暂存现在的1
ListNode node2 = curr.next.next.next;//暂存现在的3
curr.next = curr.next.next; //将curr从指向1改成指向2,改完之后curr.next就是指向2了
//接下来操作2的next指向,由原来的指向3改为指向1
curr.next.next = node1; //此时2指向1了
//接下来操作原来的1指向3
node1.next = node2;
//调整完之后,指针变成了curr-->2-->1-->3
//我们需要把curr再移动到1的位置,开始下一次遍历
//此时1就是curr.next.next
curr = curr.next.next;
}
return dummyHead.next;
}
19.删除链表中的倒数第N个节点
这一题相对来说比较简单,步骤有以下三点:
1.先算出链表的长度;
2.根据题意给出的N,找到要删除的节点的前一个节点,记为curr;
3.将要删除节点的前一个节点的指针,指向要删除节点的后继节点,也就是curr.next = curr.next.next即可。
public ListNode removeElements(ListNode head, int target) {
//这里有两种方法,为什么有两种,因为头结点和后续的节点,对应的删除操作是不一样的
//如果不借助一个虚拟头结点,则需要分开处理当前链表头结点和非头结点的删除
//这里为什么是while循环而不是if,是因为有可能新的head节点的值还会是target
while(head != null && head.val == target) {
head = head.next;
}
//处理完head之后,处理其他节点,这里设定一个临时的指针,用这个指针来移动
ListNode curr = head;
//因为后续要取curr.next和curr.next.val所以需要两个非null的判断
while(curr != null && curr.next != null) {
if(curr.next.val == target) {
curr.next = curr.next.next;
}else {
curr = curr.next;
}
}
return head;
//借助一个虚拟头结点,可以将原来节点的头结点和非头结点使用同样的操作来删除
// ListNode dummy = new ListNode();
// dummy.next = head;
//
// ListNode curr = dummy;
// while(curr != null &&curr.next != null) {
// if(curr.next.val == target) {
// curr.next = curr.next.next;
// }else {
// curr = curr.next;
// }
// }
// return dummy.next;
}
public class ListNode {
private int val;
private ListNode next;
public ListNode() {
}
ListNode(int val) {
this.val = val;
}
ListNode(int val, ListNode head) {
this.val = val;
this.next = head;
}
}
面试题2.7链表相交
这一题一开始有点迷糊,不知道从哪里下手,看了代码随想录的思路,才明白应该怎么做。
链表相交的数学解释应该是从两个链表尾部开始对齐,一直对齐到短的那个链表头部,从短链表的头部一直到尾部,都是可能得相交点,不会出现相交点在长链表的超过短链表长度之外的位置(从尾部对齐开始)。
搞清了这个数学解释之后,就比较好弄了,唯一有点容易出错的地方是对齐的边界,我代码里的distance那个地方,从0开始到distance,应该是<distance,而不是<=;
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//链表相交的数学解释是在相交点之后,两个链表合二为一,也就是相交点及之后的链表完全一样
//两个链表相交,必然是从最短的那个链表头部开始,所以应该将长的链表移动到和短的链表末尾对齐的位置,然后逐个比较
//所以要先求链表长度,再求差值,然后移动较长的那个链表指针
int lenA = len(headA);
int lenB = len(headB);
ListNode longA = null;
ListNode smallB = null;
int distance = 0;
distance = lenA > lenB ? lenA - lenB : lenB - lenA;
if(lenA > lenB) {
longA = headA;
smallB = headB;
}else {
longA = headB;
smallB = headA;
}
for(int i = 0; i < distance; i++) {
longA = longA.next;//移动长的链表到和短链表末尾对齐的位置
}
while(longA != null) {
if(longA == smallB) {
return longA;
}
longA = longA.next;
smallB = smallB.next;
}
return null;
}
public int len(ListNode head) {
int i = 0;
while(head != null) {
i++;
head = head.next;
}
return i;
}
142.环形链表II
这一题以前看过别的思路,如果只是判断链表是否有环的话,应该用一个快指针,一个慢指针,两个指针有重合就表示有环,但是这一题需要返回入环的节点,用快慢指针要涉及到计算入环节点,这里我用了个偷懒的办法,遍历链表,将元素都存入Set集合,每次存之前判断下是否已经存过,有即表示回到环的入口了,返回这个节点就行。
public ListNode detectCycle(ListNode head) {
//用一个set记录所有遍历过的node,每次遍历都去set里面查找,如果有重复的,证明是环的入口
HashSet<ListNode> set = new HashSet<ListNode>();
if(head == null) return null;
while(head != null) {
if(!set.contains(head)) {
set.add(head);
}else {
return head;
}
head = head.next;
}
return null;
}
总结:链表的操作乍一看挺简单的,就是一个节点里有value有next指针,但是真操作起来,对于移动的位置,是否需要虚拟头结点,各种交换,还是挺麻烦的,需要很仔细,并且要理清指针移动的底层逻辑。感觉这一块还是得经常复习。