一文讲解堆排序,再不要说自己不懂堆排序了!

点关注,不迷路;持续更新Java架构相关技术及资讯热文!!!

什么是堆

堆的基本特点有以下两项

  • 堆是一棵完全二叉树或者是近似完全二叉树
  • 堆里面的每个父节点都要大于或等于(或者小于等于)其子树节点的每个节点值。

什么是完全二叉树

要求除了最后一层以外,其余层的节点都要是满的。

大顶堆

每个节点的值都大于其子节点的值,我们通常称之为大顶堆。

小顶堆

每个节点的值都小于其子节点的值,我们通常称之为小顶堆。

那么我们该如何来进行一个堆的构建呢?
先别急,我们来了解以下的几个概念先:

堆的存储方式

如上图所示,当我们对一个堆进行节点数据存储的时候,将每个节点的值都按照二叉树从上到下,从左到右的顺序存储到数组里面去。拿图中的6号节点来看(暂时命名数组为arr),它的存储位置是arr[6],它的父亲节点为arr[3],如果你有细心发现的话,会发现父子节点之间的编号存在有以下的关系:

leftNo = parentNo * 2

rightNo = parentNo * 2 + 1

parentNo = (nodeNo) / 2

这三个规律在下文中会涉及到。

向上堆化

所谓的向上堆化,是指在构建堆的过程中,当一个节点的值大于其父节点的值的时候,就会发生节点值的交换,直至当前节点的值小于或等于父节点的值。如下图所示:

图中有一个初始化的堆,现在需要往堆里面插入一个新的元素39。

由于堆里面每个节点的存储顺序都是按照二叉树从上至下,从左到右的顺序来进行存储,而且为了满足完全二叉树的要求,所以新的节点会被插入到 5 的位置。

插入之后,就要开始进行向上堆化的过程了。

首先39需要和自己的父节点进行大小比较,然后进行交换节点的值。

再次进行节点大小的比较,然后交换节点的值。

所有的比较节点完毕之后,就会如上图所示

向下堆化

所谓的向下堆化,是指在构建堆的过程中,当一个节点的值小于其父节点的值的时候,就会发生节点值的交换,直至当前节点的值大于或等于父节点的值。

同样我们结合图片的形式来进行描述:
图中有一个初始化的堆结构,然后现在需要进行节点的插入操作

新插入的节点8位于1号索引的位置,然后依次进行向下堆化处理

所有的比较节点完毕之后,就会如上图所示

在建堆(大顶堆)完成之后,数组中的堆顶元素通常都是最大的元素,这个时候,我们只需要将堆顶的元素进行移除,就能获取到当前数组的最大元素了。但是新的问题又来了,如何进行对于堆顶元素的弥补呢?

这个时候我们会将存储节点的数组的最后一个元素弥补到头部节点处,然后从上往下进行向下堆化处理,不断的重复堆顶元素移除,弥补头结点,向下堆化,不断的循环这段操作。

基于大顶堆的排序完整代码

/**
 * @author idea
 * @data 2019/2/19
 */
public class TopHeap {

    private int[] arr;

    private int maxSize;

    private int count;

    public TopHeap(int size) {
        this.arr = new int[size];
        this.maxSize = size;
        this.count = 0;
    }


    /**
     * 添加元素
     *
     * @param data
     */
    private void add(int data) {
        if (count > maxSize) {
            return;
        }
        count++;
        arr[count] = data;
        int i = count;
        //向上堆化
        while (i / 2 > 0 && arr[i] > arr[i / 2]) {
            swap(arr, i, i / 2);
            i = i / 2;
        }
    }


    /**
     * 删除节点
     *
     * @return
     */
    private int removeNode() {
        if (count < 0) {
            return -1;
        }
        int result = arr[1];
        arr[1] = arr[count];
        count--;
        //向下堆化
        heapify(arr, count, 1);
        return result;
    }


    /**
     * 排序
     *
     * @return
     */
    public int[] sort() {
        int[] result = new int[count];
        int size = count;
        for (int j = 0; j < size; j++) {
            result[j] = removeNode();
        }
        return result;
    }


    /**
     * 堆化
     *
     * @param arr   数组
     * @param total 数组总长度
     * @param i     指堆化的开始索引值
     */
    private void heapify(int[] arr, int total, int i) {
        while (true) {
            int maxPos = i;
            if (i * 2 <= total && arr[i] < arr[i * 2]) {
                maxPos = i * 2;
            }
            if (i * 2 + 1 <= total && arr[maxPos] < arr[i * 2 + 1]) {
                maxPos = i * 2 + 1;
            }
            if (maxPos == i) {
                break;
            }
            swap(arr, i, maxPos);
            i = maxPos;
        }
    }


    /**
     * 交换数组元素值
     *
     * @param arr
     * @param n
     * @param k
     */
    private void swap(int[] arr, int n, int k) {
        int temp = arr[n];
        arr[n] = arr[k];
        arr[k] = temp;
    }

    public static void main(String[] args) {
        int[] arr=new int[]{12, 3, -45, 234, 123, 12, 55, 33};

        TopHeap topHeap = new TopHeap(12);
        for (int i : arr) {
            topHeap.add(i);
        }
        int[] res = topHeap.sort();
        for (int re : res) {
            System.out.print(re + " ");
        }
    }

}

堆在java应用中的具体实践

java里面的优先级队列PriorityQueue的底层操作很多都是基于了堆进行排序的,我们知道队列是遵循先进先出(First-In-First-Out)模式的,但有些时候需要在队列中基于优先级处理对象。

例如说当在某个特殊的应用场景中,需要对居民的年龄进行排序操作,那么这个时候可以通过使用PriorityQueue来对每一个Person对象的年龄进行排序,然后进入队列进行年龄的从小到大的排序处理。

import java.util.PriorityQueue;

/**
 * @author idea
 * @date 2019/5/15
 */
class Person implements Comparable<Person> {

    public int age;

    Person(int age) {
        this.age = age;
    }


    @Override
    public int compareTo(Person other) {
        return other.age - age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                '}';
    }
}

public class PriorityQueueDemo {

    public static void main(String[] args) {
        PriorityQueue<Person> queue = new PriorityQueue<>();
        Person person1 = new Person(31);
        Person person2 = new Person(22);
        Person person3 = new Person(65);
        Person person4 = new Person(15);
        Person person5 = new Person(28);

        /**
         * offer 插入新的元素 插入失败会返回false
         * add 插入新的元素 插入失败会抛异常
         */
        queue.offer(person1);
        queue.offer(person2);
        queue.offer(person3);
        queue.offer(person4);
        queue.offer(person5);

        int size = queue.size();

        /**
         * element 会弹出队首元素,但是并不会删除
         * peek 会弹出队首元素,但是也不会删除
         */
        for (int i = 0; i < size; i++) {
            /**
             * remove 移除队首元素 失败会抛异常
             * poll 移除队首元素 失败会返回null
             */
            Person person = queue.poll();
            System.out.println(person.toString());
        }
    }
}

深入源码来看offer操作

  public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length)
            grow(i + 1);
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else
            //插入元素之后需要进行向上堆化处理
            siftUp(i, e);
        return true;
    }


  private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }


  //向上堆化的核心部分
    @SuppressWarnings("unchecked")
    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            //通过比较器进行比对
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        //堆顶元素插入
        queue[k] = key;
    }

poll操作的源码部分:

  public E poll() {
        if (size == 0)
            return null;
        int s = --size;
        modCount++;
        E result = (E) queue[0];
        E x = (E) queue[s];
        queue[s] = null;
        if (s != 0)
        //向下堆化处理
            siftDown(0, x);
        return result;
    }


  private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }

       //向下堆化处理
    private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        int half = size >>> 1;        // loop while a non-leaf
        while (k < half) {
                //找到左右节点里面相对较小的那个节点
            int child = (k << 1) + 1; 
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                 ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                c = queue[child = right];
                //比较器比较元素的大小
            if (key.compareTo((E) c) <= 0)
                break;
                //取代原来节点的值
            queue[k] = c;
            k = child;
        }
        queue[k] = key;
    }

有几点地方我们需要注意一下

  1. PriorityQueue 不允许空值,而且不支持 non-comparable(不可比较)的对象,如果传入的对象是用户自定义的这类对象,队列要求该对象使用Java Comparable 和 Comparator 接口给对象排序,然后再按照优先级来进行对于元素的处理。

  2. PriorityQueue 的大小是不受限制的,但在创建时可以指定初始大小。当我们向优先队列增加元素的时候,队列大小会自动增加。

  3. PriorityQueue 是非线程安全的,所以 Java 提供了 PriorityBlockingQueue(实现 BlockingQueue接口)用于Java 多线程环境。

总体小结

  1. 堆排序在使用的时候整体的过程都是基于两个元素进行不断的比较操作,而且对于原本已经有序的数组而言,通常在初始化建立堆结构的时候,容易将原本已经有序的数据给打乱,因此数据的交换次数会较多。

  2. 堆里面比较重要的两个操作插入和删除操作都会使用到堆化的这么一个步骤,删除堆顶数据的时候,我们把数组中的最后一个元素放到堆顶,然后从上往下堆化。这两个操作时间复杂度都是 O(logn)O(log⁡n)。

  3. 通常我们在进行排序的时候都会优先选择使用快速排序之类的方法,但是在遇到一些和排序没有太大关联的问题的时候,例如说TopN类型的经典问题,这时候堆排序的优势就出来了。用堆排序可以在N个元素中找到top K,时间复杂度是O(N log K),空间复杂的是O(K),而快速排序的空间复杂度是O(N),在这种场景中,使用堆排序会更加适合。

写在最后

点关注,不迷路;持续更新Java架构相关技术及资讯热文!!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容