普林斯顿公开课 算法4-2:二叉堆

二叉树

介绍二叉堆之前首先介绍二叉树。二叉树有一个根节点,节点下又有两个子节点。完全二叉树是指一个二叉树树除了最底层,其他层都是完全平衡的。

完全二叉树最基本的性质就是它的高度是 floor(lgN)。

二叉堆

二叉堆是完全二叉树的一种,每个节点对应一个数值,而且这个数值都大于等于它子节点的数值。

下图是一个二叉堆。

二叉堆的储存

由于二叉堆是完全二叉树,所以它可以用一个数组进行储存。所以不需要创建节点对象,再建立节点之间的连接。这样节省了很多开销。

用数组a[]表示一个二叉堆有以下特性:

  • a[0]不表示任何数据,这样后续的计算就简化了很多
  • a[1]是根节点
  • a[2]a[3]是二叉树第一层的两个节点
  • a[4]a[5]a[6]a[7]是二叉树的第二层节点

以此类推

下图展示了堆的数据结构用数组进行表示的示意图。

二叉堆的访问

二叉堆有以下访问规则:

  • 二叉堆的根就是a[1],也是最大的一个元素。
  • 在a[k]位置的节点,其父节点为k/2
  • 在a[k]位置的节点,其子节点分别为2k和2k+1

二叉树的操作

二叉堆中每个子节点都不能比它的父节点大,然而在修改二叉树之后往往会违反二叉树的性质。

Swim操作

当二叉树的子节点比父节点大时,怎么办呢?没关系,只要按照以下步骤进行就行了:

  1. 将违反规则的节点与父节点进行交换
  2. 重复第一步,直到所有的节点都二叉堆的性质为止

该操作称为swim操作

Sink操作

当二叉树的父节点比任意一个子节点小时,怎么办呢?没关系,只要按照以下步骤进行就行了:

  1. 将违反规则的子节点与两个子节点中较大的子节点进行交换
  2. 重复第一步,直到符合二叉堆的性质为止

该操作称为sink操作

删除操作

从堆中删除最大的元素,需要执行以下步骤:

  • 将最小的元素与根节点进行交换
  • 对根节点执行sink操作,将最小的节点沉淀到二叉堆的最底部

这样,删除操作最多需要2lgN次比较

复杂度

二元堆的插入操作复杂度是logN,删除操作的复杂度是logN。

d元堆的插入操作复杂度是log_d(N),删除操作的复杂度是d log_d(N)。

斐波那契堆的插入操作复杂度是1,删除操作平均复杂度是logN。

不变性

对于二叉堆,需要保元素自身不会发生变化,这样才能保证二叉堆的算法能够正常工作。

因此,在Java中,二叉堆的数据类型尽量选择不可变的数据类型。所谓不可变的类型就是指当对象创建完毕之后,对象中的内容就无法再改变了。

java中不可变的类型有String Integer Double Color Vector Transaction Point2D。

可变的类型有StringBuilder Stack Counter Java数组。

不可变类型的好处就是能确保优先级队列能够正常工作,坏处就是每次有新值的时候都要创建一个新的对象,造成额外的开销。

代码

// 用二叉堆实现的最大优先级队列
public class MaxHeapPQ<Item extends Comparable<Item>> {
    private Item[] items;
    private int N;

    public MaxHeapPQ(int capacity) {
        items = (Item[])new Comparable[capacity+1]; //capacity+1是因为a[0]不储存任何数据,所以需要额外的一个元素。
    }

    public boolean isEmpty() {
        return N==0;
    }

    public void insert(Item item) {
        N++;
        items[N] = item;
        swim(N);
    }

    public Item popMax() {
        Item result = items[1];
        SortUtil.exch(items, N, 1);
        items[N] = null; // 防止产生游离对象造成内存泄露
        N--;
        sink(1);
        return result;
    }

    public int size() {
        return N;
    }

    private void swim(int k) {
        // 一直循环直到到达根节点
        while(k > 1) {
            Item child = items[k];
            Item parent = items[k/2];

            // 如果父节点比子节点还小,就将父子进行交换
            if(SortUtil.less(parent, child)) {
                SortUtil.exch(items, k, k/2);
                k /= 2;
            } else {
                break;
            }
        }
    }

    private void sink(int k) {
        // 一直循环直到没有子节点
        while(k*2 <= N) {
            int parent = k;
            int child1 = k*2;
            int child2 = k*2+1;

            // 如果父节点比任意一个子节点小
            if(less(parent, child1) || less(parent, child2)) { // 注意:这里不能用SortUtil.less进行比较,因为空值也要进行比较
                // 选出较大的子节点
                int greater;
                if(less(child1, child2)) { // 注意:这里child1和child2位置不要写反了
                    greater = child2;
                } else {
                    greater = child1;
                }

                // 将较大的子节点与父节点交换
                SortUtil.exch(items, greater, parent);

                // 准备下一次循环
                k = greater;
            } else {
                break;
            }
        }
    }

    // 判断是否小于。将null值看成无穷小
    private boolean less(int a, int b) {
        if(a > N) return true;
        if(b > N) return false;
        return SortUtil.less(items[a], items[b]);
    }

    private void debugPrint() {
        for(int i=0;i<N;i++){
            System.out.print(items[i+1]);
            System.out.print(" ");
        }
        System.out.println();
    }
}

普林斯顿公开课 算法4-2:二叉堆

时间: 2024-10-10 21:29:50

普林斯顿公开课 算法4-2:二叉堆的相关文章

普林斯顿公开课 算法1-6:内存

讲完了算法的运行时间,现在讲一下关于算法的内存占用率. 内存单位 在计算机中,内存是通过字节来表示的,一个字节表示8个位.1KB是2^10字节. 数据类型占用的内存 在32位系统中,一个指针占用4个字节.在64位系统中一个指针占用8个字节.本课程中使用64位机器,一个指针占用8字节. 基本数据类型 以下是Java中基本数据类型占用的内存 boolean 1字节 byte 1字节 char 2字节 int 4字节 float 4字节 long 8字节 double 8字节 Java数组 Java中

普林斯顿公开课 算法1-10:并查集-优化的快速合并方法

应用 渗透问题 游戏中会用到. 动态连接 最近共同祖先 等价有限状态机 物理学Hoshen-Kopelman算法:就是对网格中的像素进行分块 Hinley-Milner多态类型推断 Kruskai最小生成树 Fortran等价语句编译 形态学开闭属性 Matlab中关于图像处理的bwlabel函数 渗透问题 一个N×N的矩阵,判断顶部和底部是否连通就是渗透问题. 下图中左侧的矩阵能渗透,右侧矩阵不能渗透. 渗透问题在电学.流体力学.社会交际中都有应用. 在游戏中可能需要生成一张地图,但是作为地图

普林斯顿公开课 算法1-11:并查集的应用

应用 渗透问题 游戏中会用到. 动态连接 最近共同祖先 等价有限状态机 物理学Hoshen-Kopelman算法:就是对网格中的像素进行分块 Hinley-Milner多态类型推断 Kruskai最小生成树 Fortran等价语句编译 形态学开闭属性 Matlab中关于图像处理的bwlabel函数 渗透问题 一个N×N的矩阵,判断顶部和底部是否连通就是渗透问题. 下图中左侧的矩阵能渗透,右侧矩阵不能渗透. 渗透问题在电学.流体力学.社会交际中都有应用. 在游戏中可能需要生成一张地图,但是作为地图

普林斯顿公开课 算法2-1:排序概述

目标 对所有类型的数据进行排序. 问题 排序函数如何知道比较的是哪种类型的数据呢? 回调函数 这时候就需要引入回调函数的概念了.回调函数就是将可执行的代码作为参数进行传递. 实现回调的方法 在Java中可以通过接口来实现,在C语言中可以通过函数指针来实现,C++中可以通过class-type functor,也就是重载操作符operator ()的类,在C#中可以使用Delegate委托,在Python/Perl/ML/javascript中可以直接传递函数. JDK中提供了Comparable

普林斯顿公开课 算法1-1:算法分析

为什么要分析算法 分析算法能够预測算法的性能,比較算法之间的优劣,保证算法的正确性,理解算法的理论基础. 成功算法的样例 离散傅立叶变换,假设使用暴力方法,那么算法的复杂度是是N^2,假设使用FFT高速傅立叶变换能够实现O(N logN)复杂度 N-body模拟:使用Barnes-hut算法能够将复杂度减少到N logN 顺便发一张N-body模拟的炫图 Barnes-Hut算法示意图 算法分析的步骤 观察问题的特征和想到得到的结果 依据观察结果提出如果 使用如果来预測可能发生的情况 检測预測结

普林斯顿公开课 算法1-2:观察

这章通过一个简单的例子,详细说明算法分析的步骤. 算法 问题 给定N个不同的整数,从中任意取出三个整数.请问有几种情况,使得取出的3个整数之和为0? 解法 可以使用暴力算法,代码如下: 1 2 3 4 5 6 7 8 9 for(int i=0;i<N;i++){     for(int j=0;j<N;j++){         for(int k=0;k<N;k++){             if(a[i]+b[i]+a[k]==0){                 count+

普林斯顿公开课 算法1-3:数学模型

本节主要通过建立数学模型,来计算算法的运行时间. 公式 算法的运行时间=所有操作的开销乘以操作的次数之和 开销 下表展示了各种操作所需要的时间(单位:纳秒) 整数加法 2.1 整数乘法 2.4 整数除法 5.4 浮点加法 4.6 浮点乘法 4.2 浮点除法 13.5 sin 91.3 arctan 129.0 举例 问题 计算数据中0的个数 代码 1 2 3 4 int count = 0; for (int i= 0; i < N; i++)     if (a[i] == 0)       

普林斯顿公开课 算法1-5:算法理论

本节主要讲解的是算法的复杂度. 算法性能 算法的性能分为三种: 最佳情况:计算时间最短的情况 最差情况:计算时间最长的情况 平均情况:随机输入的期望开销 以二分查找为例 最佳情况是1,因为第一次就有可能找到需要找的整数. 最差情况是logN 平均情况是logN 算法复杂度 算法复杂度用于定义问题的难度,另外也有助于开发最优化的算法,算法复杂度可以通过分析最坏情况来减少输入数据对算法性能的影响. 为了简化问题难度的表示方法,算法复杂度减少了算法分析的细节,忽略常数系数. 最优算法 所谓的最佳算法就

普林斯顿公开课 算法1-7:并查集基本概念

本节讲的是并查集的基本概念. 算法的开发步骤 对问题进行数学建模 寻找一个能够解决问题的算法 运行算法检测速度和内存是否符合要求 如果达不到要求,找出原因 寻找一种方法来解决问题 循环步骤,直到满意为止 以上就是算法开发比较科学的方法.算法开发完成之后需要进行数学分析. 并查集问题 给定N个物体,可以提供两种操作,一种是合并操作,一种是查找操作.合并操作就是将两个节点进行连接,查找操作就是判断两个节点是否连接在一起. 应用中的物体类型 实际应用中,并查集算法可以支持各种各样的物体类型,比如: 图