简介
链表刷题系列第三篇。今天通过对两个新题的练习,让我们更好的掌握和消化链表。
LRU leetcode 146
设计一个LRU类,面试高频题目。
初见
之前一次面试,当时完全没刷题,初见这道题想了很久,磕磕巴巴。 但现在刷完了链表基础题单后,刷这道题从思考到做出来基本只用了15分钟。思路和代码都非常连贯,基本没怎么调试就搞定了。
原因有两个:
- 基础打牢:回顾熟悉了链表的基本的游戏玩法,插入、删除、虚节点、翻转、断链
- 重新遇到题目,可以将题目合理转换,变成原先题目的做法
题意分析
像这道题目,首先分析拆解题意(需求):
- 我们需要O(1)时间获取到链表中任意的节点
- 最近最少使用,get 和put都需要把节点
挪
到头部 - 大于容量,
要逐出
,也就意味着我们能O(1)的删除链表的尾部。
对这三点需求分析:
hash
没跑了,可以用一个map维护到每个链表的值- 挪到头部,我们维护
虚拟头节点
,然后挪动节点
,就可以完成这个操作 - 逐出操作,包含
删除尾节点
,删除map
逐个击破
挪动节点
1和2的做法,通过链表的基础知识,我们知道: 本质挪动一个节点,其实包含了两个操作:
- 删除
- 插入
我们每次get和put都先移除,然后再插入更新。 这里兼容一下,如果是一个新的节点,那就不做移除操作。
Golang
func (this *LRUCache) updateNodeRelation(node *Node){
//先移除
nodeNxt := node.Next
nodePre := node.Pre
if nodePre!=nil{
nodePre.Next = nodeNxt
}
if nodeNxt != nil {
nodeNxt.Pre = nodePre
}
//更新node
headNxt := this.Head.Next
headNxt.Pre = node
node.Next = headNxt
node.Pre = this.Head
this.Head.Next = node
}
逐出操作
而3的做法,我们需要再拆解下:
- 能灵活删除尾节点的,只有双向链表,这就导致我们这道题的插入删除操作,也不得不变得更复杂
- 删除map,需要我们知道尾节点的key是什么。所以存储的节点里需要多维护这个节点的key值
于是有:
Golang
if len(this.M) > this.Capacity {
delete(this.M, this.Tail.Pre.Key)
deletePre := this.Tail.Pre.Pre// tail节点是最后一个. 所以最好跳到上上个节点,方便删除,这里回去多看了下,capacity 是>=1的,不会出现取到空节点的情况,否则就需要特殊判断了
deletePre.Next = this.Tail
this.Tail.Pre = deletePre
}
code
Golang
type LRUCache struct {
Capacity int
M map[int]*Node
Head *Node
Tail *Node
}
type Node struct{
Key int
Val int
Next *Node
Pre *Node
}
func NewListNode(key,val int)*Node{
return &Node{
Key: key,
Val: val,
}
}
func Constructor(capacity int) LRUCache {
cache := LRUCache{
Capacity: capacity,
M: make(map[int]*Node),
}
cache.Head = NewListNode(0,0)
cache.Tail = NewListNode(0,0)
cache.Head.Next = cache.Tail
cache.Tail.Pre = cache.Head
return cache
}
func (this *LRUCache) Get(key int) int {
if v,exist :=this.M[key];exist{
this.updateNodeRelation(v)
return v.Val
}
return -1
}
func (this *LRUCache) Put(key int, value int) {
if this.Get(key)!=-1{
this.M[key].Val = value
this.updateNodeRelation(this.M[key])
return
}
//新节点
node := NewListNode(key,value)
this.M[key] = node
this.updateNodeRelation(node)
if len(this.M) > this.Capacity {
delete(this.M, this.Tail.Pre.Key)
deletePre := this.Tail.Pre.Pre
deletePre.Next = this.Tail
this.Tail.Pre = deletePre
}
}
func (this *LRUCache) updateNodeRelation(node *Node){
//先移除
nodeNxt := node.Next
nodePre := node.Pre
if nodePre!=nil{
nodePre.Next = nodeNxt
}
if nodeNxt != nil {
nodeNxt.Pre = nodePre
}
//更新node
headNxt := this.Head.Next
headNxt.Pre = node
node.Next = headNxt
node.Pre = this.Head
this.Head.Next = node
}
/**
* Your LRUCache object will be instantiated and called as such:
* obj := Constructor(capacity);
* param_1 := obj.Get(key);
* obj.Put(key,value);
*/
Leetcode 86 分割链表
初见
第一次做,但由于套路实在是太重复了,10分钟就搞定了。 86. 分隔链表
想把>=x的链表放到尾部,本质上容易转换成,将>=x的节点单独组成一个新链表,然后两个链表拼接,从而解决这道题。
因此只需要做链表挪到操作 (删除、插入),最后拼接链表(用到了虚拟头节点),就可以解决,用到的操作也是上一个题的子集,比较简单
code
Golang
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func partition(head *ListNode, x int) *ListNode {
//思路 把>=x的节点都往尾巴放 然后把两端拼起来就好了
tailListHead := &ListNode{}
tailNode := tailListHead
h := &ListNode{Next: head}
preNode := h
// 两种遍历方式:
// 1. 当前节点往后 记录前一个结点
// 这样子 如果是删除操作 跳跃节点不会有问题
// 2. 前一个节点往后看 有时候方便 但删除节点遍历时会有问题
for node:=head;node!=nil;{
nodenxt := node.Next
if node.Val >= x {
// 移除
preNode.Next = nodenxt
node.Next = nil // 移除时一定要把链给断开
// 拼接
tailNode.Next = node
tailNode = node
}else{
preNode = node
}
node = nodenxt
}
preNode.Next = tailListHead.Next
return h.Next
}
总结
-
核心操作
- 链表基础操作是解题根基,在本次两道题目中,插入、删除操作频繁运用。如 LRU 中挪动节点,实质就是删除原位置节点再插入到头部;分割链表时,将符合条件的节点从原链表删除并插入到新链表。虚拟节点的运用也贯穿其中,LRU 借助虚拟头、尾节点简化链表操作边界,分割链表利用虚拟头节点构建新链表与拼接链表。
-
解题思路
- LRU(LeetCode 146) :面对复杂需求,拆解题意是关键。为实现 O (1) 获取节点,采用哈希表存储节点信息;get 和 put 操作中节点挪动到头部,基于链表插入删除操作实现;处理容量溢出的逐出操作,借助双向链表结构特性删除尾节点,并同步哈希表数据。解题过程将新问题合理转化为对链表基础操作的组合运用。
- 分割链表(LeetCode 86) :把复杂的链表分割需求,巧妙转换为将满足特定条件的节点构建新链表再拼接的过程。通过遍历链表,依据节点值与给定值的大小关系,执行相应的删除、插入操作,最终完成链表的分割与重组。
-
例题解法
- LRU :代码实现中,定义双向链表节点结构并维护哈希表。挪动节点函数
updateNodeRelation
精确完成节点的删除与插入;逐出操作在哈希表长度超过容量时,删除双向链表尾节点及哈希表对应键值对。整体代码通过合理的数据结构设计与基础操作组合,实现 LRU 缓存机制。 - 分割链表 :在遍历链表过程中,记录前一个节点,当节点值大于等于给定值
x
时,将其从原链表移除并插入到新链表尾部,最后拼接两个链表。虚拟头节点tailListHead
和h
的使用,简化了链表操作流程,确保代码逻辑清晰、运行正确。
- LRU :代码实现中,定义双向链表节点结构并维护哈希表。挪动节点函数