目录
9.扩展:如果原链表结构指明含有链表长度,再次重写其基本的操作
干货分享,感谢您的阅读!祝你逢考必过!
一、背景知识
(一)简单认知
链表(Linked List)是一种常见的数据结构,用于存储一系列元素。它由一组节点(Node)组成,每个节点包含两个部分:数据(Data)和指向下一个节点的指针(Next Pointer)。
链表中的节点可以在内存中不连续地分布,它们之间通过指针连接起来。这样的设计使得链表能够动态地进行插入、删除等操作,而无需像数组一样需要预先分配固定大小的空间。
链表的基本操作包括插入、删除和遍历。插入和删除节点的时间复杂度为O(1),遍历链表的时间复杂度为O(n)。链表还有很多变种,如双向链表、循环链表、跳表等,它们各自适用于不同的场景。
在计算机科学中,链表常被用来实现队列、栈等抽象数据类型,也常被用于算法的实现和优化。
(二)简单操作展示
我们已基本链表操作代码举例简单了解,定义ListNode结构如下
package org.zyf.javabasic.letcode.list.base;
/**
* @author yanfengzhang
* @description 节点结构
* @date 2022/10/17 23:18
*/
public class ListNode {
/**
* 该节点存储的值
*/
public int val;
/**
* 下一个节点的引用
*/
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
1.创建一个链表
/**
* 创建一个链表
*/
public static ListNode createList(int[] nums) {
ListNode head = null;
ListNode tail = null;
for (int num : nums) {
ListNode node = new ListNode(num);
if (head == null) {
head = tail = node;
} else {
tail.next = node;
tail = node;
}
}
return head;
}
2.遍历一个列表
/**
* 遍历一个链表
*/
public static void traverseList(ListNode head) {
while (head != null) {
System.out.print(head.val + "->");
head = head.next;
}
System.out.println("null");
}
3.在链表头部插入一个节点
- 创建一个新节点 node。
- 将 node 的 next 指向当前的头节点 head。
- 将 head 指向 node。
/**
* 在链表头部插入一个节点
*/
public static ListNode insertAtHead(ListNode head, int val) {
ListNode node = new ListNode(val);
node.next = head;
head = node;
return head;
}
4.在链表头部删除一个节点
- 将头节点 head 指向下一个节点 next。
/**
* 在链表头部删除一个节点
*/
public static ListNode deleteAtHead(ListNode head) {
if (head == null) {
return null;
}
ListNode next = head.next;
head.next = null;
head = next;
return head;
}
5.在链表尾部插入一个节点
- 找到链表的最后一个节点 tail。
- 创建一个新节点 node。
- 将 tail 的 next 指向 node。
/**
* 在链表尾部插入一个节点
*/
public static ListNode insertAtTail(ListNode head, int val) {
ListNode node = new ListNode(val);
if (head == null) {
return node;
}
ListNode tail = head;
while (tail.next != null) {
tail = tail.next;
}
tail.next = node;
return head;
}
6.在链表尾部删除一个节点
在链表尾部删除一个节点的过程比较麻烦,需要找到倒数第二个节点 prev,然后将 prev 的 next 指向 null。
/**
* 在链表尾部删除一个节点
*/
public static ListNode deleteAtTail(ListNode head) {
if (head == null || head.next == null) {
return null;
}
ListNode prev = head;
ListNode curr = head.next;
while (curr.next != null) {
prev = curr;
curr = curr.next;
}
prev.next = null;
return head;
}
7.在链表中间插入一个节点
- 找到要插入的位置的前驱节点 prev。
- 创建一个新节点 node。
- 将 node 的 next 指向 prev 的下一个节点 next。
- 将 prev 的 next 指向 node。
/**
* 在链表中间插入一个节点
*/
public static ListNode insertInMiddle(ListNode head, int val, int position) {
ListNode node = new ListNode(val);
if (position == 1) {
node.next = head;
return node;
}
ListNode prev = head;
for (int i = 1; i < position - 1 && prev != null; i++) {
prev = prev.next;
}
if (prev == null) {
return head;
}
ListNode next = prev.next;
prev.next = node;
node.next = next;
return head;
}
8.在链表中间删除一个节点
- 找到要删除的节点 curr 和它的前驱节点 prev。
- 将 prev 的 next 指向 curr 的下一个节点 next。
- 将 curr 的 next 指向 null。
/**
* 在链表中间删除一个节点
*/
public static ListNode deleteInMiddle(ListNode head, int position) {
if (head == null) {
return null;
}
if (position == 1) {
return head.next;
}
ListNode prev = head;
for (int i = 1; i < position - 1 && prev != null; i++) {
prev = prev.next;
}
if (prev == null || prev.next == null) {
return head;
}
ListNode curr = prev.next;
prev.next = curr.next;
curr.next = null;
return head;
}
9.扩展:如果原链表结构指明含有链表长度,再次重写其基本的操作
package org.zyf.javabasic.letcode.list.base;
/**
* @author yanfengzhang
* @description 链表结构
* @date 2022/8/2 23:49
*/
public class ListNodeWithSize {
/**
* 链表长度
*/
int size;
/**
* 链表头节点
*/
ListNode head;
/**
* Initialize your data structure here.
*/
public ListNodeWithSize() {
this.size = 0;
this.head = null;
}
/**
* Get the value of the index-th node in the linked list.
* If the index is invalid, return -1.
*/
public int get(int index) {
/*1.边界条件考虑:插入点非法和本身喂空链表情况*/
if (index < 0 || index >= size || head == null) {
return -1;
}
/*2.遍历处理*/
ListNode temp = this.head;
for (int i = 0; i < index; i++) {
temp = temp.next;
}
return temp.val;
}
/**
* Add a node of value val before the first element of the linked list.
* After the insertion, the new node will be the first node of the linked list.
*/
public void addAtHead(int val) {
/*头部直接插入即可*/
ListNode node = new ListNode(val);
node.next = this.head;
this.head = node;
size++;
}
/**
* Append a node of value val to the last element of the linked list.
*/
public void addAtTail(int val) {
/*如果是空链表则进行初始化head即可*/
if (size == 0) {
this.head = new ListNode(val);
head.next = null;
size++;
return;
}
/*非空链表进行遍历插入*/
ListNode temp = this.head;
while (temp.next != null) {
temp = temp.next;
}
ListNode tail = new ListNode(val);
tail.next = null;
temp.next = tail;
size++;
}
/**
* Add a node of value val before the index-th node in the linked list.
* If index equals to the length of linked list, the node will be appended to the end of linked list.
* If index is greater than the length, the node will not be inserted.
*/
public void addAtIndex(int index, int val) {
/*1.边界条件考虑:大于链表长度的直接返回即可*/
if (index > this.size) {
return;
}
/*2.边界条件考虑:插入点直接小于等于0视为在头部插入*/
if (index <= 0) {
addAtHead(val);
return;
}
/*3.边界条件考虑:插入点大于链表长度的视为在尾部插入*/
if (index == this.size) {
addAtTail(val);
return;
}
/*4.非边界条件:需要遍历插入点后进行插入操作*/
ListNode temp = this.head;
for (int i = 0; i < index - 1; i++) {
temp = temp.next;
}
ListNode insertNode = new ListNode(val);
insertNode.next = temp.next;
temp.next = insertNode;
size++;
}
/**
* Delete the index-th node in the linked list, if the index is valid.
*/
public void deleteAtIndex(int index) {
/*1.边界条件考虑:删除点不合法直接返回即可*/
if (index < 0 || index >= this.size) {
return;
}
/*2.删除点为0,则需要进行分析链表情况*/
if (index == 0) {
/*2.1 链表只有一个节点时,直接置空链表*/
if (size == 1) {
this.head = null;
size--;
return;
}
/*2.2 链表多节点时,相当于删除头节点*/
ListNode temp = this.head.next;
this.head = temp;
size--;
return;
}
/*3.遍历删除指定位置节点*/
ListNode temp = this.head;
for (int i = 0; i < index - 1; i++) {
temp = temp.next;
}
ListNode deleteNode = temp.next;
temp.next = deleteNode.next;
size--;
}
}
二、链表的应用
(一)Spring中或其他其他框架中的链表应用
- Spring AOP中的链表应用:Spring中的AOP机制中,切面(Aspect)和切点(Pointcut)之间的关系可以使用链表来实现。AOP中的切点可以看作是一个链表,每个节点表示一个方法,链表中的每个节点都保存有下一个节点的引用,从而实现了一种递归调用的结构。
- LRU Cache实现:LRU(Least Recently Used)算法可以使用链表来实现。在Java中,可以使用LinkedHashMap来实现LRU Cache,LinkedHashMap内部使用双向链表实现。
- Java中LinkedList的应用: Java中的LinkedList实现了List和Deque接口,可以用作队列或栈。它还提供了一些方便的操作,如addFirst()、addLast()、removeFirst()、removeLast()等。
- Redis中的链表:Redis中的列表(List)可以通过链表来实现,每个节点都保存有前驱和后继节点的指针,从而实现了高效的插入、删除等操作。
- Nginx中的链表: Nginx中的事件模块和HTTP模块中,都使用了链表数据结构来存储事件和请求。Nginx中的链表是双向链表,每个节点都保存有前驱和后继节点的指针。
- Linux内核中的链表: Linux内核中的链表(list)实现了双向循环链表,被广泛用于进程管理、文件系统等模块中。Linux内核中的链表提供了丰富的操作函数,如list_add()、list_del()等。
- TensorFlow中的链表: TensorFlow中的图模型中,节点之间的依赖关系可以使用链表来实现。TensorFlow中的链表使用了多种不同的实现方式,如单链表、双向链表等。
- Java中HashMap的实现: Java中的HashMap实现了Map接口,可以用来存储键值对。HashMap内部使用了数组和链表的结合来实现,数组用于存储元素,链表用于解决哈希冲突。每个数组元素是一个链表的头节点,该节点保存了链表的第一个节点的引用,从而实现了高效的查找、插入、删除等操作。
(二)业务开发中的应用
- 链表实现LRU缓存: 在我们平时的Web开发中,缓存是非常常见的需求。而LRU缓存策略可以使用链表来实现,每次访问一个元素时,将其从链表中删除并插入到链表头部。当链表满了时,删除链表尾部的元素即可。
- 链表实现分页查询: 在数据库分页查询中,我们通常需要将查询结果分页返回给用户。这时可以使用链表来实现分页查询,每次查询时,将查询结果存储在链表中,并返回链表的一页数据给用户。
- 链表实现消息队列: 在消息队列中,我们通常需要将消息按照一定的顺序存储起来,然后逐个取出并处理。这时可以使用链表来实现消息队列,每个消息可以看作是一个节点,节点之间通过指针连接,从而实现了高效的插入、删除、查找等操作。
- 链表实现多级菜单: 在Web应用中,多级菜单是常见的需求。这时可以使用链表来实现多级菜单,每个菜单项可以看作是一个节点,节点之间通过指针连接,从而实现了高效的插入、删除、查找等操作。
三、相关编程练习
说明:如果有写错的,请留言指正,感谢阅读者!
1、反转链表(Reverse Linked List)
题目描述:反转一个单链表。
示例:输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
解题思路
对于反转链表这道题目,其高效最优解法是使用迭代实现。具体思路如下:
- 初始化三个指针,分别为 prev、curr 和 next。初始化时,prev 和 curr 指向 NULL,next 指向链表头节点 head。
- 循环遍历链表,直到遍历完整个链表。循环中进行如下操作:
- 将 curr 的 next 指针指向 prev,实现 curr 的指针反转。
- 将 prev 和 curr 往后移动一位,即 prev 指向 curr,curr 指向 next。
- 将 next 指向 curr 的下一个节点,即 next 指向 curr->next。
- 当遍历完整个链表后,prev 就指向了原链表的最后一个节点,也就是反转后的链表头节点。因此,我们返回 prev 即可。
该算法的时间复杂度为 O(n),其中 n 是链表的长度,空间复杂度为 O(1)。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 反转一个单链表。
* 示例:输入: 1->2->3->4->5->NULL
* 输出: 5->4->3->2->1->NULL
* 进阶:你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
* @date 2023/4/5 23:07
*/
public class ReverseList {
/**
* 对于反转链表这道题目,其高效最优解法是使用迭代实现。具体思路如下:
* 1 初始化三个指针,分别为 prev、curr 和 next。初始化时,prev 和 curr 指向 NULL,next 指向链表头节点 head。
* 2 循环遍历链表,直到遍历完整个链表。循环中进行如下操作:
* 将 curr 的 next 指针指向 prev,实现 curr 的指针反转。
* 将 prev 和 curr 往后移动一位,即 prev 指向 curr,curr 指向 next。
* 将 next 指向 curr 的下一个节点,即 next 指向 curr->next。
* 3 当遍历完整个链表后,prev 就指向了原链表的最后一个节点,也就是反转后的链表头节点。因此,我们返回 prev 即可。
*/
public ListNode reverseList(ListNode head) {
/*初始化前驱节点为NULL*/
ListNode prev = null;
/*初始化当前节点为NULL*/
ListNode curr = null;
/*初始化后驱节点为头节点*/
ListNode next = head;
/*遍历列表*/
while (next != null) {
/*当前节点为后继节点*/
curr = next;
/*后继节点后移*/
next = next.next;
/*反转当前节点的指针*/
curr.next = prev;
/*前驱节点后移*/
prev = curr;
}
/*返回反转后的节点*/
return prev;
}
public static void main(String[] args) {
/*创建一个链表:1->2->3->4->5->NULL*/
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(5);
head.next.next.next.next.next = null;
/*反转链表*/
ListNode reversed = new ReverseList().reverseList(head);
/*输出反转后的链表:5->4->3->2->1->NULL*/
while (reversed != null) {
System.out.print(reversed.val + "->");
reversed = reversed.next;
}
System.out.println("NULL");
}
}
2、链表中环的检测
题目描述:给定一个链表,判断链表中是否有环。
进阶:你能否不使用额外空间解决此题?
解题思路
判断链表中是否有环的高效最优解法是使用快慢指针,也称为龟兔赛跑算法。具体步骤如下:
- 定义两个指针 slow 和 fast,初始值都指向链表头节点。
- 每次将 slow 指针向后移动一步,将 fast 指针向后移动两步。
- 如果链表中有环,则快指针一定会追上慢指针,此时可以返回 true。
- 如果链表中没有环,则快指针会先到达链表末尾,此时可以返回 false。
在这个算法中,时间复杂度是 O(n),空间复杂度是 O(1),因为只使用了两个指针来遍历链表,没有使用额外的数据结构来存储中间结果。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给定一个链表,判断链表中是否有环。
* 进阶:你能否不使用额外空间解决此题?
* @date 2023/4/5 23:27
*/
public class CycleList {
/**
* 判断链表中是否有环的高效最优解法是使用快慢指针,也称为龟兔赛跑算法。具体步骤如下:
* 定义两个指针 slow 和 fast,初始值都指向链表头节点。
* 每次将 slow 指针向后移动一步,将 fast 指针向后移动两步。
* 如果链表中有环,则快指针一定会追上慢指针,此时可以返回 true。
* 如果链表中没有环,则快指针会先到达链表末尾,此时可以返回 false。
* <p>
* 在这个算法中,时间复杂度是 O(n),空间复杂度是 O(1),
* 因为只使用了两个指针来遍历链表,没有使用额外的数据结构来存储中间结果。
*/
public boolean hasCycle(ListNode head) {
/*判断链表为空或链表中只有一个节点的情况*/
if (head == null || head.next == null) {
return false;
}
/*定义快慢指针,初始值都指向链表头节点*/
ListNode slow = head;
ListNode fast = head.next;
/*当快指针没有追上慢指针且链表中还有节点时,继续循环*/
while (slow != fast) {
/*判断快指针是否已经到达链表末尾,如果是则说明链表中没有环*/
if (fast == null || fast.next == null) {
return false;
}
/*移动快慢指针*/
slow = slow.next;
fast = fast.next.next;
}
/*如果快指针追上了慢指针,则说明链表中有环*/
return true;
}
public static void main(String[] args) {
/*构建一个有环链表*/
ListNode head = new ListNode(1);
ListNode node1 = new ListNode(2);
ListNode node2 = new ListNode(3);
ListNode node3 = new ListNode(4);
ListNode node4 = new ListNode(5);
head.next = node1;
node1.next = node2;
node2.next = node3;
node3.next = node4;
/*这里将链表尾部指向 node1,形成一个环*/
node4.next = node1;
/*检测链表是否有环*/
boolean hasCycle = new CycleList().hasCycle(head);
if (hasCycle) {
System.out.println("该链表有环");
} else {
System.out.println("该链表无环");
}
}
}
3、链表中环的入口点
题目描述:链表中环的入口节点是指一个有环链表中,环的入口节点。给定一个链表,若其中包含环,则输出环的入口节点;否则输出null。
例如,在如下图所示的链表中,环的入口节点是3(注意,这里环的入口节点不是算链表的第3个节点)。
说明:
不允许修改给定的链表。
要求空间复杂度为O(1)。
解题思路
链表中环的入口节点可以通过 Floyd 算法来解决,具体步骤如下:
- 使用快慢指针法判断链表是否有环,如果有,快指针回到链表头部,然后快慢指针以相同的速度向前移动,当两个指针再次相遇时,该节点就是环的入口节点。
- 如何证明这个算法是正确的?假设链表的头部到环的入口节点的距离为a,环的入口节点到两个指针相遇点的距离为b,相遇点到环的入口节点的距离为c。同时,假设快指针速度为慢指针的两倍,即v{fast}=2v{slow},慢指针走过的距离为x。
当快指针第一次到达相遇点时,快指针走过的距离为 a + nb + x,慢指针走过的距离为 a + x,由于快指针走过的距离是慢指针的两倍,因此有2(a+x)=a+nb+x,化简得到 a=(n-1)b+c。
当快指针回到链表头部时,慢指针距离环的入口节点的距离为a,因此快慢指针分别从链表头部和相遇点开始移动,相遇的节点即为环的入口节点。
因此,这种方法可以在O(1)的空间复杂度下找到链表中环的入口节点。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description
* 链表中环的入口节点是指一个有环链表中,环的入口节点。给定一个链表,若其中包含环,则输出环的入口节点;否则输出null。
* 说明:
* 不允许修改给定的链表。
* 要求空间复杂度为O(1)。
* @date 2023/4/5 23:45
*/
public class EnterNodeInCycleList {
/**
* 使用快慢指针法判断链表是否有环,
* 如果有,快指针回到链表头部,然后快慢指针以相同的速度向前移动,
* 当两个指针再次相遇时,该节点就是环的入口节点。
* <p>
* 该算法的时间复杂度为O(n),空间复杂度为O(1)。
*/
public ListNode entryNodeOfLoop(ListNode head) {
ListNode fast = head;
ListNode slow = head;
/*1. 判断是否有环*/
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
break;
}
}
if (fast == null || fast.next == null) {
/*说明没有环*/
return null;
}
/*2. 有环,找入口节点*/
fast = head;
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
public static void main(String[] args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(5);
head.next.next.next.next.next = new ListNode(6);
head.next.next.next.next.next.next = head.next.next;
ListNode entryNode = new EnterNodeInCycleList().entryNodeOfLoop(head);
System.out.println(entryNode.val);
}
}
4、删除链表中倒数第K个节点
题目描述:给你一个链表,删除链表中倒数第 n 个节点,并且返回链表的头结点。
示例:输入:head = [1,2,3,4,5], n = 2. 输出:[1,2,3,5]
提示:
- 链表中节点的数目为 sz
- 1 <= sz <= 30
- 0 <= Node.val <= 100
- 1 <= n <= s
解题思路
最优的解法是使用快慢指针,快指针先走n步,然后快慢指针一起走,当快指针到达链表末尾时,慢指针就是要删除的节点的前一个节点。接着,通过改变指针的指向,将慢指针指向的节点删除即可。
具体步骤如下:
- 定义两个指针:快指针和慢指针,初始都指向头节点。
- 快指针先走n步,如果此时快指针已经到达了链表末尾,说明要删除的是头节点,直接返回head->next即可。
- 否则,让快慢指针一起走,直到快指针到达链表末尾。
- 此时,慢指针指向的节点就是要删除的节点的前一个节点。
- 改变慢指针的next指向即可删除节点。
需要注意的是,如果要删除的是头节点,需要特殊处理。
时间复杂度为O(n),空间复杂度为O(1)。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给你一个链表,删除链表中倒数第 n 个节点,并且返回链表的头结点。
* 示例:输入:head = [1,2,3,4,5], n = 2. 输出:[1,2,3,5]
* 提示:
* 链表中节点的数目为 sz
* 1 <= sz <= 30
* 0 <= Node.val <= 100
* 1 <= n <= s
* @date 2023/4/5 23:56
*/
public class RemoveKthFromEnd {
/**
* 最优的解法是使用快慢指针,快指针先走n步,然后快慢指针一起走,当快指针到达链表末尾时,
* 慢指针就是要删除的节点的前一个节点。接着,通过改变指针的指向,将慢指针指向的节点删除即可。
* <p>
* 具体步骤如下:
* 定义两个指针:快指针和慢指针,初始都指向头节点。
* 快指针先走n步,如果此时快指针已经到达了链表末尾,说明要删除的是头节点,直接返回head->next即可。
* 否则,让快慢指针一起走,直到快指针到达链表末尾。
* 此时,慢指针指向的节点就是要删除的节点的前一个节点。
* 改变慢指针的next指向即可删除节点。
* 需要注意的是,如果要删除的是头节点,需要特殊处理。
* <p>
* 时间复杂度为O(n),空间复杂度为O(1)。
*/
public ListNode removeNthFromEnd(ListNode head, int n) {
if (n <= 0) {
throw new IllegalArgumentException("n must be positive integer");
}
/*创建 dummy 节点,用于处理删除头节点的情况*/
ListNode dummy = new ListNode(0);
dummy.next = head;
/*创建 slow 和 fast 指针,初始指向 dummy 节点*/
ListNode slow = dummy;
ListNode fast = dummy;
/*fast 指针先向前移动 n 步*/
for (int i = 0; i < n; i++) {
/*如果 fast 已经移动到了链表末尾,但还未移动 n 步,说明 k 的值超出了链表的长度,抛出异常*/
if (fast.next == null) {
throw new IllegalArgumentException("n must be less than or equal to the length of the list");
}
fast = fast.next;
}
/*同时移动 slow 和 fast 指针,直到 fast 指向链表末尾*/
while (fast.next != null) {
slow = slow.next;
fast = fast.next;
}
/*删除 slow 指针指向的节点*/
slow.next = slow.next.next;
/*返回头节点*/
return dummy.next;
}
public static void main(String[] args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(5);
int n = 2;
System.out.println("原链表:" + head.toString());
ListNode newHead = new RemoveKthFromEnd().removeNthFromEnd(head, n);
System.out.println("删除倒数第" + n + "个节点后的链表:" + newHead.toString());
}
}
5、两个链表的第一个公共节点
题目描述:给定两个单链表,判断两个链表是否相交。若相交,返回相交的起始节点。若不相交,返回 null。可以假定整个链表结构中没有循环。
注意,函数返回结果后,链表必须保持其原始结构。
提示:
- 如果两个链表相交,它们的最后一个节点一定是共同的。
- 由于单链表的节点只有一个 next 指针,所以每次只能遍历一个节点。
- 本题与 160 题相同:力扣
解题思路
链表相交问题可以采用双指针法来解决,具体步骤如下:
- 首先分别遍历两个链表,得到它们的长度,以及它们的尾节点;
- 如果两个链表的尾节点不同,说明它们不相交,直接返回null;
- 然后再分别从两个链表的头节点开始,让长链表的指针先走 abs(len1-len2) 步,这样两个链表的指针就在同一起跑线上了;
- 接下来,同时遍历两个链表,比较它们每个节点是否相同,直到找到相交节点,或者到达链表的尾部。
时间复杂度为 O(m+n),其中m和n分别为两个链表的长度。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给定两个单链表,判断两个链表是否相交。
* 若相交,返回相交的起始节点。若不相交,返回 null。可以假定整个链表结构中没有循环。
* 注意,函数返回结果后,链表必须保持其原始结构。
* 提示:
* 如果两个链表相交,它们的最后一个节点一定是共同的。
* 由于单链表的节点只有一个 next 指针,所以每次只能遍历一个节点。
* @date 2023/4/5 23:06
*/
public class FirstCommonNode {
/**
* 求两个单链表的交点===链表相交问题可以采用双指针法来解决,具体步骤如下:
* 首先分别遍历两个链表,得到它们的长度,以及它们的尾节点;
* 如果两个链表的尾节点不同,说明它们不相交,直接返回null;
* 然后再分别从两个链表的头节点开始,让长链表的指针先走 abs(len1-len2) 步,这样两个链表的指针就在同一起跑线上了;
* 接下来,同时遍历两个链表,比较它们每个节点是否相同,直到找到相交节点,或者到达链表的尾部。
* 时间复杂度为 O(m+n),其中m和n分别为两个链表的长度。
*
* @param headA 单链表A的头节点
* @param headB 单链表B的头节点
* @return 两个单链表的交点,若不存在交点则返回 null
*/
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int lenA = getLength(headA);
int lenB = getLength(headB);
/*将较长的链表头节点指针移动到与另一个链表相同位置*/
if (lenA > lenB) {
headA = moveHead(headA, lenA - lenB);
} else {
headB = moveHead(headB, lenB - lenA);
}
/*同时遍历两个链表,找到第一个相同的节点*/
while (headA != null && headB != null) {
if (headA == headB) {
return headA;
}
headA = headA.next;
headB = headB.next;
}
/*两个链表没有相交的节点*/
return null;
}
/**
* 计算链表的长度
*/
private int getLength(ListNode head) {
int len = 0;
while (head != null) {
len++;
head = head.next;
}
return len;
}
/**
* 将链表头节点指针移动 n 步
*/
private ListNode moveHead(ListNode head, int n) {
while (n > 0) {
head = head.next;
n--;
}
return head;
}
public static void main(String[] args) {
/*创建两个链表:1->2->3->4->5 和 6->7->4->5*/
ListNode commonNode = new ListNode(4);
commonNode.next = new ListNode(5);
ListNode headA = new ListNode(1);
headA.next = new ListNode(2);
headA.next.next = new ListNode(3);
headA.next.next.next = commonNode;
ListNode headB = new ListNode(6);
headB.next = new ListNode(7);
headB.next.next = commonNode;
/*找到两个链表的第一个公共节点*/
ListNode intersectionNode = new FirstCommonNode().getIntersectionNode(headA, headB);
/*输出第一个公共节点的值*/
if (intersectionNode != null) {
/*4*/
System.out.println(intersectionNode.val);
} else {
System.out.println("null");
}
}
}
6、链表的中间节点
题目描述:给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
例如,给定链表 1->2->3->4->5->NULL,返回结点 3;给定链表 1->2->3->4->5->6->NULL,返回结点 4。
提示:给定链表的结点数介于 1 和 100 之间。
原题目链接:力扣
解题思路
最优的解法是快慢指针法。
使用两个指针,一个快指针和一个慢指针,快指针每次移动两个节点,慢指针每次移动一个节点,当快指针到达链表末尾时,慢指针就指向链表的中间节点。
这个算法的时间复杂度为O(n),其中n为链表的长度,空间复杂度为O(1),只需要两个指针的空间。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
* 例如,给定链表 1->2->3->4->5->NULL,返回结点 3;给定链表 1->2->3->4->5->6->NULL,返回结点 4。
* <p>
* 提示:给定链表的结点数介于 1 和 100 之间。
* @date 2023/4/5 23:14
*/
public class MiddleNode {
/**
* 最优的解法是快慢指针法。
* <p>
* 使用两个指针,一个快指针和一个慢指针,
* 快指针每次移动两个节点,慢指针每次移动一个节点,
* 当快指针到达链表末尾时,慢指针就指向链表的中间节点。
* <p>
* 这个算法的时间复杂度为O(n),其中n为链表的长度,空间复杂度为O(1),只需要两个指针的空间。
*/
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
/*快指针每次走两步,
慢指针每次走一步,当快指针走到链表末尾时,慢指针刚好在中间节点。*/
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
public static void main(String[] args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(5);
ListNode middle = new MiddleNode().middleNode(head);
System.out.println(middle.val);
}
}
7、合并两个有序链表
题目描述:合并两个有序链表,返回一个新的链表,新链表是这两个链表中的所有节点按照从小到大的顺序排列而成。
示例:输入:1->2->4, 1->3->4. 输出:1->1->2->3->4->4
解题思路
首先,我们定义一个新的链表作为合并后的链表,然后设置两个指针分别指向两个原始链表的头节点。我们每次比较两个指针指向节点的值,将较小的节点加入新的链表中,并将指针后移一位。重复这个过程,直到某一个指针为空,此时我们将另一个链表剩下的部分全部加入新链表中即可。
这种解法的时间复杂度为O(min(m,n)),空间复杂度为O(1),是最优解法之一。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 合并两个有序链表,返回一个新的链表,新链表是这两个链表中的所有节点按照从小到大的顺序排列而成。
* 示例:输入:1->2->4, 1->3->4. 输出:1->1->2->3->4->4
* @date 2023/4/5 23:21
*/
public class MergeTwoLists {
/**
* 首先,我们定义一个新的链表作为合并后的链表,然后设置两个指针分别指向两个原始链表的头节点。
* 我们每次比较两个指针指向节点的值,将较小的节点加入新的链表中,并将指针后移一位。
* 重复这个过程,直到某一个指针为空,此时我们将另一个链表剩下的部分全部加入新链表中即可。
* <p>
* 这种解法的时间复杂度为O(min(m,n)),空间复杂度为O(1),是最优解法之一。
*/
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
/*定义一个哨兵节点,简化操作*/
ListNode dummy = new ListNode(-1);
/*定义一个指针,指向哨兵节点*/
ListNode cur = dummy;
/*循环比较两个链表中的节点,直到其中一个链表为空*/
while (l1 != null && l2 != null) {
/*如果l1的节点值小于等于l2的节点值,将l1的节点接入新链表中*/
if (l1.val <= l2.val) {
cur.next = l1;
l1 = l1.next;
}
/*否则将l2的节点接入新链表中*/
else {
cur.next = l2;
l2 = l2.next;
}
/*指针向后移动*/
cur = cur.next;
}
/*如果l1链表还有剩余节点,则将其接入新链表中*/
if (l1 != null) {
cur.next = l1;
}
/*如果l2链表还有剩余节点,则将其接入新链表中*/
if (l2 != null) {
cur.next = l2;
}
/*返回哨兵节点的下一个节点,即为新链表的头节点*/
return dummy.next;
}
public static void main(String[] args) {
ListNode l1 = new ListNode(1);
l1.next = new ListNode(2);
l1.next.next = new ListNode(4);
ListNode l2 = new ListNode(1);
l2.next = new ListNode(3);
l2.next.next = new ListNode(4);
ListNode mergedList = new MergeTwoLists().mergeTwoLists(l1, l2);
while (mergedList != null) {
System.out.print(mergedList.val + " ");
mergedList = mergedList.next;
}
}
}
8、删除链表中的重复元素I
题目描述:给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 1:输入: 1->1->2. 输出: 1->2
示例 2:输入: 1->1->2->3->3. 输出: 1->2->3
来源:力扣
解题思路
最优的解法是使用双指针,遍历整个链表,如果发现有相邻节点的值相同,就将其中一个节点删除。
具体步骤如下:
- 初始化一个指针cur指向链表的头节点。
- 遍历链表,如果当前节点和下一个节点的值相同,就将当前节点删除,否则继续遍历。
- 如果删除了当前节点,需要将指针指向下一个节点。
需要注意的是,因为头节点可能会被删除,因此可以添加一个哨兵节点作为头节点,这样就不用单独考虑头节点的情况。
时间复杂度为O(n),空间复杂度为O(1)。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
* 示例 1:输入: 1->1->2. 输出: 1->2
* 示例 2:输入: 1->1->2->3->3. 输出: 1->2->3
* @date 2023/4/5 23:14
*/
public class DeleteDuplicates {
/**
* 题目描述:删除链表中的重复元素
* 思路:使用双指针,判断当前节点的值是否和下一个节点的值相同,
* 若相同则将当前节点指向下一个节点的下一个节点,直到当前节点和下一个节点的值不同
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*
* @param head 链表头结点
* @return 删除重复元素后的链表头结点
*/
public ListNode deleteDuplicates(ListNode head) {
/*判断链表是否为空或只有一个节点*/
if (head == null || head.next == null) {
return head;
}
/*设置哑节点dummy,指向头结点head*/
ListNode dummy = new ListNode(-1);
dummy.next = head;
/*定义双指针,cur指向当前节点,遍历整个链表*/
ListNode cur = head;
while (cur != null) {
/*若当前节点和下一个节点的值相同,将当前节点指向下一个节点的下一个节点,直到当前节点和下一个节点的值不同*/
while (cur.next != null && cur.val == cur.next.val) {
cur.next = cur.next.next;
}
cur = cur.next;
}
/*返回哑节点dummy的下一个节点,即删除重复元素后的链表头结点*/
return dummy.next;
}
public static void main(String[] args) {
ListNode head = new ListNode(1);
ListNode node1 = new ListNode(1);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(3);
ListNode node4 = new ListNode(3);
head.next = node1;
node1.next = node2;
node2.next = node3;
node3.next = node4;
ListNode newHead = new DeleteDuplicates().deleteDuplicates(head);
while (newHead != null) {
System.out.print(newHead.val + "->");
newHead = newHead.next;
}
}
}
9、删除链表中的重复元素II
题目描述:给定一个排序链表(默认正整数),删除所有含有重复数字的节点,只保留原始链表中没有重复出现的数字。
示例 1:输入:1->1->2->2->3->4->4 输出:3
示例 2:输入: 1->1->2->3->3. 输出: 2
示例 3:输入:1->2 ->2 ->2 ->5 ->2 ->3 ->3 ->9,输出1 ->5 ->2 ->9
注意
- 不考虑删除之后在连续重复的元素,如2 ->3 ->3 ->2,处理后为2 ->2
- 空间复杂度O(1)
写一个函数实现该功能
来源:力扣
解题思路
最优解法是使用迭代的方法,在一次遍历中删除连续重复的元素,同时保持空间复杂度为 O(1)。
这可以通过双指针来实现。一个指针用于表示当前已处理的部分,另一个指针用于遍历链表。具体步骤如下:
- 创建一个虚拟头节点 `dummy`,将其 `next` 指向链表的头节点 `head`,这样可以避免处理头节点时的特殊情况。
- 定义两个指针 `prev` 和 `curr`,初始时都指向虚拟头节点 `dummy`。
- 使用 `curr` 指针进行遍历,如果当前节点与下一个节点的值相同,则进入循环,继续移动 `curr` 指针,直到找到一个不同值的节点或链表结束。
- 检查 `curr` 与 `prev` 之间是否有重复元素。如果没有,将 `prev` 指针后移一位;如果有重复元素,将 `prev` 的 `next` 指针直接连接到 `curr` 的下一个节点,跳过中间的重复部分。
- 继续移动 `curr` 指针到下一个节点,并重复步骤 3 和 4,直到遍历完整个链表。
- 返回虚拟头节点的 `next`,即处理后的链表。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给定一个排序链表(默认正整数),删除所有含有重复数字的节点,只保留原始链表中没有重复出现的数字。
* 示例 1:输入:1->1->2->2->3->4->4 输出:3
* 示例 2:输入: 1->1->2->3->3.输出: 2
* 示例 3:输入:1->2 ->2 ->2 ->5 ->2 ->3 ->3 ->9,输出1 ->5 ->2 ->9
* * 注意
* * 1.不考虑删除之后在连续重复的元素,如2 ->3 ->3 ->2,处理后为2 ->2
* * 2.空间复杂度O(1)
* * 写一个函数实现该功能
* @date 2023/7/28 23:53
*/
public class DeleteDuplicatesII {
/**
* 最优解法是使用迭代的方法,在一次遍历中删除连续重复的元素,同时保持空间复杂度为 O(1)。
* 这可以通过双指针来实现。一个指针用于表示当前已处理的部分,另一个指针用于遍历链表。具体步骤如下:
* 1. 创建一个虚拟头节点 `dummy`,将其 `next` 指向链表的头节点 `head`,这样可以避免处理头节点时的特殊情况。
* 2. 定义两个指针 `prev` 和 `curr`,初始时都指向虚拟头节点 `dummy`。
* 3. 使用 `curr` 指针进行遍历,如果当前节点与下一个节点的值相同,则进入循环,继续移动 `curr` 指针,直到找到一个不同值的节点或链表结束。
* 4. 检查 `curr` 与 `prev` 之间是否有重复元素。如果没有,将 `prev` 指针后移一位;如果有重复元素,将 `prev` 的 `next` 指针直接连接到 `curr` 的下一个节点,跳过中间的重复部分。
* 5. 继续移动 `curr` 指针到下一个节点,并重复步骤 3 和 4,直到遍历完整个链表。
* 6. 返回虚拟头节点的 `next`,即处理后的链表。
*/
public ListNode deleteDuplicates(ListNode head) {
// 创建虚拟头节点,简化删除操作
ListNode dummy = new ListNode(0);
dummy.next = head;
// prev指向不重复部分的尾节点
ListNode prev = dummy;
// curr用于遍历整个链表
ListNode curr = head;
while (curr != null) {
boolean isDuplicate = false;
// 查找连续重复的元素
while (curr.next != null && curr.val == curr.next.val) {
curr = curr.next;
isDuplicate = true;
}
// 如果有重复元素,跳过中间部分,连接prev和curr之间
if (isDuplicate) {
prev.next = curr.next;
} else {
// 如果没有重复元素,更新prev指向
prev = prev.next;
}
// 移动curr指针到下一个节点
curr = curr.next;
}
// 返回处理后的链表
return dummy.next;
}
public static void main(String[] args) {
ListNode head = new ListNode(1);
ListNode node1 = new ListNode(1);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(3);
ListNode node4 = new ListNode(3);
ListNode node5 = new ListNode(4);
head.next = node1;
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
ListNode newHead = new DeleteDuplicatesII().deleteDuplicates(head);
newHead.traverseList(newHead);
// 创建链表:1 -> 2 -> 2 -> 2 -> 5 -> 2 -> 3 -> 3 -> 9
ListNode head2 = new ListNode(1);
head2.next = new ListNode(2);
head2.next.next = new ListNode(2);
head2.next.next.next=new ListNode(2);
head2.next.next.next.next=new ListNode(5);
head2.next.next.next.next.next=new ListNode(2);
head2.next.next.next.next.next.next=new ListNode(3);
head2.next.next.next.next.next.next.next=new ListNode(3);
head2.next.next.next.next.next.next.next.next=new ListNode(9);
ListNode newHead2 = new DeleteDuplicatesII().deleteDuplicates(head2);
newHead2.traverseList(newHead2);
// 创建链表:2 -> 2 -> 3
ListNode head3 = new ListNode(2);
head3.next = new ListNode(2);
head3.next.next = new ListNode(3);
ListNode newHead3 = new DeleteDuplicatesII().deleteDuplicates(head3);
newHead3.traverseList(newHead3);
// 创建链表:1 -> 2 -> 2
ListNode head1 = new ListNode(1);
head1.next = new ListNode(2);
head1.next.next = new ListNode(2);
// ... 继续添加节点 ...
ListNode result1 = new DeleteDuplicatesII().deleteDuplicates(head1);
// 输出链表:1 -> null
result1.traverseList(result1);
}
}
10、排序链表
题目描述:给你链表的头节点 head ,请将其按 升序 排列并返回 排序后的链表 。
进阶:你可以在 O(nlogn) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?
示例 1:输入:head = [4,2,1,3]. 输出:[1,2,3,4]
示例 2:输入:head = [-1,5,3,4,0]. 输出:[-1,0,3,4,5]
示例 3:输入:head = []. 输出:[]
提示:链表中节点的数目在范围 [0, 5 * 104] 内
-105 <= Node.val <= 105
来源:力扣
解题思路
最优解法是使用归并排序(Merge Sort),将链表分成两个子链表进行排序,然后合并两个已排序的子链表。时间复杂度为 O(nlogn),空间复杂度为 O(1)。
具体步骤如下:
- 找到链表的中间节点,可以使用快慢指针来实现;
- 将链表从中间节点断开成两个子链表;
- 递归地对左右两个子链表进行排序;
- 合并两个已排序的子链表,得到完整的排序后的链表。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给你链表的头节点 head ,请将其按 升序 排列并返回 排序后的链表 。
* 进阶:你可以在 O(nlogn) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?
* 示例 1:输入:head = [4,2,1,3]. 输出:[1,2,3,4]
* 示例 2:输入:head = [-1,5,3,4,0]. 输出:[-1,0,3,4,5]
* 示例 3:输入:head = []. 输出:[]
* <p>
* 提示:链表中节点的数目在范围 [0, 5 * 104] 内
* -105 <= Node.val <= 105
* @date 2023/4/5 23:25
*/
public class SortList {
/**
* 对链表进行归并排序
*
* @param head 链表头节点
* @return 排序后的链表头节点
*/
public ListNode sortList(ListNode head) {
/*特判:当链表为空或者只有一个节点时,无需排序,直接返回*/
if (head == null || head.next == null) {
return head;
}
/*获取链表中间节点*/
ListNode mid = getMiddle(head);
/*对链表左半部分和右半部分分别进行归并排序*/
ListNode left = sortList(head);
ListNode right = sortList(mid);
/*合并左半部分和右半部分*/
return merge(left, right);
}
/**
* 获取链表中间节点
*
* @param head 链表头节点
* @return 链表中间节点
*/
private ListNode getMiddle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
/*快慢指针寻找中间节点,快指针一次移动两个节点,慢指针一次移动一个节点*/
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
/*将链表分为两部分,并将前一部分的尾节点的next置为null*/
ListNode mid = slow.next;
slow.next = null;
return mid;
}
/**
* 合并两个有序链表
*
* @param l1 第一个有序链表头节点
* @param l2 第二个有序链表头节点
* @return 合并后的有序链表头节点
*/
private ListNode merge(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
/*将l1和l2的节点逐个比较,将较小的节点接到cur的后面*/
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
/*将剩余节点连接到cur的后面*/
if (l1 != null) {
cur.next = l1;
}
if (l2 != null) {
cur.next = l2;
}
return dummy.next;
}
public static void main(String[] args) {
ListNode head = new ListNode(4);
head.next = new ListNode(2);
head.next.next = new ListNode(1);
head.next.next.next = new ListNode(3);
ListNode sorted = new SortList().sortList(head);
while (sorted != null) {
System.out.print(sorted.val + " ");
sorted = sorted.next;
}
}
}
10、K 个一组翻转链表
题目描述:给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例:输入:head = [1,2,3,4,5], k = 2. 输出:[2,1,4,3,5]
输入:head = [1,2,3,4,5], k = 3 输出:[3,2,1,4,5]
提示:
- 链表中节点的数量在范围 sz 内,1 <= sz <= 5000
- 0 <= Node.val <= 1000
- 1 <= k <= sz
解题思路
K 个一组翻转链表问题可以使用递归或迭代的方法来实现。其中,迭代方法比递归方法更优,因为递归方法在处理大规模数据时容易出现栈溢出的问题。
迭代方法的基本思路是,每 k 个节点为一组进行翻转,如果剩余节点不足 k 个,则保持原有顺序不变。具体实现时,需要使用 3 个指针:pre、end 和 next,其中 pre 表示待翻转区域的前驱节点,end 表示待翻转区域的后继节点,next 则是遍历链表的指针。
具体步骤如下:
- 初始化指针 pre 为 null,指针 end 和指针 next 均指向头节点 head。
- 对于每个 k 个节点为一组的区间,执行以下操作:
- 从当前位置开始,将指针 next 向后移动 k 个节点,如果剩余节点不足 k 个,则不进行翻转。
- 使用 end 指针记录当前待翻转区域的后继节点。
- 使用 next 指针记录当前待翻转区域的下一个节点。
- 将当前待翻转区域翻转,并返回翻转后的头节点。具体操作可参考翻转链表。
- 如果 pre 为 null,说明当前翻转区域为链表的第一组,需要更新链表头节点为翻转后的头节点,否则将翻转后的头节点接到 pre 的后面。
- 将 pre 更新为当前翻转区域的尾节点,即指针 end。
- 将指针 end 更新为 next,继续处理下一组待翻转区域。
- 返回翻转后的链表头节点。
时间复杂度为 O(n),其中 n 为链表的长度,空间复杂度为 O(1)。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
* k 是一个正整数,它的值小于或等于链表的长度。
* 如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
* 示例:输入:head = [1,2,3,4,5], k = 2. 输出:[2,1,4,3,5]
* 输入:head = [1,2,3,4,5], k = 3 输出:[3,2,1,4,5]
* <p>
* 提示:
* 链表中节点的数量在范围 sz 内,1 <= sz <= 5000
* 0 <= Node.val <= 1000
* 1 <= k <= sz
* @date 2023/4/5 23:32
*/
public class ReverseKGroup {
/*解法:
* 使用迭代法实现链表翻转,每 k 个节点为一组,处理完后将前后两组连接起来。
* 具体实现时,使用 pre 指向待翻转链表的前一个节点,end 指向待翻转链表的尾部节点,
* 然后遍历链表,处理 k 个节点为一组的子链表,将其翻转,并将翻转后的头节点与前面的子链表连接,
* 并将 pre 指向下一组子链表的头节点的前一个节点,end 指向下一组子链表的尾部节点,
* 直到链表遍历完成。
*
* 时间复杂度:O(n),其中 n 是链表的长度。head 节点会在 O(n/k) 段中被翻转,每次翻转的时间复杂度是 O(k)。
* 空间复杂度:O(1)。*/
public ListNode reverseKGroup(ListNode head, int k) {
/*如果链表为空、链表只有一个元素或k=1,则直接返回原链表*/
if (head == null || head.next == null || k == 1) {
return head;
}
/*dummy节点可以避免很多判断和特殊处理,所以一般在链表题目中都会用到*/
ListNode dummy = new ListNode(0);
dummy.next = head;
/*每一段翻转前的前一个节点*/
ListNode pre = dummy;
/*每一段翻转后的尾节点*/
ListNode end = dummy;
while (end.next != null) {
for (int i = 0; i < k && end != null; i++) {
/*将end指针移动到需要翻转的段的最后一个节点*/
end = end.next;
}
if (end == null) {
break;
}
/*每一段翻转前的第一个节点*/
ListNode start = pre.next;
/*记录下一段翻转前的第一个节点*/
ListNode next = end.next;
/*切断当前段和下一段的连接,便于翻转当前段*/
end.next = null;
/*翻转当前段*/
pre.next = reverse(start);
/*连接上一段和当前段,连接当前段和下一段*/
start.next = next;
/*将pre、end移动到下一段翻转前的位置*/
pre = start;
end = start;
}
return dummy.next;
}
/**
* 翻转链表
*/
private ListNode reverse(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
public static void main(String[] args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(5);
int k = 2;
System.out.println("Original List: " + printLinkedList(head));
head = new ReverseKGroup().reverseKGroup(head, k);
System.out.println("Reversed List: " + printLinkedList(head));
}
private static String printLinkedList(ListNode head) {
StringBuilder res = new StringBuilder();
while (head != null) {
res.append(head.val).append(" -> ");
head = head.next;
}
res.append("null");
return res.toString();
}
}
11、旋转链表
题目描述:给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
示例 1:输入:head = [1,2,3,4,5], k = 2 输出:[4,5,1,2,3]
示例 2:输入:head = [0,1,2], k = 4 输出:[2,0,1]
提示:
- 链表中节点的数目在范围 [0, 5000] 内
- -100 <= Node.val <= 100
- 0 <= k <= 5000
解题思路
旋转链表的最优解法的时间复杂度是O(n),其中n是链表的长度。
首先,需要找到倒数第k个节点,然后将其作为新的头结点。因为k有可能大于链表长度,所以需要先统计链表长度,并对k做一些处理。
具体的做法如下:
- 先遍历一遍链表,找到链表的长度,并把链表的尾部节点指向链表的头结点,形成一个环形链表。同时可以记录下原始的链表长度len。
- 然后,从头节点开始,往后移动len - k % len个节点,找到新的头结点。
- 以新的头结点为界,将原始的链表断开,形成新的链表,并返回新链表的头结点。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
* 示例 1:输入:head = [1,2,3,4,5], k = 2 输出:[4,5,1,2,3]
* 示例 2:输入:head = [0,1,2], k = 4 输出:[2,0,1]
* <p>
* 提示:
* 链表中节点的数目在范围 [0, 5000] 内
* -100 <= Node.val <= 100
* 0 <= k <= 5000
* @date 2023/4/5 23:52
*/
public class RotateList {
/**
* 旋转链表的最优解法的时间复杂度是O(n),其中n是链表的长度。
* 首先,需要找到倒数第k个节点,然后将其作为新的头结点。
* 因为k有可能大于链表长度,所以需要先统计链表长度,并对k做一些处理。
* <p>
* 具体的做法如下:
* 先遍历一遍链表,找到链表的长度,并把链表的尾部节点指向链表的头结点,形成一个环形链表。同时可以记录下原始的链表长度len。
* 然后,从头节点开始,往后移动len - k % len个节点,找到新的头结点。
* 以新的头结点为界,将原始的链表断开,形成新的链表,并返回新链表的头结点。
*/
public ListNode rotateRight(ListNode head, int k) {
if (head == null || head.next == null || k == 0) {
return head;
}
/*统计链表长度*/
int len = 1;
ListNode tail = head;
while (tail.next != null) {
len++;
tail = tail.next;
}
/*计算需要移动的步数*/
int step = len - k % len;
/*链表变为环形,连接表头和表尾*/
tail.next = head;
/*寻找新的表头*/
for (int i = 0; i < step; i++) {
tail = head;
head = head.next;
}
/*断开环形链表,返回新的表头*/
tail.next = null;
return head;
}
public static void main(String[] args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(5);
int k = 2;
ListNode newHead = new RotateList().rotateRight(head, k);
while (newHead != null) {
System.out.print(newHead.val + " ");
newHead = newHead.next;
}
}
}
12、分隔链表
题目描述:给你一个链表和一个特定值 x ,请你对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。
你应当保留两个分区中每个节点的初始相对位置。
示例:输入:head = 1->4->3->2->5->2, x = 3. 输出:1->2->2->4->3->5
解释:小于 3 的元素放在左边,大于等于 3 的元素放在右边。
解题思路
最优解法的时间复杂度为O(n),空间复杂度为O(1)。
具体而言,可以维护两个指针smaller和larger,它们分别指向分隔后链表中值小于x和不小于x的节点。
初始时,smaller和larger都为null。遍历原始链表,对于每个节点,如果节点的值小于x,则将其插入到smaller链表的末尾;否则,将其插入到larger链表的末尾。最后,将smaller链表的末尾指向larger链表的开头即可。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给你一个链表和一个特定值 x ,请你对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。
* 你应当保留两个分区中每个节点的初始相对位置。
* 示例:输入:head = 1->4->3->2->5->2, x = 3. 输出:1->2->2->4->3->5
* 解释:小于 3 的元素放在左边,大于等于 3 的元素放在右边。
* @date 2023/4/5 23:11
*/
public class PartitionList {
/**
* 最优解法的时间复杂度为O(n),空间复杂度为O(1)。
* 具体而言,可以维护两个指针smaller和larger,它们分别指向分隔后链表中值小于x和不小于x的节点。
* 初始时,smaller和larger都为null。
* 遍历原始链表,对于每个节点,如果节点的值小于x,则将其插入到smaller链表的末尾;
* 否则,将其插入到larger链表的末尾。
* 最后,将smaller链表的末尾指向larger链表的开头即可。
*/
public ListNode partition(ListNode head, int x) {
ListNode smaller = new ListNode(0);
ListNode larger = new ListNode(0);
ListNode smallerHead = smaller;
ListNode largerHead = larger;
while (head != null) {
if (head.val < x) {
/*如果节点的值小于 x,则插入到 smaller 链表的末尾*/
smaller.next = head;
smaller = smaller.next;
} else {
/*否则插入到 larger 链表的末尾*/
larger.next = head;
larger = larger.next;
}
head = head.next;
}
/*将 larger 链表末尾设置为 null,否则会出现循环引用的情况*/
larger.next = null;
/*将 smaller 链表的末尾指向 larger 链表的开头*/
smaller.next = largerHead.next;
return smallerHead.next;
}
public static void main(String[] args) {
/*构造链表 1->4->3->2->5->2*/
ListNode head = new ListNode(1);
head.next = new ListNode(4);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(2);
head.next.next.next.next = new ListNode(5);
head.next.next.next.next.next = new ListNode(2);
/*分隔链表*/
int x = 3;
ListNode newHead = new PartitionList().partition(head, x);
/*验证结果*/
while (newHead != null) {
System.out.print(newHead.val + " ");
newHead = newHead.next;
}
}
}
13、奇偶链表
题目描述:给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
例如,给定链表 1->2->3->4->5->NULL,将其重新排列后得到 1->3->5->2->4->NULL。
要求空间复杂度为 O(1),时间复杂度为 O(n)。
示例 1:输入: 1->2->3->4->5->NULL. 输出: 1->3->5->2->4->NULL
示例 2:输入: 2->1->3->5->6->4->7->NULL 输出: 2->3->6->7->1->5->4->NULL
说明:
- 应当保持奇数节点和偶数节点的相对顺序。
- 链表的长度不超过 2000。
来源:力扣
请你编写函数实现这个功能,并提供一组测试数据进行验证。
解题思路
最优解法是创建两个链表,分别存储原始链表的奇数节点和偶数节点,然后将这两个链表连接起来。
具体而言,可以维护两个指针odd和even,分别指向奇数节点和偶数节点链表的末尾。遍历原始链表,对于每个节点,如果其为奇数节点,则将其插入到odd链表的末尾;否则,将其插入到even链表的末尾。最后,将odd链表的末尾指向even链表的开头即可。
时间复杂度为O(n),空间复杂度为O(1)。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。
* 链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
* 例如,给定链表 1->2->3->4->5->NULL,将其重新排列后得到 1->3->5->2->4->NULL。
* 要求空间复杂度为 O(1),时间复杂度为 O(n)。
* 示例 1:输入: 1->2->3->4->5->NULL. 输出: 1->3->5->2->4->NULL
* 示例 2:输入: 2->1->3->5->6->4->7->NULL 输出: 2->3->6->7->1->5->4->NULL
* <p>
* 说明:
* 应当保持奇数节点和偶数节点的相对顺序。
* 链表的长度不超过 2000。
* 来源:力扣
* 请你编写函数实现这个功能,并提供一组测试数据进行验证。
* @date 2023/4/5 23:27
*/
public class OddEvenList {
/**
* 最优解法是创建两个链表,分别存储原始链表的奇数节点和偶数节点,然后将这两个链表连接起来。
* <p>
* 具体而言,可以维护两个指针odd和even,分别指向奇数节点和偶数节点链表的末尾。
* 遍历原始链表,对于每个节点,如果其为奇数节点,则将其插入到odd链表的末尾;
* 否则,将其插入到even链表的末尾。最后,将odd链表的末尾指向even链表的开头即可。
* <p>
* 时间复杂度为O(n),空间复杂度为O(1)。
*/
public ListNode oddEvenList(ListNode head) {
/*如果链表为空或者只有一个节点,直接返回*/
if (head == null || head.next == null) {
return head;
}
/*定义两个指针,一个指向奇数节点,一个指向偶数节点*/
ListNode odd = head;
ListNode even = head.next;
/*记录偶数链表的头节点*/
ListNode evenHead = even;
/*遍历链表,将奇数节点和偶数节点分别连接起来*/
while (even != null && even.next != null) {
/*将奇数节点连接到下一个奇数节点*/
odd.next = even.next;
odd = odd.next;
/*将偶数节点连接到下一个偶数节点*/
even.next = odd.next;
even = even.next;
}
/*将奇数链表的末尾连接到偶数链表的头部,形成新的链表*/
odd.next = evenHead;
/*返回链表头部*/
return head;
}
public static void main(String[] args) {
/*构造链表 1->2->3->4->5*/
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(5);
/*调用方法*/
ListNode result = new OddEvenList().oddEvenList(head);
/*输出结果 1->3->5->2->4*/
while (result != null) {
System.out.print(result.val + "->");
result = result.next;
}
System.out.print("null");
}
}
14、合并k个排序链表
题目描述:合并k个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例:
输入:[1->4->5,1->3->4,2->6] 输出: 1->1->2->3->4->4->5->6
来源:力扣(LeetCode)
链接:力扣
解题思路
要实现合并多个有序链表,我们可以使用分治法。具体来说,我们可以将所有链表两两合并,直到所有链表都被合并为止。如果有奇数个链表,我们可以将最后一个链表和前面合并后得到的链表再次合并。
除了使用分治法外,我们还可以使用优先队列(Priority Queue)来合并多个有序链表。具体来说,我们可以将所有链表的头节点放入一个优先队列中,每次取出队列中值最小的节点,并将它的下一个节点插入队列中。重复这个过程直到队列为空,这样就得到了合并后的链表。
时间复杂度:假设所有链表的平均长度是n,那么分治的层数是O(logk),每层合并的时间复杂度是O(n),所以总时间复杂度是O(nlogk)。
空间复杂度:每次合并产生一个新链表,所以需要O(k)的额外空间。此外,在递归过程中还需要O(logk)的栈空间,因此总空间复杂度是O(k+logk)。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
import java.util.PriorityQueue;
/**
* @author yanfengzhang
* @description 题目描述:合并k个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
* 示例:
* 输入:[1->4->5,1->3->4,2->6] 输出: 1->1->2->3->4->4->5->6
* 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/merge-k-sorted-lists
* @date 2023/5/8 23:45
*/
public class MergeKLists {
/**
* 要实现合并多个有序链表,我们可以使用分治法。
* 具体来说,我们可以将所有链表两两合并,直到所有链表都被合并为止。
* 如果有奇数个链表,我们可以将最后一个链表和前面合并后得到的链表再次合并。
*/
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) {
/*判断特殊情况*/
return null;
}
int n = lists.length;
/*当链表数量大于1时,不断循环*/
while (n > 1) {
/*计算本轮需要合并的链表数量*/
int k = (n + 1) / 2;
/*每次循环合并相邻的两个链表*/
for (int i = 0; i < n / 2; i++) {
lists[i] = mergeTwoLists(lists[i], lists[i + k]);
}
/*将链表数量更新为本轮合并后的链表数量*/
n = k;
}
/*返回合并后的链表*/
return lists[0];
}
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
/*创建dummy节点*/
ListNode dummy = new ListNode(-1);
/*cur指针指向dummy*/
ListNode cur = dummy;
/*当l1和l2都不为空时,不断循环*/
while (l1 != null && l2 != null) {
/*如果l1的值小于l2的值*/
if (l1.val < l2.val) {
/*将l1接在新链表的尾部*/
cur.next = l1;
/*将l1指针指向下一个节点*/
l1 = l1.next;
}
/*如果l2的值小于等于l1的值*/
else {
/*将l2接在新链表的尾部*/
cur.next = l2;
/*将l2指针指向下一个节点*/
l2 = l2.next;
}
/*将tail指针移动到新链表的尾部*/
cur = cur.next;
}
/*如果l1还有剩余节点*/
if (l1 != null) {
/*将剩余节点接在新链表的尾部*/
cur.next = l1;
}
/*如果l2还有剩余节点*/
if (l2 != null) {
/*将剩余节点接在新链表的尾部*/
cur.next = l2;
}
/*返回合并后的链表*/
return dummy.next;
}
/**
* 除了使用分治法外,我们还可以使用优先队列(Priority Queue)来合并多个有序链表。
* 具体来说,我们可以将所有链表的头节点放入一个优先队列中,
* 每次取出队列中值最小的节点,并将它的下一个节点插入队列中。
* 重复这个过程直到队列为空,这样就得到了合并后的链表。
*/
public ListNode mergeKListsByPriorityQueue(ListNode[] lists) {
if (lists == null || lists.length == 0) {
/*判断特殊情况*/
return null;
}
/*创建优先队列*/
PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
/*将所有链表的头节点加入优先队列中*/
for (ListNode head : lists) {
if (head != null) {
pq.offer(head);
}
}
/*创建dummy节点*/
ListNode dummy = new ListNode(-1);
/*tail指针指向dummy*/
ListNode tail = dummy;
/*当优先队列不为空时,不断循环*/
while (!pq.isEmpty()) {
/*取出优先队列中最小的节点*/
ListNode node = pq.poll();
/*将这个节点接在新链表的尾部*/
tail.next = node;
/*将tail指针移动到新链表的尾部*/
tail = tail.next;
/*如果这个节点有下一个节点*/
if (node.next != null) {
/*将下一个节点加入优先队列中*/
pq.offer(node.next);
}
}
/*返回合并后的链表*/
return dummy.next;
}
}
15、链表相加
题目描述:给定两个非空链表 A 和 B,链表中的每个节点表示一个数字的一位,将两个链表表示的数字相加,并以链表形式返回相加后的结果。
链表相加
A: 3->8->6
B: 6->3->3->9
输出
9->1->0->0->1
解题思路
为了实现链表的相加,可以使用迭代的方式从头到尾遍历两个链表,并按照相应位置的节点值相加,同时考虑进位的情况。具体步骤如下:
- 创建一个新的链表 result 用于保存相加后的结果。
- 初始化两个指针 p 和 q 分别指向链表 A 和 B 的头节点。
- 初始化进位值 carry 为 0。
- 遍历链表 A 和 B,直到两个链表都遍历完:• 获取当前节点的值 x 和 y,如果某个链表已经遍历完,则将对应节点的值设为 0。• 计算当前位置的和 sum = x + y + carry,以及进位值 carry = sum / 10。• 创建一个新的节点,值为 sum % 10,将该节点添加到 result 链表的末尾。• 将指针 p 和 q 向后移动一位。
- 如果遍历结束后,进位值 carry 不为 0,则创建一个新节点,值为 carry,将该节点添加到 result 链表的末尾。
- 返回 result 链表。
通过上述步骤,我们可以得到两个链表相加的结果。该算法的时间复杂度为 O(max(m, n)),其中 m 和 n 分别是链表 A 和 B 的长度。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给定两个非空链表 A 和 B,链表中的每个节点表示一个数字的一位,
* 将两个链表表示的数字相加,并以链表形式返回相加后的结果。
* 链表相加
* A: 3->8->6
* B: 6->3->3->9
* 输出
* 9->1->0->0->1
* @date 2023/6/14 00:48
*/
public class AddTwoListNumbers {
/**
* 为了实现链表的相加,可以使用迭代的方式从头到尾遍历两个链表,并按照相应位置的节点值相加,同时考虑进位的情况。具体步骤如下:
*
* 1. 创建一个新的链表 result 用于保存相加后的结果。
* 2. 初始化两个指针 p 和 q 分别指向链表 A 和 B 的头节点。
* 3. 初始化进位值 carry 为 0。
* 4. 遍历链表 A 和 B,直到两个链表都遍历完:
* • 获取当前节点的值 x 和 y,如果某个链表已经遍历完,则将对应节点的值设为 0。
* • 计算当前位置的和 sum = x + y + carry,以及进位值 carry = sum / 10。
* • 创建一个新的节点,值为 sum % 10,将该节点添加到 result 链表的末尾。
* • 将指针 p 和 q 向后移动一位。
* 5. 如果遍历结束后,进位值 carry 不为 0,则创建一个新节点,值为 carry,将该节点添加到 result 链表的末尾。
* 6. 返回 result 链表。
*
* 通过上述步骤,我们可以得到两个链表相加的结果。该算法的时间复杂度为 O(max(m, n)),其中 m 和 n 分别是链表 A 和 B 的长度。
*/
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 哑节点,简化操作
ListNode dummy = new ListNode(0);
ListNode p = l1;
ListNode q = l2;
// 当前节点
ListNode curr = dummy;
// 进位值
int carry = 0;
while (p != null || q != null) {
//查看当前节点p对应的数值
int x = 0;
if(p != null){
x= p.val;
}
//查看当前节点q对应的数值
int y=0;
if(q != null){
y=q.val;
}
//对应链表加和计算
int sum = x + y + carry;
carry = sum / 10;
curr.next = new ListNode(sum % 10);
curr = curr.next;
if (p != null) {
p = p.next;
}
if (q != null) {
q = q.next;
}
}
if (carry > 0) {
curr.next = new ListNode(carry);
}
return dummy.next;
}
public static void main(String[] args) {
ListNode l1 = new ListNode(3);
l1.next = new ListNode(8);
l1.next.next = new ListNode(6);
ListNode l2 = new ListNode(6);
l2.next = new ListNode(3);
l2.next.next = new ListNode(3);
l2.next.next.next = new ListNode(9);
AddTwoListNumbers solution = new AddTwoListNumbers();
ListNode result = solution.addTwoNumbers(l1, l2);
// 打印结果链表
while (result != null) {
System.out.print(result.val + " -> ");
result = result.next;
}
System.out.println("null");
}
}
16、链表相加(反方向右对齐方式)
题目描述:两个不定长单向链表,将链表的数据按右对齐的方式,对应位相加,给出一个新链表。相加结果超过10,向前进位。
不推荐使用链表反转方式
示例
Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 8 -> 0 -> 7
解题思路
关于两个不定长单向链表的相加操作。链表中的每个节点表示一个个位数,需要对链表的对应位进行相加,形成一个新的链表。如果相加结果超过 10,那么需要进位。由于题目不建议使用链表反转方式,所以我们需要从右向左进行逐位相加。
-
链表长度差处理:首先计算两个链表的长度差,并在较短的链表前面补零,使两个链表的长度相等。
-
递归处理:使用递归从最末端开始相加,递归回溯时逐位处理进位。
-
构建新链表:每次递归返回时根据当前位的和及进位信息构造新的节点。
复杂度分析
- 时间复杂度:O(n),其中 n 是较长链表的长度。因为我们需要逐位遍历两个链表一次。
- 空间复杂度:O(n),主要用于递归栈的空间开销。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
/**
* @program: zyfboot-javabasic
* @description: 链表相加(反方向右对齐方式)
* @author: zhangyanfeng
* @create: 2024-09-03 12:00
**/
public class AddTwoListReverNumbers {
static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
// 计算链表长度
private int getLength(ListNode node) {
int length = 0;
while (node != null) {
length++;
node = node.next;
}
return length;
}
// 辅助递归函数:在链表长度对齐之后进行相加
private ListNode addLists(ListNode l1, ListNode l2, int[] carry) {
if (l1 == null && l2 == null) {
return null;
}
// 递归调用
ListNode nextNode = addLists(l1.next, l2.next, carry);
// 当前位的计算
int sum = l1.val + l2.val + carry[0];
carry[0] = sum / 10; // 计算进位
// 构造当前节点
ListNode currentNode = new ListNode(sum % 10);
currentNode.next = nextNode;
return currentNode;
}
// 主函数:处理链表长度对齐,并调用递归函数
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
int len1 = getLength(l1);
int len2 = getLength(l2);
// 将较短的链表前面补零,使得两个链表长度相等
while (len1 < len2) {
l1 = new ListNode(0, l1);
len1++;
}
while (len2 < len1) {
l2 = new ListNode(0, l2);
len2++;
}
int[] carry = new int[1];
ListNode result = addLists(l1, l2, carry);
// 如果最高位有进位,需要在前面增加一个新节点
if (carry[0] != 0) {
result = new ListNode(carry[0], result);
}
return result;
}
public static void main(String[] args) {
AddTwoListReverNumbers solution = new AddTwoListReverNumbers();
// 创建链表1: 7 -> 2 -> 4 -> 3
ListNode l1 = new ListNode(7, new ListNode(2, new ListNode(4, new ListNode(3))));
// 创建链表2: 5 -> 6 -> 4
ListNode l2 = new ListNode(5, new ListNode(6, new ListNode(4)));
// 计算和
ListNode result = solution.addTwoNumbers(l1, l2);
// 打印结果
while (result != null) {
System.out.print(result.val + " ");
result = result.next;
}
}
}
17、回文链表判定
题目描述:给定一个单链表,判断它是否是回文链表。
示例 1:
输入: 1 -> 2
输出: false
示例 2:
输入: 1 -> 2 -> 2 -> 1
输出: true
解题思路
最优解法可以在O(n)的时间复杂度和O(1)的空间复杂度内判断链表是否是回文结构。这里给出一个步骤简述:
- 使用快慢指针找到链表的中间节点,可以通过快指针走两步,慢指针走一步的方式实现。当快指针到达链表末尾时,慢指针就指向链表的中间节点。
- 反转链表的后半部分,从中间节点开始到链表末尾的部分都需要反转。
- 比较链表的前半部分和反转后的后半部分是否完全相同。比较的过程可以使用两个指针分别遍历前半部分和后半部分进行逐个节点的比较。
如果链表长度为奇数,中间节点不需要参与比较,直接跳过。通过这种方法,我们可以高效地判断给定链表是否是回文结构。
这种解法的关键在于找到链表的中间节点并对后半部分进行反转。这样做的好处是节省了额外的空间,并且可以在一次遍历内完成判断。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给定一个单链表,判断它是否是回文链表。
* 示例 1:
* 输入: 1 -> 2
* 输出: false
* 示例 2:
* 输入: 1 -> 2 -> 2 -> 1
* 输出: true
* @date 2023/8/2 23:59
*/
public class PalindromeLinkedList {
/**
* 最优解法可以在O(n)的时间复杂度和O(1)的空间复杂度内判断链表是否是回文结构。这里给出一个步骤简述:
* * 使用快慢指针找到链表的中间节点,可以通过快指针走两步,慢指针走一步的方式实现。当快指针到达链表末尾时,慢指针就指向链表的中间节点。
* * 反转链表的后半部分,从中间节点开始到链表末尾的部分都需要反转。
* * 比较链表的前半部分和反转后的后半部分是否完全相同。比较的过程可以使用两个指针分别遍历前半部分和后半部分进行逐个节点的比较。
* 如果链表长度为奇数,中间节点不需要参与比较,直接跳过。通过这种方法,我们可以高效地判断给定链表是否是回文结构。
* 这种解法的关键在于找到链表的中间节点并对后半部分进行反转。这样做的好处是节省了额外的空间,并且可以在一次遍历内完成判断。
*/
public static boolean isPalindrome(ListNode head) {
// 1. 使用快慢指针找到链表的中间节点
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 2. 反转链表的后半部分
ListNode prev = null, current = slow;
while (current != null) {
ListNode nextNode = current.next;
current.next = prev;
prev = current;
current = nextNode;
}
// 3. 比较链表的前半部分和反转后的后半部分是否完全相同
ListNode left = head, right = prev;
while (right != null) {
if (left.val != right.val) {
return false;
}
left = left.next;
right = right.next;
}
return true;
}
public static void main(String[] args) {
// 创建一个回文链表 1 -> 2 -> 2 -> 1
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(2);
head.next.next.next = new ListNode(1);
boolean result = isPalindrome(head);
// 输出结果为true
System.out.println("Is the linked list palindrome? " + result);
}
}
18、回文链表重排
题目描述:给定一个回文链表,将其重新排列,要求空间复杂度为 O(1)。
示例 1:
输入: 1 -> 2 -> 3 -> 4 -> 5 -> 4 -> 3 -> 2 -> 1
输出: 1 -> 1 -> 2 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5
在这个例子中,给定的回文链表为 1 -> 2 -> 3 -> 4 -> 5 -> 4 -> 3 -> 2 -> 1。重排后,将其变为1 -> 1 -> 2 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5。
对于这个问题,要求空间复杂度为 O(1),意味着我们不能使用额外的数据结构来存储链表的中间部分或者逆序后的后半部分。
解题思路
回文链表重排问题可以通过以下步骤解决,要求空间复杂度为O(1):
- 使用快慢指针找到链表的中间节点。
- 反转链表的后半部分。
- 将后半部分的节点依次插入到前半部分的节点之间。
以下是具体的步骤分析:
- 使用快慢指针找到链表的中间节点。快指针每次走两步,慢指针每次走一步,当快指针到达链表末尾时,慢指针就指向链表的中间节点。
- 反转链表的后半部分。从中间节点开始,将链表的后半部分反转,得到一个新的链表。
- 将后半部分的节点依次插入到前半部分的节点之间。将前半部分和反转后的后半部分依次交叉合并。
这样,就可以重新排列回文链表,满足题目要求的空间复杂度为O(1)。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给定一个回文链表,将其重新排列,要求空间复杂度为 O(1)。
* 示例 1:
* 输入: 1 -> 2 -> 3 -> 4 -> 5 -> 4 -> 3 -> 2 -> 1
* 输出: 1 -> 1 -> 2 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5
* 在这个例子中,给定的回文链表为 1 -> 2 -> 3 -> 4 -> 5 -> 4 -> 3 -> 2 -> 1。
* 重排后,将其变为1 -> 1 -> 2 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5。
* 对于这个问题,要求空间复杂度为 O(1),意味着我们不能使用额外的数据结构来存储链表的中间部分或者逆序后的后半部分。
* @date 2023/8/2 22:03
*/
public class ReorderPalindromeLinkedList {
/**
* 回文链表重排问题可以通过以下步骤解决,要求空间复杂度为O(1):
* * 使用快慢指针找到链表的中间节点。
* * 反转链表的后半部分。
* * 将后半部分的节点依次插入到前半部分的节点之间。
* 以下是具体的步骤分析:
* * 使用快慢指针找到链表的中间节点。快指针每次走两步,慢指针每次走一步,当快指针到达链表末尾时,慢指针就指向链表的中间节点。
* * 反转链表的后半部分。从中间节点开始,将链表的后半部分反转,得到一个新的链表。
* * 将后半部分的节点依次插入到前半部分的节点之间。将前半部分和反转后的后半部分依次交叉合并。
* 这样,就可以重新排列回文链表,满足题目要求的空间复杂度为O(1)。
*/
public void reorderList(ListNode head) {
if (head == null || head.next == null) {
return;
}
// 使用快慢指针找到链表的中间节点
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 反转链表的后半部分
ListNode prev = null, current = slow;
while (current != null) {
ListNode nextNode = current.next;
current.next = prev;
prev = current;
current = nextNode;
}
// 将后半部分的节点依次插入到前半部分的节点之间
ListNode firstHalf = head, secondHalf = prev;
while (secondHalf.next != null) {
ListNode nextFirst = firstHalf.next;
ListNode nextSecond = secondHalf.next;
firstHalf.next = secondHalf;
secondHalf.next = nextFirst;
firstHalf = nextFirst;
secondHalf = nextSecond;
}
}
public static void main(String[] args) {
// 创建一个回文链表 1 -> 2 -> 3 -> 4 -> 5 -> 4 -> 3 -> 2 -> 1
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(5);
head.next.next.next.next.next = new ListNode(4);
head.next.next.next.next.next.next = new ListNode(3);
head.next.next.next.next.next.next.next = new ListNode(2);
head.next.next.next.next.next.next.next.next = new ListNode(1);
// 打印原始链表
System.out.println("原始链表:");
printLinkedList(head);
// 重排链表
ReorderPalindromeLinkedList reorder = new ReorderPalindromeLinkedList();
reorder.reorderList(head);
// 打印重排后的链表
System.out.println("\n重排后的链表:");
printLinkedList(head);
}
private static void printLinkedList(ListNode head) {
ListNode current = head;
while (current != null) {
System.out.print(current.val + " -> ");
current = current.next;
}
System.out.println("null");
}
}
19、交换相邻节点
题目描述:交换链表中相邻的节点,如1->2->3->4变成2->1->4->3。
解题思路
最优解法是迭代法,其时间复杂度为 O(n),其中 n 是链表的节点数。
在迭代法中,我们使用三个指针来交换节点:prev、first_node 和 second_node。初始时,prev 指向虚拟头节点(dummy),first_node 指向当前节点,second_node 指向 first_node 的下一个节点。
首先判断 first_node 和 second_node 是否为空,若为空,则说明已经遍历到链表末尾或只有一个节点,不需要再进行交换。然后,执行节点交换的操作:
- prev.next 指向 second_node,使得 prev 和 first_node 之间的连接断开,prev 与 second_node 相连。
- first_node.next 指向 second_node.next,使得 first_node 和 second_node 之间的连接断开,first_node 指向 second_node 后面的节点。
- second_node.next 指向 first_node,使得 second_node 成为头节点,与 prev 成为相邻节点。
接着,更新 prev、first_node 和 second_node 的指针位置,继续迭代下一个节点对的交换。最后返回虚拟头节点的下一个节点,即交换后的链表的头节点。
该算法遍历了链表一次,每次交换操作只需要常数时间,因此时间复杂度为 O(n)。空间复杂度为 O(1),因为只使用了常数个额外指针来完成交换操作。这使得这种解法成为最优解法。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 交换链表中相邻的节点,如1->2->3->4变成2->1->4->3
* @date 2023/3/3 00:37
*/
public class SwapPairsLinkedList {
/**
* 最优解法是迭代法,其时间复杂度为 O(n),其中 n 是链表的节点数。
* 在迭代法中,我们使用三个指针来交换节点:prev、first_node 和 second_node。初始时,prev 指向虚拟头节点(dummy),first_node 指向当前节点,second_node 指向 first_node 的下一个节点。
* 首先判断 first_node 和 second_node 是否为空,若为空,则说明已经遍历到链表末尾或只有一个节点,不需要再进行交换。然后,执行节点交换的操作:
* * prev.next 指向 second_node,使得 prev 和 first_node 之间的连接断开,prev 与 second_node 相连。
* * first_node.next 指向 second_node.next,使得 first_node 和 second_node 之间的连接断开,first_node 指向 second_node 后面的节点。
* * second_node.next 指向 first_node,使得 second_node 成为头节点,与 prev 成为相邻节点。
* 接着,更新 prev、first_node 和 second_node 的指针位置,继续迭代下一个节点对的交换。最后返回虚拟头节点的下一个节点,即交换后的链表的头节点。
* 该算法遍历了链表一次,每次交换操作只需要常数时间,因此时间复杂度为 O(n)。空间复杂度为 O(1),因为只使用了常数个额外指针来完成交换操作。这使得这种解法成为最优解法。
*/
public static ListNode swapPairs(ListNode head) {
// 创建一个虚拟头节点(dummy),用于处理头节点的交换
ListNode dummy = new ListNode(0);
dummy.next = head;
// prev指向虚拟头节点(dummy),firstNode指向当前节点,secondNode指向firstNode的下一个节点
ListNode prev = dummy;
ListNode firstNode = head;
while (firstNode != null && firstNode.next != null) {
ListNode secondNode = firstNode.next;
// 交换节点
prev.next = secondNode;
firstNode.next = secondNode.next;
secondNode.next = firstNode;
// 更新prev、firstNode和secondNode的指针位置
prev = firstNode;
firstNode = firstNode.next;
}
// 返回虚拟头节点(dummy)的下一个节点,即交换后的链表的头节点
return dummy.next;
}
public static void main(String[] args) {
// 构建一个测试链表:1->2->3->4
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
// 调用交换相邻节点的函数
ListNode swappedHead = swapPairs(head);
// 验证交换后的链表:2->1->4->3
ListNode node = swappedHead;
while (node != null) {
System.out.print(node.val + " ");
node = node.next;
}
// 输出结果为:2 1 4 3
}
}
20、删除指定元素
题目描述:从链表中删除所有指定值的节点。
解题思路
最优解法是迭代法,其时间复杂度为 O(n),其中 n 是链表的节点数。
在迭代法中,我们使用两个指针 prev 和 curr 来遍历链表。初始时,prev 指向虚拟头节点(dummy),curr 指向链表的头节点。
我们需要遍历整个链表,检查每个节点的值是否等于目标值 val。如果当前节点的值等于目标值,我们将 prev 的 next 指针指向 curr 的下一个节点,从而跳过当前节点,实现删除操作。如果当前节点的值不等于目标值,我们更新 prev 为当前节点,并将 curr 指向下一个节点,继续遍历。
通过这样的遍历过程,我们可以删除所有值为目标值 val 的节点,最后返回虚拟头节点(dummy)的 next,即删除指定元素后的链表的头节点。
该算法遍历了链表一次,每次删除操作只需要常数时间,因此时间复杂度为 O(n)。空间复杂度为 O(1),因为只使用了常数个额外指针来完成删除操作。这使得这种解法成为最优解法。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 从链表中删除所有指定值的节点。
* @date 2022/2/3 22:40
*/
public class RemoveElementsLinkedList {
/**
* 最优解法是迭代法,其时间复杂度为 O(n),其中 n 是链表的节点数。
* 在迭代法中,我们使用两个指针 prev 和 curr 来遍历链表。初始时,prev 指向虚拟头节点(dummy),curr 指向链表的头节点。
* 我们需要遍历整个链表,检查每个节点的值是否等于目标值 val。
* 如果当前节点的值等于目标值,我们将 prev 的 next 指针指向 curr 的下一个节点,从而跳过当前节点,实现删除操作。
* 如果当前节点的值不等于目标值,我们更新 prev 为当前节点,并将 curr 指向下一个节点,继续遍历。
* 通过这样的遍历过程,我们可以删除所有值为目标值 val 的节点,最后返回虚拟头节点(dummy)的 next,即删除指定元素后的链表的头节点。
* 该算法遍历了链表一次,每次删除操作只需要常数时间,因此时间复杂度为 O(n)。
* 空间复杂度为 O(1),因为只使用了常数个额外指针来完成删除操作。这使得这种解法成为最优解法。
*/
public static ListNode removeElements(ListNode head, int val) {
// 创建一个虚拟头节点(dummy),用于处理头节点的删除
ListNode dummy = new ListNode(0);
dummy.next = head;
// prev指向虚拟头节点(dummy),curr指向当前节点
ListNode prev = dummy;
ListNode curr = head;
while (curr != null) {
if (curr.val == val) {
// 删除当前节点,将prev与curr的下一个节点相连
prev.next = curr.next;
} else {
// 当前节点值不等于目标值,更新prev为当前节点
prev = curr;
}
// 更新curr为下一个节点
curr = curr.next;
}
// 返回虚拟头节点(dummy)的下一个节点,即删除指定元素后的链表的头节点
return dummy.next;
}
public static void main(String[] args) {
// 构建一个测试链表:1->2->6->3->4->5->6
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(6);
head.next.next.next = new ListNode(3);
head.next.next.next.next = new ListNode(4);
head.next.next.next.next.next = new ListNode(5);
head.next.next.next.next.next.next = new ListNode(6);
int target = 6;
// 调用删除指定元素的函数
ListNode modifiedHead = removeElements(head, target);
// 验证删除指定元素后的链表:1->2->3->4->5
ListNode node = modifiedHead;
while (node != null) {
System.out.print(node.val + " ");
node = node.next;
}
// 输出结果为:1 2 3 4 5
}
}
21、反转链表指定部分
题目描述:给定一个单链表和两个整数 left 和 right,反转链表中从第 left 个节点到第 right 个节点的部分。
例如,对于链表 1->2->3->4->5,left = 2,right = 4,则反转链表中从第 2 个节点到第 4 个节点的部分,得到链表 1->4->3->2->5。
解题思路
最优解法是使用迭代法,在一次遍历中完成链表反转的某一部分。其时间复杂度为 O(n),其中 n 是链表的节点数。
在迭代法中,我们需要使用三个指针:prev、curr 和 next,分别表示当前节点的前一个节点、当前节点和下一个节点。
具体步骤如下:
- 定位到要反转部分的前一个节点,记为 prev。开始时,prev 指向虚拟头节点(dummy)。
- 将 curr 指向 prev 的下一个节点,即要反转部分的第一个节点。
- 反转从第 left 到第 right 的部分,类似反转整个链表的过程。在反转过程中,使用一个临时指针 next 来记录当前节点的下一个节点,以保证反转后能继续遍历。
- 将 prev 的 next 指向反转后的部分的头节点,将反转部分的尾节点的 next 指向 next。
- 返回虚拟头节点(dummy)的 next,即反转链表的头节点。
该算法只需要进行一次遍历,每次操作都只涉及常数个指针变量,因此时间复杂度为 O(n)。空间复杂度为 O(1),因为只使用了常数个额外指针来完成反转操作。这使得这种解法成为最优解法。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给定一个单链表和两个整数 left 和 right,
* 反转链表中从第 left 个节点到第 right 个节点的部分。
* 例如,对于链表 1->2->3->4->5,left = 2,right = 4,
* 则反转链表中从第 2 个节点到第 4 个节点的部分,得到链表 1->4->3->2->5。
* @date 2022/1/3 22:45
*/
public class ReverseLinkedListBetween {
/**
* 最优解法是使用迭代法,在一次遍历中完成链表反转的某一部分。其时间复杂度为 O(n),其中 n 是链表的节点数。
* 在迭代法中,我们需要使用三个指针:prev、curr 和 next,分别表示当前节点的前一个节点、当前节点和下一个节点。
* 具体步骤如下:
* * 定位到要反转部分的前一个节点,记为 prev。开始时,prev 指向虚拟头节点(dummy)。
* * 将 curr 指向 prev 的下一个节点,即要反转部分的第一个节点。
* * 反转从第 left 到第 right 的部分,类似反转整个链表的过程。在反转过程中,使用一个临时指针 next 来记录当前节点的下一个节点,以保证反转后能继续遍历。
* * 将 prev 的 next 指向反转后的部分的头节点,将反转部分的尾节点的 next 指向 next。
* * 返回虚拟头节点(dummy)的 next,即反转链表的头节点。
* 该算法只需要进行一次遍历,每次操作都只涉及常数个指针变量,因此时间复杂度为 O(n)。
* 空间复杂度为 O(1),因为只使用了常数个额外指针来完成反转操作。这使得这种解法成为最优解法。
*/
public static ListNode reverseBetween(ListNode head, int left, int right) {
// 创建一个虚拟头节点(dummy),用于处理头节点的反转
ListNode dummy = new ListNode(0);
dummy.next = head;
// 定位要反转部分的前一个节点,开始时指向虚拟头节点(dummy)
ListNode prev = dummy;
for (int i = 0; i < left - 1; i++) {
prev = prev.next;
}
// curr指向prev的下一个节点,即要反转部分的第一个节点
ListNode curr = prev.next;
// 反转从第 left 到第 right 的部分
for (int i = 0; i < right - left; i++) {
ListNode next = curr.next;
curr.next = next.next;
next.next = prev.next;
prev.next = next;
}
// 返回虚拟头节点(dummy)的下一个节点,即反转链表的头节点
return dummy.next;
}
public static void main(String[] args) {
// 构建一个测试链表:1->2->3->4->5
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(5);
int left = 2;
int right = 4;
// 调用反转链表部分的函数
ListNode reversedHead = reverseBetween(head, left, right);
// 验证反转部分后的链表:1->4->3->2->5
ListNode node = reversedHead;
while (node != null) {
System.out.print(node.val + " ");
node = node.next;
}
// 输出结果为:1 4 3 2 5
}
}
22、存在回文链表
题目描述:给定一个单链表和两个整数 left 和 right,判断从第 left 个节点到第 right 个节点的部分是否是回文链表。
例如,对于链表 1->2->3->2->1,left = 2,right = 4,则从第 2 个节点到第 4 个节点的部分 2->3->2 是一个回文链表,因此返回 true。
解题思路
要解决这个问题,可以使用快慢指针的方法,首先使用快慢指针找到第 left 个节点和第 right 个节点,然后将它们之间的部分提取出来,将该部分构建成一个新的链表,最后判断该新链表是否是回文链表。具体步骤如下:
- 使用快慢指针找到第 left 个节点和第 right 个节点。
- 将 left 节点的前一个节点(记为 prevLeft)和 right 节点的后一个节点(记为 nextRight)记录下来,用于后续连接链表。
- 将 left 节点的前一个节点的 next 指针置为 null,将 right 节点的 next 指针置为 null,断开这两个节点与前后节点的连接,得到独立的链表。
- 将独立链表进行反转。
- 比较反转后的链表与原链表是否相同,如果相同则说明从第 left 个节点到第 right 个节点的部分是回文链表。
这个解法的时间复杂度为 O(n),其中 n 是链表的节点数。需要遍历两次链表:一次找到 left 和 right 节点,一次反转链表进行比较。空间复杂度为 O(1),因为只使用了常数个额外指针来完成操作。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给定一个单链表和两个整数 left 和 right,判断从第 left 个节点到第 right 个节点的部分是否是回文链表。
* 例如,对于链表 1->2->3->2->1,left = 2,right = 4,
* 则从第 2 个节点到第 4 个节点的部分 2->3->2 是一个回文链表,因此返回 true。
* @date 2023/5/3 00:50
*/
public class PalindromeSubLinkedList {
/**
* 这道题的解题思路如下:
* 首先,我们需要找到从第 left 个节点到第 right 个节点的部分链表。
* 找到第 left 个节点的前一个节点 leftPrev 和第 right 个节点 rightNode。
* 截取链表,将 leftPrev 的 next 指针指向 null,将 rightNode 的 next 指针指向 null,这样就得到了从第 left 个节点到第 right 个节点的部分链表。
* 反转从第 left 个节点到第 right 个节点的部分链表,得到反转后的链表 reverseNode。
* 比较原链表中从第 left 个节点到第 right 个节点的部分链表和反转后的链表是否相等,如果相等,则说明是回文链表。
* 最后,将截取的链表恢复原状,即将 leftPrev 的 next 指针指向 reverseNode,将 reverseNode 的 next 指针指向 rightNext。
* 返回判断结果。
* 这样,我们就可以判断从第 left 个节点到第 right 个节点的部分是否是回文链表。
*
* 这个算法的时间复杂度为O(n),其中n是链表的长度。
* 这是因为我们需要遍历链表找到第left个节点和第right个节点,然后截取链表和反转链表都需要O(n)的时间复杂度。
* 最后,我们需要比较两个链表的值是否相等,这也需要O(n)的时间复杂度。
* 空间复杂度为O(1),因为我们只使用了常数个额外的指针来存储节点的引用,没有使用额外的数据结构。
* 综上所述,该算法的时间复杂度为O(n),空间复杂度为O(1)。
*
* @param head 链表头节点
* @param left 左边界
* @param right 右边界
* @return true:是回文链表;false:不是回文链表
*/
public static boolean isPalindrome(ListNode head, int left, int right) {
if (head == null) {
return false;
}
// 找到第 left 个节点的前一个节点
ListNode leftPrev = null;
ListNode cur = head;
for (int i = 1; i < left; i++) {
leftPrev = cur;
cur = cur.next;
}
// 找到第 right 个节点
ListNode rightNode = cur;
for (int i = left; i < right; i++) {
rightNode = rightNode.next;
}
// 截取链表
ListNode leftNode = leftPrev == null ? head : leftPrev.next;
ListNode rightNext = rightNode.next;
rightNode.next = null;
// 反转链表
ListNode reverseNode = reverseList(leftNode);
// 判断是否回文
boolean isPalindrome = true;
ListNode p1 = leftNode;
ListNode p2 = reverseNode;
while (p1 != null && p2 != null) {
if (p1.val != p2.val) {
isPalindrome = false;
break;
}
p1 = p1.next;
p2 = p2.next;
}
// 恢复链表
leftNode.next = rightNext;
if (leftPrev != null) {
leftPrev.next = reverseNode;
} else {
head = reverseNode;
}
return isPalindrome;
}
/**
* 反转链表.
*
* @param head 链表头节点
* @return 反转后的链表头节点
*/
private static ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
public static void main(String[] args) {
// 构建一个测试链表:1->2->3->2->1
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(2);
head.next.next.next.next = new ListNode(1);
int left = 2;
int right = 4;
// 调用判断是否为回文链表的函数
boolean result = isPalindrome(head, left, right);
// 验证结果为true,说明从第2个节点到第4个节点的部分2->3->2是回文链表
System.out.println("Is it a palindrome? " + result);
}
}
23、链表最长递增子序列
题目描述:给定一个未排序的链表,找到其中最长的递增子序列的长度。
例如,对于链表 1->3->5->4->7,最长的递增子序列是 1->3->4->7,长度为 4。
解题思路
最优解法使用动态规划结合二分查找,其时间复杂度为 O(nlogn),其中 n 是链表的节点数。
具体步骤如下:
- 定义一个数组 tails 来保存递增子序列,其中 tails[i] 表示长度为 i+1 的递增子序列的最后一个元素的值。
- 对于链表中的每个节点,进行二分查找找到它在 tails 数组中的插入位置。
- 如果当前节点的值大于 tails 数组中的最后一个元素,说明可以将当前节点添加到递增子序列中,长度加一。
- 否则,找到插入位置并更新 tails 数组中对应位置的值为当前节点的值,这样可以保持 tails 数组的递增性。
- 最终,tails 数组的长度即为最长的递增子序列的长度。
使用动态规划结合二分查找的方法,每次查找操作的时间复杂度为 O(logn),总共需要进行 n 次查找,因此总时间复杂度为 O(nlogn)。空间复杂度为 O(n),需要一个额外的 tails 数组来保存中间结果。
这种解法在寻找最长递增子序列的长度时非常高效,适用于规模较大的链表。
具体代码展示
package org.zyf.javabasic.letcode.list.application;
import org.zyf.javabasic.letcode.list.base.ListNode;
/**
* @author yanfengzhang
* @description 给定一个未排序的链表,找到其中最长的递增子序列的长度。
* 例如,对于链表 1->3->5->4->7,最长的递增子序列是 1->3->4->7,长度为 4。
* @date 2023/2/3 22:55
*/
public class LongestIncreasingSubsequence {
/**
* 最优解法使用动态规划结合二分查找,其时间复杂度为 O(nlogn),其中 n 是链表的节点数。
* 具体步骤如下:
* * 定义一个数组 tails 来保存递增子序列,其中 tails[i] 表示长度为 i+1 的递增子序列的最后一个元素的值。
* * 对于链表中的每个节点,进行二分查找找到它在 tails 数组中的插入位置。
* * 如果当前节点的值大于 tails 数组中的最后一个元素,说明可以将当前节点添加到递增子序列中,长度加一。
* * 否则,找到插入位置并更新 tails 数组中对应位置的值为当前节点的值,这样可以保持 tails 数组的递增性。
* * 最终,tails 数组的长度即为最长的递增子序列的长度。
* 使用动态规划结合二分查找的方法,每次查找操作的时间复杂度为 O(logn),总共需要进行 n 次查找,因此总时间复杂度为 O(nlogn)。空间复杂度为 O(n),需要一个额外的 tails 数组来保存中间结果。
* 这种解法在寻找最长递增子序列的长度时非常高效,适用于规模较大的链表。
*/
public static int lengthOfLIS(ListNode head) {
if (head == null) {
return 0;
}
// 定义一个数组 tails 来保存递增子序列,其中 tails[i] 表示长度为 i+1 的递增子序列的最后一个元素的值
int[] tails = new int[headLength(head)];
int len = 0;
while (head != null) {
// 进行二分查找找到当前节点在 tails 数组中的插入位置
int left = 0;
int right = len;
while (left < right) {
int mid = left + (right - left) / 2;
if (tails[mid] < head.val) {
left = mid + 1;
} else {
right = mid;
}
}
// 如果当前节点的值大于 tails 数组中的最后一个元素,说明可以将当前节点添加到递增子序列中,长度加一
if (left == len) {
tails[len++] = head.val;
} else {
// 否则,找到插入位置并更新 tails 数组中对应位置的值为当前节点的值,这样可以保持 tails 数组的递增性
tails[left] = head.val;
}
// 继续处理下一个节点
head = head.next;
}
// 最终,tails 数组的长度即为最长的递增子序列的长度
return len;
}
// 计算链表的长度
private static int headLength(ListNode head) {
int length = 0;
while (head != null) {
length++;
head = head.next;
}
return length;
}
public static void main(String[] args) {
// 构建一个测试链表:1->3->5->4->7
ListNode head = new ListNode(1);
head.next = new ListNode(3);
head.next.next = new ListNode(5);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(7);
// 调用求最长递增子序列长度的函数
int result = lengthOfLIS(head);
// 验证结果为4,因为最长递增子序列是 1->3->4->7
System.out.println("Length of LIS: " + result);
}
}