一、理论(详见Day3)
- 链表定义和结构:节点(指针域+数据域)通过指针串联在一起的线性结构
- 链表类型:单链表(上图,只能沿一个方向走);双链表(有两个指针域,可以沿两个方向走);循环链表(收尾相连,上图单链表E的指针指向A即可)
- 链表存储方式:与数组连续存储不同,链表的节点在内存中不是连续分布,具体分配机制取决于操作系统的内存管理方法,通过指针链接内存中的各个节点
- 链表操作:删除(Java有内存回收机制,直接一步到位即可,无需回收,C++中需要手动释放要删除的节点);添加(分两步且前后顺序不能调,先F接到D,再C接到F)
二、实战
24两两交换链表中的节点
整体思路:两两交换物理节点。
1.考虑加上一个虚拟节点,这样头结点的处理就和普通节点一样
2.厘清节点之间的交换步骤,顺序不重要,但是这种两两交换牵涉到4个节点,厘清步骤是为了防止遗漏,导致结果错误
package org.example.Node;
public class swapPairs24 {
public ListNode swapPairs(ListNode head) {
//如果只有一个节点或者为空直接输出
if(head==null || head.next==null)return head;
//虚拟节点,将头结点与普通节点一样
ListNode dummy=new ListNode();
dummy.next=head;
//四个节点,cur和temp记录位置,pre和after做交换
ListNode cur=dummy;
ListNode pre;
ListNode after;
ListNode temp;
//防止空指针,确保交换的pre和after不为空
while(cur.next!=null && cur.next.next!=null)
{
pre=cur.next;
after=pre.next;
temp=after.next;
//交换的三个步骤,因为都设置了节点,所以顺序不重要
cur.next=after;
after.next=pre;
pre.next=temp;
//注意这里是pre不是after,因为这时已经完成位置的交换
cur=pre;
}
return dummy.next;
}
public static void main(String[] args) {
//创建链表
ListNode head = new ListNode(7);
head.next = new ListNode(6);
head.next.next = new ListNode(5);
head.next.next.next = new ListNode(4);
//打印原始链表
System.out.println("Original List:");
printList(head);
swapPairs24 solution = new swapPairs24();
head = solution.swapPairs(head);
//打印移除后的链表
System.out.println("After reverse:");
printList(head);
}
//辅助函数:打印链表
private static void printList(ListNode node) {
while (node != null) {
System.out.print(node.val + " ");
node = node.next;
}
System.out.println();
}
}
时间复杂度:O(n) 遍历整个链表一次
空间复杂度:O(1) 只使用了几个额外的指针变量
19删除链表的倒数第N个节点
19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
思路:删除倒数第n个节点,即要找到倒数第n+1个节点进行操作,这样需要两个节点,初始值都在虚拟节点上,一个指针先走n+1距离,固定距离后两个指针一起移动直到先走的指针到达null。主要难点就在于n+1的距离以及整体的过程需要多模拟几遍。

package org.example.Node;
public class removeNthFromEnd19 {
public ListNode removeNthFromEnd(ListNode head, int n) {
//如果只有一个节点或者为空直接输出
if(head==null)return head;
//虚拟节点,将头结点与普通节点一样
ListNode dummy=new ListNode();
dummy.next=head;
//pre要删除的节点前面一个节点,after用于给pre定位
ListNode pre=dummy;
ListNode after=dummy;
//最后after与要删除的节点距离n,所以after与pre距离n+1
//after先走出距离差之后再一起移动
while(n>=0)
{
after=after.next;
n--;
}
//after到null的时候,pre正好到删除的前一个位置
while(after!=null)
{
after=after.next;
pre=pre.next;
}
//删除
pre.next=pre.next.next;
return dummy.next;
}
public static void main(String[] args) {
//创建链表
ListNode head = new ListNode(7);
head.next = new ListNode(6);
head.next.next = new ListNode(5);
head.next.next.next = new ListNode(4);
//打印原始链表
System.out.println("Original List:");
printList(head);
removeNthFromEnd19 solution = new removeNthFromEnd19();
int val=2;
head = solution.removeNthFromEnd(head,val);
//打印移除后的链表
System.out.println("After reverse:");
printList(head);
}
//辅助函数:打印链表
private static void printList(ListNode node) {
while (node != null) {
System.out.print(node.val + " ");
node = node.next;
}
System.out.println();
}
}
160链表相交
面试题 02.07. 链表相交 - 力扣(LeetCode)
思路:求两个链表交点节点的指针。求出两个链表长度后,将两个链表末尾对齐,比较每个节点是否相同,不同则同步下一位,相同则找到交点。
注意:这里的相同是指针相同而非数值相同
package org.example.Node;
public class getIntersectionNode0207 {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode curA=headA;
ListNode curB=headB;
int lenA=0,lenB=0;
while (curA != null) { // 求链表A的长度
lenA++;
curA = curA.next;
}
while (curB != null) { // 求链表B的长度
lenB++;
curB = curB.next;
}
//恢复curA,curB的值
curA=headA;
curB=headB;
//统一为A长,B短
if (lenB > lenA) {
//1. swap (lenA, lenB);
int tmpLen = lenA;
lenA = lenB;
lenB = tmpLen;
//2. swap (curA, curB);
ListNode tmpNode = curA;
curA = curB;
curB = tmpNode;
}
int delta=lenA-lenB;
while(delta>0)
{
curA=curA.next;
delta--;
}
// 遍历curA 和 curB,遇到相同则直接返回
while (curA != null) {
if (curA == curB) {
return curA;
}
curA = curA.next;
curB = curB.next;
}
//没有找到交点
return null;
}
}
- 时间复杂度:O(n + m)
- 空间复杂度:O(1)
142环形链表
1.判断链表是否有环
双指针:快慢指针相遇,快指针每次走两个节点,慢节点一次走一个节点,在环中的时候快指针相对于慢指针的速度是2-1=1,以一次一个节点的速度去靠近慢指针,因此在有环的情况下不会错过
2.环的入口
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。
那么相遇时: slow指针走过的节点数为: x + y
, fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:
(x + y) * 2 = x + y + n (y + z)
因为要找环形的入口x,x = n (y + z) - y,整理公式之后为如下公式:x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
当 n为1的时候,公式就化解为 x = z
,这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
当n大于1的时候,就是fast指针在环形转n圈之后才遇到 slow指针。其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
为什么判断是slow都默认一圈呢,是因为如上图,每一段都是一圈的展开,我们能发现在slow转一圈的时候,快指针无论在前一圈的哪个节点,两倍距离都能追上。
package org.example.Node;
public class detectCycle142 {
public ListNode detectCycle(ListNode head) {
ListNode fast=head;
ListNode slow=head;
//因为快指针一次跳两个,所以要检查两个位置
while(fast!=null && fast.next!=null)
{
fast=fast.next.next;
slow=slow.next;
if(fast==slow)
//快慢指针相遇,说明有环
{
//x==z
ListNode index1=fast;
ListNode index2=head;
while(index2!=index1)
{
index2=index2.next;
index1=index1.next;
}
return index1;
}
}
return null;
}
}