数据结构---堆

导语

  • 堆的逻辑数据结构实际上是一个可以使用数组实现的完全二叉树(堆也一定是平衡二叉树),所以学习堆,完全二叉树不是很了解的,可以看一下树结构---基础中完全二叉树的简单介绍。
  • 本篇实现自定义最大堆时,使用之前自定义的动态数组作为物理存储结构

1.堆基础

  • 最大堆:某个节点的值总是大于等于其左右孩子的值
  • 最小堆:某个节点的值总是小于等于其左右孩子的值
  • 完全二叉树:由完全二叉树的性质,可知除了叶子层之外,其他层可以组成一个满二叉树,而叶子则是由从左右向缺失节点的。这一特性,使用层序存储(即层序遍历的顺序)的方式,可以使用一个数组来完成
  • 树节点的父子关系:可以转化成数组中的索引之间的关系
堆.png

由上述特性,我们在使用数组存储堆中一个元素后,可以轻松找到这个元素的父亲节点的位置和其左右孩子的位置。

   /**
     * 返回完全二叉树的数组表示中
     * 一个索引所表示的元素的父亲结点的索引
     *
     * @param index :传入元素索引
     * @return :父亲元素索引
     */
    private int parent(int index) {
        if (index == 0) {
            throw new IllegalArgumentException("index-0 doesn't have parent!");
        }
        return (index - 1) / 2;
    }

    /**
     * 返回完全二叉树的数组表示中
     * 一个索引所表示的元素的左孩子结点的索引
     *
     * @param index:传入元素索引
     * @return :其左孩子索引
     */
    private int leftChild(int index) {
        return index * 2 + 1;
    }

    /**
     * 返回完全二叉树的数组表示中
     * 一个索引所表示的元素的右孩子结点的索引
     *
     * @param index:传入元素索引
     * @return :其右孩子索引
     */
    private int rightChild(int index) {
        return index * 2 + 2;
    }

2.使用动态数组实现堆

2.1.使用数组作为物理内存结构

public class MaxHeap<E extends Comparable<E>> {

    private Array<E> data;

    public MaxHeap(int capacity) {     data = new Array<>(capacity);   }

    public MaxHeap() {   data = new Array<>();  }

    public MaxHeap(E[] arr) {
        data = new Array<>(arr);
        //当传入一个数组构造最大堆的时候
        //从最大堆的数组中第一个非叶子节点开始,一直到根节点进行下沉操作
        //第一个非叶子节点的索引 = 最后一个叶子节点的父节点索引
        for (int i = parent(data.getSize() - 1); i >= 0; i--) {
            siftDown(i);
        }
    }

    public int getSize() {  return data.getSize();   }

    public boolean isEmpty() {  return data.isEmpty();  }
}
  1. 根据堆的性质,每个父节点的值比左右孩子值要大,每个元素要满足可以比较,因此泛型 E extends Comparable<E>
  2. 代码中提供了三个构造函数,前两个不需要解释,第三个构造函数是传入一个数组,构造成一个堆结构的数据,相对比较复杂,单独拿出来讨论

2.2 参数为数组的构造函数

  1. 在构造过程中主要有两步操作:
    1.首先先将所有传进来的数组添加到堆中的数组中
    2.之后在第一个非叶子节点的位置开始,一直到根节点进行下沉操作

  2. 为何从第一个非叶子节点进行下沉操作,而不是从最后一个元素开始,便可使整个数组满足堆性质
    2.1.为何从第一个非叶子节点开始?

    为了满足堆的性质,需要把节点元素与其左右子节点元素中最大的元素发生位置交换,之后再交换其最大子节点与最大子节点的子节点中的最大的元素,一直交换到该节点为整棵树的叶子节点为止,而这个交换的过程实质上是下沉操作。由于所有的叶子节点不再有非空子结点,并不需要与子节点发生比较这个操作,因此只需要从第一个非叶子节点开始就可以,并不需要从最后一个叶子节点开始。

    2.2.如何找到第一个非叶子节点

    由于完全二叉树是以数组形式层序存储的,因此我们可以轻松的找到第一个叶子节点的索引,而由于是完全二叉树的性质,最后一层非满层都会在左侧,而右侧的叶子节点必然在上一层中,所以第一个非叶子节点一定会是最后一个叶子节点的父节点


    堆-第一个非叶子节点.png
  3. 下沉操作

实质上某节点与其最大子节点比较,若值小于最大子节点,则发生交换。这个过程就是下沉过程


下沉操作.png

在上述构造函数执行的实质上是从第一个非叶子节点到根结点进行下沉操作。

 /**
     * 下沉操作:
     * 若该节点的值比其左右孩子结点的元素值要写:与其孩子结点中最大的元素值进行交换,直到满足最大堆性质
     *
     * @param i :索引 = 0
     */
    private void siftDown(int i) {
        //在下沉过程中,若其左孩子结点的索引值 <= 元素的最大索引值,才进行交换操作
        //否则,说明其左右孩子并不存在
        while (leftChild(i) < data.getSize()) {
            //说明左孩子一定存在,右孩子不一定
            //此时左右孩子默认左孩子最大,且保存其索引值
            int maxIndex = leftChild(i);
            //其右孩子索引值是否存在
            if (maxIndex + 1 < data.getSize()) {
                //则比较左右孩子中值最大,并返回最大的索引值
                if (data.get(maxIndex).compareTo(data.get(maxIndex + 1)) < 0) {
                    maxIndex = maxIndex + 1;
                }
            }
            if (data.get(i).compareTo(data.get(maxIndex)) >= 0) {
                break;
            }
            data.swap(i, maxIndex);
            i = maxIndex;
        }
    }

2.3.删除操作(取出堆中最大的元素)

  • 删除操作的思路

1.在最大堆中取出最大元素,即取出根元素
2.后继节点选用最后一个叶子节点,用这个值替换根元素的值,并删除最后一个节点
3.再对这个新的根元素进行下沉操作,来满足堆的性质

  /**
     * 取出堆中最大的元素
     *
     * @return :索引为0 的数据
     */
    public E extractMax() {
        //获得最大的元素
        E e = findMax();
        //以最后一个元素为后继,将最后一个元素赋值给头元素(交换)
        data.swap(0, data.getSize() - 1);
        //删除最后一个元素
        data.removeLast();
        //此时最后一个元素继承了第一个元素位置,但是可能比其孩子元素小,因此进行下沉操作
        siftDown(0);
        return e;
    }

2.4.添加操作

  • 添加操作思路

1.按照完全二叉树的性质,挨着最后一个叶子节点添加新结点
2.新节点并不满足堆的性质,所以需要对新添节点进行上浮操作

  /**
     * 向堆中添加元素
     *
     * @param e :待添加的元素
     */
    public void add(E e) {
        data.addLast(e);
        //上浮新添加元素,根据其索引来比较与其父元素大小来满足最大二叉堆性质
        siftUp(data.getSize() - 1);
    }
  • 上浮操作

下沉操作是让一些小于最大子节点的节点往下移动,而上浮操作正好相反,是让一些大于父节点的节点,与父节点交换位置。两个操作的目的都是为了保证父节点大于左右子节点。


上浮操作.png
   /**
     * 上浮操作:
     * 当往一个二叉树的数组中添加一个元素时,为了满足最大二叉堆的性质
     * 即新添加元素比其父元素要小的性质:若新添元素小于其父元素,需要交换二叉堆组成的数组中相应的 新添元素和其父元素的值
     *
     * @param i :新添元素所在最大二叉堆的数组中的索引
     */
    private void siftUp(int i) {
        //若i索引不是根且值大于其父索引的值,交换值
        while (i > 0 && data.get(i).compareTo(data.get(parent(i))) > 0) {
            data.swap(i, parent(i));
            i = parent(i);
        }
    }

2.5.替换操作

  • 替换操作思路

1.先查找出堆中最大的元素,即根元素,替换成新的元素
2.对新的元素进行下沉操作

  /**
     * 查看堆中最大的元素
     *
     * @return :返回索引为0 的元素
     */
    public E findMax() {
        if (data.getSize() == 0) {
            throw new IllegalArgumentException("Can not findMax when heap is empty!");
        }
        return data.get(0);
    }

    /**
     * 取出堆中最大的元素,并替换成元素E
     *
     * @param e :替换的元素
     * @return :
     */
    public E replace(E e) {
        //查询最大的元素并返回
        E ret = findMax();
        //替换e到最大元素位置
        data.set(0, e);
        //下沉保持最大堆性质
        siftDown(0);
        return ret;
    }

3.堆的时间复杂度

  • 堆的主要操作发生在上浮和下沉两个核心操作上,而这两个操作对于堆的逻辑结构完全二叉树来说,是一个O(logn)的操作。因此堆的增删改操作时间复杂度都是O(logn)
  • 直接对一个数组转换构成一个堆,时间复杂度为O(n)
  • 堆有一个排序算法,即堆排序,简单说一下,对n个元素进行操作,而每个元素需要进行一次下沉操作,所以堆排序的时间复杂度是O(nlogn)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354