[路飞]_JavaScript大、小根堆

1,138 阅读5分钟

序言

最近在刷leetcode遇到一个名为【大根堆】的东西,首先我不太清楚这是什么,所以我不能改厨准确的定义;它是方法?或者一个阶梯思路?或者一个数据结构?或者是其他什么。因为这个名字广泛出现在各种题解中。比如:

既然这么多题出现【大根堆】,它应该很重要,而我又不会;所以我决定搞清楚它

百度百科上说:堆通常是一个可以被看做一棵完全二叉树的数组对象

比如:数组array = [7,3,8,5,1,2]

数组array可以看做如下完全二叉树

          7
        /   \
       3     8
      / \   /
     5   1 2

是要将数组转换成二叉树?
不不不,只是把数组类比成二叉树,要不然我说8和7要调换位置,你会问为什么要调换?

什么是大根堆?

什么是大根堆:每个结点的值都大于等于其左右孩子结点的值
比如:数组array = [7,3,8,5,1,2]
数组array大根堆可以是

          8
        /   \
       5     7
      / \   /
     3   1 2

上述二叉树是不是节点值都大于等于左右子节点?

这就是大根堆;

疑问:你这还是将数组转换为二叉树了呀。
没有啊,数组由[7,3,8,5,1,2] 变成了 [8,5,7,3,1,2]

数组还是数组,只是数组中的值位置发生了改变;

改变的规则是按照二叉树的大根堆规则来的;这样在数组添加,或者删除元素后,可以快速找到数组中最大值,最小值

总结:找最值用大根堆,小根堆

如何构建大根堆呢?

添加

比如:数组array = [7,3,8,5,1,2],二叉树根节点位0; 构建后的大根堆位list = [0],其中0没有实际意义,表示根节点

1、取数组第一位7放入二叉树

          7

list = [0,7]
2、取第2位值3放在二叉树中;

  • list = [0,7,3]
  • list符合大根堆吗?
          7
        /   
       3

符合,不处理
3、取数组array第3为值8放在二叉树中;

  • list = [0,7,3,8]
  • list符合大根堆吗?
          7
        /   \
       3     8
    

不符合,8>7所以8要与7交换位置;

在二叉树视图上比较好观察,但是怎么在程序中交换他们的位置呢?

这里需要补充几个概念;

在完全二叉树中,对于非根节点x都有

父级值在array数组的下标为: Math.floor(x/2)
左子级值在array数组的下标为:2x 右子级值在array数组的下标为: 2x + 1

          1
        /   \
       2     3
      / \   /  \
     4   5 6    7

所以:7与8交换位子就是8所在的下标3,与7所在的下标1交换位置,所以数组变成了[0,8,3,7] 直观的看二叉树变成了

          8
        /   \
       3     7
    

4、取数组array第4位值5放入list中

  • list = [0,8,3,7,5]
          8
        /   \
       3     7
      /
     5
  • 不符合大根堆条件,因为5 > 3
    如何调整数组呢?将5的位置与3互换;
    5的下标是多少呢?是4;3的下标可以计算出来是2;所以5与3调换后得到list = [0,8,5,7,3]
          8
        /   \
       5     7
      /
     3

总结

通过以上4个步骤,可以知道,在向数组中添加数据的时候,现添加到数组末尾,数组末尾表示数组的叶子节点;将该数据放在叶子节点后,将该值与该值的父级节点值对比,如果该值大于父级节点值,交换两个位置;参考步骤3、4

因为从叶子节点向上交换,所以该值也父级节点交换后,要判断交换后的父级节点与父级的父级节点比较,直至比较到根节点或者小于父级节点;

删除

现在list = [0,8,5,7,3] 与list对应的二叉树如下:

          8
        /   \
       5     7
      /
     3

获取数组最大值,返回array[1]即可;

删除数组最大值呢?

首先:将最后一个节点值与第一个节点子调换;调换后如下 list = [0,3,5,7,8]

          3
        /   \
       5     7
      /
     8

第2步:删除叶子节点;list.pop() list = [0,3,5,7]

          3
        /   \
       5     7

第3步:重新构建大根堆

在这个例子中,3应该与左子节点交换还是应该去右子节点交换?这个是关键;

假如3与左子节点交换,交换后

          5
        /   \
       3     7

不行吧,还是没构成大根堆,因为7>5
假如3与右子节点交换,交换后

          7
        /   \
       5     3

符合大根堆;

通过上述两个假设,可以到的一个结论,如果根节点与子节点交换,可以重新构成大根堆,则与该子节点交换

了解以上内容,可以编辑代码

手写堆

先搭个架子

class Heap {
  constructor(compare) {
    this.list = [0] //数组,存放数据
    this.compare =
        typeof compare === 'function' ? compare : this.defaultCompare
  }
  //控制堆升序排列还是降序排列
   defaultCompare(a, b) {
   return a > b
  }
  isEmpty() {} //是否为空
  getSize() {}// 数组长度
  top() {}// 最大值
  pop() {}// 删除最大值
  push() {}//添加值
  left = (x) => 2 * x //根据当前节点下标获取节点左侧子节点下标
  right = (x) => 2 * x + 1
  parent = (x) => Math.floor(x / 2)
}

isEmpty

  isEmpty() {
      return this.num === 0
    }

top

 top() {
      return this.list[1]
    }

push

  push(val) {
      // 新增数据,向堆尾添加
      this.list.push(val)

      this.up(this.list.length - 1)
    }
    // 上浮
    up(k) {
      const { list, parent, compare } = this
      //迭代交换当前节点与父节点
      while (k > 1 && compare(list[k], list[parent(k)])) {
        this.swap(parent(k), k)
        k = parent(k)
      }
    }

pop

核心代码,重点难点,需要着重理解down函数;从第1位,将数据【下沉】到对应位置,

 pop() {
      const { list } = this
      if (list.length === 0) return null
      this.swap(1, list.length - 1)
      const top = list.pop()
      this.down(1)
      return top
    }

    down(k) {
      const { list, left, right, compare } = this
      const size = this.getSize()
      console.log('size', size)
      while (left(k) <= size) {
        let _left = left(k)
        if (right(k) <= size && compare(list[right(k)], list[_left])) {
          _left = right(k)
        }
        if (compare(list[k], list[_left])) return
        this.swap(k, _left)
        k = _left
      }
    }

完整代码

  class Heap {
    constructor(compare) {
      this.list = [0]
      this.compare =
        typeof compare === 'function' ? compare : this.defaultCompare
    }

    defaultCompare(a, b) {
      return a > b
    }

    swap(x, y) {
      const t = this.list[x]
      this.list[x] = this.list[y]
      this.list[y] = t
    }
    isEmpty() {
      return this.list.length === 1
    }
    getSize() {
      return this.list.length - 1
    }
    top() {
      return this.list[1]
    }

    left(x) {
      return 2 * x
    }
    right(x) {
      return 2 * x + 1
    }
    parent(x) {
      return Math.floor(x / 2)
    }

    push(val) {
      // 新增数据,向堆尾添加
      this.list.push(val)

      this.up(this.list.length - 1)
    }
    // 上浮
    up(k) {
      const { list, parent, compare } = this
      while (k > 1 && compare(list[k], list[parent(k)])) {
        this.swap(parent(k), k)
        k = parent(k)
      }
    }
    pop() {
      const { list } = this
      if (list.length === 0) return null
      this.swap(1, list.length - 1)
      const top = list.pop()
      this.down(1)
      return top
    }

    down(k) {
      const { list, left, right, compare } = this
      const size = this.getSize()
      while (left(k) <= size) {
        let _left = left(k)
        if (right(k) <= size && compare(list[right(k)], list[_left])) {
          _left = right(k)
        }
        if (compare(list[k], list[_left])) return
        this.swap(k, _left)
        k = _left
      }
    }
  }

参考文档

堆-百度百科