算法 - 堆排序 - 随时找到数据流的中位数

堆排序

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

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

  • 父结点 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 的过程。

posted @ 2019-11-19 21:52  学习趁早  阅读(494)  评论(0编辑  收藏  举报