zoukankan      html  css  js  c++  java
  • 算法

    堆排序

    注意,堆这个结构需要知道什么是满二叉树、完全二叉树。堆就是完全二叉树。

    使用数组存储数据,用数组模拟二叉堆结构,此时下标的关系有:

    • 父结点 i 的左子结点为 2i + 1,右子结点为 2i + 2
    • 子结点 i 找父结点公式为:(i - 1) / 2

    堆分大根堆和小根堆,每一个结点为子树的最大值称为大根堆,同理可知小根堆。

    以大根堆为例学习堆排序算法,小根堆同理做一些转换即可。

    heapInsert

    构建堆时,heapInsert 是非常重要的构建方法。

    建立 i 个数的堆,算法的时间复杂度为

    log(!(i-1)) = log1 + log2 + ··· + log(i-1)

    数学证明,最终得到建立过程的时间复杂度为 O(N)。

    public static void heapInsert(int[] arr, int i) {
        //每一次插入新结点,与父结点做比较,调整堆结构
        while (arr[i] > arr[(i - 1) / 2]) {
            swap(arr, i, (i - 1) / 2);
            i = (i - 1) / 2;
        }
    }
    

    堆的大小 size

    堆在内存中可以使用数组实现,数组有数组的长度 length,堆有堆的大小 size,关系为:size <= length。当堆增加一个数时,使用 heapInsert,size++;当堆大小减少时,直接使用 size--,数组中的元素可以保留。

    heapify

    两个字:下沉!堆排序的核心。

    堆中的数据进行修改的时候 heapify 是非常重要的调整方法。调整的时间复杂度为 O(logN)。

    当大根堆构建完成后,堆内第 i 个数变小,此时堆结构需要调整。

    思路:确保 i 的子结点仍是堆内数据,比较得到两个子结点中的最大值,与 i 结点比较大小。如果 i 结点数大,则堆结构保持不变;否则,将 i 结点与子结点交换位置。循环上述堆结构调整流程。

    public static void heapify(int[] arr, int index, int heapSize) {
        int left = index * 2 + 1;
        while (left < heapSize) {
            //比较得到子结点较大值索引
            int largest = (left + 1) < heapSize && arr[left] < arr[left + 1]
                ? left + 1
                : left;
            //父结点和子结点比较
            largest = arr[index] < arr[largest] 
                ? largest
                : index;
            //数据改变了,但是仍保持大根堆结构
            if (largest == index) {
                break;
            }
            swap(arr, index, largest);
            //准备下一轮循环的条件
            index = largest;
            left = index * 2 + 1;
        }
    }
    

    pop

    将堆顶的数弹出,可以用 size - 1 位上的数和堆顶的数交换位置,然后 size--,最后进行 heapify 操作。

    问题一:随时找到数据流的中位数

    有一个源源不断地吐出整数的数据流,要求做到可以随时取得之前吐出所有数的中位数。这时可以使用两个堆:一个大根堆和一个小根堆存放数据,大根堆最大值小于小根堆的最小值。

    思路:

    //处理数据流
    假设大根堆堆顶为 big,小根堆堆顶为 small
    获取第一个数,先进大根堆,这一步不硬性规定的
    获取第二个数,比较
    	if (num <= big) 进大根堆
    	else 进小根堆
    在处理了数据后,根据 Math.abs(big_size - small_size) < 2 的条件决定是否调整堆结构
    	if true 不调整
    	else
    		if big_size 大,则大根堆执行 pop 操作,小根堆执行 heapInsert 操作
    		else 小根堆执行 pop 操作,大根堆执行 heapInsert 操作
    	
    //计算中位数
    if big_size = small_size return (big + small) / 2
    else
        if big_size > small_size return big
        else return small
    
    public class _295_FindMedianfromDataStream {
        PriorityQueue<Integer> small_heap;
        PriorityQueue<Integer> big_heap;
    
        /** initialize your data structure here. */
        public _295_FindMedianfromDataStream() {
            small_heap = new PriorityQueue<>();
            big_heap = new PriorityQueue<>((a, b) -> b - a);
        }
    
        // version 1.0
        /*public void addNum(int num) {
            if (big_heap.isEmpty() && small_heap.isEmpty()) {
                big_heap.offer(num);
                return;
            }
            if (big_heap.size() == 1 && small_heap.isEmpty()) {
                if (big_heap.peek() > num) {
                    small_heap.offer(big_heap.poll());
                    big_heap.offer(num);
                } else {
                    small_heap.offer(num);
                }
                return;
            }
            //当堆为空时,num 和 堆内元素比较会出现空指针错误
            if (small_heap.size() == big_heap.size()) {
                if (num < big_heap.peek()) {
                    big_heap.offer(num);
                } else {
                    small_heap.offer(num);
                }
            } else if (small_heap.size() < big_heap.size()) {
                if (num > big_heap.peek()) {
                    small_heap.offer(num);
                } else {
                    small_heap.offer(big_heap.peek());
                    big_heap.poll();
                    big_heap.offer(num);
                }
            } else {
                if (num < small_heap.peek()) {
                    big_heap.offer(num);
                } else {
                    big_heap.offer(small_heap.peek());
                    small_heap.poll();
                    small_heap.offer(num);
                }
            }
        }
         */
    
        // version 2.0
        public void addNum(int num) {
            if (big_heap.isEmpty() && small_heap.isEmpty()) {
                big_heap.offer(num);
                return;
            }
            if (num <= big_heap.peek()) {
                big_heap.offer(num);
            } else {
                small_heap.offer(num);
            }
    
            if (Math.abs(big_heap.size() - small_heap.size()) > 1) {
                if (big_heap.size() > small_heap.size()) {
                    small_heap.offer(big_heap.poll());
                } else {
                    big_heap.offer(small_heap.poll());
                }
            }
        }
    
        public double findMedian() {
            double res;
            if (small_heap.size() == big_heap.size()) {
                res = (small_heap.peek() + big_heap.peek()) / 2.0;
            } else if (small_heap.size() > big_heap.size()) {
                res = small_heap.peek();
            } else {
                res = big_heap.peek();
            }
            return res;
        }
    }
    

    上面是我做 LeetCode_295 题的代码,原理是相近的。此前做的时候绕了一些弯路,得到 version 1.0 版。

    代码中使用了 Java 的优先级队列 ProrityQueue,在创建大根堆过程中使用了比较器。其实优先级队列就是堆的一种实现,不需要自己实现 heapInsert 和 heapify。

    注意:PriorityQueue 的一些方法异同点

    作用 抛出异常 返回值
    插入 add(e) offer(e)
    删除 remove() poll()
    查看 element() peek()

    堆排序

    三步:

    • 堆顶和堆尾部交换位置
    • 堆的大小减 1
    • heapify 调整

    大根堆完成从小到大的排序,小根堆完成从大到小的排序。

    public static void heapSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            heapInsert(arr, i);
        }
        int heapSize = arr.length;
        while (heapSize != 0) {
            swap(arr, 0, heapSize - 1);
            heapSize--;
            //此处可以简化成一步
            //swap(arr, 0, --heapSize);
            heapify(arr, 0, heapSize);
        }
    }
    
    /*和左神代码局部对比*/
    //basic
    int heapSize = arr.length;
    while (heapSize != 0) {
        swap(arr, 0, heapSize - 1);
        heapSize--;
        heapify(arr, 0, heapSize);
    }
    
    //advanced
    int size = arr.length;
    swap(arr, 0, --size);
    while (size > 0) {
        heapify(arr, 0, size);
        swap(arr, 0, --size);
    }
    

    从局部代码中可以看到,左神的代码利用了前面判断的 arr.length > 2 的条件,少使用了一次 while 判断 size 的过程。

  • 相关阅读:
    生产者-消费者模式
    Java中数字操作
    Java中的装箱拆箱
    Java中的匿名类
    JAVA中抽象类的一些总结
    JAVA继承时this和super关键字
    elasticsearch查询
    elasticsearch的映射
    kibana——es的批量操作
    kibana简单使用——elaticsearch的文档,索引的CRUD操作
  • 原文地址:https://www.cnblogs.com/chenxianbin/p/11892688.html
Copyright © 2011-2022 走看看