TopN与小顶堆

2022-06-20 19:43:56 浏览数 (1)

如何找出一个数列中的最大的N个值?

这是一个在面试中经常遇见的问题,此问题的关键是应尽可能的减少节点的比较次数,从而降低时间复杂度.因此选择小顶堆这个数据结构.

那么小顶堆是什么样的数据结构,又为什么能降低时间复杂度呢?

小顶堆是二叉树结构,但其存储结构是数组.

如果对小顶堆的定义以及父子节点在数组中的关系还不了解,建议先阅读文章二叉树.

下面通过一个例子来看一下小顶堆是如何添加和删除节点的.

array=[42, 41, 31, 7, 17, 2]

一. 添加节点

节点添加时,首先将要加入的节点放入树的最后一个节点上(数组的末尾),再将节点与自己的父节点比较,如果比父节点小,则交换位置,按此逻辑递归处理,一直遇到比父节点值大的情况或者已经换到了小顶堆的根节点位置.

1. 节点42,空树,直接放入根节点.对应数组放入首位.

2. 节点41,先放入树的最后节点上,即节点42的左叶子节点上,并与父节点(节点42)比较,比父节点值小,交换位置.

3. 节点31,放入树最后节点,并与父节点41比较,比父节点值小,交换位置.

4. 节点7,首先放在树最末节点,比父节点42值小,交换位置.在与当前节点位置的父节点31比较,仍小于父节点,再次交换位置.

5. 其他节点的添加过程,不再赘述了,我们看下最后结果.可以发现小顶堆对应的数组并不是有序递增的,这也是小顶堆的特点之一.

二. 删除节点

小顶堆的删除过程,主要是指删除数组的最小节点,也就是array[0],但通过数据节点交换,是不需要对各元素移位或进行数组复制的.

这也是在TopN问题中,能始终保持N个元素,并且很高效的一个原因.

删除最小节点过程是用树的最后一个节点替换为根节点,并重新调整为小顶堆.

在调整的过程中是将父节点与叶子节点中较小的节点交换,并递归处理,直到遇到较大节点或到了最底层叶子节点.

按上图树结构,观察下节点移除过程.

array=[2,17,7,42,31,41]

1. 删除根节点2

将节点41替换为根节点,并找到较小的叶子节点7,交换位置.

2. 删除根节点7

将原有最后一个节点代替为根节点,并与自己较小的叶子节点(17)比较,并交换位置.再次与自己当前位置的叶子节点(42)比较,小于叶子节点值,不需要再次交换.

3.其他节点的删除过程也类似这样,不在赘述了.

在节点的添加和删除过程中,并不需要遍历数组中的所有元素,就能找到自己合适的位置,比较次数也明显少了很多.

在java中,解决TopN问题,可以直接使用优先队列类(PriorityBlockingQueue),这个类已经替我们实现了添加和删除操作,并且能通过扩展Comparator能自定义排序方法.有兴趣的可以看看源码,方法的实现过程也与上述过程一致,非常好理解.

添加节点方法:

siftUpComparable()

siftUpUsingComparator()

删除节点方法:

siftDownComparable()

siftDownUsingComparator()

我们通过TopN问题,发现小顶堆是最优解,并详细了解了节点的添加和删除过程.最后附上Java版的小顶堆(优先队列)如何解决TopN问题的.

附上代码:

代码语言:javascript复制
 static void topN(int[] data, int N) {
        PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue<>();
        int minData = data[0];
        for (int i = 0; i < data.length; i  ) {
            if (queue.size() == N) {
                if (minData < data[i]) {
                    // 大于原来最小的,就放进去新的,删除旧的
                    queue.offer(data[i]);
                    int header = queue.poll();
                    minData = header;
                }
            } else {
                // 最开始 N个,直接保留
                minData = Math.min(minData, data[i]);
                queue.offer(data[i]);
            }
        }
        while (!queue.isEmpty()) {
            System.out.println(queue.poll());

        }
    }

0 人点赞