链表
链表基础操作:增删改查
size维护好链表,作为非法增删改查的判断依据。
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
class MyLinkedList {
int size;
ListNode head; // sentinel node as pseudo-head
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}
/** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */
public int get(int index) {
// if index is invalid
if (index < 0 || index >= size) return -1;
ListNode curr = head;
// index steps needed
// to move from sentinel node to wanted index
for(int i = 0; i < index + 1; ++i) curr = curr.next;
return curr.val;
}
/** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */
public void addAtHead(int val) {
addAtIndex(0, val);
}
/** Append a node of value val to the last element of the linked list. */
public void addAtTail(int val) {
addAtIndex(size, val);
}
/** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */
public void addAtIndex(int index, int val) {
// If index is greater than the length,
// the node will not be inserted.
if (index > size) return;
// [so weird] If index is negative,
// the node will be inserted at the head of the list.
if (index < 0) index = 0;
++size;
// find predecessor of the node to be added
ListNode pred = head;
for(int i = 0; i < index; ++i) pred = pred.next;
// node to be added
ListNode toAdd = new ListNode(val);
// insertion itself
toAdd.next = pred.next;
pred.next = toAdd;
}
/** Delete the index-th node in the linked list, if the index is valid. */
public void deleteAtIndex(int index) {
// if the index is invalid, do nothing
if (index < 0 || index >= size) return;
size--;
// find predecessor of the node to be deleted
ListNode pred = head;
for(int i = 0; i < index; ++i) pred = pred.next;
// delete pred.next
pred.next = pred.next.next;
}
}
虚拟头结点
引导
链表操作的两种方式:
「直接使用原来的链表来进行删除操作。」
「设置一个虚拟头结点在进行删除操作。」
移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。
所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。
「可以设置一个虚拟头结点」,这样原链表的所有节点就都可以按照统一的方式进行移除了。
例题1:断链
要点:
- sentinel设置:sentinel.next = head;return sentinel.next。防止头部断链。
- 断链参与者:prev、current、next(可以不要)。初始化:prev = sentinel;cur = 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 removeElements(ListNode head, int val) {
ListNode sentinel = new ListNode(0);
sentinel.next = head;
ListNode prev = sentinel, curr = head; // 只有next,因此需要两个node执行
while (curr != null) {
if (curr.val == val) prev.next = curr.next;
else prev = curr;
curr = curr.next;
}
return sentinel.next;
}
}
双指针/节点的操作
与数组双指针比起来,多了断链的操作。
一般双指针类型有:
- fast与slow:处理指定位置
- head与tail:返回新链表类型。一般head加哨兵,cur就是tail
基础:反转链表
LeetCode题目206,最基础的双指针/节点处理
要点:反转与断链相似
- 断链参与者:prev、current、next(可以不要)。初始化:prev = null;cur = head;返回prev
- 断链过程中的引用移动
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
}
快慢指针:删除倒数第N个节点
题解链接
要点:断链操作 + 快慢指针
- sentinel设置:sentinel.next = head;return sentinel.next。防止头部断链。
- 快慢指针fast、slow。其中快指针通常为了遍历到底,慢指针则是找到对应位置。
- 断链参与者:prev、current、next(可以不要)。初始化:prev = sentinel;cur = head;
- 断链过程中的引用移动
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode sential = new ListNode(-1);
sential.next = head;
ListNode fast = head;
ListNode slow = head;
// 快指针比慢指针多走n步
for(int i = 0; i < n; i++) {
// 因为题目条件限制,移动N步一定不会导致越界
fast = fast.next;
}
// 为了断链,必须记录slow的前一位
ListNode prev = sential;
// fast到end,slow就是倒数第n个
while(fast != null) {
slow = slow.next;
fast = fast.next;
prev = prev.next;
}
prev.next = slow.next;
return sential.next;
}
}
两个链表操作
两数相加:基本的head、tail操作
数字操作:
- /10取进位与%10取留下的数字
返回新链表:
- head与tail:返回新链表类型。head加哨兵,cur就是tail
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
int carry = 0;
// 哨兵
ListNode sentinel = new ListNode();
ListNode cur = sentinel;
while(l1 != null || l2 != null || carry != 0) {
int n1 = 0;
int n2 = 0;
// 链表到末尾不能继续next
if(l1 != null) {
n1 = l1.val;
l1 = l1.next;
}
if(l2 != null) {
n2 = l2.val;
l2 = l2.next;
}
int sum = carry + n1 + n2;
carry = sum / 10;
// 每一步都是为下一位赋当前值,保证最末位的下一位为null
cur.next = new ListNode();
cur.next.val = sum % 10;
cur = cur.next;
}
return sentinel.next;
}
两数相加2
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
Deque<Integer> stack1 = new ArrayDeque<Integer>();
Deque<Integer> stack2 = new ArrayDeque<Integer>();
while (l1 != null) {
stack1.push(l1.val);
l1 = l1.next;
}
while (l2 != null) {
stack2.push(l2.val);
l2 = l2.next;
}
int carry = 0;
ListNode ans = null;
while (!stack1.isEmpty() || !stack2.isEmpty() || carry != 0) {
int a = stack1.isEmpty() ? 0 : stack1.pop();
int b = stack2.isEmpty() ? 0 : stack2.pop();
int cur = a + b + carry;
carry = cur / 10;
cur %= 10;
ListNode curnode = new ListNode(cur);
curnode.next = ans;
ans = curnode;
}
return ans;
}
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 先进后出的特性,satck完成链的反转
Stack<Integer> stack1 = buildStack(l1);
Stack<Integer> stack2 = buildStack(l2);
Stack<Integer> stack3 = new Stack();
// 构建stack的结果
int carry = 0;
while(!stack1.isEmpty() || !stack2.isEmpty() || carry != 0) {
int add1 = 0;
int add2 = 0;
if(!stack1.isEmpty()) {
add1 = stack1.pop();
}
if(!stack2.isEmpty()) {
add2 = stack2.pop();
}
int sum = add1 + add2 + carry;
carry = sum / 10;
// 先进后出的特性,计算从末尾逐步到头部。stack3出的顺序是从头部到末尾
stack3.push(sum % 10);
}
// stack build ListNode,stack3出的顺序是从头部到末尾
ListNode sentinel = new ListNode();
ListNode cur = sentinel;
while(!stack3.isEmpty()) {
cur.next = new ListNode();
cur.next.val = stack3.pop();
cur = cur.next;
}
return sentinel.next;
}
private Stack<Integer> buildStack(ListNode l) {
Stack<Integer> stack = new Stack<>();
while(l != null) {
stack.push(l.val);
l = l.next;
}
return stack;
}
}
作者:LeetCode-Solution
链接:https://leetcode.cn/problems/add-two-numbers-ii/solution/liang-shu-xiang-jia-ii-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
高频:LRU缓存。map存储,链表维护LRU顺序
无赖写法,直接利用LinkedHashMap的能力。
LinkedHashMap是HashMap的子类,但是内部还有一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于双向链表中。LinkedHashMap支持两种顺序插入顺序 、 访问顺序(即遍历map时的顺序)
【插入顺序】:先添加的在前面,后添加的在后面。修改操作不影响顺序
【访问顺序】:所谓访问指的是get/put操作,对一个键执行get/put操作后,其对应的键值对会移动到链表末尾,所以最末尾的是最近访问的,最开始的是最久没有被访问的,这就是访问顺序。(头部结点为最不常访问结点)
LinkedHashMap有5个构造方法,其中4个都是按插入顺序,只有一个是可以指定按访问顺序:(可以用于实现LRU缓存)
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
// 其中参数accessOrder就是用来指定是否按访问顺序,如果为true,就是访问顺序。
class LRUCache extends LinkedHashMap<Integer, Integer>{
private int capacity;
public LRUCache(int capacity) {
// 设置为true即开启LRU缓存
super(capacity, 0.75F, true);
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
@Override
// 必须实现,否则无法移除
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}
public class LRUCache {
class DLinkedNode {
// 维持内部的LRU顺序,下面的map只是get、put存储的内存结构
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size; // 动态容量
private int capacity; // 固定的容量
private DLinkedNode head, tail; // head与tail,tail尾缀,head是第一个节点
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}
排序链表:归并排序(递归 + 快慢指针选中间节点 + 合并两个有序链表)
https://leetcode.cn/problems/sort-list/
通过递归实现链表归并排序,有以下两个环节:
分割 cut 环节:
找到当前链表中点,并从中点将链表断开(以便在下次递归 cut 时,链表片段拥有正确边界);
我们使用 fast,slow 快慢双指针法,奇数个节点找到中点,偶数个节点找到中心左边的节点。
找到中点 slow 后,执行 slow.next = None 将链表切断。
递归分割时,输入当前链表左端点 head 和中心节点 slow 的下一个节点 tmp(因为链表是从 slow 切断的)。
cut 递归终止条件: 当head.next == None时,说明只有一个节点了,直接返回此节点。
合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。
双指针法合并,建立辅助ListNode h 作为头部。
设置两指针 left, right 分别指向两链表头部,比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。
返回辅助ListNode h 作为头部的下个节点 h.next。
时间复杂度 O(l + r),l, r 分别代表两个链表长度。
class Solution {
public ListNode sortList(ListNode head) {
// 1、递归结束条件,拆到一个节点为止
if (head == null || head.next == null) {
return head;
}
// 2、找到链表中间节点并断开链表(直接拆分) & 递归下探。分的不太均匀,问题也不大
ListNode midNode = middleNode(head);
ListNode rightHead = midNode.next;
// 一定要断链
midNode.next = null;
ListNode left = sortList(head);
ListNode right = sortList(rightHead);
// 3、当前层业务操作(合并2个有序链表:无论是一个节点的还是多个节点出来的)
return mergeTwoLists(left, right);
}
// 找到链表中间节点(876. 链表的中间结点),快慢指针找到中间节点
private ListNode middleNode(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode slow = head;
ListNode fast = head.next.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// 合并两个有序链表(21. 合并两个有序链表),这个是最最最基础的
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head= new ListNode(-1); // 哨兵,真正的结果是head.next
ListNode tail = head;
while(l1 != null && l2 != null) {
// 多个链表的三个坐标
if(l1.val < l2.val) {
tail.next = l1;
l1 = l1.next;
} else {
tail.next = l2;
l2 = l2.next;
}
tail = tail.next;
}
tail.next = l1 != null ? l1 : l2;
return head.next;
}
同类问题:合并K个有序链表
最朴素的方法:用一个变量 ans 来维护以及合并的链表,第 ii 次循环把第 ii 个链表和 ans 合并,答案保存到 ans 中。
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode ans = null;
for (int i = 0; i < lists.length; ++i) {
ans = mergeTwoLists(ans, lists[i]);
}
return ans;
}
}
实际上还是归并排序的问题,是其中的一个子规模问题:
即不是深入到单个节点, 而是部分节点已经开始合并为一个list了,接着继续向上排序即可。
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
return merge(lists, 0, lists.length - 1);
}
public ListNode merge(ListNode[] lists, int l, int r) {
// 将每个list视为一个单节点,l、r自然的下标
if (l == r) {
return lists[l];
}
if (l > r) {
return null;
}
int mid = (l + r) >> 1;
// 合并两个链表
return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
}
public ListNode mergeTwoLists(ListNode a, ListNode b) {
if (a == null || b == null) {
return a != null ? a : b;
}
ListNode head = new ListNode(0);
ListNode tail = head, aPtr = a, bPtr = b;
while (aPtr != null && bPtr != null) {
if (aPtr.val < bPtr.val) {
tail.next = aPtr;
aPtr = aPtr.next;
} else {
tail.next = bPtr;
bPtr = bPtr.next;
}
tail = tail.next;
}
tail.next = (aPtr != null ? aPtr : bPtr);
return head.next;
}
}
环形链表1、2
存在环的证明:快慢指针重合即有环;直接到末尾即无环
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
找到进入的节点:head与slow的遇见
// https://leetcode-cn.com/problems/linked-list-cycle-ii/solution/huan-xing-lian-biao-ii-by-leetcode-solution/
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head, fast = head;
while (fast != null) {
slow = slow.next;
if (fast.next != null) {
fast = fast.next.next;
} else {
return null;
}
if (fast == slow) {
ListNode ptr = head;
// 寻找进入节点的方法,head + slow遇见
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
}
return null;
}
}