堆的特性:
1、必须是完全二叉树
2、任一结点的值是其子树所有结点的最大值或最小值

Tips:

最大值时,称为“最大堆”,也称大顶堆;

最小值时,称为“最小堆”,也称小顶堆。

        public class MinHeap<E extends Comparable<E>> {
        private List<E> data;

        public MinHeap(int capacity) {
            data = new ArrayList<>(capacity);
        }

        public MinHeap() {
            data = new ArrayList<>();
        }

        // 返回堆中的元素个数
        public int size() {
            return data.size();
        }

        // 返回一个布尔值, 表示堆中是否为空
        public boolean isEmpty() {
            return data.isEmpty();
        }

        // 返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
        private int parent(int index) {
            return (index - 1) / 2;
        }

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

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

        /**
         *
         * @param i
         * @param j
         */
        public void swap(int i, int j) {
            if (i < 0 || i >= size() || j < 0 || j >= size())
                throw new IllegalArgumentException("Index is illegal.");

            E temp = data.get(i);
            data.set(i, data.get(j));
            data.set(j, temp);
        }
        /**
         * index 为i位置元素上浮。
         *
         * @param i
         */
        private void siftUp(int i) {
            //特性2:比较插入值和其父结点的大小关系,小于父结点则用父结点替换当前值,index位置上升为父结点
            // 当上浮元素大于父亲,继续上浮。并且不能上浮到0之上
            // 直到i 等于 0 或 比 父亲节点小了。
            while (i > 0 && data.get(i).compareTo(data.get(parent(i))) > 0) {
                // 数组Array中添加方法swap
                swap(i, parent(i));
                i = parent(i); // 这句话让i来到新的位置,使得循环可以查看新的位置是否还要大。
            }
        }
        /**
         * 堆中添加元素方法。
         *
         * @param e
         */
        public void add(E e) {
            //特性1:新插入的元素首先放在数组最后,保持完全二叉树的特性
            data.add(e);
            siftUp(data.size() - 1);
        }

        public E findMin() {
            return data.get(0);
        }

        public E extractMin() {

            E ret = findMin();

            swap(0, data.size() - 1); // 0位置元素和最后一个元素互换。
            data.remove(data.size() - 1); // 删除此时的最后一个元素(最小值)
            siftDown(0); // 对于0处进行siftDown操作

            return ret;
        }

        /**
         * k位置元素下移
         *
         * @param k
         */
        private void siftDown(int k) {

            while(leftChild(k) < data.size()){
                int j = leftChild(k); // 在此轮循环中,data[k]和data[j]交换位置
                if( j + 1 < data.size() &&
                        data.get(j + 1).compareTo(data.get(j)) < 0 )
                    j ++;
                // data[j] 是 leftChild 和 rightChild 中的最小值

                if(data.get(k).compareTo(data.get(j)) >= 0 )
                    break;

                swap(k, j);
                k = j;
            }
        }
    }

知道了堆的实现,那么堆排序就比较简单了。
第一步:让数组形成堆有序状态;
第二步:把堆顶的元素放到数组最末尾,末尾的放到堆顶,在剩下的元素中下沉到正确位置,重复操作即可。

时间复杂度

堆排序一开始需要将n个数据存进堆里,所需时间为O(nlogn)。排序过程中,堆从空堆的状态开始,逐渐被数据填满。由于堆的高度小于log(2)n ,所以插入 1 个数据所需要的时间为logn。
每轮取出最大的数据并重构堆所需要的时间为O(logn)。由于总共有n轮,所以重构后排序的时间也是O(nlogn)。因此,整体来看堆排序的时间复杂度为O(nlogn)。