JS实现优先队列

JS实现优先队列

优先队列是一种特殊的队列,一般的队列,数据都是先进先出的,没有什么顺序。而优先队列不同,优先队列出队列的时候是具有优先级的,出队列的元素是优先级别最高的。(如最大值或者最小值),下面就以大顶堆为例实现一个优先队列。

预备知识

要实现优先队列前,需要先了解一些必要的知识,优先队列主要是基于二叉树来进行实现的,所以可以使用数组和链表实现,由于使用数据在内存上更具优势,本篇文章优先使用数组来实现一个优先队列。
那么就需要知道将数组映射到二叉树的一些基础知识,以数组[6, 5, 4, 1, 2, 3]为例,映射成二叉树就如下图所示
数组映射二叉树
其实,就是,从上往下,从左往右依次排列,那么需要记住以下几点:

  1. 最后一个非叶子节点(上图中为4)的下标为N/2 - 1向下取整
  2. n个节点的左子节点和右子节点分别为2*n+12*n+2

然后我们需要实现的大顶堆就是父节点总是大于其子节点(上图中的二叉树就是一个大顶堆),那么可以知道根节点,也就是数组首位就是整个队列中的最大值,这在需要经常读取一堆大数据中的最大值的操作,非常有帮助,而且将根节点拿出,再重构一个大顶堆,再将根节点拿出,然后重构,直到队列为空,那么得到的就是一个从大到小排列的数据了,这就是堆排序。

大顶堆的构建

实现一个大顶堆的主要部分是要理解大顶堆的构建过程,简而言之就是,从最后一个非叶子节点开始,遍历到根节点(从下往上,从右往左),将节点与左右子节点进行比较,如果改节点不是最大值,则将该节点与左右子节点中最大值进行交换,交换后,记住需要检查交换后的子节点的子树是否符合最大的规则。

我们以数组[1, 2, 3, 4, 5, 6]为例来演示下构建过程
首先数组映射到二叉树的结构如下图所示,最后一个非叶子节点为3,比较它和子节点的值
大顶堆的构建过程1
可以看到3 < 6所以交换两个节点位置后如下图:

大顶堆的构建过程2
然后比较节点2与其子节点的值:
大顶堆的构建过程3
发现5是最大的,所以交换节点2和节点5:
大顶堆的构建过程4
接着比较节点1与其子节点:
大顶堆的构建过程5
可以看到6最大,交换节点1和节点6:
大顶堆的构建过程6
节点1和节点6交换后,我们会发现节点1的位置不符合大顶堆的规则,那么我们需要再对节点1进行同样的调整
大顶堆的构建过程7
将节点1和节点3交换后,就构成了一个大顶堆:
大顶堆的构建过程8
我们把上面的步骤转化为代码后如下:

  init () {
    // 从倒数第一个非叶子节点(从下往上,从右往左数)依次重新排列节点顺序
    let lastParentNodeIndex = Math.floor(this.queue.length / 2) - 1
    for (let i = lastParentNodeIndex; i >= 0; i--) {
      this.buildChildNodes(i)
    }
  }

  buildChildNodes (index) {
    // 在数组中某个节点的左右子节点分别为 2*i+1 和 2*i+2
    // this.queue中存储着数组
    let arr = this.queue
    let left = index * 2 + 1
    let right = index * 2 + 2
    let maxIndex = index
    let length = arr.length

    // 找出值最大的节点下标
    if (left < length && arr[left] > arr[maxIndex]) {
      maxIndex = left
    }
  
    if (right < length && arr[right] > arr[maxIndex]) {
      maxIndex = right
    }

    if (maxIndex != index) {
      // 如果最大值不是父节点的话,需要交换父子节点位置
      // 并且对父节点交换后的子树递归地进行重新排列
      this.swap(index, maxIndex)
      this.buildChildNodes(maxIndex)
    }
  }

最重要的构造方法完成后我们来补充下大顶堆的一些其他方法。

大顶堆的其他方法补充

获取堆顶元素,也就是最大值peak方法,很简单,只要返回第一个元素即可:

peak () {
  return this.queue[0]
}

添加一个元素push方法,只需要往数组中push该元素,然后重新调整二叉树就行:

push (val) {
 this.queue.push(val)
  this.init()
}

出队列pop方法,pop方法可以仿照push方法,把数组第一个元素剔除后再重新调整二叉树,但把数组第一个元素剔除的操作是需要O(N)的时间复杂度的,所以,我们可以先保存数组第一个元素,然后将数组的最后一个元素pop出来赋值给第一个元素(时间复杂度为O(1)),然后再重新调整二叉树,并将保存的值返回:

pop () {
 // pop操作,就是将根节点拿出后,用最后一个节点填充根节点然后重新构建
  // 这样重新构建前的操作就是O(1)
  // 如果直接把根节点shift出去再重构建则,重新构建前的操作就是O(n)
  let val = this.queue[0]
  this.queue[0] = this.queue.pop()
  this.init()
  return val
}

将队列排序,也就是堆排序,因为上面的pop方法每次都会出队列一个最大值,所以只要pop N次就可以了:

sort () {
  // 排序的话就是把队列一个个pop出来
  var res = []
  var length = this.queue.length
  for (var i = 0; i < length; i++) {
    res.push(this.pop())
  }
  this.queue = res
  return res
}

完整代码

完整代码如下:

class PriorityQueue {
  constructor (arr = []) {
    this.queue = arr
    this.init()
  }

  init () {
    // 从倒数第一个非叶子节点(从下往上,从右往左数)依次重新排列节点顺序
    let lastParentNodeIndex = Math.floor(this.queue.length / 2) - 1
    for (let i = lastParentNodeIndex; i >= 0; i--) {
      this.buildChildNodes(i)
    }
  }

  buildChildNodes (index) {
    // 在数组中某个节点的左右子节点分别为 2*i+1 和 2*i+2
    let arr = this.queue
    let left = index * 2 + 1
    let right = index * 2 + 2
    let maxIndex = index
    let length = arr.length

    // 找出值最大的节点下标
    if (left < length && arr[left] > arr[maxIndex]) {
      maxIndex = left
    }
  
    if (right < length && arr[right] > arr[maxIndex]) {
      maxIndex = right
    }

    if (maxIndex != index) {
      // 如果最大值不是父节点的话,需要交换父子节点位置
      // 并且对父节点交换后的子树递归地进行重新排列
      this.swap(index, maxIndex)
      this.buildChildNodes(maxIndex)
    }
  }

  peak () {
    return this.queue[0]
  }

  push (val) {
    this.queue.push(val)
    this.init()
  }

  pop () {
    // pop操作,就是将根节点拿出后,用最后一个节点填充根节点然后重新构建
    // 这样重新构建前的操作就是O(1)
    // 如果直接把根节点shift出去再重构建则,重新构建前的操作就是O(n)
    let val = this.queue[0]
    this.queue[0] = this.queue.pop()
    this.init()
    return val
  }

  sort () {
    // 排序的话就是把队列一个个pop出来
    var res = []
    var length = this.queue.length
    for (var i = 0; i < length; i++) {
      res.push(this.pop())
    }
    this.queue = res
    return res
  }

  swap (index1, index2) {
    var arr = this.queue
    ;[arr[index1], arr[index2]] = [arr[index2], arr[index1]]
  }
}

也欢迎到我的github上查看


版权声明:本文为u014470209原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。