算法练习-链表(一)
文章目录
- 算法练习-链表(一)
- 解题技巧
- 1. 实现链表
- 1.1 节点的定义
- 1.2 链表的遍历
- 1.3 节点的查找
- 1.4节点的插入
- 1.4.1 链头插入
- 1.4.2 链尾插入
- 1.4.3 在给定节点之后插入
- 1.5 删除节点
- 1.5.1 删除给定节点之后的节点、
- 1.5.2 删除给定节点
- 1.6 完整代码
- 1.7 优化
- 1.7.1 链尾插入的优化
- 1.7.2 删除给定节点的优化
- 2. 移除链表元素
- 2.1 题目
- 2.2 题解
- 2.2.1 没引入虚拟头节点
- 2.2.2 引入虚拟头节点
- 2.2.3 创建一个新节点,将符合条件的节点串起来(更好理解,但时间和空间复杂度都没变)
- 3. 链表的中间节点
- 3.1 题目
- 3.2 题解
- 4. 合并两个排序链表
- 4.1 题目
- 4.2 题解
- 5. 删除排序链表的重复元素
- 5.1 题目
- 5.2 题解
- 6. 两数相加
- 6.1 题目
- 6.2 题解
- 7. 反转链表
- 7.1 题目
- 7.2 题解
- 8. 回文链表
- 8.1 题目
- 8.2 题解
解题技巧
链表相关的问题都会涉及到遍历,要确定遍历的三要素
- 遍历的结束条件:p == null || p.next == null …
- 指针的初始值: p = head …
- 遍历的核心逻辑
特殊情况的处理:是否需要对头节点、尾节点、空链表做特殊处理
引入虚拟节点:是否可以通过虚拟节点来简化操作
1. 实现链表
1.1 节点的定义
public class Node {
public int data;
public Node next;
public Node (int data, Node next) {
this.data = data;
this.next = next;
}
}
1.2 链表的遍历
private Node head = null;
// 遍历
public void travel(Node head) {
Node p = head;
while (p != null) {
System.out.println(p.data);
p = p.next;
}
}
1.3 节点的查找
// 查找
public Node find(int value) {
Node p = head;
while (p != null) {
if (p.data == value) {
return p;
}
}
return null;
}
1.4节点的插入
1.4.1 链头插入
// 链头插入
public void insertAtHead(int value) {
Node node = new Node(value, null);
node.next = head;
head = node;
}
1.4.2 链尾插入
// 链尾插入
public void InsertAtTail(int value) {
Node node = new Node(value, null);
if (head == null) {
head = node;
} else {
Node p = head;
while (p.next != null) {
p = p.next;
}
p.next = node;
}
}
1.4.3 在给定节点之后插入
// 在给定节点之后插入
public void insertAfter(Node p, int value) {
if (p == null) {
return;
}
Node node = new Node(value, null);
node.next = p.next;
p.next = node;
}
1.5 删除节点
1.5.1 删除给定节点之后的节点、
// 删除给定节点之后的节点
public void deleteNextNode(Node p) {
if (p == null || p.next == null) {
return;
}
p.next = p.next.next;
}
1.5.2 删除给定节点
// 删除给定节点
public Node deleteNode(Node head, Node p) {
if (p == null || head == null) {
return null;
}
// 使用pre保存q前面的节点
Node pre = null;
Node q = head;
while (q != null) {
// 如果q等于p就break,这时pre指向的是q前面的节点
if (p == q) {
break;
}
pre = q;
q = q.next;
}
// 没找到
if (q == null) {
return null;
}
// 删除头节点
if (pre == null) {
head = head.next;
} else { // 删除非空节点
pre.next = pre.next.next;
}
return head;
}
1.6 完整代码
public class NodeTest {
public class Node {
public int data;
public Node next;
public Node (int data, Node next) {
this.data = data;
this.next = next;
}
}
private Node head = null;
// 遍历
public void travel(Node head) {
Node p = head;
while (p != null) {
System.out.println(p.data);
p = p.next;
}
}
// 查找
public Node find(int value) {
Node p = head;
while (p != null) {
if (p.data == value) {
return p;
}
}
return null;
}
// 插入
// 链头插入
public void insertAtHead(int value) {
Node node = new Node(value, null);
node.next = head;
head = node;
}
// 链尾插入
public void InsertAtTail(int value) {
Node node = new Node(value, null);
if (head == null) {
head = node;
} else {
Node p = head;
while (p.next != null) {
p = p.next;
}
p.next = node;
}
}
// 在给定节点之后插入
public void insertAfter(Node p, int value) {
if (p == null) {
return;
}
Node node = new Node(value, null);
node.next = p.next;
p.next = node;
}
// 删除
// 删除给定节点之后的节点
public void deleteNextNode(Node p) {
if (p == null || p.next == null) {
return;
}
p.next = p.next.next;
}
// 删除给定节点
public Node deleteNode(Node head, Node p) {
if (p == null || head == null) {
return null;
}
// 使用pre保存q前面的节点
Node pre = null;
Node q = head;
while (q != null) {
// 如果q等于p就break,这时pre指向的是q前面的节点
if (p == q) {
break;
}
pre = q;
q = q.next;
}
// 没找到
if (q == null) {
return null;
}
// 删除头节点
if (pre == null) {
head = head.next;
} else { // 删除非空节点
pre.next = pre.next.next;
}
return head;
}
}
1.7 优化
1.7.1 链尾插入的优化
- 增加tail指针,时刻指向链表的尾部
private Node head = null;
private Node tail = null;
public void insertAtTail (int value) {
Node node = new Node(value, null);
if (head == null) {
head = node;
tail = node;
} else {
tail.next = node;
tail = node;
}
}
- 引入虚拟头节点,在1的基础上进一步优化。这时可以不用考虑null的情况,以为无论何时,链表都是有节点的
private Node head = new Node();
private Node tail = head;
public void insertAtTail(int value) {
Node node = new Node(value, null);
tail.next = node;
tail = node;
}
1.7.2 删除给定节点的优化
删除给定节点也可以用虚拟头节点进行优化,不用再考虑删除的是头节点的情况
public Node deleteNode(Node head, Node p) {
if (p == null || head == null) {
return null;
}
Node newHead = new Node();
newHead.next = head;
Node pew = newHead;
Node q = head;
while (q != null) {
if (p == q) {
break;
}
pre = q;
q = q.next;
}
// 没找到
if (q == null) {
return head;
}
pre.next = pre.next.next;
return newHead.next;
}
2. 移除链表元素
链接:https://leetcode.cn/problems/remove-linked-list-elements
2.1 题目
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
2.2 题解
要删除一个节点,需要得到删除节点的前驱节点,让前驱节点指向删除节点的下一个节点
所以要先通过遍历得到前驱节点prev
**遍历结束条件为:**前驱节点的下一个节点为null,prev.next == null
**指针的初始值:**prev = head;
遍历的核心逻辑: if prev.next.val == val -> prev.next = prev.next.next
else prev = prev.next
特殊情况:要删除的节点为头节点,可以引入虚拟头节点来简化这种情况的处理
2.2.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 removeElements(ListNode head, int val) {
if (head == null) {
return null;
}
ListNode prev = head;
while (prev.next != null) {
if (prev.next.val == val) {
prev.next = prev.next.next;
} else {
prev = prev.next;
}
}
if (head.val == val) {
head = head.next;
}
return head;
}
}
2.2.2 引入虚拟头节点
/**
* 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 removeElements(ListNode head, int val) {
ListNode newHead = new ListNode();
newHead.next = head;
ListNode prev = newHead;
while (prev.next != null) {
if (prev.next.val == val) {
prev.next = prev.next.next;
} else {
prev = prev.next;
}
}
return newHead.next;
}
}
2.2.3 创建一个新节点,将符合条件的节点串起来(更好理解,但时间和空间复杂度都没变)
/**
* 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 removeElements(ListNode head, int val) {
if(head == null) {
return null;
}
ListNode newHead = new ListNode();
ListNode tail = newHead;
ListNode p = head;
while(p != null) {
ListNode tmp = p.next;
if (p.val != val) {
tail.next = p;
p.next = null;
tail = tail.next;
}
p = tmp;
}
return newHead.next;
}
}
3. 链表的中间节点
链接:https://leetcode.cn/problems/middle-of-the-linked-list
3.1 题目
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例 2:
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
3.2 题解
使用快慢指针,快指针每次移动两个节点,慢指针依次移动一个节点。
当快指针移动到链表尾部,慢指针就处于链表的中间节点
指针初始值:fast = head, slow = head
遍历的结束条件: fast == null && fast.next == null
遍历的核心逻辑: slow = slow.next; fast = fast.next.next
/**
* 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) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
4. 合并两个排序链表
链接:https://leetcode.cn/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof
4.1 题目
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
示例1:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
4.2 题解
创建一个结果链表,将符合条件的链表进行拼接
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
ListNode p1 = l1;
ListNode p2 = l2;
ListNode head = new ListNode();
ListNode tail = head;
while (p1 != null && p2 != null) {
if (p1.val <= p2.val) {
tail.next = p1;
p1 = p1.next;
} else {
tail.next = p2;
p2 = p2.next;
}
tail = tail.next;
}
if (p1 != null) {
tail.next = p1;
}
if (p2 != null) {
tail.next = p2;
}
return head.next;
}
}
5. 删除排序链表的重复元素
链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-list
5.1 题目
给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
示例 1:
输入:head = [1,1,2]
输出:[1,2]
示例 2:
输入:head = [1,1,2,3,3]
输出:[1,2,3]
5.2 题解
使用虚拟头节点,如果后面两个节点的元素相同就删掉后面的节点
如果最后只剩下一个节点则没必要再进行比较
/**
* 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) {
ListNode newHead = new ListNode();
newHead.next = head;
ListNode p = newHead;
while (p.next != null && p.next.next != null) {
if (p.next.val == p.next.next.val) {
p.next = p.next.next;
} else {
p = p.next;
}
}
return newHead.next;
}
}
6. 两数相加
链接:https://leetcode.cn/problems/add-two-numbers
6.1 题目
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
6.2 题解
/**
* 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 addTwoNumbers(ListNode l1, ListNode l2) {
int carry = 0;
ListNode head = new ListNode(0);
ListNode q = head;
while (l1 != null || l2 != null || carry != 0) {
int val1 = l1 != null ? l1.val : 0;
int val2 = l2 != null ? l2.val : 0;
int sum = val1 + val2 + carry;
carry = sum / 10;
ListNode node = new ListNode(sum % 10);
q.next = node;
q = q.next;
if (l1 != null) {
l1 = l1.next;
}
if (l2 != null) {
l2 = l2.next;
}
}
return head.next;
}
}
7. 反转链表
链接:https://leetcode.cn/problems/reverse-linked-list
7.1 题目
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]
7.2 题解
/**
* 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) {
ListNode cur = null;
ListNode pre = head;
while (pre != null) {
ListNode tmp = pre.next;
pre.next = cur;
cur = pre;
pre = tmp;
}
return cur;
}
}
8. 回文链表
链接:https://leetcode.cn/problems/palindrome-linked-list
8.1 题目
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
示例 1:
输入:head = [1,2,2,1]
输出:true
示例 2:
输入:head = [1,2]
输出:false
8.2 题解
/**
* 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;
ListNode low = head;
if (head == null) {
return true;
}
while(fast != null && fast.next != null) {
low = low.next;
fast = fast.next.next;
}
ListNode q = reverse(low);
while(q != null) {
if (head.val != q.val) {
return false;
}
head = head.next;
q = q.next;
}
return true;
}
public ListNode reverse(ListNode head) {
ListNode q = null;
ListNode p = head;
while(p != null) {
ListNode next = p.next;
p.next = q;
q = p;
p = next;
}
return q;
}
}