链表
一、概述
链表(Linked List)是最简单的线性的、动态数据结构。理解它是理解树结构、图结构的基础。
区别于数组,链表中的元素不是存储在内存中连续的一片区域,链表中的数据存储在每一个称之为「结点」复合区域里,在每一个结点除了存储数据以外,还保存了到下一个节点的指针(Pointer)。
由于不必按顺序存储,链表在插入数据的时候可以达到 O(1) 的复杂度,但是查找一个节点或者访问特定编号的节点则需要 O(n) 的时间。
二、常见算法
1. 反转:反转链表(单链表)
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
解题思路:
需要引入两个变量来记录当前节点和上一个节点,用于链表反转的时候使用。
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
// 定义一个前置节点,用于设置反转后的next值。
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
// 1.取当前Node节点的下一个Node节点,因为当前Node节点反转后,Node.next要指向上一个Node节点,所以这里要先获取到。
ListNode next = cur.next;
// 2.将上一个Node节点放到当前节点的next属性中,表示反转过程。
cur.next = pre;
// 3.pre和cur两个Node节点向右平移一个Node,从而进入下次循环。
pre = cur;
cur = next;
}
// 4.最终pre指向最后一个Node节点,而它又是反转后的新链表头。
return pre;
}
2. 反转:反转链表(双链表)
给你双链表的头节点 head ,请你反转链表,并返回反转后的链表。
解题思路:
相比于单链表,双链表多了一个当前Node节点指向上一个Node节点的操作。
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = pre;
// 相比于单链表,双链表多了一个当前Node节点指向上一个Node节点的操作。
cur.last = next;
pre = cur;
cur = next;
}
return pre;
}
3. 反转:K 个一组翻转链表
LeetCode K 个一组翻转链表
LeetCode 两两交换链表中的节点
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
- k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
解题思路:
- 要对一个单链表进行每k个节点为一组进行反转。所以需要有一个函数可以返回第n组的第k个的节点。
- 对第n组的k个元素进行反转,这就是单纯的单链表反转。
- 链表头尾连接。
- 将第 i-1 组的尾节点与第 i 组的头结点进行连接。
- 将第 i+1 组的头结点与第 i 组的尾节点进行连接。
- 移动前一个节点和当前节点的Node节点指向。
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || head.next == null) {
return head;
}
ListNode virtualNode = new ListNode(0);
virtualNode.next = head;
// 设置虚拟节点。
ListNode pre = virtualNode;
// 从第1个虚拟节点开始。
ListNode end = virtualNode;
while (end != null) {
// 找到第1组的最后一个节点。
end = findKNode(end, k);
// 如果end为null,说明最后一组节点个数不足k个,因此不需要执行后面的链表反转。
if (end == null) {
break;
}
// 获取实际需要反转的链表。
// pre.next为第i组的头节点。
ListNode start = pre.next;
// end.next为第i组的尾节点,也是第i+1组的头结点。
ListNode next = end.next;
// 反转第i组的k个元素,并返回第i组的头结点。
ListNode groupHead = reverseK(start, next);
// 将第i组的头结点与第i-1组的尾结点进行关联。
pre.next = groupHead;
// 将第i组的尾结点与第i+1的头结点进行关联。
start.next = next;
// 移动pre和end节点到第i组元素的尾结点。
pre = start;
end = pre;
}
// 虚拟节点的下一个节点为实际的链表头。
return virtualNode.next;
}
// 获取传入节点的后面第k个节点。
public ListNode findKNode(ListNode head, int k) {
for (int i = 0; i < k; i++) {
if (head != null) {
head = head.next;
} else {
break;
}
}
return head;
}
// 单链表反转,传入的end节点为结束节点,该节点不包含在当前这一组中。并返回反转后的头结点。
public ListNode reverseK(ListNode head, ListNode end) {
ListNode pre = null;
ListNode cur = head;
while (cur != end) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
4. 合并:两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
解题思路:
- 两数和的关键点在于同一个位置上的两个数之和如果大于等于十,需要进位。
- 通过设置虚拟节点作为头部方便操作。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
if (l1 == null && l2 == null) {
return null;
}
ListNode virtualNode = new ListNode(0);
ListNode curNode = virtualNode;
// 用于求和
int sum = 0;
while (l1 != null || l2 != null) {
// 计算l1节点与sum的和。
if (l1 != null) {
sum += l1.val;
l1 = l1.next;
}
// 计算l2节点与sum的和。
if (l2 != null) {
sum += l2.val;
l2 = l2.next;
}
// 利用除余来计算个位数的值。
curNode.next = new ListNode(sum % 10);
curNode = curNode.next;
// 计算十位数,如果sum>=10,则值为1;如果sum<10,则值为0。
sum /= 10;
}
// 判断最后一位是否有进位。
if (sum == 1) {
curNode.next = new ListNode(sum);
}
return virtualNode.next;
}
5. 合并:合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
解题思路:
- 利用双指针分别指向两个链表,然后遍历比较两个链表的值,将值较小的节点放入合并后的链表中。
- 利用虚拟节点作为头结点来简化操作。
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if (list1 == null && list2 == null) {
return null;
}
// 构建虚拟节点,方便头结点的操作。
ListNode virtualNode = new ListNode();
ListNode curNode = virtualNode;
while (list1 != null || list2 != null) {
// 两个链表都有值。
if (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
curNode.next = new ListNode(list1.val);
list1 = list1.next;
curNode = curNode.next;
} else {
curNode.next = new ListNode(list2.val);
curNode = curNode.next;
list2 = list2.next;
}
} else if (list1 != null) {
// list1有值,list2已经没有值了,所以将list1剩余部分添加到链表尾部。
curNode.next = list1;
break;
} else if (list2 != null) {
// list2有值,list1已经没有值了,所以将list2剩余部分添加到链表尾部。
curNode.next = list2;
break;
}
}
return virtualNode.next;
}
6. 合并:合并K个升序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
解题思路:
从上一题合并两个有序链表我们可以知道,要将K个有序链表合并成1个,就需要对K个链表的头结点进行值的比较。如果采用遍历的方式进行,效率会相对较低。此时我们可以利用最小堆的特点,即最小的元素在堆顶此时我们只需要将堆顶元素取出并放入合并后的链表中,并将取出的节点的下一个节点放入最小堆中进行比较,如此往复即可合并K个有序链表。
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) {
return null;
}
// 构建最小堆。
PriorityQueue<ListNode> heap = new PriorityQueue(new Comparator<ListNode>() {
@Override
public int compare(ListNode o1, ListNode o2) {
return o1.val - o2.val;
}
});
ListNode virtualNode = new ListNode();
ListNode curNode = virtualNode;
// 先将K个链表的头结点放入堆中。
for (int i=0; i<lists.length; i++) {
if (lists[i] != null) { //这里需要过滤null的值。
heap.offer(lists[i]);
}
}
while (!heap.isEmpty()) {
// 从堆顶取出元素放入合并后的链表中,并将堆顶元素的下一个节点放入堆中。
ListNode pop = heap.poll();
if (pop.next != null) {
heap.offer(pop.next);
}
curNode.next = new ListNode(pop.val);
curNode = curNode.next;
}
return virtualNode.next;
}
7. 删除:删除链表的倒数第 N 个结点
LeetCode 删除链表的倒数第 N 个结点
LeetCode 删除排序链表中的重复元素
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
解题思路:
- 利用虚拟节点作为头结点,避免删除第1个节点引起的操作复杂性。
- 利用快慢指针来实现。
- 让快指针先往前走n步,然后两个指针一起走。当快指针指向的节点为null时,此时慢指针所指向的节点即为要删除的节点。但是由于这是一个单链表,慢指针所指向的节点无法知道它的前一个节点,所以我们需要使用一个变量保存慢指针的前一个节点,方便进行节点的删除操作。
public ListNode removeNthFromEnd(ListNode head, int n) {
if (head == null || n <= 0) {
return head;
}
ListNode virtualNode = new ListNode(0);
virtualNode.next = head;
// 快指针
ListNode fastNode = virtualNode;
// 慢指针
ListNode slowNode = virtualNode;
// 保存慢指针的前一个节点
ListNode preSlowNode = null;
// 结束条件为快指针指向null。
while (fastNode != null) {
if (n == 0) {
// 移动慢指针的指向。
preSlowNode = slowNode;
slowNode = slowNode.next;
} else {
// 快指针先向后走n步。
n--;
}
// 移动快指针的指向。
fastNode = fastNode.next;
}
// 通过慢指针的前一个节点删除慢指针节点。
preSlowNode.next = slowNode.next;
return virtualNode.next;
}
8. 删除:删除排序链表中的重复元素
给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
解题思路:
- 构建虚拟节点,方便操作。
- 保存当前节点的前一个节点,用于比较前后两个节点值是否相同,如果相同,则当前节点继续向后执行。
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode virtualNode = new ListNode();
virtualNode.next = head;
ListNode pre = virtualNode;
ListNode cur = virtualNode;
while (cur != null) {
if (pre != virtualNode && pre.val == cur.val) {
pre.next = cur.next; // 删除节点
} else {
pre = cur;
}
cur = cur.next;
}
return virtualNode.next;
}
9. 删除:删除排序链表中的重复元素 II
给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
解题思路:
- 解法与上一题类似,但是需要跳过所有相同元素的节点,所以需要一个while循环执行此操作。
- 需要注意对 pre 和 pre.next 进行赋值操作的条件判断。
- 如果对 pre 进行赋值,说明 cur 的节点值是唯一的。
- 如果对 pre.next 进行赋值,说明 cur 的节点值不唯一,需要直接跳到下一个与cur 不同值的节点。
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode virtualNode = new ListNode(-1);
virtualNode.next = head;
ListNode pre = virtualNode;
ListNode cur = head;
while (cur != null) {
// 循环跳过相同的元素。
while (cur.next != null && cur.val == cur.next.val) {
cur = cur.next;
}
// 这个条件成立,说明cur指向的节点元素只有一个。
if (pre.next == cur) {
pre = pre.next;
} else {
// 否则,就将pre.next指向cur相同元素的最后一个节点。
pre.next = cur.next;
}
cur = cur.next;
}
return virtualNode.next;
}
10. 环形:环形链表的判断 I
- 给你一个链表的头节点 head ,判断链表中是否有环。
- 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
- 如果链表中存在环 ,则返回 true 。 否则,返回 false 。
解题思路:
- 利用快慢指针,快指针一次走2步,慢指针一次走1步。
- 如果链表有环,必然存在快慢指针指向同一个Node节点。
- 如果快指针值为 null 时,则表示链表无环。
有环:
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head;
// 如果两个指针不指向同一个节点,就一直循环下去,直到快指针指向链表末尾
while (true) {
// 退出条件,fast指针优先走到链表末尾。
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true;
}
}
}
11. 环形:环形链表 II (入环点)
如果单项链表有环,请返回入环点的节点。
解题思路:
- 利用快慢指针,快指针一次走2步,慢指针一次走1步。
- 如果链表有环,必然存在快慢指针指向同一个Node节点。
- 如果快指针值为 null 时,则表示链表无环。
- 当两个指针指向同一个Node节点时,此时将快指针指向链表头,然后让快慢指针每次直走1个节点,当他们再次相遇时,该节点即为入环节点。
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
ListNode slow = head;
ListNode fast = head;
// 这部分与校验链表是否有环一样。
while (true) {
if (fast == null || fast.next == null) {
return null;
}
slow = slow.next;
fast = fast.next.next;
if (fast == slow) break;
}
// 将快指针挪回到链表头,然后与慢指针一起每次向后移动1个节点,当快慢指针指向同一个节点时,该节点即为入环点。
fast = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
// 返回入环点
return fast;
}