代码随想录–链表相关题目整理
1. LeetCode203 移除链表中指定元素
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
解题思路1: 采用虚拟头结点的方式删除结点。这种的好处是因为在链表中删除结点我们需要找到被删除结点的前一个结点才可以。如果要删除链表中第一个元素,它没有前驱结点,因此删除的方式可能会不统一。所以我们在链表前面在添加一个虚拟结点。这样删除第一个元素的操作和删除其他元素的操作就统一了。
public ListNode removeElements(ListNode head, int val) {
// 如果链表为空
if (head == null) return null;
// 创建虚拟头结点
ListNode pHead = new ListNode(0);
pHead.next = head;
ListNode cur = head;
ListNode pre = pHead;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return pHead.next;
}
解题思路2: 不设置虚拟头结点。如果不设置虚拟头结点,那么当头结点的值和val相等的时候,我们只需要将头结点向后移动一个即可。对于其他的结点,和上面的操作是一样的。
public ListNode removeElements(ListNode head, int val) {
if (head == null) return null;
// 处理头结点
while (head != null && head.val == val) head = head.next;
ListNode cur = head;
while (cur != null && cur.next != null) {
// 要删除的元素始终是cur的下一个元素
if (cur.next.val == val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
}
总结:这两种方法的目的实际上都是让一个指针指向当前判断是否需要删除的元素的上一个结点。
- 在使用虚拟头结点的方法中,我们是虚拟了一个头结点,让链表中所有的元素都拥有前驱结点。其中cur指向的就是要判断是否删除的结点,pre指向的是当前要判断是否删除元素的前驱结点。
- 在不使用头结点的方法中。是需要先判断头结点是不是要删除,如果要删除就直接将头指针向后移动。如果不是头结点,而此时cur正好指向头结点,所以我们在判断条件的时候判断的是
cur.next.val == val
,而不是cur.val == val
,所以说在这个方法中cur指向的是当前要判断是否删除元素的前驱结点。
2.LeetCode707 设计链表
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
这道题相对简单,主要需要注意的是,这道题中链表的索引是从0开始的。
class ListNode {
int val;
ListNode next;
public ListNode(int val) {
this.val = val;
this.next = null;
}
}
class MyLinkedList {
int size;
ListNode head;
public MyLinkedList() {
this.size = 0;
this.head = new ListNode(0);
}
/*
获取第index个位置的值
*/
public int get(int index) {
// 判断index是否满足条件
if (index >= size || index < 0) {
return -1;
}
ListNode cur = head;
for (int i = 0; i <= index; i++) {
cur = cur.next;
}
return cur.val;
}
public void addAtHead(int val) {
// 先创建出这个结点
ListNode node = new ListNode(val);
node.next = head.next;
head.next = node;
this.size++;
}
public void addAtTail(int val) {
ListNode node = new ListNode(val);
// 遍历到最后一个结点位置
ListNode cur = head;
while (cur.next != null) {
cur = cur.next;
}
// 此时cur指向最后一个元素
cur.next = node;
this.size++;
}
public void addAtIndex(int index, int val) {
if (index == size) {
addAtTail(val);
return;
}
if (index < 0) {
addAtHead(val);
return;
}
ListNode cur = head;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
ListNode node = new ListNode(val);
node.next = cur.next;
cur.next = node;
this.size++;
}
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指向的就是要删除的元素的前一个元素
cur.next = cur.next.next;
this.size--;
}
}
要删除链表中的某一个元素,一定要找到这个元素的前驱元素
3. LeetCode206 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
解题思路:使用两个指针来做
反转链表实际上就是将每一个链表的next指针指向前驱结点。因此可以定义两个指针,一个是pre指向当前遍历元素的前驱结点,初始值为null,因为第一个结点反转后,变成最后一个结点,而最后一个结点的next正好是null。cur指向当前正在遍历的结点
在循环中执行下面的操作
- 使用temp保存当前遍历元素的next指针
- 然后将当前元素的next指针赋值为pre
- 然后将per=cur
- 最后cur=cur.next
注意:这里一定要将cur.next先使用temp保存起来,否则因为在第二步cur.next值已经被修改了,再次访问cur=cur.next就不对了。
public ListNode reverseList(ListNode head) {
// 如果链表为空,则返回空
if (head == null) return null;
// 如果链表中只有只有一个元素,则直接返回
if (head.next == null) return head;
ListNode pre = null, cur = head;
ListNode temp;
while (cur != null) {
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
解题思路2: 只要是反转顺序的都可以考虑用栈来做。
- 首先将所有的结点入栈
- 然后创建一个虚拟虚拟头结点,让cur指向虚拟头结点。然后开始循环出栈,每出来一个元素,就把它加入到以虚拟头结点为头结点的链表当中,最后返回即可。
public ListNode reverseList(ListNode head) {
// 如果链表为空,则返回空
if (head == null) return null;
// 如果链表中只有只有一个元素,则直接返回
if (head.next == null) return head;
// 创建栈 每一个结点都入栈
Stack<ListNode> stack = new Stack<>();
ListNode cur = head;
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
// 创建一个虚拟头结点
ListNode pHead = new ListNode(0);
cur = pHead;
while (!stack.isEmpty()) {
ListNode node = stack.pop();
cur.next = node;
cur = cur.next;
}
// 最后一个元素的next要赋值为空
cur.next = null;
return pHead.next;
}
采用这种方法需要注意一点。就是当整个出栈循环结束以后,cur正好指向原来链表的第一个结点,而此时结点1中的next指向的是结点2,因此最后还需要
cur.next = null
4.LeetCode24 两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
解题思路:
- 首先判断链表是否为空和链表中是否只有一个元素的条件
- 创建一个指针cur指向头结点,然后依次交换cur和cur的下一个元素的值。
- 交换完成后,cur向后移动两个单位
- 循环的终止条件是
cur != null && cur.next != null
,也就是只有当cur和cur的下一个元素都不为空的时候,才继续交换。
public ListNode swapPairs(ListNode head) {
// 如果链表为空
if (head == null) return null;
// 如果链表只有一个元素 则直接返回这个元素
if (head.next == null) return head;
ListNode cur = head;
while (cur != null && cur.next != null) {
// 交换cur和cur的下一个结点
swap(cur, cur.next);
cur = cur.next.next;
}
return head;
}
private void swap(ListNode a, ListNode b) {
int temp = a.val;
a.val = b.val;
b.val = temp;
}
上边那个代码是交换的数值,虽然AC了但是不符合题意。
public ListNode swapPairs(ListNode head) {
// 如果链表为空
if (head == null) return null;
// 如果链表只有一个元素 则直接返回这个元素
if (head.next == null) return head;
// 创建虚拟头结点
ListNode pHead = new ListNode(0);
pHead.next = head;
ListNode cur = pHead;
while (cur.next != null && cur.next.next != null) {
ListNode temp = cur.next;
ListNode temp1 = cur.next.next.next;
cur.next = cur.next.next;
cur.next.next = temp;
temp.next = temp1;
cur = temp;
}
return pHead.next;
}
画图示意如下:
5. LeetCode19 删除链表的倒数第N个结点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
解题思路: 使用快慢指针,快指针先走N步,然后快慢指针同时走,最终当快指针指向链表中最后一个元素时,慢指针正好指向倒数N个结点的前一个结点。然后进行删除即可。
这里需要注意n给的值可能不合法的情况。
public ListNode removeNthFromEnd(ListNode head, int n) {
if (head == null) return null;
// 定义快慢指针
ListNode pHead = new ListNode(0);
pHead.next = head;
ListNode slow = pHead, fast = pHead;
for (int i = 1; i <= n; i++) {
fast = fast.next;
}
// n不合法
if (fast == null) return null;
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return pHead.next;
}
和这个题类似的,找到链表中倒数第K个元素。
public ListNode FindKthToTail(ListNode pHead, int k) {
// write code here
ListNode fast = pHead;
ListNode slow = pHead;
for (int i = 0; i < k; i++) {
// 需要注意的是 如果链表长度小于k的情况
// 通过下面的if判断判断了链表为空的情况和链表长度小于k的情况
if (fast == null)
return null;
fast = fast.next;
}
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
6. 面试题 02.07. 链表相交
输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
例如,输入{1,2,3},{4,5},{6,7}时,两个无环的单向链表的结构如下图所示:
可以看到它们的第一个公共结点的结点值为6,所以返回结点值为6的结点。
解题思路:分别建立两个指针,从两个链表开始遍历,如果有公共结点,那么两个指针第一次相遇的时候就是第一个公共结点。
当链表1遍历到头结点后,将链表1的下一个指针指向链表2的头结点。
同理,当链表2遍历到头结点后,将链表1的下一个指针指向链表1的头结点。
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
ListNode cur1 = headA;
ListNode cur2 = headB;
while (cur1 != cur2) {
if (cur1 == null) {
cur1 = headB;
continue;
}
if (cur2 == null) {
cur2 = headA;
continue;
}
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur2;
}
有人可能会有疑问,如果链表中没有公共结点,则while会不会是死循环。实际上不会,因为如果没有公共结点,那么最终cur1 == cur2 == null
解题思路2: 利用双指针。
通过上面的图可以看到,如果我们使用两个指针同时指向两个链表的头结点。在两个链表长度相等的情况下,可以一一遍历。但是现在的情况是两个链表长度不相同,因此可以让长的链表指针先移动几位,和短链表的指针保持到同一个位置,然后两个指针同时遍历链表,并比较指向的元素是不是同一个即可。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode curA = headA;
ListNode curB = headB;
int lenA = 0, lenB = 0;
// 求两个链表的长度
while (curA != null) {
lenA++;
curA = curA.next;
}
while (curB != null) {
lenB++;
curB = curB.next;
}
curA = headA;
curB = headB;
if (lenA > lenB) {
int gap = lenA - lenB;
for (int i = 0; i < gap; i++) {
curA = curA.next;
}
} else {
int gap = lenB - lenA;
for (int i = 0; i < gap; i++) {
curB = curB.next;
}
}
// 目前两个指针指向的位置是一样的 然后开始同时向前移动,然后判断两个指针指向的元素是不是一样
// 如果一样,则返回元素
System.out.print(1);
while (curA != null || curB != null) {
if (curA == curB)
return curA;
curA = curA.next;
curB = curB.next;
}
return null;
}
解题思路3:可以利用set集合,将链表1中所有元素都保存到set中,然后遍历链表2,每遍历一个元素,就去set中看一看是否包含,如果包含则直接返回。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null) return null;
// 创建一个set集合,保存链表A中所有元素
Set<ListNode> set = new HashSet<>();
ListNode cur = headA;
while (cur != null) {
set.add(cur);
cur = cur.next;
}
// 遍历链表B
cur = headB;
while (cur != null) {
if (set.contains(cur))
return cur;
cur = cur.next;
}
return null;
}
7. Leetcode142 环形链表II
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
解题思路1: 这个方法很难想到,可以作为结论记录下来。
- 首先定两个指针,一个快指针,一个满指针,都指向链表的头结点。快指针一次走两个元素,慢指针一次走一个元素。
- 开始遍历,如果存在环,那么快慢指针一定会相遇。如果没有环,那么fast指针会率先指向null
- 两个指针相遇后,再让满指针重新指向链表头结点,然后快慢指针同时一次移动一个单位。
- 最后快慢指针再次相遇的时候,就是环中第一个结点位置。
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) return null;
// 定义快慢指针
ListNode fast = head, slow = head;
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;
slow = head;
while (slow != fast) {
slow =slow.next;
fast =fast.next;
}
return slow;
}
如果单纯判断链表中是否有环,不需要返回环的入口结点,那么直接判断快慢指针是否会相遇即可。相遇则一定有环,否则就没有环。
解题思路2: 利用set集合。遍历链表,将元素存入set,在存入之间判断当前元素是否已经存在于set中,如果存在则直接返回该元素即可。
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) return null;
Set<ListNode> set = new HashSet<>();
ListNode cur= head;
while (cur != null) {
if (set.contains(cur))
return cur;
set.add(cur);
cur = cur.next;
}
return null;
}