二叉堆的应用

二叉堆的应用

有一个无序数组,要求你找出数组中第k大的元素。
给定的无序数组如下:

7 5 15 3 17 2 20 24 1 9 12 8

如果 k=6,也就是要寻找第6大的元素,这个元素是哪一个呢?
显然,数组中第一大的元素是24,第二大的元素是20,第三大的元素是17 … 第6大的元素是9。

7 5 15 3 17 2 20 24 1 9 12 8
4 3 2 1 6 5

经典的做法:利用小顶堆的特性。
维护一个容量为k的小顶堆,堆中的k个节点代表着当前最大的k个元素,而堆顶显然是这k个元素中的最小值。

遍历原数组,每遍历一个元素,就和堆顶比较,如果当前元素小于等于堆顶,则继续遍历;如果元素大于堆顶,则把当前元素放在堆顶位置,并调整二叉堆(下沉操作)。
遍历结束后,堆顶就是数组的最大k个元素中的最小值,也就是第k大元素。
假设k=5,具体的执行步骤如下:
1.把数组的前k个元素构建成堆。
二叉堆的应用_第1张图片
2.继续遍历数组,和堆顶比较,如果小于等于堆顶,则继续遍历;如果大于堆顶,则取代堆顶元素并调整堆。
遍历到元素2,由于 2<3,所以继续遍历。
遍历到元素20,由于 20>3,20取代堆顶位置,并调整堆。
二叉堆的应用_第2张图片
以此类推,我们一个一个遍历元素,当遍历到最后一个元素8的时候,小顶堆的情况如下:
二叉堆的应用_第3张图片
此时的堆顶,就是堆中的最小值,也就是数组中的第k大元素
这个方法的时间复杂度是多少呢?
1.构建堆的时间复杂度是 O(k)
2.遍历剩余数组的时间复杂度是O(n-k)
3.每次调整堆的时间复杂度是 O(logk)
其中2和3是嵌套关系,1和2,3是并列关系,所以总的最坏时间复杂度是 O ( ( n − k ) l o g k + k ) O((n-k)logk + k) O((nk)logk+k)。当k远小于n的情况下,也可以近似地认为是 O ( n l o g k ) O(nlogk) O(nlogk),方法的空间复杂度是 O ( 1 ) O(1) O(1)

public class Solution{
    /**
     * 寻找第k大的元素
     * @param array 待调整的堆
     * @param k 第几大
     */
    public static int findNumberK(int[] array, int k){
        //1.用前k个元素构建小顶堆
        buildHeap(array, k);
        //2.继续遍历数组,和堆顶比较
        for(int i=k; i<array.length;i++){
            if(array[i] > array[0]){
                array[0] = array[i];
                downAdjust(array, 0, k);
            }
        }
        //3.返回堆顶元素
        return array[0];
    }

    /**
     * 构建堆
     * @param array 待调整的堆
     * @param length 堆的有效大小
     */
    private static void buildHeap(int[] array, int length) {
        // 从最后一个非叶子节点开始,依次下沉调整
        for (int i = (length-2)/2; i >= 0; i--) {
            downAdjust(array, i, length);
        }
    }

    /**
     * 下沉调整
     * @param array   待调整的堆
     * @param index  要下沉的节点
     * @param length  堆的有效大小
     */
    private static void downAdjust(int[] array, int index, int length) {
        // temp保存父节点值,用于最后的赋值
        int temp = array[index];
        int childIndex = 2 * index + 1;
        while (childIndex < length) {
            // 如果有右孩子,且右孩子小于左孩子的值,则定位到右孩子
            if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) {
                childIndex++;
            }
            // 如果父节点小于任何一个孩子的值,直接跳出
            if (temp <= array[childIndex])
                break;
            //无需真正交换,单向赋值即可
            array[index] = array[childIndex];
            index = childIndex;
            childIndex = 2 * childIndex + 1;
        }
        array[index] = temp;
    }
    public static void main(String[] args) {
        int[] array = new int[] {7,5,15,3,17,2,20,24,1,9,12,8};
        System.out.println(findNumberK(array,5));
    }
}

此题还有一个分治法
大家都了解快速排序,快速排序利用分治法,每一次把数组分成较大和较小的两部分。
我们在寻找第k大元素的时候,也可以利用这个思路,以某个元素A为基准,把大于于A的元素都交换到数组左边,小于A的元素都交换到数组右边。
比如我们选择以元素7作为基准,把数组分成了左侧较大,右侧较小的两个区域,交换结果如下:
二叉堆的应用_第4张图片
包括元素7在内的较大元素有8个,但我们的k=5,显然较大元素的数目过多了。于是我们在较大元素的区域继续分治,这次以元素12位基准:
二叉堆的应用_第5张图片
这样一来,包括元素12在内的较大元素有5个,正好和k相等。所以,基准元素12就是我们所求的。
这就是分治法的大体思想,这种方法的时间复杂度甚至优于小顶堆法,可以达到 O ( n ) O(n) O(n)

来源于「程序员小灰」公众号的文章,原文地址

你可能感兴趣的:(算法)