一.一些经验总结
- 链表天然具有
递归性质
,单链表可以看做一个单叉树
,很多可以应用到二叉树的题目也可以应用到链表的题目之中,下面是一个体现单链表递归性质很好的例子逆序打印链表的值
java
private void reversePrint(ListNode head) {
if(head == null) return;
reversePrint(head.next);
System.out.println(head.val)
}
不难发现这种打印方式很像二叉树中的后序遍历
- 对于链表的题目,思路往往很容易想到,只是过程可能有点复杂,
一定要多画图,一定要舍得用变量
二.例题讲解
01.删除排序链表中重复的元素
链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-list/description/
分析
- 这是链表去重的经典问题,有多种解法
- 最容易想到的解法就是使用一个
去重的数据结构(Set)
,遍历整个链表 - 最优秀的解法是
一个指针,一次遍历
,注意题目条件,数组是有序的,那么重复元素一定是相邻的,每遍历到一个节点,就判断cur.val == cur.next.val
,如果相等,就删除cur.next(完成一次删除操作);如果不等,直接让cur向后走一步即可
代码:
java
/**
* 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 deleteDuplicates(ListNode head) {
if(head == null) return null;
// 原地去重
ListNode cur = head;
while(cur.next != null) {
if(cur.val ==cur.next.val) cur.next = cur.next.next;
else cur = cur.next;// 有可能直接走到null
}
return head;
}
}
说明
:
- 循环的条件往往是根据下面的判断条件决定的,判断条件是
cur.val == cur.next.val
,如果cur.next ==null,则会触发空指针异常,所以循环的条件是cur.next != null
,对于链表的最后一个元素,要么是重复元素,要么不是重复元素,如果是重复元素,则前一个节点的值和当前节点的值相等,在上一步就会执行删除操作;如果不是重复元素,不用删除 - 为什么存在重复元素的情况,删除重复元素之后不让cur走一步?因为有可能删除的是最后一个元素,这样
cur.next = null
,如果让cur向后走一步,在下一步的循环判断中就会触发空指针异常
删除重复元素的进阶版
链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/description/
分析
- 本题相较于上一题,需要将所有的重复元素都删除,而上一题是只保留重复元素的一个
- 同样也可以采用
一个指针,一次遍历
的操作,不过本题要删除所有的重复元素,就不能让指针走到重复元素的位置,应该走到第一个重复元素的前去节点
,即判断cur.next.val == cur.next.next.val
,如果相等,则一直循环删除所有等于cur.next.val(设为x)的所有节点,直到值不为x - 循环条件和判断条件的确立同上一题
代码:
java
/**
* 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 deleteDuplicates(ListNode head) {
// 个人经验 使用一个指针更容易理解 反而维护多个指针容易把自己绕晕
if(head == null) return null;
// 一次遍历的思路
ListNode phead = new ListNode(0);
phead.next = head;
ListNode cur = phead;
while(cur.next != null && cur.next.next != null) {
if(cur.next.val == cur.next.next.val) {
int x = cur.next.val;
while(cur.next != null && cur.next.val == x)// 一直走到值不等于x的节点
cur.next = cur.next.next;
}else {
cur = cur.next;
}
}
return phead.next;
}
}
02.反转链表
链接:https://leetcode.cn/problems/reverse-linked-list/
分析
1.方法一:使用两个指针迭代完成局部的链表的反转
java
/**
* 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 reverseList(ListNode head) {
if(head == null) return null;
ListNode pre = null, cur = head;
while(cur != null) {
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;// pre此时是原链表的最后一个节点 反转链表的头结点
}
}
2.方法2:递归写法
- 你给我反转当前节点(head)后面的所有节点,并且把反转后的头节点返回
- 反转当前节点和后面的节点,将后面的节点当做一个节点即可
java
/**
* 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 reverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode newHead = reverseList(head.next);
// 完成一次局部的反转
head.next.next = head;
head.next = null;
return newHead;
}
}
- 为什么返回newHead而不是head?因为newHead是完成反转之后的新的头结点
03.回文链表
链接:https://leetcode.cn/problems/palindrome-linked-list/description/
分析
方法1:栈
遇到对称有关的问题应该先考虑能否使用stack这种数据结构解决,对称问题最大的特点就是
从前往后遍历的结果和从后往前遍历的结果相同
,从前往后遍历容易,关键在于对于某些问题从后往前遍历
很困难(比如单链表),这是就可以使用栈这种数据结构,充分利用栈后进先出
的结构特点完成从后往前遍历
java
/**
* 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 boolean isPalindrome(ListNode head) {
// 借助栈
Stack<Integer> st = new Stack<>();
ListNode i1 = head;
// 1.将所有元素入栈
while(i1 != null) {
st.push(i1.val);
i1 = i1.next;
}
// 2.依次出栈进行比较
ListNode i2 = head;
while(i2 != null) {
if(st.peek() != i2.val) return false;
else {
i2 = i2.next;
st.pop();
}
}
return true;
}
}
方法2:反转后半部分链表,依次进行比较
- 利用快慢指针找到中间节点
- 根据fast是否为null判断节点的个数是奇数还是偶数,如果是奇数,向前走一步:如果是偶数,无需移动
- 反转后面的所有节点,依次向后比较
java
/**
* 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 boolean isPalindrome(ListNode head) {
ListNode fast = head, slow = head;
// 通过快慢指针找到中点
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
// 如果fast不为空,说明链表的长度是奇数个
if (fast != null) {
slow = slow.next;
}
// 反转后半部分链表
slow = reverse(slow);
fast = head;
while (slow != null) {
// 然后比较,判断节点值是否相等
if (fast.val != slow.val)
return false;
fast = fast.next;
slow = slow.next;
}
return true;
}
// 反转链表
public ListNode reverse(ListNode head) {
if(head == null || head.next == null) return head;
ListNode newHead = reverse(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}