优先级队列的设计(2016年的博客)

 前年写了一篇关于"史上对BM25模型最全面最深刻解读以及lucene排序深入解读"的博客,lucene最后排序用到的思想是"从海量数据中寻找topK"的时间空间最优算法。在特定的场合,比如solr自带的搜索智能提示公能,当构建完三叉树,前缀匹配查找出所有的节点之后,也要用这种思想进行排序。根据这个思想构造出一个优先级队列,具有容量限制(K),精确的时间复杂度为KlgK+(n-k)lgK,最坏的时间复杂度:(n-k)*lgk +lg(k-1)!。远远优于目前任何的排序算法,在学术论文里已经进行了理论论证。今天根据这个思想,尝试着写了这个数据结构,经过修改,已经接近完美。这个数据结构的适用场景就是从海量数据中寻找出topK的数据来,可以解决lucene排序,解决搜索推荐的冷启动问题(从用户的搜索日志中寻找出topK,推荐出来)。凡是有容量限制的这类问题,都可以使用它。而TreeSet或者TreeMap的排序的时间复杂度是O(lgn),并且底层基于可排序的二叉树(通过红黑树获取平衡),然后中序遍历得到最后结果。在IK分词的消除歧义分词中用到了TreeSet。这两个数据结构常常用于Key-value数据形式并且容量不是很大或者没有容量限制的场景。其他的基础排序算法比如快排,堆排,mergeSort还有近几年新的排序TimSort,常常用于Array的排序。从底层深刻理解这些基础算法很重要!机器学习排序在搜索中的应用,最后仍然要用今天要写的PriorityQueue。JDK中有自带的PriorityQueue,但是没有容量限制,性能比较差。上传本人写的代码:

package com.txq.test;

/**

 * 优先级队列,从海量数据中寻找topK的时间空间最优算法,时间复杂度为n * lgK,空间为K,其中K << n.

 * @author XueQiang Tong

 * @since JDK1.8

 * @param <T>

 */

public abstract class PriorityQueue<T> {

    protected  T heap[];//堆

    protected  int size;//容量

    protected  int heapSize;//最大容量

     

    public PriorityQueue(int capacity){

        if (capacity >= (1 << 31) - 1) {

            throw new IllegalArgumentException("max size must be <= (1 << 31) -1;got:" + capacity);

        }

        this.heapSize = capacity;

        this.size = 0;

        Object o[] = new Object[this.heapSize];

        heap = (T[]) o;    

    }

     

    public PriorityQueue(){

        this(5);

    }

     

    protected abstract boolean lessThan(T t, T data);

    /**

     * 向队列中添加元素,如果没有达到容量限制,直接添加并且构建小根堆,如果超出了容量,用大于堆顶的元素替换堆顶,然后调整堆

     * @param data

     */

    protected synchronized final void insertWithOverFlow(T data){

        if(data == null) return;

        if(this.size < this.heapSize){

            add(data);

        } else{

            if(this.size > 0 && lessThan(heap[0],data)){

                heap[0] = data;

                minify(0);

            }

        }

    }

    /**

     * 按照降序依次取出topK元素,第一次取时,先构建大根堆,以后直接取堆顶元素,然后重新调整大根堆

     * @return

     */

    protected synchronized final T pop(){

        if(this.size == 0) return null;

        if(this.size == this.heapSize){

            for(int i = this.heapSize / 2 - 1;i >= 0;i--){

                maxnify(i);

            }

        }

        T result = heap[0];

        heap[0] = heap[this.size - 1];

        heap[this.size - 1] = null;

        this.size --;

        maxnify(0);

        return result;

    }

    public final int size(){

        return this.size;

    }

     

    protected final T top(){

        return this.heap[0];

    }

    /**

     * 调整大根堆

     * @param i

     */

    private void maxnify(int i) {

        int left = 2 * i + 1;

        int right = 2 * i + 2;

        int max;

         

        if(left < this.size && lessThan(heap[i],heap[left])) max = left;

        else max = i;

        if(right < this.size && lessThan(heap[max],heap[right])) max = right;

         

        if (max == i || max >= this.size) return;

        swap(heap,i,max);

        maxnify(max);

    }

    private void swap(T[] heap, int i, int j) {

        T tmp;

        tmp = heap[i];

        heap[i] = heap[j];

        heap[j] = tmp;

    }

    /**

     * 调整小根堆

     * @param i

     */

    private void minify(int i) {       

        int left = 2 * i + 1;

        int right = 2 * i + 2;

        int min;

         

        if(left < this.size && lessThan(heap[left],heap[i])) min = left;

        else min = i;

        if(right < this.size && lessThan(heap[right],heap[min])) min = right;

         

        if (min == i || min >= this.size) return;

        swap(heap,i,min);

        minify(min);

    }

    /**

     * 添加元素并且构建小根堆

     * @param data

     */

    private void add(T data) {     

        this.heap[this.size++] = data;

        for(int i = this.size / 2 - 1;i >= 0;i--){

            minify(i);

        }

    }

     

    public synchronized final void clear() {

        for (int i = 0; i <= this.size; ++i) {

            this.heap[i] = null;

        }

        this.size = 0;

    }

}

 

测试类:

package chinese.utility.utils;

import org.junit.Test;

public class PriorityQueueTest {
    PriorityQueue<Integer> q;
    

    @Test
    public void test() {
        q = new MyPriorityQueue(3);
        q.insertWithOverFlow(99);
        q.insertWithOverFlow(78);
        q.insertWithOverFlow(109);
        q.insertWithOverFlow(123);
        q.insertWithOverFlow(23);
        q.insertWithOverFlow(45);
        q.insertWithOverFlow(56);
        Integer element = (Integer) q.pop();
        while(element != null){
            System.out.println(element);
            element = (Integer) q.pop();
        }        
    }

}

运行结果:
123

109

99

另外一个场景,比如现在有一个矩阵,没行数据都是降序排列的,维度有很多,要求找出其中matrix.length个最大值,用上面的算法就不行了,时间复杂度太高了。因为每行数据都排序好了,可以采取以下策略:

+ View Code

另外,Python中也有类似于优先级队列的数据结构,JDK中有自带的优先级队列,都没有容量限制。Python更加倾向于函数式编程。Python的实现是靠堆操作函数的模块,叫heapq。今天试着使用一下:

from heapq import *;
from random import shuffle;
data = [x for x in range(10)];
shuffle(data);
heap = [];
for n in data:
    heappush(heap,n);

print(heap);
print(type(heappop(heap)))
print(nlargest(5,heap))#输出前5个最大值
print(nsmallest(5,heap))#输出前5个最小值
[0, 1, 6, 3, 2, 7, 9, 5, 4, 8]
<class 'int'>
[9, 8, 7, 6, 5]
[1, 2, 3, 4, 5]
可以看出,Python比Java更加灵活!从海量数据中寻找出topK问题的最优解是前面写的优先级队列解决方案,Python仍然可以完成这个功能,现在来模拟这个场景,对Python中 的heap增加容量限制:

#从海量数据中找出top4
from heapq import *;

data = [2,2,6,7,9,12,34,0,76,-12,45,79,102];#模拟海量数据
s = set();
#首先从海量数据中构造出容量为4的set,然后加载到heap中
for num in data:
    s.add(data.pop(0));
    if s.__len__() == 4:
        break;

heap = [];
for n in s:
    heappush(heap,n);

print(heap);


for num in data:
    if num > heap[0]:
        heapreplace(heap,num);#对剩余的海量数据继续迭代,如果比堆顶元素大的话,替换之并且调整小根堆

print(nlargest(4,heap))#输出前4个最大值,最后输出的时候执行堆排序!
[2, 7, 6, 9]
[102, 79, 76, 45]

猜你喜欢

转载自blog.csdn.net/randy_01/article/details/82835837