目录
Java中并没有定义ListNode这个节点类,包括单向链表,双向链表,循环链表,都需要去学习它的写法,自定义
在处理链表的题目时,要注意通过画图的方法来摸清楚各个节点的应用和处理模式
在Java中自定义一个节点类(ListNode)
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) {
this.val = val;
}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
203.移除链表元素
简单
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1 输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7 输出:[]
提示:
- 列表中的节点数目在范围
[0, 104]
内 1 <= Node.val <= 50
0 <= val <= 50
方法一:不添加哨兵节点,使用原链表
对头结点和头结点之后的节点的处理方式不同
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return null;
}
// 处理头节点值等于val的情况,直到找到一个节点值不为val的节点当做头结点
//因为要调用head.next,必须保证head不为空
while (head != null && head.val == val) {
head = head.next;
}
// 遍历链表,移除节点
//因为要调用cur.next和cur.next.next,必须保证cur和cur.next不为空
ListNode cur = head;
while (cur != null && cur.next != null) {
if (cur.next.val == val) {
cur.next = cur.next.next; // 跳过当前节点的下一个节点
} else {
cur = cur.next; // 继续遍历下一个节点
}
//上述代码的目的是更新cur.next的值
}
return head; // 返回移除节点后的链表头节点
}
}
方法二:添加哨兵节点作为头结点
在处理头节点时,引入了一个虚拟节点(dummy),它的值设置为Integer.MIN_VALUE
,并且将原来的头节点作为虚拟节点的下一个节点。这样可以避免对头节点的特殊处理
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
// 处理空链表的情况
if (head == null) {
return null;
}
// 使用虚拟节点作为头节点,简化对头节点的操作
ListNode dummy = new ListNode(Integer.MIN_VALUE, head);
ListNode cur = dummy;
// 遍历链表,移除节点
while (cur != null && cur.next != null) {
if (cur.next.val == val) {
cur.next = cur.next.next; // 跳过当前节点的下一个节点
} else {
cur = cur.next; // 继续遍历下一个节点
}
}
return dummy.next; // 返回移除节点后的链表头节点
}
}
707.设计链表
中等
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList
类:
MyLinkedList()
初始化MyLinkedList
对象。int get(int index)
获取链表中下标为index
的节点的值。如果下标无效,则返回-1
。void addAtHead(int val)
将一个值为val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)
将一个值为val
的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)
将一个值为val
的节点插入到链表中下标为index
的节点之前。如果index
等于链表的长度,那么该节点会被追加到链表的末尾。如果index
比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为index
的节点。
示例:
输入 ["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"] [[], [1], [3], [1, 2], [1], [1], [1]] 输出 [null, null, null, null, 2, null, 3] 解释 MyLinkedList myLinkedList = new MyLinkedList(); myLinkedList.addAtHead(1); myLinkedList.addAtTail(3); myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3 myLinkedList.get(1); // 返回 2 myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3 myLinkedList.get(1); // 返回 3
提示:
0 <= index, val <= 1000
- 请不要使用内置的 LinkedList 库。
- 调用
get
、addAtHead
、addAtTail
、addAtIndex
和deleteAtIndex
的次数不超过2000
。
单链表
//单链表
class ListNode {
int val;
ListNode next;
ListNode(){}
ListNode(int val) {
this.val=val;
}
}
class MyLinkedList {
//size存储链表元素的个数
int size;
//虚拟头结点
ListNode head;
//初始化链表
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}
//获取第index个节点的数值
public int get(int index) {
//如果index非法,返回-1
if (index < 0 || index >= size) {
return -1;
}
ListNode currentNode = head;
//包含一个虚拟头节点,所以查找第 index+1 个节点
for (int i = 0; i <= index; i++) {
currentNode = currentNode.next;
}
return currentNode.val;
}
//在链表最前面插入一个节点
public void addAtHead(int val) {
addAtIndex(0, val);
}
//在链表的最后插入一个节点
public void addAtTail(int val) {
addAtIndex(size, val);
}
// 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
size++;
//找到要插入节点的前驱
ListNode pred = head;
for (int i = 0; i < index; i++) {
pred = pred.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = pred.next;
pred.next = toAdd;
}
//删除第index个节点
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
ListNode pred = head;
for (int i = 0; i < index; i++) {
pred = pred.next;
}
pred.next = pred.next.next;
}
}
双链表
//双链表类
class MyLinkedList {
//内部节点类
class ListNode {
int val; //节点值
ListNode next,prev; //指向前后节点的指针
ListNode(int x) {val = x;} //构造函数,初始化节点值
}
int size; //链表长度
ListNode head,tail; //头尾哨兵节点
//构造函数,初始化链表
public MyLinkedList() {
size = 0;
head = new ListNode(0); //头节点值为0
tail = new ListNode(0); //尾节点值为0
head.next = tail; //头节点指向尾节点
tail.prev = head; //尾节点指向头节点
}
//获取指定索引位置的值
public int get(int index) {
if(index < 0 || index >= size){return -1;} //如果索引越界,返回-1
ListNode cur = head; //从头节点开始遍历
// 通过判断 index < (size - 1) / 2 来决定是从头结点还是尾节点遍历,提高效率
if(index < (size - 1) / 2){
for(int i = 0; i <= index; i++){
cur = cur.next; //向前遍历
}
}else{
cur = tail; //从尾节点开始遍历
for(int i = 0; i <= size - index - 1; i++){
cur = cur.prev; //向后遍历
}
}
return cur.val; //返回当前节点的值
}
//在头部添加元素
public void addAtHead(int val) {
ListNode cur = head; //从头节点开始遍历
ListNode newNode = new ListNode(val); //创建新节点
newNode.next = cur.next; //新节点指向原头节点的下一个节点
cur.next.prev = newNode; //原头节点的下一个节点指向新节点
cur.next = newNode; //头节点指向新节点
newNode.prev = cur; //新节点指向头节点
size++; //链表长度加1
}
//在尾部添加元素
public void addAtTail(int val) {
ListNode cur = tail; //从尾节点开始遍历
ListNode newNode = new ListNode(val); //创建新节点
newNode.next = tail; //新节点指向尾节点
newNode.prev = cur.prev; //新节点的前一个节点指向原尾节点的前一个节点
cur.prev.next = newNode; //原尾节点的前一个节点指向新节点
cur.prev = newNode; //尾节点指向新节点
size++; //链表长度加1
}
//在指定索引位置插入元素
public void addAtIndex(int index, int val) {
if(index > size){return;} //如果索引大于链表长度,直接返回
if(index < 0){index = 0;} //如果索引小于0,将索引设置为0
ListNode cur = head; //从头节点开始遍历
for(int i = 0; i < index; i++){
cur = cur.next; //向前遍历
}
ListNode newNode = new ListNode(val); //创建新节点
newNode.next = cur.next; //新节点指向原索引位置的节点的下一个节点
cur.next.prev = newNode; //原索引位置的节点的下一个节点指向新节点
cur.next = newNode; //原索引位置的节点指向新节点
newNode.prev = cur; //新节点指向原索引位置的节点
size++; //链表长度加1
}
//删除指定索引位置的元素
public void deleteAtIndex(int index) {
if(index >= size || index < 0){return;} //如果索引越界,直接返回
ListNode cur = head; //从头节点开始遍历
for(int i = 0; i < index; i++){
cur = cur.next; //向前遍历
}
cur.next.next.prev = cur; //删除节点后,更新前后节点的指针关系
cur.next = cur.next.next; //删除节点后,更新当前节点的下一个节点指针
size--; //链表长度减1
}
}
206.反转链表
已解答
简单
相关标签
相关企业
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2] 输出:[2,1]
示例 3:
输入:head = [] 输出:[]
提示:
- 链表中节点的数目范围是
[0, 5000]
-5000 <= Node.val <= 5000
进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?
双指针法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
/**
* 反转单链表的方法
* @param head 给定链表的头节点
* @return 反转后的链表头节点
*/
public ListNode reverseList(ListNode head) {
ListNode cur = head; // 当前节点
ListNode pre = null; // 前一个节点
// 遍历链表
while (cur != null) {
ListNode temp = cur.next; // 暂存下一个节点的引用
cur.next = pre; // 当前节点的下一个节点指向前一个节点,实现反转
pre = cur; // 更新前一个节点为当前节点
cur = temp; // 更新当前节点为下一个节点
}
return pre; // 反转后的链表头节点
}
}
递归法
原理和双指针法相同
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
/**
* 反转单链表的方法(入口方法)
* @param head 给定链表的头节点
* @return 反转后的链表头节点
*/
public ListNode reverseList(ListNode head) {
// 调用递归函数,初始时前一个节点为null
return reverse(head, null);
}
/**
* 递归函数:反转单链表
* @param cur 当前节点
* @param pre 前一个节点
* @return 反转后的链表头节点
*/
public ListNode reverse(ListNode cur, ListNode pre) {
// 递归终止条件:当前节点为空,返回前一个节点作为新的头节点
if (cur == null) {
return pre;
}
// 暂存当前节点的下一个节点的引用
ListNode temp = cur.next;
// 反转当前节点的next指针,指向前一个节点
cur.next = pre;
// 更新前一个节点为当前节点
pre = cur;
// 更新当前节点为下一个节点
cur = temp;
// 递归调用,继续反转下一个节点
return reverse(cur, pre);
}
}
92.反转链表II
中等
给你单链表的头指针 head
和两个整数 left
和 right
,其中 left <= right
。请你反转从位置 left
到位置 right
的链表节点,返回 反转后的链表 。
示例 1:
输入:head = [1,2,3,4,5], left = 2, right = 4 输出:[1,4,3,2,5]
示例 2:
输入:head = [5], left = 1, right = 1 输出:[5]
提示:
- 链表中节点数目为
n
1 <= n <= 500
-500 <= Node.val <= 500
1 <= left <= right <= n
进阶: 你可以使用一趟扫描完成反转吗?
自己的思路,看的时候可以画图来理解
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummyHead = new ListNode(0, head); // 创建一个虚拟头结点
ListNode previous = dummyHead; // 前一个结点的指针
ListNode current = head; // 当前结点的指针
int position = 1; // 当前结点在链表中的位置
// 创建指针用于记录待翻转部分的起始位置和结束位置
ListNode reverseStartPrev = dummyHead; // 待翻转部分的起始位置的前一个节点
ListNode reverseEnd = head; // 待翻转部分的结束位置,第一个翻转的节点就是翻转后的结束位置
// 遍历链表
while (current != null) {
// 如果当前结点在待翻转部分之前,更新待翻转部分的起始位置和结束位置
if (position < left) {
reverseStartPrev = reverseStartPrev.next;
previous = previous.next;
current = current.next;
reverseEnd = reverseEnd.next;
}
// 如果当前结点在待翻转部分之内,则进行翻转操作
if (position >= left && position <= right) {
ListNode temp = current.next; // 临时保存当前结点的下一个结点
current.next = previous; // 将当前结点的 next 指针指向前一个结点
previous = current; // 更新前一个结点的指针为当前结点
current = temp; // 更新当前结点为临时保存的下一个结点
}
position++; // 位置加一
// 如果遍历到待翻转部分的结束位置,进行部分链表的重连接并结束循环
if (position == right + 1) {
reverseStartPrev.next = previous; // 将待翻转部分的起始位置的前一个结点的 next 指针指向翻转后的起始位置
reverseEnd.next = current; // 将待翻转部分的结束位置的 next 指针指向翻转后的下一个结点
break;
}
}
// 返回虚拟头结点的下一个结点,即为翻转后的链表的头结点
return dummyHead.next;
}
}
官方题解,思路更清晰
class Solution {
/**
* 反转链表中从第 left 个节点到第 right 个节点的部分。
*
* @param head 给定链表的头节点
* @param left 起始位置
* @param right 结束位置
* @return 反转后的链表头节点
*/
public ListNode reverseBetween(ListNode head, int left, int right) {
// 创建虚拟头节点,避免头节点变化带来的分类讨论
ListNode dummyNode = new ListNode(-1);
dummyNode.next = head;
ListNode pre = dummyNode;
// 从虚拟头节点出发,移动 left - 1 步,到达 left 节点的前一个节点
for (int i = 0; i < left - 1; i++) {
pre = pre.next;
}
// 移动 right - left + 1 步,到达 right 节点
ListNode rightNode = pre;
for (int i = 0; i < right - left + 1; i++) {
rightNode = rightNode.next;
}
// 截取从 left 到 right 节点的子链表
ListNode leftNode = pre.next;
ListNode curr = rightNode.next;
// 切断链接
pre.next = null;
rightNode.next = null;
// 反转子链表
reverseLinkedList(leftNode);
// 将反转后的子链表重新接回原链表中
pre.next = rightNode;
leftNode.next = curr;
// 返回虚拟头节点的下一个节点,即为反转后的链表头节点
return dummyNode.next;
}
/**
* 反转链表。
*
* @param head 给定链表的头节点
*/
private void reverseLinkedList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
}
}
25.K个一组翻转链表
困难
给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:
输入:head = [1,2,3,4,5], k = 2 输出:[2,1,4,3,5]
示例 2:
输入:head = [1,2,3,4,5], k = 3 输出:[3,2,1,4,5]
提示:
- 链表中的节点数目为
n
1 <= k <= n <= 5000
0 <= Node.val <= 1000
进阶:你可以设计一个只用 O(1)
额外内存空间的算法解决此问题吗?
class Solution {
// 反转从 head 到 tail 的链表,并返回新的头尾节点
public ListNode[] reverse(ListNode head, ListNode tail) {
ListNode prev = tail.next;
ListNode curr = head;
while (prev != tail) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return new ListNode[]{tail, head};
}
public ListNode reverseKGroup(ListNode head, int k) {
// 创建虚拟头结点,方便处理头部反转的情况
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode pre = dummy;
while (head != null) {
ListNode tail = pre;
// 查看剩余部分长度是否大于等于 k
for (int i = 0; i < k; ++i) {
tail = tail.next;
if (tail == null) {
return dummy.next;
}
}
ListNode nex = tail.next;
// 反转从 head 到 tail 的链表
ListNode[] reversed = reverse(head, tail);
head = reversed[0];
tail = reversed[1];
// 将子链表重新接回原链表
pre.next = head;
tail.next = nex;
pre = tail;
head = tail.next;
}
return dummy.next;
}
}
24.两两交换链表中的节点
中等
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4] 输出:[2,1,4,3]
示例 2:
输入:head = [] 输出:[]
示例 3:
输入:head = [1] 输出:[1]
提示:
- 链表中节点的数目在范围
[0, 100]
内 0 <= Node.val <= 100
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
/**
* 交换相邻节点的方法
* @param head 给定链表的头节点
* @return 交换相邻节点后的链表头节点
*/
public ListNode swapPairs(ListNode head) {
// 创建一个虚拟节点作为头节点的前一个节点
ListNode dummyNode = new ListNode(0, head);
// 当前节点指向虚拟节点
ListNode cur = dummyNode;
// 遍历链表,交换相邻节点
while (cur.next != null && cur.next.next != null) {
// 暂存当前相邻节点的前一个节点和后一个节点
ListNode temp1 = cur.next;
ListNode temp2 = cur.next.next.next;
// 交换相邻节点
cur.next = cur.next.next;
cur.next.next = temp1;
cur.next.next.next = temp2;
// 移动到下一组相邻节点的前一个节点
cur = cur.next.next;
}
return dummyNode.next; // 返回交换相邻节点后的链表头节点
}
}
19.删除链表的倒数第N个节点
中等
提示
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1 输出:[]
示例 3:
输入:head = [1,2], n = 1 输出:[1]
提示:
- 链表中结点的数目为
sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
进阶:你能尝试使用一趟扫描实现吗?
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyHead = new ListNode(0,head);
ListNode slow = dummyHead; //快慢指针均指向虚拟头节点
ListNode fast = dummyHead;
while(n >= 0){
n--;
fast = fast.next; //将快慢指针之间的间距变为n+1
}
while(fast != null){ //使快指针指向最后一个节点的next(null)
fast = fast.next; //此刻慢指针指向要删除的节点的前一个节点
slow = slow.next;
}
slow.next = slow.next.next; //执行删除操作
return dummyHead.next; //返回头结点
}
}
这道题的思路大致如注释,这里需要关注的是快慢节点之间的间距,以及快指针最后指向的位置,此处用作图的方式能更好的理解
-
定义fast指针和slow指针,初始值为虚拟头结点,如图:
-
fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图:
-
fast和slow同时移动,直到fast指向末尾,如题:
-
删除slow指向的下一个节点,如图:
有点纠结在是fast为空还是fast.next为空的终止条件,看了图解才真正理解
82.删除排序链表中的重复元素II
中等
给定一个已排序的链表的头 head
, 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
示例 1:
输入:head = [1,2,3,3,4,4,5] 输出:[1,2,5]
示例 2:
输入:head = [1,1,1,2,3] 输出:[2,3]
提示:
- 链表中节点数目在范围
[0, 300]
内 -100 <= Node.val <= 100
- 题目数据保证链表已经按升序 排列
class Solution {
public ListNode deleteDuplicates(ListNode head) {
// 如果头结点为空,则无需删除重复节点,直接返回头结点
if (head == null) {
return head;
}
// 创建一个虚拟节点来简化处理边界情况
ListNode dummy = new ListNode(0, head);
// 初始化一个指针'cur'指向虚拟节点
ListNode cur = dummy;
// 循环直到'cur'后面至少还有两个节点
while (cur.next != null && cur.next.next != null) {
// 检查下两个节点是否具有相同的值
if (cur.next.val == cur.next.next.val) {
// 如果是,将下一个节点的值存储在'x'中
int x = cur.next.val;
// 循环直到下一个节点的值不等于'x'
while (cur.next != null && cur.next.val == x) {
// 跳过值为'x'的节点
cur.next = cur.next.next;
}
} else {
// 如果下两个节点的值不相等,移动'cur'到下一个节点
cur = cur.next;
}
}
// 返回虚拟节点后面的下一个节点,即修改后链表的头结点
return dummy.next;
}
}
面试题02.07.链表相交
简单
提示
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
图示两个链表在节点 c1
开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3 输出:Intersected at '8' 解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 输出:Intersected at '2' 解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。 在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2 输出:null 解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。 由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。 这两个链表不相交,因此返回 null 。
提示:
listA
中节点数目为m
listB
中节点数目为n
0 <= m, n <= 3 * 104
1 <= Node.val <= 105
0 <= skipA <= m
0 <= skipB <= n
- 如果
listA
和listB
没有交点,intersectVal
为0
- 如果
listA
和listB
有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]
进阶:你能否设计一个时间复杂度 O(n)
、仅用 O(1)
内存的解决方案?
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
// 获取两个链表的交点
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 创建两个指针,分别指向两个链表的头节点
ListNode curA = headA;
ListNode curB = headB;
// 计算两个链表的长度
int lengthA = 0;
int lengthB = 0;
while(curA != null){
lengthA++;
curA = curA.next;
}
while(curB != null){
lengthB++;
curB = curB.next;
}
// 创建两个指针,分别指向两个链表的头节点
ListNode longer = headA;
ListNode shorter = headB;
// 计算两个链表的长度差值
int distance = lengthA - lengthB;
// 如果链表B比链表A长,则将longer指向链表B的头节点,shorter指向链表A的头节点,distance为长度差值
if(lengthB > lengthA){
longer = headB;
shorter = headA;
distance = lengthB - lengthA;
}
// 将longer指针向前移动distance步
while (distance > 0){
longer = longer.next;
distance--;
}
// 同时遍历两个链表,直到找到交点或者遍历结束
while(longer != null && shorter != null){
// 如果找到交点,则返回交点
if(longer == shorter){
return longer;
}
// 否则,将longer和shorter指针向前移动一步
longer = longer.next;
shorter = shorter.next;
}
// 如果没有找到交点,则返回null
return null;
}
}
简单的思路描述:找出两个链表中长度更长的一条,由于两条链表相交后面的部分均是一样的,所以使长度较长的链表移动到和长度较短的链表的长度一样时,此时的两节点才可能相交。
142.环形链表II
中等
相关标签
相关企业
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1 输出:返回索引为 1 的链表节点 解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0 输出:返回索引为 0 的链表节点 解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1 输出:返回 null 解释:链表中没有环。
提示:
- 链表中节点的数目范围在范围
[0, 104]
内 -105 <= Node.val <= 105
pos
的值为-1
或者链表中的一个有效索引
进阶:你是否可以使用 O(1)
空间解决此题?
自己的想法:
看到这道题的第一点,首先想到的是采用空间换时间的方式,这样思路比较简单,如下:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
// 检测链表中是否存在环,并返回环的起始节点
public ListNode detectCycle(ListNode head) {
// 创建一个列表用于存储遍历过的节点
List<ListNode> node = new ArrayList<>();
// 初始化当前节点为头节点
ListNode cur = head;
// 遍历链表
while(cur != null){
// 如果当前节点已经在列表中,说明存在环,返回当前节点作为环的起始节点
if(node.contains(cur)){
return cur;
}
// 将当前节点添加到列表中
node.add(cur);
// 移动到下一个节点
cur = cur.next;
}
// 如果遍历完链表都没有发现环,返回null
return null;
}
}
题解这里就照搬了
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢
首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。
那么来看一下,为什么fast指针和slow指针一定会相遇呢?
可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。
会发现最终都是这种情况, 如下图:
fast和slow各自再走一步, fast和slow就相遇了
这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。
动画如下:
如果有环,如何找到这个环的入口
此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:
那么相遇时: slow指针走过的节点数为: x + y + m (y + z)
, fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈才遇到slow指针,m为慢指针走了m圈 (y+z)为 一圈内节点的个数A。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:
(x + y + m (y + z)) * 2 = x + y + n (y + z)
两边消掉一个(x+y)和 (y + z): x + y = n (y + z)
因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:x = n (y + z) - y
,
再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
这个公式说明什么呢?
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。
当 n为1的时候,公式就化解为 x = z
,
这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。
让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
动画如下:
那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。
其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
代码如下:
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {// 有环
ListNode index1 = fast;
ListNode index2 = head;
// 两个指针,从头结点和相遇结点,各走一步,直到相遇,相遇点即为环入口
while (index1 != index2) {
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}
return null;
}
}
- 时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n
- 空间复杂度: O(1)
141.环形链表
简单
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
示例 1:
输入:head = [3,2,0,-4], pos = 1 输出:true 解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0 输出:true 解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1 输出:false 解释:链表中没有环。
提示:
- 链表中节点的数目范围是
[0, 104]
-105 <= Node.val <= 105
pos
为-1
或者链表中的一个 有效索引 。
进阶:你能用 O(1)
(即,常量)内存解决此问题吗?
使用列表的方法:
public class Solution {
public boolean hasCycle(ListNode head) {
// 初始化当前节点为头节点
ListNode cur = head;;
// 创建一个列表用于存储遍历过的节点
List<ListNode> list = new ArrayList();
// 遍历链表
while(cur != null){
// 如果当前节点已经在列表中,说明存在环,返回true
if(list.contains(cur)){
return true;
}
// 将当前节点添加到列表中
list.add(cur);
// 移动到下一个节点
cur = cur.next;
}
// 如果遍历完链表都没有发现环,返回false
return false;
}
}
快慢指针:
public class Solution {
public boolean hasCycle(ListNode head) {
// 初始化快慢指针,都指向头节点
ListNode slow = head;
ListNode fast = head;
// 遍历链表,直到快指针或者快指针的下一个节点为空
while (fast != null && fast.next != null) {
// 快指针每次移动两步
fast = fast.next.next;
// 慢指针每次移动一步
slow = slow.next;
// 如果快慢指针相遇,说明链表中存在环
if (fast == slow) {
return true;
}
}
// 如果遍历完链表都没有相遇,说明链表中不存在环
return false;
}
}
61.旋转链表
中等
给你一个链表的头节点 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, 500]
内 -100 <= Node.val <= 100
0 <= k <= 2 * 109
先将链表首尾闭合成环,找到旋转后的头节点的前一个节点,然后将它设为尾结点,返回旋转后的头结点
class Solution {
public ListNode rotateRight(ListNode head, int k) {
// 如果头结点为空,则无法旋转,直接返回null
if(head == null){
return null;
}
// 初始化指针cur指向头结点,length为链表长度初始值1
ListNode cur = head;
int length = 1;
// 计算链表长度
while(cur.next != null){
cur = cur.next;
length++;
}
// 将尾节点的next指向头结点,形成循环链表
ListNode tail = cur;
tail.next = head;
// 计算实际需要旋转的步数
int count = length - (k % length);
// 将current指针重新指向头结点
ListNode current = head;
// 移动current指针到新的头结点的前一个节点
while(--count > 0){
current = current.next;
}
// 新的头结点为当前节点的下一个节点
ListNode newHead = current.next;
// 将当前节点的next设为null,断开循环链表
current.next = null;
// 返回新的头结点
return newHead;
}
}
2.两数相加
中等
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4] 输出:[7,0,8] 解释:342 + 465 = 807.
示例 2:
输入:l1 = [0], l2 = [0] 输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] 输出:[8,9,9,9,0,0,0,1]
提示:
- 每个链表中的节点数在范围
[1, 100]
内 0 <= Node.val <= 9
- 题目数据保证列表表示的数字不含前导零
这道题其实是一道模拟的题目,因为给出的顺序就是反的,要求得的答案也是反的,故直接遍历两个链表,每一位相加后出去进位加到下一个结果节点的值上,两个链表均遍历完后如果还有进位就再加一个结果节点存放进位。
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = null, tail = null; // 定义头节点和尾节点
int carry = 0; // 进位
// 循环遍历两个链表,直到两个链表都为空
while (l1 != null || l2 != null) {
// 获取当前节点的值,如果节点为空则值为0
int n1 = l1 != null ? l1.val : 0;
int n2 = l2 != null ? l2.val : 0;
// 计算当前位的和,并加上进位
int sum = n1 + n2 + carry;
// 将当前位的值添加到结果链表中
if (head == null) {
head = tail = new ListNode(sum % 10); // 如果结果链表为空,创建新节点作为头节点
} else {
tail.next = new ListNode(sum % 10); // 如果结果链表不为空,创建新节点作为尾节点的下一个节点
tail = tail.next; // 更新尾节点
}
carry = sum / 10; // 更新进位
// 移动到下一个节点
if (l1 != null) {
l1 = l1.next;
}
if (l2 != null) {
l2 = l2.next;
}
}
// 如果最后还有进位,则在结果链表的尾部添加一个新节点
if (carry > 0) {
tail.next = new ListNode(carry);
}
return head; // 返回结果链表的头节点
}
}
21.合并两个有序链表
简单
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = [] 输出:[]
示例 3:
输入:l1 = [], l2 = [0] 输出:[0]
提示:
- 两个链表的节点数目范围是
[0, 50]
-100 <= Node.val <= 100
l1
和l2
均按 非递减顺序 排列
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode prehead = new ListNode(-1); // 创建哑节点作为结果链表的头节点
ListNode prev = prehead; // prev指针用于链接结果链表
// 循环遍历两个链表,直到其中一个链表为空
while (l1 != null && l2 != null) {
// 比较两个链表当前节点的值
if (l1.val <= l2.val) {
prev.next = l1; // 将 l1 当前节点链接到 prev 后
l1 = l1.next; // 移动 l1 指针到下一个节点
} else {
prev.next = l2; // 将 l2 当前节点链接到 prev 后
l2 = l2.next; // 移动 l2 指针到下一个节点
}
prev = prev.next; // 移动 prev 指针到下一个节点
}
// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
prev.next = l1 == null ? l2 : l1;
return prehead.next; // 返回结果链表的头节点
}
}
138.随机链表的复制
中等
提示
给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next
指针和 random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X
和 Y
两个节点,其中 X.random --> Y
。那么在复制链表中对应的两个节点 x
和 y
,同样有 x.random --> y
。
返回复制链表的头节点。
用一个由 n
个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index]
表示:
val
:一个表示Node.val
的整数。random_index
:随机指针指向的节点索引(范围从0
到n-1
);如果不指向任何节点,则为null
。
你的代码 只 接受原链表的头节点 head
作为传入参数。
示例 1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]] 输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:
输入:head = [[1,1],[2,1]] 输出:[[1,1],[2,1]]
示例 3:
输入:head = [[3,null],[3,0],[3,null]] 输出:[[3,null],[3,0],[3,null]]
提示:
0 <= n <= 1000
-104 <= Node.val <= 104
Node.random
为null
或指向链表中的节点。
通过一个map集合将新旧节点一一映射,防止重复创建节点,通过回溯的方式,回溯的值是创建好的节点,用来给next或者random来赋值。
class Solution {
// 创建一个哈希表用于存储已经复制过的节点,避免重复复制
Map<Node, Node> map = new HashMap<>();
// 复制带随机指针的链表
public Node copyRandomList(Node head) {
// 如果原链表为空,直接返回null
if (head == null) {
return head;
}
// 如果当前节点已经被复制过,则直接返回其对应的复制节点
if (!map.containsKey(head)) {
// 创建当前节点的复制节点,并将其加入哈希表中
Node headNew = new Node(head.val);
map.put(head, headNew);
// 递归复制下一个节点和随机指针所指向的节点
headNew.next = copyRandomList(head.next);
headNew.random = copyRandomList(head.random);
}
// 返回当前节点的复制节点
return map.get(head);
}
}
86.分隔链表
中等
给你一个链表的头节点 head
和一个特定值 x
,请你对链表进行分隔,使得所有 小于 x
的节点都出现在 大于或等于 x
的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
示例 1:
输入:head = [1,4,3,2,5,2], x = 3 输出:[1,2,2,4,3,5]
示例 2:
输入:head = [2,1], x = 2 输出:[1,2]
提示:
- 链表中节点的数目在范围
[0, 200]
内 -100 <= Node.val <= 100
-200 <= x <= 200
class Solution {
public ListNode partition(ListNode head, int x) {
// 如果头结点为空,则无法进行分区,直接返回null
if(head == null){
return null;
}
// 创建虚拟节点,并将其next指向头结点
ListNode dummy = new ListNode(-10000,head);
// 初始化指针cur指向头结点,preLow和pre指向虚拟节点dummy
ListNode cur = head;
ListNode preLow = dummy; //上一个比x小的节点
ListNode pre = dummy; //当前节点的前一个节点
// 遍历链表
while(cur != null){
// 如果当前节点的值小于x
if(cur.val < x){
// 如果前一个节点的值大于等于x,需要进行插入操作
if(pre.val >= x){
// 保存preLow节点的下一个节点
ListNode temp = preLow.next;
// 将当前节点插入到preLow节点后面
preLow.next = cur;
// 更新preLow指向当前节点
preLow = cur;
// 将pre的next指向当前节点的下一个节点,即跳过当前节点
pre.next = cur.next;
// 将当前节点的next指向temp,即将当前节点插入到preLow后面
cur.next = temp;
// 更新cur为pre的next节点,即下一个需要判断的节点
cur = pre.next;
} else {
// 如果前一个节点的值小于x,不需要插入操作,继续下一个节点的判断
preLow = cur;
cur = cur.next;
pre = pre.next;
}
// 继续下一次循环
continue;
}
// 如果当前节点的值不小于x,继续遍历下一个节点
cur = cur.next;
pre = pre.next;
}
// 返回dummy的下一个节点,即分区后的头结点
return dummy.next;
}
}