目录
回文链表 (双指针+反转链表) 链表为奇数时,慢指针再走一步,避开中间节点
19 删除倒数第N个节点(**双指针法) √ 指向虚拟头结点 快指针先走n+1步**
链表
虚拟头结点
链表的一大问题就是操作当前节点必须要找前一个节点才能操作。这就造成了,头结点的尴尬,因为头结点没有前一个节点了。
每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题。根据传入的参数确定需不需要指定dummy.next=head;
链表定义方式
public class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
移除元素(链表)使用虚拟头结点的方式
class Solution {
public ListNode removeElements(ListNode head, int val) {
//使用虚拟头结点
if(head==null){
return head;
}
ListNode dummy=new ListNode(-1,head);
//初始化两个指针 pre 和 cur,分别指向虚拟头结点 dummy 和当前节点 head。
ListNode pre=dummy;
ListNode cur=head;
while(cur!=null){
//在循环中,检查当前节点 cur 的值是否等于目标值 val。如果相等,
//则将前一个节点 pre 的 next 指针指向当前节点 cur 的下一个节点,跳过当前节点,实现移除操作。
if(cur.val==val){
pre.next=cur.next;
}else{
pre=cur;
}
cur=cur.next;
}
return dummy.next;//返回移除元素后的链表头。
}
}
160 相交**链表** +
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
使用双指针来找到两个链表的交点(引用完全相同,即:内存地址完全相同的交点)
求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置
此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。
需要保证A的长度大于B的长度
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode curA = headA;
ListNode curB = headB;
int lenA = 0;
int lenB = 0;
while (curA != null) {
lenA++;
curA = curA.next;
}
while (curB != null) {
lenB++;
curB = curB.next;
}
curA = headA;
curB = headB;
// 让curA为最长链表的头,lenA为其长度
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 gap = lenA - lenB;
// 让curA和curB在同一起点上(末尾位置对齐)
while (gap-- > 0) {
curA = curA.next;
}
while (curA != null) {
if (curA == curB) {
return curA;
} else {
curA = curA.next;
curB = curB.next;
}
}
return null;
}
}
方法二
解决这个问题的关键是,通过某些方式,让 p1
和 p2
能够同时到达相交节点 c1
**。**
如果用两个指针 p1
和 p2
分别在两条链表上前进,我们可以让 p1
遍历完链表 A
之后开始遍历链表 B
,让 p2
遍历完链表 B
之后开始遍历链表 A
,这样相当于「逻辑上」两条链表接在了一起。
如果这样进行拼接,就可以让 p1
和 p2
同时进入公共部分,也就是同时到达相交节点 c1
:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// p1 指向 A 链表头结点,p2 指向 B 链表头结点
ListNode p1 = headA, p2 = headB;
while (p1 != p2) {
// p1 走一步,如果走到 A 链表末尾,转到 B 链表
if (p1 == null)
p1 = headB;
else
p1 = p1.next;
// p2 走一步,如果走到 B 链表末尾,转到 A 链表
if (p2 == null)
p2 = headA;
else
p2 = p2.next;
}
return p1;
}
}
206 反转**链表** +
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。
最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。
class Solution {
public ListNode reverseList(ListNode head) {
//双指针法
ListNode pre=null;
ListNode cur=head;
ListNode temp=null;
while(cur!=null){
temp=cur.next; //记录下一节点的地址(箭头的指向)
cur.next=pre; //指针倒转
pre=cur;
cur=temp; //cur继续遍历
}
return pre;//返回 pre 作为反转后的链表的头结点。
}
}
234 回文**链表** +
给你一个单链表的头节点 head
,请你判断该链表是否为回文链表
如果是,返回 true
;否则,返回 false
。
回文串(双指针)
寻找回文串的核心思想是从中心向两端扩展、从两端向中间逼近
判断一个字符串是不是回文串就简单很多,不需要考虑奇偶情况,只需要双指针技巧,从两端向中间逼近即可:
boolean isPalindrome(String s) {
// 一左一右两个指针相向而行 从两边走到中间
int left = 0, right = s.length() - 1;
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
回文链表 (双指针+反转链表) 链表为奇数时,慢指针再走一步,避开中间节点
-
先通过 双指针技巧 中的快慢指针来找到**链表的中点**
-
如果
fast
指针没有指向null
**,说明链表长度为奇数,slow
** 还要再前进一步 -
从
slow
开始反转后面的**链表,现在就可以开始比较回文串了**
class Solution {
public boolean isPalindrome(ListNode head) {
//1.寻找链表的中间节点
ListNode slow,fast;
slow=fast=head;
//寻找中点,使用两个 next
while(fast.next!=null&&fast.next.next!=null){
slow=slow.next;
fast=fast.next.next;
}
//2.如果链表的长度是奇数,那么 fast 指针将指向最后一个节点。
//在这种情况下,需要将 slow 指针向前移动一步,跳过中间节点,求的是两端回文链表。
if(fast!=null){
slow=slow.next;
}
//3.将 left 指针指向链表的头节点,
//将 right 指针指向以 slow 为头的后半部分链表的反转。
//为了实现反转,调用了 reverse 函数。
ListNode left=head;
ListNode right=reverse(slow);
//4.比较反转后字符串是否相同,相同即为回文
while(right!=null){
if(left.val!=right.val){
return false;
}
left=left.next;
right=right.next;
}
return true;
}
//5.翻转链表
ListNode reverse(ListNode head){
ListNode pre =null,cur=head;
while(cur!=null){
ListNode next=cur.next;
cur.next=pre;
pre=cur;
cur=next;
}
return pre;
}
}
141 环形**链表** i (快慢指针) √
给你一个链表的头节点 head
,判断链表中是否有环。如果链表中存在环 ,则返回 true
。 否则,返回 false
。
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) {
return false;
}
ListNode fastNode = head;
ListNode slowNode = head;
while ( fastNode!= null && fastNode.next != null) {
fastNode = fastNode.next.next;
slowNode = slowNode.next;
if (fastNode == slowNode) {
return true;
}
}
return false;
}
}
142 环形**链表** ii (快慢指针)√
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
**。
-
判断链表是否环(上一题)
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
-
如果有环,如何找到这个环的入口
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。
从头结点出发一个指针,从相遇节点 也出发一个指针,**这两个指针每次只走一个节点,** 那么当这两个指针相遇的时候就是 环形入口的节点。
找到相遇点后,新建两节点 head slow x=z
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;
}
}
21 合并两个有序**链表** √ (新链表记录数据)
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
//使用虚拟头结点,接收结果
ListNode dummy = new ListNode(-1);
ListNode p = dummy; //新链表接收结果
ListNode p1 = list1, p2 = list2;
while (p1 != null && p2 != null) {
if (p1.val > p2.val) {
p.next = p2;
p2 = p2.next;
} else {
p.next = p1;
p1 = p1.next;
}
p = p.next;
}
//存在某个链表为空时,将剩下链表接至末尾
if (p1 != null) {
p.next = p1;
}
if (p2 != null) {
p.next = p2;
}
return dummy.next;
}
}
2 两数相加 (**双指针、进位计算)+ √ 逆序存储**
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
逆序存储很友好了,直接遍历链表就是从个位开始的,符合我们计算加法的习惯顺序。如果是正序存储,那倒要费点脑筋了,可能需要 翻转链表 或者使用栈来辅助。
这道题主要考察 链表双指针技巧 和加法运算过程中对进位的处理。注意这个 carry 变量的处理,在我们手动模拟加法过程的时候会经常用到。
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 在两条链表上的指针
ListNode p1 = l1, p2 = l2;
// 虚拟头结点(构建新链表时的常用技巧) 用于构建新链表
ListNode dummy = new ListNode(-1);
// 指针 p 负责构建新链表
ListNode p = dummy;
// 记录进位
int carry = 0;
// 开始执行加法,两条链表走完且没有进位时才能结束循环
while (p1 != null || p2 != null || carry > 0) {
// 先加上上次的进位 每次都是新的一轮叠加
int val = carry; //符合数学逻辑,先加上进位
if (p1 != null) {
val += p1.val;
p1 = p1.next;
}
if (p2 != null) {
val += p2.val;
p2 = p2.next;
}
// 处理进位情况
carry = val / 10;//十位数字
val = val % 10; // 10 % 10 =0 将得到的 余数 作为当前位的值,并将其赋给变量 val。这样可以获取当前相加的结果中当前位的值。
//构建新节点
p.next = new ListNode(val);
p = p.next;
}
// 返回结果链表的头结点(去除虚拟头结点)
return dummy.next;
}
}
19 删除倒数第N个节点(**双指针法) √ 指向虚拟头结点 快指针先走n+1步**
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
结合虚拟头结点 和 双指针法来移除链表倒数第N个节点。
双指针的经典应用,如果要删除倒数第n个节点,让fast移动n+1步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
-
定义fast指针和slow指针,初始值为虚拟头结点(从这个点开始走),如图:
-
fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图:
-
fast和slow同时移动,直到fast指向末尾,如题:
-
删除slow指向的下一个节点,如图:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy=new ListNode(-1,head);
//都指向虚拟头结点
ListNode fastIndex=dummy;
ListNode slowIndex=dummy;
//快指针先走n+1步
for(int i=0;i<n+1;i++){ //while(n-- >= 0)也可以,只是一种循环方式
fastIndex=fastIndex.next;
}
//遍历使用while 快慢节点相差n+1 当快指针到null 慢指针的下一节点要删除节点
while(fastIndex!=null){
fastIndex=fastIndex.next;
slowIndex=slowIndex.next;
}
//删除节点
slowIndex.next=slowIndex.next.next;
return dummy.next;
}
}
24 两两交换**链表中的节点 √ (反转链表进阶)**
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
一次前进两个节点,进行一次反转 需要临时节点记录数值
临时节点 更新节点(下一轮循环)
如果需要记录的节点是有三层next,循环终止条件记录到一层next+两层next;
如果需要记录的节点是有两层next,循环终止条件记录到自身+一层next;
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummyhead = new ListNode(-1,head);
ListNode cur = dummyhead;//需要操作的前一节点
ListNode temp; // 临时节点,保存两个节点后面的节点
ListNode firstnode; // 临时节点,保存两个节点之中的第一个节点
ListNode secondnode; // 临时节点,保存两个节点之中的第二个节点
//开始交换
while (cur.next != null && cur.next.next != null) //分别对应奇数和偶数情况,条件判断也是从左往右,先写右边的会出现空指针
{ //保存指针指向 根据
temp = cur.next.next.next;
firstnode = cur.next;
secondnode = cur.next.next;
cur.next = secondnode;
secondnode.next = firstnode;
firstnode.next = temp;
//移动节点!!!
cur = firstnode;
}
return dummyhead.next;
}
}
25 K个一组翻转链表
给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
解法1 代码量大
-
每次取出k个元素
-
翻转后拼接到结果上
public ListNode reverseKGroup(ListNode head, int k) {
ListNode root = new ListNode(); // 创建一个虚拟头节点,方便操作
root.next = head; // 将虚拟头节点的下一个节点指向原链表的头节点
ListNode pre = root; // pre指针用于标记每个子链表翻转前的头节点的前一个节点
while (true) {
ListNode t = nextTail(pre, k); // 找到当前子链表翻转的尾节点
if (t == null) { // 如果尾节点为null,说明剩余节点不足k个,结束循环
break;
}
ListNode next = t.next; // next指针保存当前子链表尾节点的下一个节点,即下一组的头节点
ListNode tail = pre.next; // 当前子链表的头节点,翻转后将成为尾节点
t.next = null; // 断开当前子链表与下一组的连接
ListNode newHead = reverse(pre.next); // 翻转当前子链表
pre.next = newHead; // 将翻转后的子链表连接回原链表
tail.next = next; // 将原来的头节点(现在的尾节点)连接到下一组的头节点
pre = tail; // 移动pre指针到当前组的尾节点,为处理下一组做准备
}
return root.next; // 返回虚拟头节点的下一个节点,即翻转后的链表头节点
}
// 寻找当前组的尾节点
public ListNode nextTail(ListNode head, int k) {
while(k > 0) {
if (head != null) {
k--; // 计数器减1
head = head.next; // 向前移动head指针
} else {
return null; // 如果head为null,说明节点不足k个,返回null
}
}
return head; // 返回当前组的尾节点
}
// 翻转链表
private ListNode reverse(ListNode head) {
ListNode pre = null; // 前指针
ListNode curr = head; // 当前指针
while (curr != null) {
ListNode next = curr.next; // 保存当前节点的下一个节点
curr.next = pre; // 将当前节点指向前一个节点,实现翻转
pre = curr; // 移动前指针
curr = next; // 移动当前指针
}
return pre; // 返回翻转后的头节点
}
这个方法首先通过nextTail函数找到每组的尾节点,然后断开当前组与下一组的连接,对当前组进行翻转,最后将翻转后的子链表重新连接到原链表中。通过循环处理,直到链表末尾。这种方式不仅可以实现每k个节点的翻转,而且通过虚拟头节点简化了对头节点的特殊处理,使代码更加简洁易懂。
解法2 递归做法 使用原函数定义
1、先反转以 head
开头的 k
个元素。
2、将第 k + 1
个元素作为 head
递归**调用** reverseKGroup
函数。
3、将上述两个过程的结果连接起来。
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null) return null; // 如果头节点为空,直接返回null
// 初始化两个指针a和b,它们定义了当前要反转的区间[a, b)
ListNode a, b;
a = b = head;
for (int i = 0; i < k; i++) {
// 如果在达到k个节点之前b已经为null,说明剩余节点不足k个,不需要反转,直接返回head
if (b == null) return head;
b = b.next; // 否则,b向前移动
}
// 反转区间[a, b)的节点
ListNode newHead = reverse(a, b);
// 递归反转b之后的节点,并将a的next指针指向递归结果,连接反转后的链表部分
a.next = reverseKGroup(b, k);
return newHead; // 返回新的头节点
}
/* 反转区间 [a, b) 的元素,注意是左闭右开 */
ListNode reverse(ListNode a, ListNode b) {
ListNode pre, cur, next;
pre = null;
cur = a;
next = null;
while (cur != b) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
138 随机链表的复制
对于数据结构复制,甭管他怎么变,你就记住最简单的方式:一个**哈希表** + 两次遍历。
\1. 克隆所有节点:第一次遍历**链表,为每个节点创建一个新的克隆节点,并将原节点与克隆节点的映射关系存储在哈希表中。**
\2. 连接克隆节点的next和random指针:第二次遍历链表,根据原节点的next和random指针的指向,使用哈希表找到对应的克隆节点,并正确设置克隆节点的next和random指针。
通过这种方式,可以在不修改原链表的情况下,复制一个完全相同的新链表,其中包括复杂的random指针关系。
class Solution {
public Node copyRandomList(Node head) {
// 创建一个哈希表,用于存储原节点到克隆节点的映射 参数是原始节点和复制节点
HashMap<Node, Node> originToClone = new HashMap<>();
// 第一次遍历,先把所有节点克隆出来
for (Node p = head; p != null; p = p.next) {
// 如果当前节点还没有被克隆,则克隆当前节点并存入哈希表
if (!originToClone.containsKey(p)) {
originToClone.put(p, new Node(p.val));
//创建了一个新的Node对象,这个新对象的val属性被设置为与原节点p相同的值 java中是值传递,传递的是引用的值,因此需要新建节点
}
}
// 第二次遍历,把克隆节点的结构连接好
for (Node p = head; p != null; p = p.next) {
// 设置克隆节点的next指针
if (p.next != null) {
originToClone.get(p).next = originToClone.get(p.next);
}
// 设置克隆节点的random指针
if (p.random != null) {
originToClone.get(p).random = originToClone.get(p.random);
}
}
// 返回克隆之后的头结点
return originToClone.get(head);
}
}
23 合并K个升序链表(优先级队列)
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
\21. 合并两个有序链表 的延伸,利用 优先级队列(二叉堆) 进行节点排序即可。
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
// 如果输入的链表数组为空,则直接返回null
if (lists.length == 0) return null;
// 创建一个虚拟头结点,方便操作
ListNode dummy = new ListNode(-1);
// p指针用于构建最终的合并后的链表
ListNode p = dummy; //这两个总是在一起
// 创建一个优先级队列(最小堆),根据链表节点的值的大小进行排序
PriorityQueue<ListNode> pq = new PriorityQueue<>(
lists.length, (a, b)->(a.val - b.val));
// 将k个链表的头结点加入到最小堆中
for (ListNode head : lists) {
if (head != null)
pq.offer(head);
}
// 当优先级队列不为空时,循环继续
while (!pq.isEmpty()) {
// 从优先级队列中取出最小的节点
ListNode node = pq.poll();
// 将这个最小的节点接到结果链表的末尾
p.next = node;
// 如果这个最小的节点的next不为空,将next节点加入到优先级队列中,重新排序,出堆
if (node.next != null) {
pq.offer(node.next);
}
// 移动p指针到结果链表的末尾
p = p.next;
}
// 返回合并后的链表的头结点
return dummy.next;
}
}
这行代码创建了一个PriorityQueue<ListNode>对象,即一个优先级队列,用于存储ListNode类型的元素。优先级队列是一种特殊的队列,其出队顺序是根据元素的优先级而不是元素的插入顺序。在这个场景中,ListNode的val值决定了其在队列中的优先级。
PriorityQueue<ListNode> pq = new PriorityQueue<>(lists.length, (a, b) -> (a.val - b.val));
这个方法的核心是使用优先级队列(最小堆)来维护当前各个链表的头节点,这样每次都可以O(log k)的时间复杂度内取出当前最小的节点,然后将其加入到结果链表中。这个过程重复进行,直到所有链表的节点都被处理完毕。这种方法的时间复杂度是O(N log k),其中N是所有链表中元素的总数,k是链表的个数。这是因为每个节点都要被加入和取出优先级队列一次,每次操作的时间复杂度是O(log k)。这种方法相比逐一比较k个链表的头节点,可以大大提高合并的效率。