记录 4 道算法题
复制带随机指针的链表
138. 复制带随机指针的链表 - 力扣(LeetCode) (leetcode-cn.com)
要求:
* 每个节点都有一个 random 指向随机节点
* 需要复制新节点,指向新节点
复制的时候会遇到的难点是random的指向。当遍历一遍的时候,旧节点是可以替换成新节点的,因为联系的纽带是next,前后紧紧相连。但是 random 很难确定,首先 random 是可以指向任意地方,意味着替换了新节点后 random所指的节点可能还没有复制。如果是全部复制一遍后再指向random的话,也会面临不知道哪个是random的复制节点。
所以需要解决的问题是如何保存random,正确的指向random。
我们可以把新复制的节点插入到旧节点后面,这样新旧节点的联系就确定了。A -> A' -> B -> B'。我们想找到的 random 节点就是旧节点的next。
但是最少要进行3次链表的遍历。第一次是复制节点插入,第二次是赋值 random节点,第三是把复制的新节点收集起来形成新链表。
function copyRandomList(head) {
let node = head
while(node) {
// 创建新节点并插入
const copy = new Node(node.val, node.next, null)
node.next = copy
node = node.next.next
}
node = head
while(node) {
// 指random
const copy = node.next
copy.random = node.random?.next ?? null
node = node.next.next
}
node = head
let p = new Node()
head = p
while(node) {
// 收集复制节点
p = p.next = node.next
node = node.next = node.next?.next ?? null
}
return head.next
}
还有第二个方法就是递归,这种一看结构就和斐波那契数列的递归很像。节点里面有节点,一直到最里面节点生成才逐步返回。
const map = new Map()
function copyRandomList(head) {
// head可能是null
if (!head) return head
if (map.has(head)) {
return map.get(head)
}
const copy = new Node(head.val)
// 要马上保存,因为后面递归会先用到
map.set(head, copy)
copy.next = copyRandomList(head.next)
copy.random = copyRandomList(head.random)
// 把节点返回出去给 next或者random
return copy
}
设计循环队列
622. 设计循环队列 - 力扣(LeetCode) (leetcode-cn.com)
就是个简单的数组,遵循先进先出的规则,直接上代码吧。也可以用链表写。
class MyCircularQueue {
constructor(k) {
this.queue = []
this.n = k
// 其实读数组的length也是可以的,我这里用了一个变量指着队列的结尾
this.i = 0
}
enQueue(value) {
if (this.isFull()) return false
this.queue.push(value)
this.i++
return true
}
deQueue() {
if (this.isFull()) return false
this.queue.shift()
this.i--
return true
}
Front() {
if (this.isEmpty()) return -1
return this.queue[0]
}
Rear() {
if (this.isEmpty()) return -1
return this.queue[this.i - 1]
}
isEmpty() {
return this.i === 0
}
isFull() {
return this.i === this.n
}
}
设计双端队列
641. 设计循环双端队列 - 力扣(LeetCode) (leetcode-cn.com)
和循环队列比起来多了可以头部插入节点,用数组写就会很简单,和上面的代码差不多。
class MyCircularDeque {
constructor(k) {
this.queue = []
this.n = k
// 其实读数组的length也是可以的,我这里用了一个变量指着队列的结尾
this.i = 0
}
insertFront(value) {
if (this.isFull()) return false
this.i++
this.queue.unshift(value)
return true
}
insertLast(value) {
if (this.isFull()) return false
this.i++
this.queue.push(value)
return true
}
deleteFront() {
if (this.isEmpty()) return false
this.i--
this.queue.shift()
return true
}
deleteLast() {
if (this.isEmpty()) return false
this.i--
this.queue.pop()
return true
}
getFront() {
if (this.isEmpty()) return -1
return this.queue[0]
}
getRear() {
if (this.isEmpty()) return -1
return this.queue[this.i - 1]
}
isEmpty() {
return this.i === 0
}
isFull() {
return this.i === this.n
}
}
设计前中后队列
多了插入中间节点,分为单双数的情况,中间是不一样的。双数的时候中间是中间两个的前一个,例如 1, 2, 3, 4。中间是 2。随着数组长度的变化,中间节点会变化,但一定是 +1. 另外,如果用链表实现的话, 寻找中间节点的方法可以是用一个变量记录个数。或者用两个指针一个走一步,一个走两步。等走得快的指针走完,另一个就是指中间的节点,之类的。
class FrontMiddleBackQueue {
constructor() {
this.queue = []
}
pushFront(val) {
this.queue.unshift(val)
}
pushMiddle(val) {
let i = this.queue.length
if (i % 2 === 0) {
i = i / 2
} else {
i = i >> 1
}
// splice方法 是在 i 的位置插入,然后原本 i 上的数往后移
this.queue.splice(i, 0, val)
}
pushBack(val) {
this.queue.push(val)
}
popFront() {
if (this.queue.length === 0) return -1
return this.queue.shift()
}
popMiddle() {
if (this.queue.length === 0) return -1
let i = this.queue.length
if (i % 2 === 0) {
i = i / 2 - 1
} else {
i = i >> 1
}
return this.queue.splice(i, 1)[0]
}
popBack() {
if (this.queue.length === 0) return -1
return this.queue.pop()
}
}
结束