一.一些经验总结
- 链表天然具有
递归性质
,单链表可以看做一个单叉树
,很多可以应用到二叉树的题目也可以应用到链表的题目之中,下面是一个体现单链表递归性质很好的例子逆序打印链表的值
private void reversePrint(ListNode head) {
if(head == null) return;
reversePrint(head.next);
System.out.println(head.val)
}
不难发现这种打印方式很像二叉树中的后序遍历
2. 对于链表的题目,思路往往很容易想到,只是过程可能有点复杂,一定要多画图,一定要舍得用变量
二.例题讲解
01.删除排序链表中重复的元素
链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-list/description/
分析
- 这是链表去重的经典问题,有多种解法
- 最容易想到的解法就是使用一个
去重的数据结构(Set)
,遍历整个链表 - 最优秀的解法是
一个指针,一次遍历
,注意题目条件,数组是有序的,那么重复元素一定是相邻的,每遍历到一个节点,就判断cur.val == cur.next.val
,如果相等,就删除cur.next(完成一次删除操作);如果不等,直接让cur向后走一步即可
代码:
/**
* 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 - 循环条件和判断条件的确立同上一题
代码:
/**
* 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.方法一:使用两个指针迭代完成局部的链表的反转
/**
* 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)后面的所有节点,并且把反转后的头节点返回
- 反转当前节点和后面的节点,将后面的节点当做一个节点即可
/**
* 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这种数据结构解决,对称问题最大的特点就是从前往后遍历的结果和从后往前遍历的结果相同
,从前往后遍历容易,关键在于对于某些问题从后往前遍历
很困难(比如单链表),这是就可以使用栈这种数据结构,充分利用栈后进先出
的结构特点完成从后往前遍历
/**
* 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判断节点的个数是奇数还是偶数,如果是奇数,向前走一步:如果是偶数,无需移动
- 反转后面的所有节点,依次向后比较
/**
* 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;
}
}