这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战 | 创作学习持续成长,夺宝闯关赢大奖 - 掘金 (juejin.cn)
题目链接
- 删除排序链表中的重复元素II leetcode-cn.com/problems/re…
- 分隔链表 leetcode-cn.com/problems/pa…
- 复制带随机指针的链表 leetcode-cn.com/problems/co…
- 设计循环队列 leetcode-cn.com/problems/de…
- 设计循环双端队列 leetcode-cn.com/problems/de…
题解及分析
删除排序链表中的重复元素II
给定一个已排序的链表的头head,删除原始链表中所有重复数字的节点,只留下不同的数字。返回已排序的链表。
这道题是链表折磨专题二 - 掘金 (juejin.cn)最后一道题的拓展。两道题不同的点在于,这一道题要求“只留下不同的数字”,那么我们不只是需要找出值重复的节点,同时也要注意将链表节点连接的顺序
var deleteDuplicates = function(head) {
if(!head) return head
const dummyNode = new ListNode(0, head)
let cur = dummyNode
// 首先找到重复的节点值
while(cur.next && cur.next.next) {
if(cur.next.val === cur.next.next.val) {
// 注意之后的若干个节点可能都是同个值
const val =cur.next.val
while(cur.next && cur.next.val === val) {
cur.next = cur.next.next
}
} else {
cur = cur.next
}
}
return dummyNode.next
};
分隔链表
给你一个链表的头节点head和一个特定值x,请你对链表进行分隔,使得所有小于x的节点都出现在大于或等于x的节点之前。
你应当保留两个分区中每个节点的初始相对位置。
这道题的需要维护两个链表,分别用来存储值小于x和值大于x的节点,遍历并分类完所有节点后,将两个链表分别组转即可
var partition = function(head, x) {
// 维护两个链表,用来存储大小值节点
let less = new ListNode(0)
lessHead = less
let more = new ListNode(0)
moreHead = more
// 遍历链表进行分类
while(head) {
if(head.val < x) {
lessHead.next = head
lessHead = lessHead.next
}else {
moreHead.next = head
moreHead = moreHead.next
}
head = head.next
}
组装链表
moreHead.next = null
lessHead.next = more.next
return less.next
};
复制带随机指针的链表
给你一个长度为n的链表,每个节点包含一个额外增加的随机指针random,该指针可以指向链表中的任何节点或空节点。
构造这个链表的深拷贝。深拷贝应该正好由n个全新节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有X和Y两个节点,其中X.random --> Y 。那么在复制链表中对应的两个节点x和y,同样有x.random --> y 。
返回复制链表的头节点。
用一个由n个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从0到n-1);如果不指向任何节点,则为null 。
你的代码只接受原链表的头节点head作为传入参数。
这道题在普通的链表节点中加入了新的random指针,这个指针指向随机的一个其他节点或者为空,我们实现深拷贝的时候需要考虑:当前复制节点的random会不会指向一个尚未被创建的节点
思路一:维护map来记录每一个链表的头节点,当random指向的节点未被创建时,则递归的创建一个
var copyRandomList = function(head, cachedNode = new Map()) {
if (head === null) {
return null;
}
if(!cachedNode.has(head)) {
// map中不存在某个头节点时,向map添加这个头节点
cachedNode.set(head, {val: (head.val)})
// 利用js的引用关系,对节点的next和random做更新
Object.assign(cachedNode.get(head), {
next: copyRandomList(head.next, cachedNode),
random: copyRandomList(head.random, cachedNode)
})
}
return cachedNode.get(head)
}
思路二:在每一个原节点后面接入一个复制后的节点,最后再将复制的节点拆出去形成新的链表,这么做的优势在于不需要考虑random指向的节点是否存在的情况
var copyRandomList = function(head) {
if (head === null) {
return null;
}
// 在每个原节点后方插入一个复制节点
for(let node = head; node !== null; node = node.next.next ) {
const newNode = new Node(node.val, node.next, null)
node.next = newNode
}
// 定位random,这时候复制节点的random需要指向原节点random的复制节点
for(let node = head; node !== null; node = node.next.next) {
const nodeNew = node.next;
nodeNew.random = (node.random !== null) ? node.random.next : null;
}
// 重新组装复制后的链表,先保存第二个节点作为头节点
const headNew = head.next
for(let node = head; node !== null; node = node.next) {
const newNode = node.next
node.next = node.next.next
newNode.next = newNode.next ? newNode.next.next : null
}
return headNew
}
设计循环队列
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k): 构造器,设置队列长度为k。
Front: 从队首获取元素。如果队列为空,返回-1。
Rear: 获取队尾元素。如果队列为空,返回-1。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。
题目表现的很专业也很复杂,但实际上重点在循环这两个字上。
思路一:维护一个数组来实现循环队列。我们需要维护以下的几个相关变量:
- 头部节点下标
- 计数器
- 列表的最大长度
- 数据存储的数组 一个定长列表的尾部下标可以通过(头部节点下标+计数器-1)%列表的最大长度来获得,原理如下图
var MyCircularQueue = function(k) {
this.frontIndex = 0
this.count = 0
this.capacity = k
this.queue = Array(k)
};
MyCircularQueue.prototype.enQueue = function(value) {
if(this.isFull() || value === undefined) return false
this.queue[(this.frontIndex + this.count) % this.capacity] = value
this.count ++
return true
};
MyCircularQueue.prototype.deQueue = function() {
if(this.isEmpty()) return false
this.queue[this.frontIndex] = undefined
// 头指针前移一位,对总长度取mod保证溢出时正确取值
this.frontIndex = (this.frontIndex + 1) % this.capacity
this.count -= 1
return true
};
MyCircularQueue.prototype.Front = function() {
const result = this.queue[this.frontIndex]
return result === undefined ? -1 : result
};
MyCircularQueue.prototype.Rear = function() {
const rear = this.queue[(this.frontIndex + this.count -1) % this.capacity]
return rear === undefined ? -1 : rear
};
MyCircularQueue.prototype.isEmpty = function() {
return this.count === 0
};
MyCircularQueue.prototype.isFull = function() {
return this.count === this.capacity
};
思路二:链表维护。链表只依赖next指针确定与另一个节点的关系,根本不需要考虑顺序问题,因此可以忽略数组中对头下表的维护
function ListNode(val, next) {
this.val = (val===undefined ? 0 : val)
this.next = (next===undefined ? null : next)
}
var MyCircularQueue = function(k) {
// 维护指向尾节点的指针,用以返回尾值
this.front = this.rear = null
this.count = 0
this.capacity = k
};
MyCircularQueue.prototype.enQueue = function(value) {
if(this.isFull() || value === undefined) return false
const newNode = new ListNode(value, null)
if(this.isEmpty()) {
this.front = newNode
this.rear = newNode
} else {
this.rear.next = newNode
this.rear = this.rear.next
}
this.count ++
return true
};
MyCircularQueue.prototype.deQueue = function() {
if(this.isEmpty()) return false
this.front = this.front.next
this.count -= 1
return true
};
MyCircularQueue.prototype.Front = function() {
return this.count ? this.front.val : -1
};
MyCircularQueue.prototype.Rear = function() {
return this.count ? this.rear.val : -1
};
MyCircularQueue.prototype.isEmpty = function() {
return this.count === 0
};
MyCircularQueue.prototype.isFull = function() {
return this.count === this.capacity
};
设计循环双端队列
设计实现双端队列。 你的实现需要支持以下操作:
MyCircularDeque(k):构造函数,双端队列的大小为k。
insertFront():将一个元素添加到双端队列头部。 如果操作成功返回 true。
insertLast():将一个元素添加到双端队列尾部。如果操作成功返回 true。
deleteFront():从双端队列头部删除一个元素。 如果操作成功返回 true。
deleteLast():从双端队列尾部删除一个元素。如果操作成功返回 true。
getFront():从双端队列头部获得一个元素。如果双端队列为空,返回 -1。
getRear():获得双端队列的最后一个元素。 如果双端队列为空,返回 -1。
isEmpty():检查双端队列是否为空。
isFull():检查双端队列是否满了。
这道题目和上一道题相比,多了'需要从头部插入元素'和'需要从头部删除元素',两个要求实际上的实现差不多,但是由于需要考虑从头部插入元素和从尾部删除元素,需要考虑的情景变多了
思路一:维护数组来处理。
var MyCircularDeque = function(k) {
this.front = 0
this.count = 0
this.capacity = k
this.queue = Array(k)
};
MyCircularDeque.prototype.insertFront = function(value) {
if(this.count === this.capacity) return false
this.queue = [value, ...this.queue]
this.count += 1
this.front = 0
return true
};
MyCircularDeque.prototype.insertLast = function(value) {
if(this.count === this.capacity) return false
const index = this.front + this.count -1 >= 0 ? (this.front + this.count)% this.capacity : 0
this.queue[index] = value
this.count += 1
return true
};
MyCircularDeque.prototype.deleteFront = function() {
if(this.isEmpty()) return false
this.queue.splice(0, 1)
this.count --
return true
};
MyCircularDeque.prototype.deleteLast = function() {
if(this.isEmpty()) return false
this.queue[(this.front + this.count -1) % this.capacity] = undefined
this.count --
return true
};
MyCircularDeque.prototype.getFront = function() {
return this.queue[this.front] === undefined ? -1 : this.queue[this.front]
};
MyCircularDeque.prototype.getRear = function() {
return this.queue[(this.front + this.count -1) % this.capacity] === undefined ? -1 : this.queue[(this.front + this.count -1) % this.capacity]
};
MyCircularDeque.prototype.isEmpty = function() {
return this.count === 0
};
MyCircularDeque.prototype.isFull = function() {
return this.count === this.capacity
};
思路二:双指针链表解法。唯一要注意的是计数器为0时的插入和删除需要对头指针和尾指针进行处理
/**
* @param {number} k
*/
function ListNode(val, next) {
this.val = (val===undefined ? 0 : val)
this.next = (next===undefined ? null : next)
}
var MyCircularDeque = function(k) {
this.front = this.rear = null
this.count = 0
this.capacity = k
};
MyCircularDeque.prototype.insertFront = function(value) {
if(this.isFull()) return false
const newNode = new ListNode(value, this.front)
this.front = newNode
// 如果尾节点为空时插入元素,需要把尾指针重新定位
if(!this.rear) {
this.rear = this.front
while(this.rear.next) {
this.rear = this.rear.next
}
}
this.count++
return true
};
MyCircularDeque.prototype.insertLast = function(value) {
if(this.isFull()) return false
if(!this.count) {
// 如果链表为空时插入元素,需要把尾指针和头指针指向同个节点
this.front = this.rear = new ListNode(value)
} else {
this.rear.next = new ListNode(value)
this.rear = this.rear.next
}
this.count++
return true
};
MyCircularDeque.prototype.deleteFront = function() {
if(this.isEmpty()) return false
this.front = this.front.next
this.count--
// 如果链表删除后为空,需要把尾指针和头指针重置
if(!this.count) this.rear = this.front
return true
};
MyCircularDeque.prototype.deleteLast = function() {
if(this.isEmpty()) return false
let cache = this.count - 1
/**
* 如果链表中仅有一个节点,那么直接将头尾节点置空
* 如果链表多个节点,那么删除后重新定位尾节点
*/
if(!cache) {
this.front = this.rear = null
} else {
let newRear = this.front
while(--cache) {
newRear = newRear.next || null
}
newRear.next = null
this.rear = newRear
}
this.count--
return true
};
MyCircularDeque.prototype.getFront = function() {
return this.front? this.front.val : -1
};
MyCircularDeque.prototype.getRear = function() {
return this.rear ? this.rear.val : -1
};
MyCircularDeque.prototype.isEmpty = function() {
return this.count === 0
};
MyCircularDeque.prototype.isFull = function() {
return this.count === this.capacity
};
题目总结
- 1道题没有太多的变化,但需要考虑多个重复数据的情景
- 2道题可以近似的看作快速排序的链表实现
- 3道题有点类似于双向链表,需要考虑场景问题
- 4,5道题属于队列处理类题目。这两道题都可以考虑使用数组或者链表的方式来处理