今日任务:
-
- 两两交换链表中的节点
- 19.删除链表的倒数第N个节点
- 面试题 02.07. 链表相交
- 142.环形链表II
- 总结
两两交换链表中的节点
首先看到这个题目我想到的是定义两个指针,然后不断移动两个指针的位置,在移动过程中交换它们的值从而实现交换他们的节点,但是在实现过程中遇到了一些问题,也就是我定义的两个指针会出现空指针的异常,下面是错误代码的示例:
if (head == null) return head;
ListNode frontPointer = head.next;
ListNode endPointer = head;
while (frontPointer != null && endPointer != null) {
swap(endPointer, frontPointer);
frontPointer = frontPointer.next.next;
endPointer = endPointer.next;
}
交换两个节点的值, 因为可以进入这个 while 循环的话就说明它们不是空指针 之后将两个指针往后面进行移动,这时候就出现问题了,因为 举个例子刚开始是 1 2 3 4 endPointer 执向的是 1,frontPointer 指向的是 2 那么往后移动需要 frontPointer.next.next,endPointer.next 问题就出在 frontPointer.next.next 因为我们给的例子是比较好,如果是 1 2 那么当 frontPointer.next 的结果为 null 然后继续 null.next 就会报空指针异常 因此我想的是要保证 frontPointer.next 也不为空,但是如果直接在 while 加入这个条件 就会导致 1 2 直接进不来,无法交换
因此我想到了虚拟头节点,也不知道为什么反正就是想试一下虚拟头节点可以不可以避免空指针异常,所以我写了如下的代码:
public static ListNode swapPairs(ListNode head) {
if (head == null) return head;
ListNode root = new ListNode();
ListNode virtualNode = root;
ListNode frontPointer = virtualNode;
ListNode endPointer = virtualNode;
root.next = head;
//head
while (virtualNode.next != null && virtualNode.next.next != null) {
//执行交换操作
frontPointer = virtualNode.next.next;
endPointer = virtualNode.next;
swap(frontPointer, endPointer);
virtualNode = frontPointer;
}
return head;
}
正当我在感叹,虚拟头节点的奇妙,思考为什么这样就可以避免空指针异常的时候,我发现,其实根本不需要虚拟头节点,因为我可以直接将,好吧直接写不行,需要定义一个虚拟头节点,不过我想说的其实是,在操作指针的时候如果要避免空指针异常的话,那就需要先在条件判断中,对可能出现的空指针进行判断,然后在里面进行操作,先判断 virtualNode.next != null && virtualNode.next.next != null 然后,再在内部进行两者指针的移动,这样才可以避免空指针异常,因为如果导致了异常,他直接就不会进去,进去的出来之后也是正常的,如果他后面有空指针就可直接判断出来! 代码随想录里面也有一种写法:
public static ListNode swapTwoNodes(ListNode head) {
if (head == null) return head;
//创建一个虚拟的节点
ListNode root = new ListNode();
ListNode virtualNode = root;
ListNode temp = null;
ListNode temp1 = null;
while (head != null && head.next != null) {
temp = head.next;//3
temp1 = head.next.next;//4
virtualNode.next = temp;
temp.next = head;
head.next = temp1;
head = temp1;
virtualNode = virtualNode.next.next;
}
System.out.println("Man Stop!");
return root.next;
}
虽然我感觉自己以后想不到这种写法,不过思路可以借鉴,跟我上面说的一样,还是需要在操作之前对 head != null 和 head.next != null 进行判断,然后再进入 while 循环里面进行指针的移动,这样来避免空指针的出现!
删除链表的倒数第N个节点
删除倒数第 N 个节点,我刚开始想的是如果可以获得到它的链表长度就好了,但是很显然我们无法直接获得到链表的长度,因此需要对链表进行一遍遍历才可以获得它的长度,这需要 O(n) 的时间复杂度,然后需要 size - n 也就是我们要删除的元素所在的索引,但是我们知道链表的删除操作需要先获取到它之前的节点然后把中间的这个节点进行删除,所以需要我们 size - n - 1,不过这需要考虑一种情况,比如说 1 2 3 4 我们要删除倒数第 4 个元素,那么我们就需要进行判断总不能遍历到 -1 吧,就是在 size - n 等于 0 的时候返回 head.next 即可。不过显然代码随想录提供了一种更加简便的解法,就是使用双指针,一个快指针,一个慢指针,快指针先移动倒数 n 的位置
while (N-- != 0) {
fast = fast.next;
}
然后如果移动到了 null 就说明它移动了 size 的位置,就直接返回 head.next 即可,否则的话,就可以移动 slowPointer 从 head 开始,当 fast.next 等于 null 的时候结束,此时 slowPointer 就位于我们要删除的节点之前,此时我们就可以执行具体的删除操作了,完整代码:
public static ListNode remove(ListNode list, int N) {
if (list == null) return list;
//将头指针移动到
ListNode fast = list;
ListNode slow = list;
while (N-- != 0) {
fast = fast.next;
}
if (fast == null) {
return list.next;//说明删除的是倒数第size个元素
}
while (fast.next != null) {
slow = slow.next;
fast = fast.next;
}
ListNode removedNode = slow.next;
slow.next = slow.next.next;
removedNode.next = null;
return list;
}
链表相交
链表相交说实话,我开始看的时候连题意都没有看懂。。。,不过知道大致意思是说找到它们相交的节点,然后返回相交节点的位置,所以我找到了这段代码:
public static ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null && headB == null) return null;
ListNode curA = headA;
ListNode curB = headB;
while (curA != curB) {
curA = curA.next;
if (curA == null) curA = headB;
curB = curB.next;
if (curB == null) curB = headA;
}
return curA;
}
实际上这段代码无法在 leetcode 通过,因为没有对没有交点的情况进行判断,但是这段代码同样没有进行判断,但是它确是可以在 leetcode 运行通过的(我觉得它们是一样的,我调试了!),好傻屄(如果我错了就反过来!)
环形链表
环形链表我的第一印象就是之前做过这种找环的题目,印象中有一个简单的是判断一个链表是不是有环,他那个是 定义两个指针一个快指针,一个慢指针,然后快指针移动两步,慢指针移动一步,然后找到它们相等的情况,返回 ture,如果不相等就返回false就说明没有环
public boolean hasCycle(ListNode head) {
ListNode fastPointer = head;
ListNode slowPointer = head;
while (fastPointer != null && fastPointer.next != null) {
slowPointer = slowPointer.next;
fastPointer = fastPointer.next.next;
if(fastPointer == slowPointer) return true;
}
System.out.println("Man Stop!");
return false;
}
当然这道题目不是那么简单的,因为,它是判断找到环状链表相交的那个点,其实他说是什么弗洛依达原理(肯定不是这个,但我懒得查了)之类的,数学推论,我之前看 Youtube 上面就有这道题,if code like anime 好像吧,最后就是用的这个方式解决的 还有一个数学的推论,之前没搞懂,不过看了看Carl的视频里面公式的推导之后清晰多了 大致意思就是,只要有环,那么我们定义的两个指针,一个快指针,一个慢指针,就一定会相遇,并且是快指针追赶慢指针,然后我们先定义一个循环找到两个相交的点,然后就是定义一个慢指针,移动这个慢指针和我们之前快慢指针相遇的慢指针,相遇的点就是我们的循环的起点,不要问为什么哈,因为这是经过公式严格证明的!可以看Carl的视频里面有讲解的!
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
/*首先根据快慢指针判断是否有环,无环直接返回null
有环,则根据*/
public class Solution
{
public ListNode detectCycle(ListNode head)
{
if(head==null||head.next==null)
{
return null;
}
ListNode fast=head,slow=head;
while(fast!=null&&fast.next!=null)
{
slow=slow.next;
fast=fast.next.next;
if(fast==slow)//确定有环
{
break;
}
}
if(fast==slow)//
{
ListNode q=head;
while(q!=slow)
{
q=q.next;
slow=slow.next;
}
return q;
}
else
{
return null;
}
}
}
总结:
链表的概念:
- 什么是链表(链表的几种类型单链表,双链表,循环链表,数据结构的时候学过这些,然后它在内存中的存储方式,导致的它的一些特性,增删时间复杂度低,查找时间复杂度高,和数组的对比,数组查找方便,增删麻烦)
- 删除链表中的某一个元素(使用虚拟头节点)
- 构造一个链表(需要考虑条件的判断,数据结构的时候学过)
- 反转链表(双指针方式和递归方式)
- 将链表中的元素两两交换(虚拟头节点)
- 删除倒数第 N 个节点(快慢指针)
- 链表是否相交(类似于环形链表的解法 x + y + z = x + y + z 定义两个双指针来实现)
- 环形链表(什么原理)