11.堆和堆排序&TopK

堆是一种特殊的完全二叉树
  • 完全二叉树即除了最后一层其它层都是满的,且最后一层的数据全部靠左排列。
  • 特殊在,他的每个节点的值都大于等于(或者小于等于)其子树节点,因此堆又分为大顶堆和小顶堆。

因为是完全二叉树,我们存储堆的时候一般使用数据来存储,第一个0号元素留空,这样的话节点是a[n],左节点就是a[2n],右节点就是a[2n+1],父节点就是a[n/2]。当然不留空也可以,推算父节点的时候总是要先进行-1操作。

堆主要有两个操作
  • 删除堆顶元素
  • 插入一个元素

这两个操作都涉及到了堆化(重新调整,使其满足堆的特性),删除堆顶元素我们选择的是把最后一个元素移到堆顶来,从上而下堆化(比较当前节点和子节点的大小,不满足则互换位置)。插入一个元素我们选择把元素插入到最后,然后从下往上堆化(比对当前节点和父节点的大小,不满足则互换位置,互换后一直)。

关于堆的应用
  • 计算Top K
    原理:先去数据中前K个数字建立一个大小为K的小顶堆(堆顶元素最大),然后每进来一下新数字,比对和堆顶元素的大小。如果比堆顶元素大,则删除堆顶元素,并插入新数字,重新堆化。
  • 优先级处理,比如定时任务,等待队列等等
    原理:每插入或者删除一个新元素,重新堆化处理。堆顶元素永远是优先级最高的一个。
  • 堆排序
代码实现
/**
 * 小顶堆的实现
 * 堆顶元素最小,子元素均大于堆顶元素
 */
public class Heap {

    private int[] a;//数组,从下标1开始存储数据
    private int n;//堆存储的最大个数
    private int count;//目前个数

    public Heap(int n) {
        this.n = n;
        count = 0;
        a = new int[n + 1];
    }

    public Heap(int[] a) {
        this.a = a;
        count = a.length - 1;
        n = count;
    }


    public void removeHeader() {
        if (count == 0) {
            return;
        }
        a[1] = a[count];
        int current = 1;
        //从上而下堆化,需要用父元素和下面两个子节点比对
        heapify(a, count, current);
    }


    public boolean insert(int data) {
        if (count == n) {
            return false;
        }
        count++;
        a[count] = data;
        int i = count;
        //从下往上堆化,和父元素对比即可
        while (i / 2 > 0 & a[i / 2] > a[i]) {
            //父元素大于子元素
            swap(a, i, i / 2);
            i = i / 2;
        }
        return true;
    }

    /**
     * 这个a的第一个元素应该是个空的
     * 空,数字,数字,数字
     * 0, 1 ,2 , 3 ,
     *
     * @param a
     * @param n       a数组中数据的个数,可能a的长度是100,数字确是6个,后93个忽略。
     * @param current 给当前角标的数字寻找一个合适的位置存放堆化的a,
     *                current子节点的节点需要是已经满足堆化的数据,否则该方法不适用
     */
    private static void heapify(int a[], int n, int current) {
        while (true) {
            int minxPos = current;
            if (current * 2 <= n && a[current] > a[current * 2]) {
                minxPos = current * 2;
            }
            //这里要用if而不是elseif,且是a[mixPos]对比,是因为要找出两个子节点中最小的一个
            if (current * 2 + 1 <= n && a[minxPos] > a[current * 2 + 1]) {
                minxPos = current * 2 + 1;
            }
            if (minxPos == current) {
                break;
            }
            swap(a, current, minxPos);
            current = minxPos;
        }
    }

   
    /**
     * 从下而上堆化
     *
     * @param a
     * @return
     */
    public static Heap buildHeap1(int[] a) {
        Heap heap = new Heap(a.length);
        for (int i = 0; i < a.length; i++) {
            heap.insert(a[i]);
        }
        return heap;
    }


    /**
     * 从上而下堆化
     * 我们仅需要对下标从1到n/2的数据进行堆化,小标是n/2+1带n的节点是叶子节点,不需要堆化。
     * 从上而下堆化本就是拿父节点和两个子节点对比,找出三者中最合适做父元素的一个值。
     *
     * 这种建堆方法的复杂度是O(n)
     * @param b
     * @return
     */
    public static Heap buildHeap2(int[] b) {
        int[] a = new int[b.length + 1];
        for (int i = 0; i < b.length; i++) {
            a[i + 1] = b[i];
        }
        //必须逆序,每个值动过一次之后就是最合适的位置了(最下面的没有自节点了),不需要再动了
        for (int current = a.length / 2; current >= 1; current--) {
            heapify(a, b.length, current);
        }
        return new Heap(a);
    }


}

堆排序的思想,首先我们对数据进行从下到上的建堆操作(对每个元素进行堆化)。然后取出堆顶元素和最后一个元素交换位置。然后堆化对顶元素,直到剩余一个元素的时候,整体有序。

public class SortHeap {

    public static void main(String[] args) {
        int[] nums = new int[]{9, 1, 2, 3, 4, 5, 6, 7, 8};
        new HeapSort().sort(nums);
    }

    public void sort(int[] nums) {
        //倒着为每个数据找到一个合适的位置所有的数据,
        //最终形成了一个堆
        for (int i = nums.length / 2 - 1; i >= 0; i--) {
            heap(nums, nums.length, i);
        }
        print(nums);
        /**
         * 已形成一个堆,堆顶元素已经符合规则
         */
        for (int i = nums.length - 1; i >= 0; i--) {
            //把已经排序好的数字放到最后一个,堆化剩下的数据
            int temp = nums[i];
            nums[i] = nums[0];
            nums[0] = temp;
            //第一个数字一直在改变,一直堆化第一个
            heap(nums, i, 0);
            print(nums);
        }
        print(nums);
    }

    private void print(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            System.out.print(nums[i] + ",");
        }
        System.out.println();
    }

    /**
     * 一个完全二叉树,总是有(n+1)/2个子节点
     * 一个满二叉树,总是有(n+1)/2个子节点(n总是奇数,+1变成偶数)。
     * 然后追加成完全二叉树,每当n+1的时候,子节点数量不变,因为8/2和9/2是一样的值
     * 然后再追加一个,此时子节点数量就会+1了,还是满足(n+1)/2个子节点,
     * n是奇数的时候,有n+1/2个子节点,n为偶数的时候,有n/2个子节点
     * 所以说,n奇数的时候有n-1/2个子节点,所以说总有n/2个子节点
     * 所以index角标是n/2-1
     * <p>
     * <p>
     * <p>
     * 传进来的数组仅有index所在位置不合适,导致其不符合堆得规则。
     * index和子节点对比找到最小的一个放到堆化的节点上,
     * 然后改变了的节点在和子节点比对形成正式的堆
     *
     * @param nums
     */
    private void heap(int[] nums, int needHeapLength, int index) {
        while (true) {
            //这里要一直找到nums[i]应该在的位置
            int minIndex = index;
            if (index * 2 + 1 < needHeapLength
                    && nums[index * 2 + 1] < nums[index]) {
                //设置当前minIndex的值,是树的左下角
                minIndex = index * 2 + 1;
            }
            if (index * 2 + 2 < needHeapLength
                    && nums[index * 2 + 2] < nums[minIndex]) {
                //设置当前minIndex的值,是树的右下角
                minIndex = index * 2 + 2;
            }
            if (minIndex == index) {
                break;
            }
            //交换index和minIndex的位置
            int num = nums[index];
            nums[index] = nums[minIndex];
            nums[minIndex] = num;
            //一个值挪动了,接着验证被挪动走的值的下方是否符合堆化条件,不符合继续动
            index = minIndex;
        }

    }
}

堆排序是不稳定排,因为初始化后最后一个元素,可能会被和堆顶元素交换。然后接着被堆化的时候移动到前面去了。
比如:98,86,68,58,421,422(堆化之后)
然后对顶元素移动到最后:86,68,58,421,98
然后堆化第一个元素:86,68,58,422,421,98

依次类推,最终422,421的局面会形成。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值