目录
链表
一 基本数据结构
/* 基本的单链表节点 */
class ListNode {
int val;
ListNode next;
}
1.基本的操作
1.1 遍历链表
# for循环遍历
void traverse(ListNode head) {
for (ListNode p = head; p != null; p = p.next) {
// 迭代访问 p.val
}
}
# while循环遍历
while(head != null){
//操作
head = head.next;
}
1.2新建链表 并赋值头节点
ListNode l3 = new ListNode(0);
ListNode l4 = l3;
return l3.next();
1.3 尾插法
head.next = new ListNode(val);
head = head.next;
1.4 头插法
// 创建一条单链表
ListNode head = createLinkedList(new int[]{1, 2, 3, 4, 5});
// 在单链表头部插入一个新节点 0
ListNode newHead = new ListNode(0);
newHead.next = head;
head = newHead;
// 现在链表变成了 0 -> 1 -> 2 -> 3 -> 4 -> 5
1.5 链表中间插入
先找到要插入位置的前驱节点,然后操作前驱节点把新节点插入进去:
// 创建一条单链表
ListNode head = createLinkedList(new int[]{1, 2, 3, 4, 5});
// 在第 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;
// 现在链表变成了 1 -> 2 -> 3 -> 66 -> 4 -> 5
1.6 删除节点
还是找前驱:
// 创建一条单链表
ListNode head = createLinkedList(new int[]{1, 2, 3, 4, 5});
// 删除第 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
2.进阶操作
2.1 K个链表合并(二叉堆的应用)
注意定义二叉堆的排序规则即可
注意:第一个for存入的head只是头节点!!!每次poll之后要把头节点的后继节点存进去!!!
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> pq = new PriorityQueue<>((node1,node2)->(node1.val-node2.val));
for(ListNode head:lists){
if(head!=null){
pq.offer(head);
}
}
ListNode l1 = new ListNode(-1);
ListNode head1 = l1;
while(!pq.isEmpty()){
ListNode node = pq.poll();
head1.next = node;
head1 = head1.next;
if(node.next!=null){
pq.offer(node.next);
}
}
return l1.next;
}
}
2.2 双指针(经典例子 如何一次遍历找到链表倒数第k个元素?)
首先,我们先让一个指针 p1 指向链表的头节点 head,然后走 k 步:
现在的 p1,只要再走 n - k 步,就能走到链表末尾的空指针了对吧?
趁这个时候,再用一个指针 p2 指向链表头节点 head:
接下来就很显然了,让 p1 和 p2 同时向前走,p1 走到链表末尾的空指针时前进了 n - k 步,p2 也从 head 开始前进了 n - k 步,停留在第 n - k + 1 个节点上,即恰好停链表的倒数第 k 个节点上:
代码实现:(查找链表倒数第k个节点)
// 返回链表的倒数第 k 个节点
ListNode findFromEnd(ListNode head, int k) {
ListNode p1 = head;
// p1 先走 k 步
for (int i = 0; i < k; i++) {
p1 = p1.next;
}
ListNode p2 = head;
// p1 和 p2 同时走 n - k 步
while (p1 != null) {
p2 = p2.next;
p1 = p1.next;
}
// p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点
return p2;
}
例题:删除链表倒数第k个节点
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode head1 = new ListNode(-1);
head1.next = head;
ListNode p1 = head1;
ListNode p2 = head1;
//找到倒数第n+1个节点 即第n个节点的前驱 进行删除
for(int i=0;i<n+1;i++){
p1 = p1.next;
}
while(p1!=null){
p1 = p1.next;
p2 = p2.next;
}
p2.next = p2.next.next;
return head1.next;
}
}
例题2 找到链表的中间节点
class Solution {
public ListNode middleNode(ListNode head) {
ListNode head1 = new ListNode(-1);
head1 = head;
int length = 0;
while(head1.next!= null){
head1 = head1.next;
length++;
}
int n = length/2 + 1;
//开始双指针操作
ListNode head2 = new ListNode(-1);
head2 = head;
ListNode p1 = head2;
ListNode p2 = head2;
for(int i = 0;i<n;i++){
p1 = p1.next;
}
while(p1!=null){
p1 = p1.next;
p2 = p2.next;
}
return p2;
}
}
2.3 判断链表是否包含环(快慢指针的进阶操作)
每当慢指针 slow 前进一步,快指针 fast 就前进两步。
如果 fast 最终遇到空指针,说明链表中没有环;如果 fast 最终和 slow 相遇,那肯定是 fast 超过了 slow 一圈,说明链表中含有环。
具体实现:
boolean hasCycle(ListNode head) {
// 快慢指针初始化指向 head
ListNode slow = head, fast = head;
// 快指针走到末尾时停止
while (fast != null && fast.next != null) {
// 慢指针走一步,快指针走两步
slow = slow.next;
fast = fast.next.next;
// 快慢指针相遇,说明含有环
if (slow == fast) {
return true;
}
}
// 不包含环
return false;
}
例题:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
思路:首先让快指针走2下,慢指针走一下,那么相遇时假设慢指针走了k步,那么快指针一定走了k+k步:
接下来,假设相遇点到环的起始点的距离为m,那么快指针只需要再走k-m步就可以到达环的起始点,那k-m咋计算呢?
接下来就是应用上一小节的双指针,由下图可以得到,慢指针走的k减去相遇点到起始点的距离m就是k-m,因此只需要把慢指针放回head,快慢指针一起走,即可找到k-m个节点,即起始点。
代码:
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode p1 = head;
ListNode p2 = head;
while(p1 != null && p1.next != null){
p1 = p1.next.next;
p2 = p2.next;
if(p1 == p2){
break;
}
}
if(p1 == null || p1.next == null){
return null;
}
p2 = head;
while(p1 != p2){
p1 = p1.next;
p2 = p2.next;
}
return p1;
}
}
2.4 例题:相交链表找交点
思路:也就是考虑怎么让两个指针同时走到c1呢?
设a1到交点距离为A,b1到交点距离为B,c1到终点距离为C
考虑:如果a和b同时出发,那么当a到达终点c3时,a走了A+C步,b还差B-A步就可以到达终点
接着考虑:如果再让a从b1出发的话,那么b到达终点后,a走了B-A步,a距离交点为A步,所以再让b从a1出发,a和b遇到的点即为交点
代码:
ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// p1 指向 A 链表头结点,p2 指向 B 链表头结点
ListNode p1 = headA, p2 = headB;
while (p1 != p2) {
// p1 走一步,如果走到 A 链表末尾,转到 B 链表
if (p1 == null) p1 = headB;
else p1 = p1.next;
// p2 走一步,如果走到 B 链表末尾,转到 A 链表
if (p2 == null) p2 = headA;
else p2 = p2.next;
}
return p1;
}
2.5链表翻转(这个做不出来真的丢人)
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
思路以及代码:
做链表的翻转,千万别想着一个节点一个结点去换位置,之间改指针就行了啊
顺便采用分治的思想看图吧
因此就可以写出代码:
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode last = reverseList(head.next);
head.next.next = head;
head.next = null;
return last;
}
}
3 其他应用
3.1 LRU实现
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
示例:
输入
[“LRUCache”, “put”, “put”, “get”, “put”, “get”, “put”, “get”, “get”, “get”]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
思路以及代码:
1、显然 cache 中的元素必须有时序,以区分最近使用的和久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾位置。
2、我们要在 cache 中快速找某个 key 是否已存在并得到对应的 val;
3、每次访问 cache 中的某个 key,需要将这个元素变为最近使用的,也就是说 cache 要支持在任意位置快速插入和删除元素。
那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表 LinkedHashMap。
Put的具体实现逻辑如下:
class LRUCache {
int cap;
LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<>();
public LRUCache(int capacity) {
this.cap = capacity;
}
public int get(int key) {
if (!cache.containsKey(key)) {
return -1;
}
// 将 key 变为最近使用
makeRecently(key);
return cache.get(key);
}
public void put(int key, int val) {
if (cache.containsKey(key)) {
// 修改 key 的值
cache.put(key, val);
// 将 key 变为最近使用
makeRecently(key);
return;
}
if (cache.size() >= this.cap) {
// 链表头部就是最久未使用的 key
int oldestKey = cache.keySet().iterator().next();
cache.remove(oldestKey);
}
// 将新的 key 添加链表尾部
cache.put(key, val);
}
private void makeRecently(int key) {
int val = cache.get(key);
// 删除 key,重新插入到队尾
cache.remove(key);
cache.put(key, val);
}
}
4 例题
4.1 删除排序链表中的重复元素(快慢指针)
给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
示例 1:
输入:head = [1,1,2]
输出:[1,2]
示例 2:
输入:head = [1,1,2,3,3]
输出:[1,2,3]
思路以及代码:
思路非常简单,如下图所示:
两个指针,fast向前走,当遇到和slow的值不相同的结点,就让slow的next指向fast,并且slow指向fast的结点,以此类推,直至结尾,就可以构成一个新的链表
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null){
return null;
}
ListNode slow = head;
ListNode fast = head;
while(fast!=null){
if(fast.val != slow.val){
slow.next = fast;
slow = fast;
}
fast = fast.next;
}
//别忘记把最后一个结点的next指向null
slow.next = null;
return head;
}
}
4.2 删除排序链表中的重复元素 II
给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
示例 1:
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
示例 2:
输入:head = [1,1,1,2,3]
输出:[2,3]
思路以及代码:
如果只让你把多于的重复元素去掉,那么快慢指针可以搞定,但这道题要求你把存在重复的元素全都去掉,一个简单粗暴的解法就是借助像哈希表这样的数据结构记录哪些节点重复了,然后去掉它们。
不过这道题输入的链表是有序的,这意味着重复元素都靠在一起,其实不用额外的空间复杂度来辅助,用两个指针就可以达到去重的目的.
具体思路就是:新建个链表用于存储非重复元素,指针负责这个存储工作,指针q负责遍历原来的链表,遇到重复元素,就一直向前走直到跳过了所有重复元素;遇到非重复元素就之间接到新链表后面即可:
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode newListNode = new ListNode(-1);
ListNode p = newListNode;
ListNode q = head;
while(q!=null){
//假如发现重复结点,就把他们全部跳过
if(q.next!=null && q.val == q.next.val){
while(q.next != null && q.val == q.next.val){
q = q.next;
}
q = q.next;
//如果跳过重复元素后链表遍历完了,那新链表也没有新元素了
if(q == null){
p.next = null;
}
}else{
//假如没有遇到重复元素,之间把非重复元素接到新链表上
p.next = q;
p = p.next;
q = q.next;
}
}
return newListNode.next;
}
}
4.3 K 个一组翻转链表(翻转链表+链表递归)
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
示例 2:
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
思路以及代码:
首先,我们要先考虑给定链表头结点,如何反转整个链表?
这个问题需要三个结点,例如A->B->C->null
怎么翻转他们呢? 先让A指向null,再让B指向A,再让C指向B即可
我们大概看下代码:
//翻转[a,b)的元素
public ListNode reverse(ListNode a,ListNode b){
ListNode pre = null;
ListNode cur = a;
ListNode nxt = a;
while(cur != b){
nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
那么之后如何将每个反转后的代码链接呢?
链表是一种兼具递归和迭代性质的数据结构,认真思考一下可以发现这个问题具有递归性质。
什么叫递归性质?直接上图理解,比如说我们对这个链表调用 reverseKGroup(head, 2),即以 2 个节点为一组反转链表:
如果我设法把前 2 个节点反转,那么后面的那些节点怎么处理?后面的这些节点也是一条链表,而且规模(长度)比原来这条链表小,这就叫子问题。我们只需要递归的翻转后面的链表,并且进行连接即可。
递归过程就不进行展开了
代码:
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if(head == null){
return null;
}
ListNode a,b;
a = b = head;
//剩余元素不足K个就停止
for(int i = 0;i<k;i++){
if(b == null){
return head;
}
b = b.next;
}
//翻转[a,b) 其中得到的链表头节点为newNode,尾结点为a
ListNode newNode = reverse(a,b);
//将第一个翻转好的结点和后面反转好的结点链接
a.next = reverseKGroup(b,k);
return newNode;
}
//翻转[a,b)的元素
public ListNode reverse(ListNode a,ListNode b){
ListNode pre = null;
ListNode cur = a;
ListNode nxt = a;
while(cur != b){
nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
}
4.4 排序链表
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
示例 1:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
思路以及代码:
本题使用归并排序,基本逻辑如下:
那么如何实现呢?
- 找到链表中点:快慢指针,快走2,慢走1,快到了尾结点,则慢指针刚好指向中点
- 两个有序链表的排序:新建一个结点,依次从两个链表取出最小的,插入,之后剩余的直接尾插
因此,总代码如下:
class Solution {
public ListNode sortList(ListNode head) {
if(head == null || head.next == null){
return head;
}
//找到中点
ListNode slow = head;
ListNode fast = head;
ListNode mid = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
mid = slow;
slow = slow.next;
}
mid.next = null;
//分割进行排序
ListNode l = sortList(head);
ListNode r = sortList(slow);
//合并
return merges(l,r);
}
//合并两个有序链表
public ListNode merges(ListNode l,ListNode r){
ListNode res = new ListNode(-1);
ListNode cur = res;
while(l != null && r != null){
if(l.val <= r.val){
cur.next = l;
l = l.next;
}else{
cur.next = r;
r = r.next;
}
cur = cur.next;
}
if(l == null){
cur.next = r;
}
if(r == null){
cur.next = l;
}
return res.next;
}
}
4.5 旋转链表
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
示例 1:
输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]
示例 2:
输入:head = [0,1,2], k = 4
输出:[2,0,1]
思路以及代码:
这道题比较简单,只是考如何把链表头节点移到尾结点,用下双指针即可
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if(head == null){
return null;
}
ListNode left = head;
ListNode right = head;
int len = 0;
while(right.next!=null){
right = right.next;
len++;
}
k = k%(len+1);
for(int i = 0;i<k;i++){
for(int j = 0;j<len;j++){
right.next = left;
left = left.next;
right = right.next;
right.next = null;
}
}
return left;
}
}