算法总结——链表
链表的基本原理
一条链表不需要一整块连续的内存空间存储元素。链表的元素可以分散在内存空间的任意地方,通过每个节点上的 next, prev
指针,将零散的内存块串联起来形成一个链式结构。
-
数组的优点包括:
- 随机访问性强,可以快速查找元素,因为数组的元素是连续存储的,可以直接通过索引访问。
- 查找速度快,因为数组的查找操作不需要从头开始,而是直接定位到指定索引。
数组的缺点包括:
- 对内存的要求高,需要连续的内存空间,这可能导致在内存碎片化严重的环境中难以使用。
- 插入和删除的效率比较低,因为这通常需要移动其他元素以保持数组的连续性。
- 大小固定,动态拓展性差,可能浪费内存,因为数组在定义时需要预先指定大小。
-
链表的优点包括:
- 对内存的要求低,链表中的元素不需要连续存储在内存中。
- 大小可以不固定,内存的利用率比较高,适合动态添加或删除元素。
- 插入和删除方便,因为这只需要修改元素中的指针,不需要移动其他元素。
链表的缺点包括:
- 查找效率低,不可以随机查找,每次查找必须从头开始。
- 不能直接访问任意位置的元素,需要从第一个元素开始遍历到目标元素。
单链表:
定义:
class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
// 输入一个数组,转换为一条单链表
ListNode createLinkedList(int[] arr) {
if (arr == null || arr.length == 0) {
return null;
}
ListNode head = new ListNode(arr[0]);
ListNode cur = head;
for (int i = 1; i < arr.length; i++) {
cur.next = new ListNode(arr[i]);
cur = cur.next;
}
return head;
}
查找/修改:
// 遍历单链表
for (ListNode p = head; p != null; p = p.next) {
//进行操作
System.out.println(p.val);
}
增加:
分别为:链表头部、尾部、中间任意位置进行增加。
// 在单链表头部插入一个新节点 0
ListNode newHead = new ListNode(0);
newHead.next = head;
head = newHead;
// 链表变成了 0 -> 1 -> 2 -> 3 -> 4 -> 5
// 在单链表尾部插入一个新节点 6
ListNode p = head;
// 先走到链表的最后一个节点
while (p.next != null) {
p = p.next;
}
// p 就是链表的最后一个节点
// 在 p 后面插入新节点
p.next = new ListNode(6);
// 链表变成了 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6
// 在第 3 个节点后面插入一个新节点 66
// 先要找到前驱节点,即第 3 个节点
ListNode p = head;
for (int i = 0; i < 2; i++) {
p = p.next;
}
// 此时 p 指向第 3 个节点
// 组装新节点的后驱指针
ListNode newNode = new ListNode(66);
newNode.next = p.next;
// 插入新节点
p.next = newNode;
// 链表变成了 0 -> 1 -> 2 -> 3 -> 66 -> 4 -> 5 -> 6
删除:
分别为:删除首结点、尾结点、中间任意结点
// 创建一条单链表
ListNode head = createLinkedList(new int[]{1, 2, 3, 4, 5});
// 删除头结点
head = head.next;
// 链表变成了 2 -> 3 -> 4 -> 5
// 删除尾节点
ListNode p = head;
// 找到倒数第二个节点
while (p.next.next != null) {
p = p.next;
}
// 此时 p 指向倒数第二个节点
// 把尾节点从链表中摘除
p.next = null;
// 现在链表变成了 1 -> 2 -> 3 -> 4
// 删除第 4 个节点,要操作前驱节点
ListNode p = head;
for (int i = 0; i < 2; i++) {
p = p.next;
}
// 此时 p 指向第 3 个节点,即要删除节点的前驱节点
// 把第 4 个节点从链表中摘除
p.next = p.next.next;
// 现在链表变成了 1 -> 2 -> 3 -> 5
双链表:
定义:
class DoublyListNode {
int val;
DoublyListNode next, prev;
DoublyListNode(int x) { val = x; }
}
DoublyListNode createDoublyLinkedList(int[] arr) {
if (arr == null || arr.length == 0) {
return null;
}
DoublyListNode head = new DoublyListNode(arr[0]);
DoublyListNode cur = head;
// for 循环迭代创建双链表
for (int i = 1; i < arr.length; i++) {
DoublyListNode newNode = new DoublyListNode(arr[i]);
cur.next = newNode;
newNode.prev = cur;
cur = cur.next;
}
return head;
}
查找/修改:
// 创建一条双链表
DoublyListNode head = createDoublyLinkedList(new int[]{1, 2, 3, 4, 5});
// 从头遍历双链表
for (DoublyListNode p = head; p != null; p = p.next) {
//进行操作
System.out.println(p.val);
}
// 从尾遍历双链表(假设我们有尾节点的引用 tail)
for (DoublyListNode p = tail; p != null; p = p.prev) {
//进行操作
System.out.println(p.val);
}
增加:
头插法、尾插法、中间插入
// 创建一条双链表
DoublyListNode head = createDoublyLinkedList(new int[]{1, 2, 3, 4, 5});
// 在双链表头部插入新节点 0
DoublyListNode newHead = new DoublyListNode(0);
newHead.next = head;
head.prev = newHead;
head = newHead;
// 现在链表变成了 0 -> 1 -> 2 -> 3 -> 4 -> 5
// 创建一条双链表
DoublyListNode head = createDoublyLinkedList(new int[]{1, 2, 3, 4, 5});
DoublyListNode tail = head;
// 先走到链表的最后一个节点
while (tail.next != null) {
tail = tail.next;
}
// 在双链表尾部插入新节点 6
DoublyListNode newNode = new DoublyListNode(6);
tail.next = newNode;
newNode.prev = tail;
// 更新尾节点引用
tail = newNode;
// 现在链表变成了 1 -> 2 -> 3 -> 4 -> 5 -> 6
// 创建一条双链表
DoublyListNode head = createDoublyLinkedList(new int[]{1, 2, 3, 4, 5});
// 在第 3 个节点后面插入新节点 66
// 找到第 3 个节点
DoublyListNode p = head;
for (int i = 0; i < 2; i++) {
p = p.next;
}
// 组装新节点
DoublyListNode newNode = new DoublyListNode(66);
newNode.next = p.next;
newNode.prev = p;
// 插入新节点
p.next.prev = newNode;
p.next = newNode;
// 现在链表变成了 1 -> 2 -> 3 -> 66 -> 4 -> 5
删除:
// 创建一条双链表
DoublyListNode head = createDoublyLinkedList(new int[]{1, 2, 3, 4, 5});
// 删除头结点
DoublyListNode toDelete = head;
head = head.next;
head.prev = null;
// 清理已删除节点的指针
toDelete.next = null;
// 现在链表变成了 2 -> 3 -> 4 -> 5
// 创建一条双链表
DoublyListNode head = createDoublyLinkedList(new int[]{1, 2, 3, 4, 5});
// 删除尾节点
DoublyListNode p = head;
// 找到尾结点
while (p.next != null) {
p = p.next;
}
// 现在 p 指向尾节点
// 把尾节点从链表中摘除
p.prev.next = null;
// 把被删结点的指针都断开是个好习惯(可选)
p.prev = null;
// 现在链表变成了 1 -> 2 -> 3 -> 4
// 创建一条双链表
DoublyListNode head = createDoublyLinkedList(new int[]{1, 2, 3, 4, 5});
// 删除第 4 个节点
// 先找到第 3 个节点
DoublyListNode p = head;
for (int i = 0; i < 2; i++) {
p = p.next;
}
// 现在 p 指向第 3 个节点,我们它后面那个节点摘除出去
DoublyListNode toDelete = p.next;
// 把 toDelete 从链表中摘除
p.next = toDelete.next;
toDelete.next.prev = p;
// 把 toDelete 的前后指针都置为 null 是个好习惯(可选)
toDelete.next = null;
toDelete.prev = null;
// 现在链表变成了 1 -> 2 -> 3 -> 5
虚拟头结点
使用虚拟头结点有以下几个主要的好处:
- 简化边界条件处理:当合并两个链表时,如果其中一个链表为空,需要特殊处理头结点的情况。使用虚拟头结点可以避免这种情况,统一处理所有节点的添加。
- 减少代码量和复杂度:虚拟头结点可以帮助我们简化代码逻辑,减少需要处理的边界条件。这样可以得到更加简洁、易读的代码。
- 统一操作:使用虚拟头结点,我们可以将所有节点的添加操作统一起来,不需要单独处理头结点。这样可以提高代码的可读性和可维护性。
- 返回结果更方便:使用虚拟头结点,我们最终返回的就是合并后链表的真实头结点,即 dummy.next。这比直接返回头结点更加简单。
- 适用于各种链表操作:虚拟头结点技巧不仅适用于合并两个有序链表,在其他链表操作中如反转链表、删除节点等也都能发挥作用。
力扣第 21 题「合并两个有序链表」
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
/**
* 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 mergeTwoLists(ListNode list1, ListNode list2) {
ListNode res = new ListNode(-1), p = res;
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 res.next;
}
}
//runtime:0 ms
//memory:40.7 MB
双指针
力扣第 19 题「删除链表的倒数第 N 个结点」
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。(定义两个指针,让两个指针间位置相距n)
//leetcode submit region begin(Prohibit modification and deletion)
/**
* 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 removeNthFromEnd(ListNode head, int n) {
ListNode xn = new ListNode(-1);
xn.next = head;
ListNode p = xn;
//实际操作的是xn链表,而非head链表
ListNode p1 = xn;
//注意循环终止位置
for (int i = 0; i < n + 1; i++) {
xn = xn.next;
}
while (xn != null) {
xn = xn.next;
p = p.next;
}
p.next = p.next.next;
//返回时也要返回实际操作链表的引用
return p1.next;
}
}
//leetcode submit region end(Prohibit modification and deletion)
快慢指针
力扣第 876 题「链表的中间结点」
给你单链表的头结点 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 middleNode(ListNode head) {
if(head == null) return null;
ListNode k,m;
//初始起点一样,进入判断后开始走,k要快两步
m = head;
k = head;
while(k!=null && k.next!=null){
m = m.next;
k = k.next.next;
}
return m;
}
}
//runtime:0 ms
//memory:40.3 MB
链表互补
力扣第 160 题「相交链表」
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
//leetcode submit region begin(Prohibit modification and deletion)
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode a = headA;
ListNode b = headB;
//当两个指针相等时(要么两个节点相等,要么都走到拼接后的链表末尾)
while (a != b) {
a = (a != null) ? a.next : headB;
b = (b != null) ? b.next : headA;
}
return a;
}
}
//leetcode submit region end(Prohibit modification and deletion)