树结构(javascript)-2:二叉堆

什么是二叉堆?

二叉堆本质上是一种完全二叉树,它分为两个类型:最大堆最小堆

  • 最大堆: 任何一个父节点的值,都大于或等于它左、右孩子节点的值。根节点是整个二叉堆中最大的数。
  • 最小堆: 和最大堆相反,任何一个父节点的值,都小于或等于它左、右孩子节点的值。根节点是整个二叉堆中最小的数。

二叉堆的根节点也叫作堆顶。

二叉堆的自我调整

所谓的自我调整,就是把一个不符合二叉堆的完成二叉树,转换成一个二叉堆的过程。
这里以最小堆举例:

  • 插入节点
    插入节点的思路:在完全二叉树的最后一个位置插入一个节点,然后让这个节点向上比较(上浮过程),如果比父节点小,则与父节点交换位置,依次向上比较,直至根节点;或者出现比父节点大的情况就停止

  • 删除节点
    删除节点的思路:只要不是删除最后一个叶子节点,就将最后一个叶子节点替换到被删除节点的位置,然后让这个节点向下比较,如果比它的左右孩子节点都大,就把左右节点中较小值节点和这个节点的位置进行交换(下沉过程),直到没有孩子节点为止。

  • 构建二叉堆
    构建二叉堆,就是把一个无序的二叉树调整为二叉堆,本质就是让所有非叶子节点依次下沉。
    它从最后一个非叶子节点开始,进行下沉操作,也就是如果它比它的左右子节点都大,则它与最小子节点交换位置。然后找到倒数第二个非叶子节点,重复此操作,直至根节点下沉完,结束。

二叉堆的存储方式

虽然二叉堆本质上(形态上也是)是一个完全二叉树,但它的存储方式不是链表,而是顺序存储,也就是数组形式。

在数组方式存储中,树的节点间关系是这样的:

  • 父节点的位置下标parent, 左孩子节点位置就是:2parent+1, 右孩子节点位置就是:2parent+2,即如果父节点下标为0,则其左节点为1,右节点为2
  • 如果根据子节点来计算父节点的位置,如果子节点下标是奇数child(也就是左孩子节点),那么父节点为 (child - 1) / 2; 如果子节点是偶数(也就是右孩子节点),那么父节点为 (child - 2) / 2

二叉堆的实现方式

/*
  二叉堆的代码实现
*/

// 根据子节点下标的奇偶性,来获取对应父节点的下标
function getParentIndex(childIndex) {
  let parentIndex = 0;
  if(childIndex % 2 === 0) {
    parentIndex = (childIndex - 2) / 2  // 如果是右节点,父节点的下标
  }else{
    parentIndex = (childIndex - 1) / 2  // 如果是左节点,父节点的下标
  }
  return parentIndex;
}


// 最小堆上浮(一般对应插入操作)
function upAdjust(arr) {
  let childIndex = arr.length -1; // 初始子节点的下标
  parentIndex = getParentIndex(childIndex)
  let temp = arr[childIndex]  // 子节点值
  while((childIndex > 0) && (temp < arr[parentIndex])) {
    arr[childIndex] = arr[parentIndex]
    childIndex = parentIndex
    // 根据子节点下标的奇偶性,来获取对应父节点的下标
    parentIndex = getParentIndex(childIndex)
  }
  arr[childIndex] = temp
  return arr;
}

const testArr1 = upAdjust([4,2,9,36,7,8,1])
console.log(testArr1); // [ 1, 2, 4, 36, 7, 8, 9 ]


/**
 * @description 二叉堆下沉(最小堆)
 * @param arr: 待调整的二叉堆
 * @param parentIndex: 要下沉的父节点位置
 * @return arr: 调整后的二叉堆数组
 */
function downAdjust(arr, parentIndex) {
  const len = arr.length;
  // 保存父节点的值,用于最后赋值
  let temp = arr[parentIndex]
  // 左节点下标值
  let childIndex = parentIndex * 2 + 1;
  // 如果存在左孩子节点
  while(childIndex < len) {
    // 检测是否存在右节点,且右节点比左节点小点,则取右节点下标值;否则还是使用左节点下标
    if(childIndex + 1 < len && arr[childIndex + 1] < arr[childIndex]) {
      childIndex++;
    }
    // 如果父节点值比左右节点中最小的节点值都要小,那么结束
    if(temp < arr[childIndex]) break;
    // 否则,将爷节点与最小子节点值交换
    arr[parentIndex] = arr[childIndex]
    parentIndex = childIndex
    childIndex = parentIndex * 2 + 1
  }
  arr[parentIndex] = temp
  return arr;
}

const testArr2 = downAdjust([4,2,9,36,7,8,1,3], 0)
console.log(testArr2); // [ 2, 4, 9, 36, 7, 8, 1, 3 ]


/**
 * @description 构造二叉堆,将无序的完全二叉树转换成一个二叉堆(最小堆)
 * @param arr: 待调整的完全二叉树数组
 * @return arr: 调整后的二叉堆
 */
function buildHeap(arr) {
  let childIndex = arr.length - 1
  let parentInddex = getParentIndex(childIndex)
  for(let i = parentInddex; i >= 0; i--) {
    downAdjust(arr, i)
  }
  return arr;
}
const testArr3 = buildHeap([4,2,9,36,7,8,1,3], 0)
console.log(testArr3); // [ 1, 2, 4, 3, 7, 8, 9, 36 ]

二叉堆的应用:最大优先队列和最小优先队列

我们先看看队列的特点:先进先出
比如:[7,1,2,3,4,5]
要入队8:[7,1,2,3,4,5,8]
出队时时,如果要出队8,就必须先出队7, 1,2,3, 4,5,最后才能出队8

那么,如果我不想出队其它的,就想直接出队8呢?要怎么做到呢?或者我想每次出队最小的,怎么做到呢?

暴力遍历

这就是我们日常思考的问题,怎么从一些集合中拿到最大值和最小值,相信大家脑瓜里第一想到的是暴力解法,遍历!

for(let i =0; i< arr.length;  i++) {
  // 比较拿出最大最小值..blablabla
}

这当然可以解决问题,但必须遍历n次(集合长度),也就是时间复杂度为O(n)

最大优先队列和最小优先队列

什么是最大优先队列和最小优先队列呢?就是无论入队顺序如何,都是当前最大或最小的元素优先出队

再瞅瞅刚刚实现的最大最小堆,是不是可以做到直接拿出堆顶,就是我们想要的答案了?

然后我们再来看看,二叉堆的时间复杂度,上浮和下沉操作每一次操作平均都是n/2,所以是O(logn),构建堆的操作是O(n),所以如果使用堆来实现队列的入队和出队,是不是就可以将O(n)优化成了O(logn)

因为上面实现的是最小堆,所以这里我实现的就是最小优先队列,最大优先队列同理是用最大堆做的,上代码:

/**
 * 使用二叉堆实现最小优先队列
 */
 class ProorityQueue {
    constructor() {
      // 设置队列初始长度为32
      this.queue = new Array(32)
      this.size = 0; // 记录已有个数的最后下标
    }
    // 入队
    enQueue(key) {
      if(this.size >= this.queue.length) {
        this.resize()
      }else {
        this.queue[this.size] = key
        this.size++;
        this.upAdjust()
      }
    }
    // 出队
    deQueue() {
      if(!this.size) {
        throw new Error("this queue is empty!!")
      }
      const head = this.queue[0]
      this.queue[0] = this.queue[--this.size]
      this.downAdjust()
      return head
    }
  
    // 根据子节点下标的奇偶性,来获取对应父节点的下标
    getParentIndex(childIndex) {
      let parentIndex = 0;
      if(childIndex % 2 === 0) {
        parentIndex = (childIndex - 2) / 2  // 如果是右节点,父节点的下标
      }else{
        parentIndex = (childIndex - 1) / 2  // 如果是左节点,父节点的下标
      }
      return parentIndex;
    }
  
  
    upAdjust() {
      let childIndex = this.size -1; // 初始子节点的下标
      let parentIndex = this.getParentIndex(childIndex)
      let temp = this.queue[childIndex]  // 子节点值
      while((childIndex > 0) && (temp < this.queue[parentIndex])) {
        this.queue[childIndex] = this.queue[parentIndex]
        childIndex = parentIndex
        // 根据子节点下标的奇偶性,来获取对应父节点的下标
        parentIndex = this.getParentIndex(childIndex)
      }
      this.queue[childIndex] = temp
    }
  
    downAdjust() {
      const len = this.size;
      // 保存父节点的值,用于最后赋值
      let temp = this.queue[0]
      let parentIndex = 0;
      // 左节点下标值
      let childIndex = 1;
      // 如果存在左孩子节点
      while(childIndex < len) {
        // 检测是否存在右节点,且右节点比左节点小点,则取右节点下标值;否则还是使用左节点下标
        if(childIndex + 1 < len && this.queue[childIndex + 1] < this.queue[childIndex]) {
          childIndex++;
        }
        // 如果父节点值比左右节点中最小的节点值都要小,那么结束
        if(temp < this.queue[childIndex]) break;
        // 否则,将爷节点与最小子节点值交换
        this.queue[parentIndex] = this.queue[childIndex]
        parentIndex = childIndex
        childIndex = parentIndex * 2 + 1
      }
      this.queue[parentIndex] = temp
    }
  
    // 扩容
    resize() {
        this.queue = [...this.queue, ...new Array(this.size)]
    }
  }
  
  let queue = new ProorityQueue()
  queue.enQueue(10)
  queue.enQueue(8)
  queue.enQueue(3)
  queue.enQueue(1)
  queue.enQueue(5)
  queue.enQueue(9)
  console.log(queue);   // ProorityQueue { queue: [ 1, 3, 8, 10, 5, 9, <26 empty items> ], size: 6 }
  const min = queue.deQueue()
  console.log(min); // 1

你可能感兴趣的:(树结构(javascript)-2:二叉堆)