这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战 | 创作学习持续成长,夺宝闯关赢大奖 - 掘金 (juejin.cn)
题目链接
- 设计前中后队列 leetcode-cn.com/problems/de…
- 返回倒数第k个节点 leetcode-cn.com/problems/kt…
- 链表中倒数第k个节点 leetcode-cn.com/problems/li…
- 复杂链表的复制 leetcode-cn.com/problems/fu…
- 删除中间节点 leetcode-cn.com/problems/de…
题解及分析
设计前中后队列
请你设计一个队列,支持在前,中,后三个位置的 push 和 pop 操作。
请你完成 FrontMiddleBack 类:
FrontMiddleBack() 初始化队列。
void pushFront(int val) 将val添加到队列的最前面。
void pushMiddle(int val) 将val添加到队列的正中间。
void pushBack(int val) 将val添加到队里的最后面。
int popFront() 将最前面的元素从队列中删除并返回值,如果删除之前队列为空,那么返回-1。
int popMiddle() 将正中间的元素从队列中删除并返回值,如果删除之前队列为空,那么返回-1。
int popBack() 将最后面的元素从队列中删除并返回值,如果删除之前队列为空,那么返回-1。
请注意当有两个中间位置的时候,选择靠前面的位置进行操作。比方说:
将6添加到 [1, 2, 3, 4, 5] 的中间位置,结果数组为[1, 2, 6, 3, 4, 5]。
从[1, 2, 3, 4, 5, 6]的中间位置弹出元素,返回3,数组变为[1, 2, 4, 5, 6]。
这道题是前两道题的升级链表折磨专题三 - 掘金 (juejin.cn) 与上一道题相比,多了'将元素插入到正中间'和'从正中间删除元素'两个要求,用数组比较容易处理,但是要考虑正中间元素的边界情况 思路一:用链表来处理。在确认正中间的元素时,我们可以用快慢指针的方式来处理,详情参考链表折磨专题一 - 掘金 (juejin.cn),感谢leetcode题解,我已经忘了有这玩意儿
function ListNode(val, next) {
this.val = (val===undefined ? 0 : val)
this.next = (next===undefined ? null : next)
}
var FrontMiddleBackQueue = function() {
// 在表头插入虚拟节点,规避存在链表长度为0或者为1的尴尬
this.front = new ListNode(-1)
};
FrontMiddleBackQueue.prototype.pushFront = function(val) {
const newNode = new ListNode(val, this.front.next)
this.front.next = newNode
};
FrontMiddleBackQueue.prototype.pushMiddle = function(val) {
/**
* 快慢指针找出中间元素
* 注意fast的初始值,如果和slow对齐,那么slow最终取值达不到题目要求的'前一位'
*/
let slow = this.front
let fast = this.front.next
while(fast && fast.next) {
slow = slow.next
fast = fast.next.next
}
let next = slow.next
slow.next = new ListNode(val, next)
};
FrontMiddleBackQueue.prototype.pushBack = function(val) {
let node = this.front
while(node.next) {
node = node.next
}
node.next = new ListNode(val)
};
FrontMiddleBackQueue.prototype.popFront = function() {
const next = this.front.next
if(!next) return this.front.val
this.front.next = next.next
return next.val
};
FrontMiddleBackQueue.prototype.popMiddle = function() {
let slow = this.front
let fast = this.front.next
if(!fast) return this.front.val
while(fast && fast.next) {
fast = fast.next.next
if(fast) {
slow = slow.next
}
}
// 注意取值和删除的元素
let next = slow.next
slow.next = slow.next.next
return next.val
};
FrontMiddleBackQueue.prototype.popBack = function() {
if(!this.front.next) return this.front.val
let node = this.front
while(node.next && node.next.next) {
node = node.next
}
const next = node.next
node.next = null
return next.val
};
思路二:维护两个数组来处理。维护左右两个数组,控制其中元素的数量以保持中间元素在右边数组的第一位,以此来控制元素的取值
- 首先需要确认中间元素在哪个数组中。我们假定中间元素在右数组,那么右数组的长度应该大于等于左数组的长度
- 每次给左右两边的数组加入或者删去元素,我们都需要同时维护两个数组,保证左右数组的长度相等或者只差一
- 删除时,右数组如果为空,那么左边数组必定也为空;左数组为空,那么右数组只有一个元素或者为空
var FrontMiddleBackQueue = function() {
this.leftArray = []
this.rightArray = []
};
FrontMiddleBackQueue.prototype.pushFront = function(val) {
this.leftArray.unshift(val)
// 保证中间值在右边的数组
if(this.leftArray.length > this.rightArray.length) {
this.rightArray.unshift(this.leftArray.pop())
}
};
FrontMiddleBackQueue.prototype.pushMiddle = function(val) {
// 右边的数组一定比左边大,因此相等取右,不等取左
if(this.leftArray.length === this.rightArray.length) {
this.rightArray.unshift(val)
} else {
this.leftArray.push(val)
}
};
FrontMiddleBackQueue.prototype.pushBack = function(val) {
this.rightArray.push(val)
// 除开只有一个元素的情景,正常情况下右边会比左边多一个中间元素
if(this.rightArray.length === this.leftArray.length + 2) {
this.leftArray.push(this.rightArray.shift())
}
};
FrontMiddleBackQueue.prototype.popFront = function() {
/**
* 右数组可以判断是否为空
* 左数组为空,那么正常情况下只有一个元素
*/
if(!this.rightArray.length) return -1
if(!this.leftArray.length) return this.rightArray.shift()
let ret = this.leftArray.shift()
if(this.leftArray.length + 1 < this.rightArray.length) {
this.leftArray.push(this.rightArray.shift())
}
return ret
};
FrontMiddleBackQueue.prototype.popMiddle = function() {
if(!this.rightArray.length) return -1
if(!this.leftArray.length) return this.rightArray.shift()
if(this.leftArray.length === this.rightArray.length) {
this.rightArray.unshift(this.leftArray.pop())
}
return this.rightArray.shift()
};
FrontMiddleBackQueue.prototype.popBack = function() {
if(!this.rightArray.length) return -1
if(!this.leftArray.length) return this.rightArray.shift()
let ret = this.rightArray.pop()
if(this.leftArray.length > this.rightArray.length) {
this.rightArray.unshift(this.leftArray.pop())
}
return ret
};
顺便纪念下leetcode第一次执行用时击败了100%的其他方式
返回倒数第k个节点
实现一种算法,找出单向链表中倒数第k个节点。返回该节点的值。
这道题目题意很简单,但解法不少。下面除开遍历算出节点总数求解的方法,聊聊其他解法。
思路一:栈
借助栈的filo特性来解答这一类题目。
- 遍历链表,把节点都推入栈中
- 接下来遍历k,依次弹出栈顶的元素,直至取得第k个值
var kthToLast = function(head, k) {
const stack = []
while(head) {
stack.push(head)
head = head.next
}
let newNode = null
while(k > 0) {
newNode = stack.pop()
k--
}
return newNode.val
};
思路二:快慢指针
如果快指针比慢指针固定快k个节点,那快指针到达最后一个节点时,慢指针刚好指向倒数第k个节点
- 先声明一个快指针,向前走k个单位
- 声明一个指向链表头部的慢指针
- 循环链表,同时移动快慢指针,直到快指针触底
var kthToLast = function(head, k) {
let fast = head
let slow = head
while(--k > 0) {
fast = fast.next
}
while(fast && fast.next) {
slow = slow.next
fast = fast.next
}
return slow.val
};
思路三:递归
*这个解法不通过...但本地跑是ok的?
递归的特性是反向执行,即:在触及边界条件之前,不停的调用自身,直到触及边界条件为止
那我们把边界条件设置为k次呢?
- 我们设定一个变量来记录遍历的次数
- 设定对应的判断条件,否则递归自身
let count = 0
var kthToLast = function(head, k) {
if(head === null) return 0
const num = kthToLast(head.next, k)
count++
if(k === count) {
return head.val
}
return num
}
链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
这道题和上一题一样,只是返回的结果从值变成了节点,这里就不作题解了
复杂链表的复制
请实现copyRandomList函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个next指针指向下一个节点,还有一个random指针指向链表中的任意节点或者null。
这道题和链表折磨专题三 - 掘金 (juejin.cn)第三题一样,这里就不作题解了
删除中间节点
若链表中的某个节点,既不是链表头节点,也不是链表尾节点,则称其为该链表的「中间节点」。
假定已知链表的某一个中间节点,请实现一种算法,将该节点从链表中删除。
例如,传入节点c(位于单向链表a->b->c->d->e->f中),将其删除后,剩余链表为a->b->d->e->f
思路:
实际上链表中是没有删除这个概念的。所谓移除链表中的某个节点,本质是将该节点的上一个节点的next指针的从指向当前节点,修改为指向当前节点的下一个节点
那么题目的问题就变成:我们如何让当前节点的上一个节点的next指针跳过当前节点,指向下一个节点?
这是一个题目的思维定式陷阱,实际上,我们并不需要跳过当前节点,我们需要做的是把当前指针的下一个节点替换掉当前节点
var deleteNode = function(node) {
node.val = node.next.val
node.next = node.next.next
}
题目总结
这几道题目中比较值得关注的是快慢指针。
实际上快慢指针可以用来处理很多场景:环问题,中间值问题,相距n个单位的链表取值问题等。