【数据结构与算法学习笔记-Binary Heap优先队列和二叉堆】

本文为学习笔记,感兴趣的读者可在MOOC中搜索《数据结构与算法Python版》或阅读《数据结构(C语言版)》(严蔚敏)
目录链接:https://blog.csdn.net/floating_heart/article/details/123991211

2.2.1 优先队列

优先队列Priority Queue

普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。

在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先出队,具有最高级先出 (first in largest out)的行为特征。

优先队列也可以理解为一种排序,只是在优先队列的内部,数据的顺序并没有严格的要求,只对即将出队的数据有严格的要求。

优先队列也可以理解为一种查找,只是其查找的条件已经被队列初始化时所限定,不能修改。

例如:
重要事件可以插队;
操作系统中执行关键任务的进程或用户特别指定进程在调度队列中靠前

优先队列的实现

实现优先队列主要通过堆来完成,主要有:二叉堆、d堆、左式堆、斜堆、二项堆、斐波那契堆、pairing 堆等。

此处采用二叉堆的实现方案进行尝试。

2.2.2 从完全二叉树到二叉堆(Binary Heap)

1) 完全二叉树

对于树和二叉树在前文已经有所介绍,对于二叉树来说,我们可以根据形态将其分为三类:满二叉树、完全二叉树和非完全二叉树,如下图[1]所示。

对于完全二叉树,除最底层外,其他各层节点数均达到最大,底层节点集中在最左边。

满二叉树一定是完全二叉树,而完全二叉树不一定是满二叉树

2) 堆

堆是一类特殊的二叉树,其性质如下:

  • 堆总是一棵完全二叉树。
  • 堆中某个结点的值总是不大于或不小于其父结点的值;

3) 二叉堆

二叉堆(英语:binary heap)是一种特殊的堆,二叉堆是完全二叉树或者是近似完全二叉树。

二叉堆满足堆特性:父节点的键值总是保持固定的序关系于任何一个子节点的键值,且每个节点的左子树和右子树都是一个二叉堆。

当父节点的键值总是大于或等于任何一个子节点的键值时为“最大堆”。
当父节点的键值总是小于或等于任何一个子节点的键值时为“最小堆”。

复杂度

二叉堆是实现优先队列的经典方案之一,其能够将优先队列的入队和出队复杂度都保持在O(log n)

存储结构

二叉堆的逻辑结构上虽然像二叉树,但其可以用非嵌套列表实现。

与顺序结构存储相似,这一存储方式如下图所示:

采用这一存储方式,如果节点的下标为p,那么其左子节点下标为2p,右子节点为2p+1,其父节点下标为p//2。

灵活使用这一性质,会使我们的代码事半功倍。

二叉堆也可以使用节点链接的方案实现,只是会相对复杂,此处我们以非嵌套列表的方案进行尝试。

2.2.3 二叉堆的简单实现

ADT BinaryHeap 定义的基本操作:

**BinHeap():**创建一个空二叉堆对象;
**insert(k):**将新key加入到堆中;O(log n)
**findMin():**返回堆中的最小项,最小项仍保留在堆中;O(1)
**delMin():**返回堆中的最小项,同时从堆中删除;O(log n)
**isEmpty():**返回堆是否为空;O(1)
**size():**返回堆中key的个数;O(1)
**buildHeap(list):**从一个key列表创建新堆O(nlog n)

操作的解决方案:Python

为了方便理解,此处对每一个操作进行说明的同时,用python来实现操作。

为了练习JavaScript,最后再用JavaScript实现整体数据结构。

二叉堆初始化

class BinHeap:
  # 如之前存储结构所示,此处采用一个列表来保存堆数据
  # 表首下标为0的项无用,但为了后面代码可以用到简单的整数乘除法,仍保留它
  def __init__(self) -> None:
      self.heapList = [0]
      self.currentSize = 0

insert(key)与上浮操作

# 首先,为了保持“完全二叉树”的性质,新key应该添加到列表末尾。
# 新key添加之后,对于其到根的路径可能破坏次序
# 需要将新key沿着路径来“上浮”到其正确位置

  # 上浮操作
  # 一旦上浮开始,必定在heapList[1]根节点处结束
  # 要求hespList新添加项中,都大于0
  # 此处可以添加结束条件,在没有进行交换的时候结束
  def percUp(self,i):
    while i // 2 > 0 :
      if self.heapList[i] < self.heapList[i//2]:
        tmp = self.heapList[i//2]
        self.heapList[i//2] = self.heapList[i]
        self.heapList[i] = tmp
      i = i // 2
  # 插入操作
  # 插入之后修改currentSize
  # 插入之后,对新节点进行上浮操作
  def insert(self,key):
    self.heapList.append(key)
    self.currentSize = self.currentSize + 1
    self.percUp(self.currentSize)

插入new item:

上浮:

delMin()移除堆顶元素

# 移走堆中最小的key:根节点heapList[1]
# 为了保持“完全二叉树”的性质,用最后一个节点代替根节点
# 新的根节点进行“下沉操作”,直到比两个子节点都小

  # 下沉操作
  # 下沉路径的选择:如果比子节点大,那么选择较小的子节点交换下沉
  # 一旦下沉开始,必定到最后的叶节点结束
  # 可以添加结束条件,在没有进行交换的时候结束
  def percDown(self,i):
    while (i * 2) <= self.currentSize:
      mc = self.minChild(i)
      if self.heapList[i] > self.heapList[mc]:
        tmp = self.heapList[i]
        self.heapList[i] = self.heapList[mc]
        self.heapList[mc] = tmp
      i = mc
  # minChild()找到较小的子节点
  def minChild(self,i):
    if i * 2 + 1 > self.currentSize:
      return i * 2
    else:
      if self.heapList[i * 2] < self.heapList[i * 2 + 1]:
        return i * 2
      else:
        return i * 2 + 1
  # 移除最小值操作
  # 移除堆顶,以末尾的值替换堆顶,剔除末尾的值
  # 下沉操作
  # 返回被移除的堆顶
  def delMin(self):
    retval =  self.heapList[1]
    self.heapList[1] = self.heapList[self.currentSize]
    self.currentSize = self.currentSize - 1
    self.heapList.pop()
    self.percDown(1)
    return retval

移除堆顶元素:

下沉:

buildHeap(list)从无序表创建二叉堆

# buildHeap(lst):从无序表生成二叉堆
# 方法一:用insert()方法逐个添加,复杂度O(nlogn)
# 方法二:用下沉法,复杂度O(n)
# 此处采用方法二
  def buildHeap(self,alist):
    i = len(alist) // 2 # 从最后的父节点开始
    self.currentSize = len(alist)
    self.heapList = [0] + alist
    # print(len(self.heapList),i)
    while i > 0:
      self.percDown(i)
      i = i - 1
    print(self.heapList[1:]) # 打印二叉堆列表

其它操作

# 其它操作
  # findMin()返回堆中的最小项,最小项仍保留在堆中
  def findMin(self):
    if self.currentSize == 0:
      raise Exception('堆中没有数据')
    else:
      return self.heapList[1]
  # isEmpty()返回堆是否为空
  def isEmpty(self):
    if self.currentSize == 0:
      return True
    else:
      return False
  # size()返回堆中key的个数
  def size(self):
    return self.currentSize

完整代码:JavaScript

class BinHeap {
  constructor() {
    this.heapList = [0]
    this.currentSize = 0
  }
  // 上浮操作
  percUp(i) {
    while (parseInt(i / 2) > 0) {
      if (this.heapList[i] < this.heapList[parseInt(i / 2)]) {
        let tmp = this.heapList[i]
        this.heapList[i] = this.heapList[parseInt(i / 2)]
        this.heapList[parseInt(i / 2)] = tmp
      }
      i = parseInt(i / 2)
    }
  }
  // 插入操作
  insert(key) {
    this.heapList.push(key)
    this.currentSize += 1
    this.percUp(this.currentSize)
  }
  // 找到堆中最小值
  findMin() {
    if (this.currentSize > 0) {
      return this.heapList[1]
    } else {
      throw new Error('Value Error')
    }
  }
  // 下沉操作
  percDown(i) {
    while (i * 2 <= this.currentSize) {
      let mc = this.minChild(i)
      if (this.heapList[i] > this.heapList[mc]) {
        let tmp = this.heapList[i]
        this.heapList[i] = this.heapList[mc]
        this.heapList[mc] = tmp
      }
      i = mc
    }
  }
  // 寻找最小子节点
  minChild(i) {
    if (i * 2 + 1 > this.currentSize) {
      return i * 2
    } else {
      if (this.heapList[i * 2] < this.heapList[i * 2 + 1]) {
        return i * 2
      } else {
        return i * 2 + 1
      }
    }
  }
  // 移除堆顶元素
  delMin() {
    let retval = this.heapList[1]
    this.heapList[1] = this.heapList[this.currentSize]
    this.heapList.pop()
    this.currentSize -= 1
    this.percDown(1)
    return retval
  }
  // 堆是否为空
  isEmpty() {
    if (this.currentSize == 0) {
      return true
    } else {
      return false
    }
  }
  // 返回堆中key的个数
  size() {
    return this.currentSize
  }
  // 从一个array创建新堆
  buildHeap(arr) {
    if (!(arr instanceof Array)) {
      throw new Error('Type Error')
    }
    this.currentSize = arr.length
    this.heapList = [0].concat(arr)
    let i = parseInt(arr.length / 2)
    while (i > 0) {
      this.percDown(i)
      i -= 1
    }
    console.log(this.heapList.slice(1))
  }
}
// 测试
let a = new BinHeap()
var b = [3, 2, 1]
a.buildHeap(b)

堆排序算法O(n log(n))

补充1:算法复杂度计算

二叉堆部分操作的算法复杂度计算相对繁琐,也更加有意思。此处给出一些计算过程,仅供参考。

1.1 insert(key) 入队

insert(key)操作包括列表末尾插入数据和上浮两步。

  1. 不同语言在列表末尾插入数据的复杂度不同,大部分都是常数级复杂度,此处以常数级为标准。

  2. 上浮操作的操作步数计算如下:

    设 新 插 入 的 数 据 为 第 n 个 数 据 , 在 最 差 情 况 下 , 其 需 要 一 路 交 换 值 , 直 到 堆 顶 , 此 时 的 操 作 次 数 与 堆 的 高 度 相 同 , 设 高 度 为 h , 根 节 点 高 度 为 0 , 叶 子 节 点 高 度 最 大 : 设新插入的数据为第n个数据,在最差情况下,其需要一路交换值,直到堆顶,此时的操作次数与堆的高度相同,设高度为h,根节点高度为0,叶子节点高度最大: nh0

    n ≤ M i n ( h ) 2 0 + 2 1 + 2 2 + 2 h − 1 n ≤Min_{(h)} 2^0 + 2^1 + 2^2 + 2^{h-1} nMin(h)20+21+22+2h1

    在 极 限 情 况 下 , 插 入 第 n 个 数 据 后 , 二 叉 堆 逻 辑 结 构 为 满 二 叉 树 , 此 时 : 在极限情况下,插入第n个数据后,二叉堆逻辑结构为满二叉树,此时: n

    n = 2 0 + 2 1 + 2 2 + 2 h − 1    ( 这 一 假 设 不 影 响 复 杂 度 数 量 级 的 判 断 ) n =2^0 + 2^1 + 2^2 + 2^{h-1} \:\:(这一假设不影响复杂度数量级的判断) n=20+21+22+2h1()

    $根据等比数列求和公式,可知:n = 2^0*(1 - 2h)/(1-2)=2h-1 $

    所 以 堆 的 高 度 即 操 作 次 数 为 : h = l o g ( n + 1 ) 所以堆的高度即操作次数为:h = log(n+1) h=log(n+1)

全部的操作次数为: l o g ( n + 1 ) + a   ( a 为 常 数 ) log(n+1) + a\:(a为常数) log(n+1)+a(a)

总体的复杂度为: O ( l o g n ) O(log n) O(logn)

1.2 delMin()出队

出队操作包括提取堆顶,赋值堆顶,删除堆尾数据,堆顶下沉。

  1. 前三步可分为根据索引操作数组和删除数组末尾两种方法,对于大部分语言都是常数级复杂度。
  2. 堆顶下沉和堆尾上浮操作近似,操作次数为 l o g ( n + 1 ) log(n+1) log(n+1)

总体复杂度为: O ( l o g n ) O(log n) O(logn)

1.3 buildHeap(list) 从无序表表创建新堆

该方法属于堆下沉操作的迭代:从最后的父节点开始,至堆顶为止,依次进行下沉操作,不同节点的下沉过程操作次数不同,计算过程如下:

同 样 设 堆 的 高 度 为 h , 其 中 根 节 点 高 度 为 0 , 叶 子 节 点 高 度 最 大 , 每 一 层 数 据 个 数 不 大 于 : 2 h c    ( h c 为 数 据 所 在 层 数 ) 同样设堆的高度为h,其中根节点高度为0,叶子节点高度最大,每一层数据个数不大于:2^{h_c}\:\:(h_c为数据所在层数) h02hc(hc)

每 一 层 数 据 下 沉 需 要 的 操 作 次 数 最 多 为 : h − h c 每一层数据下沉需要的操作次数最多为:h - h_c hhc

极 限 情 况 ( 操 作 次 数 最 多 的 情 况 ) 下 , 操 作 次 数 总 和 为 : S = ∑ 0 h 2 n ( h − n ) 极限情况(操作次数最多的情况)下,操作次数总和为:S=\sum_{0}^{h}2^n(h-n) ()S=0h2n(hn)

上 述 公 式 为 等 差 数 列 乘 等 比 数 列 的 求 和 , 错 位 相 减 计 算 结 果 为 : S = 2 1 + 2 2 + . . . + 2 h − h = 2 ( h + 1 ) − h − 2 上述公式为等差数列乘等比数列的求和,错位相减计算结果为:S=2^1+2^2+...+2^h-h=2^{(h+1)}-h-2 S=21+22+...+2hh=2(h+1)h2

根 据 前 文 可 以 近 似 认 为 : h = l o g ( n + 1 ) , 操 作 次 数 总 和 可 变 换 为 : S = 2 ( n + 1 ) − l o g ( n + 1 ) − 2 = 2 n − l o g ( n + 1 ) 根据前文可以近似认为:h=log(n+1),操作次数总和可变换为:S=2(n+1)-log(n+1)-2=2n-log(n+1) h=log(n+1)S=2(n+1)log(n+1)2=2nlog(n+1)

最终总体复杂度为: O ( n ) O(n) O(n)

补充2:“堆排序”算法

堆排序算法与选择排序算法比较相似,基本思路如下:

  1. 初始化二叉堆,将需要排序的无序表构建为二叉堆;
  2. 将二叉堆中的数据分为两部分,一部分为未排序U,一部分为已排序O,开始阶段所有数据均属于U;
  3. 取出堆顶数据,放入O部分的末尾(或首位),将U的末尾置于堆顶,进行“下沉”操作;
  4. 重复步骤3,直到所有数据都属于O。

排序的结果与堆的性质(小顶堆/大顶堆)和排序后放入的位置(末尾/首位)有关。

复杂度:

  1. 构建二叉堆的复杂度为O(n)
  2. 一次“下沉”的复杂度为O(log n)
  3. 一共有n个数据,共需启动n次“下沉”

易知,算法整体复杂度为O(nlog n),这在非应对特殊场景的排序算法中算是一个良好的成绩。

算法实现中,仅需要多次调用delMin()方法,相对简单,此处不再给出结果。

想要把排序封装到BinaryHeap类中,对对象heapList进行排序,以实现为目的的话,也相对简单,想要挑战自己的话,可以拆分delMin()中的方法,设置下沉的界线,从而在一个列表中进行排序。

参考文献:

[1] 严蔚敏. 数据结构: C语言版[M]. 北京: 清华大学出版社, 2007.

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
二叉排序树(Binary Search Tree,简称BST)是一种特殊的二叉树,它具有以下性质: 1. 左子树上的所有节点的值都小于根节点的值。 2. 右子树上的所有节点的值都大于根节点的值。 3. 左右子树也分别为二叉排序树。 二叉排序树主要用于实现动态查找,可以高效地插入、删除和查找元素。下面是二叉排序树的实现步骤: 1. 建立二叉排序树: - 如果树为空,将新节点作为根节点。 - 如果树不为空,从根节点开始,比较新节点的值与当前节点的值的大小关系: - 如果新节点的值小于当前节点的值,将新节点插入到当前节点的左子树中。 - 如果新节点的值大于当前节点的值,将新节点插入到当前节点的右子树中。 - 如果新节点的值等于当前节点的值,不进行插入操作。 2. 遍历二叉排序树: - 前序遍历:先访问根节点,然后递归地遍历左子树和右子树。 - 中序遍历:先递归地遍历左子树,然后访问根节点,最后递归地遍历右子树。 - 后序遍历:先递归地遍历左子树和右子树,最后访问根节点。 3. 删除节点: - 如果要删除的节点是叶子节点,直接删除即可。 - 如果要删除的节点只有一个子节点,将子节点替换为要删除的节点。 - 如果要删除的节点有两个子节点,可以选择用其前驱节点或后继节点替换。 4. 查找并记录访问次数: - 从根节点开始,比较要查找的值与当前节点的值的大小关系: - 如果要查找的值小于当前节点的值,继续在左子树中查找。 - 如果要查找的值大于当前节点的值,继续在右子树中查找。 - 如果要查找的值等于当前节点的值,找到了目标节点,并记录访问次数。 5. 利用快速排序的思想将负数排在正数前: - 在建立二叉排序树时,可以将负数插入到左子树中,将正数插入到右子树中,这样就可以实现负数排在正数前的效果。 以下是一个二叉排序树的示例代码: ```cpp #include <iostream> struct TreeNode { int value; TreeNode* left; TreeNode* right; }; void insert(TreeNode*& root, int value) { if (root == nullptr) { root = new TreeNode; root->value = value; root->left = nullptr; root->right = nullptr; } else if (value < root->value) { insert(root->left, value); } else if (value > root->value) { insert(root->right, value); } } void inorderTraversal(TreeNode* root) { if (root != nullptr) { inorderTraversal(root->left); std::cout << root->value << " "; inorderTraversal(root->right); } } int main() { TreeNode* root = nullptr; // 插入节点 insert(root, 5); insert(root, 3); insert(root, 7); insert(root, 2); insert(root, 4); insert(root, 6); insert(root, 8); // 中序遍历 inorderTraversal(root); // 输出:2 3 4 5 6 7 8 return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值