设计前中后队列

343 阅读5分钟

正题

设计前中后队列

请你设计一个队列,支持在前,中,后三个位置的 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] 。

解析:

这个和普通队列以及双端队列不同的是,难点在于中间点,在队列元素的增加,减少的同时,中间点也会随之改变。说实话,解决这个问题花了比较多的时间,并不是因为题目的难度很大,是因为需要注意的细节很多,并且调试起来比较麻烦。

这题可以使用数组去解决,只要找到中间 middle 的位置其余采用数组的 shift, unshift, push, pop 就能解决 前后增删的问题,由于题目没有限制队列长队,所以相对来说还是比较简单的。

另外我们还可以使用单向链表的方式去解决:

构造链表数据结构

链表至少包含两个属性:

val 链表节点的数值

next 下一节点的指针

本题应用不到更复杂的链表结构,所以很简单的就能够造出一个链表数据结构。

var NodeList = function (val, next) {
  this.val = val
  this.next = next
}

构建完了之后,我们实践一下,如何通过链表去实现 三端队列!

初始化队列

FrontMiddleBackQueue 方法如何去实现呢,其实就是赋予队列一些字段属性。实际上,他只需要拥有一个链表属性就可以了,我们可以通过操作链表去实现队列的方法。所以很简单地:

var FrontMiddleBackQueue = function () {
  this.head = null
};

head 表示链表头节点。默认 null,表示队列为空。

pushFront

该方法为在队列的头部添加节点,如果我们从链表的方向去看,即在 head 节点前面再添加一个 next 指针指向 head 节点的节点 ,听上去比较拗口,但是很容易理解。我们用图解来表达:

1.gif

上图一目了然,当链表节点为空,那么直接 head 指向新节点,如果不为空,那么创建一个节点指向 head。

这两种情况我们可以用一行代码解决:

/** 
* @param {number} val
* @return {void}
*/
FrontMiddleBackQueue.prototype.pushFront = function (val) {
  this.head = new NodeList(val, this.head)
};

实际上原理都是一样的,只不过空链表的 head 为 null

pushMiddle

本题重点来了。在链表的中间节点插入节点。

我们可以知道,中间这个词是有定义的,也就是说它规定了一个特殊规则,当节点数量为奇数个:

  • 将 6 添加到 [1, 2, 3, 4, 5] 的中间位置,结果数组为 [1, 2, 6, 3, 4, 5] 。

由于链表不去遍历我们是不知道它的长度的,如果去遍历就为了寻找长度,就会对性能上造成很大损失。所以我们要找到一种解决办法,无论是奇数还是偶数都能够找到中见节点!

还记得快慢指针吗?快慢指针不仅适用于找到倒数第几个节点,也适用于找到中间节点。所以我们可以考虑使用双指针的方法。

偶数个:

1.gif

奇数个:

1.gif

原理一样,我们只需要考虑 Fast 指针 下一个节点 和 下下个节点为空的时候, Slow 就为带插入节点的位置了。

另外要考虑的细节就是,当链表为空以及链表仅有一个节点的时候。

当链表为空,我们可以认为就是 pushFront,当链表只有一个节点我们也可以认为是 pushFront

最终图解:

1.gif

实现:

FrontMiddleBackQueue.prototype.pushMiddle = function (val) {
 if (!this.head || !this.head.next) {
     this.pushFront(val)
     return
 }
  let slow = this.head
  let fast = this.head.next
  while (fast && fast.next && fast.next.next) {
    slow = slow.next
    fast = fast.next.next
  }
  slow.next = new NodeList(val, slow.next)
  console.log('push middle', this.head)
};

pushBack

比较简单的实现,即在链表尾部 next 指针指向新节点即可,考虑空队列的话直接就是 this.head 等于新节点。

/** 
* @param {number} val
* @return {void}
*/
FrontMiddleBackQueue.prototype.pushBack = function (val) {
    if (!this.head) {
        this.pushFront(val)
        return
    }
  let p = this.head
  while(p.next) {
      p = p.next
  }
  p.next = new NodeList(val, null)
};

popFont

弹出并返回队列的第一个节点。返回很简单,直接 return this.head.val,但我们还需要将第一个节点删除掉,删除链表的 head 节点,直接将 head 指向 head.next 即可,考虑细节,当链表为空,则直接返回 -1

/**
* @return {number}
*/
FrontMiddleBackQueue.prototype.popFront = function () {
  if (!this.head) {
    return -1
  }
  const res = this.head.val
  this.head = this.head.next
  return res
};

popMiddle

本题另一个难点,找 popMiddle 节点位置,同样的,我们需要找到奇数和偶数个节点时,都能够准确找到 middle 的位置。

1.gif

如图,和 pushMiddle 不同的是,fast 从第三个节点开始

/**
* @return {number}
*/
FrontMiddleBackQueue.prototype.popMiddle = function () {
  if (!this.head) {
    return -1
  }
  if (!this.head.next || !this.head.next.next) {
    // 仅有一个元素
    return this.popFront()
  }
 
  let slow = this.head
  let fast = this.head.next.next
  while (fast && fast.next && fast.next.next) {
    slow = slow.next
    fast = fast.next.next
  }
  const res = slow.next
  slow.next = slow.next.next
  console.log('pop middle', this.head)
  return res.val
};

另外在考虑空队列和仅有一个元素的队列即可

popBack

弹出最后一个节点。按照链表的性质,遍历得到,return 即可

/**
* @return {number}
*/
FrontMiddleBackQueue.prototype.popBack = function () {
  if (!this.head) {
    return -1
  }
  if (!this.head.next) {
    return this.popFront()
  }
  let backPre = this.head
  let back = this.head.next
  while (back.next) {
    backPre = backPre.next
    back = back.next
  }
  backPre.next = null
  console.log('pop back', this.head)
  return back.val
};

以上完成了所有三端队列的所有方法。

完整代码:

var NodeList = function (val, next) {
  this.val = val
  this.next = next
}
var FrontMiddleBackQueue = function () {
  this.head = null
};

/** 
* @param {number} val
* @return {void}
*/
FrontMiddleBackQueue.prototype.pushFront = function (val) {
  this.head = new NodeList(val, this.head)
};

/** 
* @param {number} val
* @return {void}
*/
FrontMiddleBackQueue.prototype.pushMiddle = function (val) {
 if (!this.head || !this.head.next) {
     this.pushFront(val)
     return
 }
  let slow = this.head
  let fast = this.head.next
  while (fast && fast.next && fast.next.next) {
    slow = slow.next
    fast = fast.next.next
  }
  slow.next = new NodeList(val, slow.next)
};

/** 
* @param {number} val
* @return {void}
*/
FrontMiddleBackQueue.prototype.pushBack = function (val) {
    if (!this.head) {
        this.pushFront(val)
        return
    }
  let p = this.head
  while(p.next) {
      p = p.next
  }
  p.next = new NodeList(val, null)
};

/**
* @return {number}
*/
FrontMiddleBackQueue.prototype.popFront = function () {
  if (!this.head) {
    return -1
  }
  const res = this.head.val
  this.head = this.head.next
  return res
};

/**
* @return {number}
*/
FrontMiddleBackQueue.prototype.popMiddle = function () {
  if (!this.head) {
    return -1
  }
  if (!this.head.next || !this.head.next.next) {
    // 仅有一个元素
    return this.popFront()
  }
 
  let slow = this.head
  let fast = this.head.next.next
  while (fast && fast.next && fast.next.next) {
    slow = slow.next
    fast = fast.next.next
  }
  const res = slow.next
  slow.next = slow.next.next
  return res.val
};

/**
* @return {number}
*/
FrontMiddleBackQueue.prototype.popBack = function () {
  if (!this.head) {
    return -1
  }
  if (!this.head.next) {
    return this.popFront()
  }
  let backPre = this.head
  let back = this.head.next
  while (back.next) {
    backPre = backPre.next
    back = back.next
  }
  backPre.next = null
  return back.val
};

/**
* Your FrontMiddleBackQueue object will be instantiated and called as such:
* var obj = new FrontMiddleBackQueue()
* obj.pushFront(val)
* obj.pushMiddle(val)
* obj.pushBack(val)
* var param_4 = obj.popFront()
* var param_5 = obj.popMiddle()
* var param_6 = obj.popBack()
*/

// const queue = new FrontMiddleBackQueue()
// queue.pushFront(1)
// queue.pushBack(2)
// queue.pushMiddle(3)
// queue.pushMiddle(4)
// console.log(queue.popFront())
// console.log(queue.popMiddle())
// console.log(queue.popMiddle())
// console.log(queue.popBack())
// console.log(queue.popFront())