大小堆以及TOP K问题

完全二叉树

如上图所示,我们可以将完全二叉树的结点按照层序遍历的顺序储存在一个数组中,那么当完全二叉树中的某个结点位于array的i处时,其左子节点必位于2i+1处(i>=0),其右结点必位于array的2i+2处。这样我们就可以轻易的实现完全二叉树的存储。

一般调整大小堆,从第一个非叶子节点位置开始,在数组中位size/2-1处,如上所示就是E。

若将结点v的编号(秩)记作i(v),则满足以下关系:

i(root) = 0;
i(leftChild(root)) = 1;
i(rightChild(root)) = 2;
i(leftChild(leftChild(root))) = 3;
....
对于任意结点,则有:
1)若v有左孩子,则 i(leftChild(v)) = 2 * i(v) + 1;
2)若v有右孩子,则 i(rightChild(v))= 2 * i(v) + 2;
3)若v有父节点,则 i(parent(v)) = i(v)/2 - 1;

  大根堆


//调整大根堆(大堆化),数组从1下标开始 (为了后面排序方便,因为要与第一个数交换)
void adjust_maxheap(int a[],int size,int i)
{

    int left = i*2; //左节点
    int right = i*2+1; //右节点
    int max_index;

    //取出根,左,右中最大的节点,保存索引
    if(left < size && a[left]>a[i])
        max_index = left;
    else
        max_index = i;

    if(right < size && a[right] > a[max_index])
        max_index = right;

    //如果原来的根不是最大值,则需要交换,交换后原来max值的节点变成了比较小的根
    if( i != max_index ) 
    {
        SWAP(a[i],a[max_index]);
        adjust_maxheap(a,size,max_index); //递归处理,直到不再变化
    }
}


//建立大根堆
void build_maxheap(int a[],int size)
{
    int start = size>>1; //第一个非叶子节点
  
    for(int i = start;i>=1;i--)
    {
        adjust_maxheap(a,size,i);
    }
}

//大堆排序
void maxheap_sort(int a[],int size)
{
    int i;
    for(i = size;i>=1;i--)
    {
        build_maxheap(a,i);
        SWAP(a[1],a[i]);
    }
}

 参考链接:https://blog.csdn.net/lhj884/article/details/47128259 

1.大小堆(树)

 堆树的定义如下:

(1)堆树是一颗完全二叉树;

(2)堆树中某个节点的值总是不大于或不小于其孩子节点的值;

(3)堆树中每个节点的子树都是堆树。

当父节点的键值总是大于或等于任何一个子节点的键值时为最大堆。 当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆

构建堆的时间复杂度为O(n),原因如下:

假如有N个节点,那么高度为H=logN,最后一层每个父节点最多只需要下调1次,倒数第二层最多只需要下调2次,顶点最多需要下调H次,而最后一层父节点共有2^(H-1)个,倒数第二层公有2^(H-2),顶点只有1(2^0)个,所以总共的时间复杂度为s = 1 * 2^(H-1) + 2 * 2^(H-2) + ... + (H-1) * 2^1 + H * 2^0,将H代入后s= 2N - 2 - log2(N),近似的时间复杂度就是O(N)。

堆的调整(如插入,删除等操作)的时间复杂度为O(logn)

优先队列:其底层实现是堆(默认为最小堆)

优先队列(PriorityQueue):就是在队列中根据某一个特征值自动进行排序,优先队列分为两种,最大优先队列和最小优先队列,优先队列的一个最大特性就是,当插入元素或者删除元素的时候,队列会自动进行调整,保证队首元素一定是优先权最大/最小。正是由于优先队列的这种特性,优先队列可以被用在很多地方,比如作业调度,进程调度等。

详情可以移步:https://blog.csdn.net/guoweimelon/article/details/50904346

2.堆排序

堆排序的基本思想就是:从最大(小)堆得顶部不断取走堆顶元素放到有序序列中,直到堆的元素被全部取完。堆排序完全依赖于最大(小)堆的相关操作。

堆排序的时间复杂度为O(nlogn)空间复杂度为O(1);是不稳定的排序算法

详情可以移步:https://blog.csdn.net/guoweimelon/article/details/50904231

3.TOP K问题

Top K 和第K大基本等价,以下我们以第K大为例且假设第K大一定存在,Top K 可以在第k大基础上稍微改动获得。 

  • 快排的思想 -时间复杂度O(n)

    调用降序快排的partition函数,设区间为[low,high],返回index,则index左边都是大于data[index]的。 
1. 若index及index左边数字有k个则data[index]就是第k大,index及其左边元素为Top K元素 
2. 左边数字大于k个则继续在[low,index]里找 

3. 左边数字小于k个则去右边[index+1,high] 找 ( k - 左边数字个数)

  • 小根堆-时间复杂度O(nlogk)

维护一个k个元素的小根堆,保持堆里元素为最大的K个且堆顶为第k大(堆里最小的),扫一遍数据,若堆里个数小于k则插入,否则看新的数和堆顶数大小关系: 
1. 若新来的数小于等于堆顶,即新元素比Top K里最小的还小,则新来的数显然不可能是前k大 

2. 若新来的数大于堆顶,则删掉堆顶,将新数字放到堆里且调整堆来保持堆的属性

详情可以移步:https://blog.csdn.net/u012469987/article/details/52203434

当出现海量数据时,求top k 问题:

例如: 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。),请你统计最热门的10个查询串,要求使用的内存不能超过1G。

第一步:统计Query(查询串)出现的次数—哈希表法

虽然有一千万个Query,但是由于重复度比较高,因此事实上只有300万的Query,每个Query 255Byte,因此我们可以考虑把他们都放进内存中去,因为Hash Table的查询速度非常的快,几乎是O(1)的时间复杂度。维护一 个Key为Query字串,Value为该Query出现次数的HashTable,每次读取一个Query,如果该字串不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内完成了对该海量数据的处理。(这一步也可以采用Trie树来统计出现的次数,时间复杂度为O(N*len),len为查询串(或单词)平均长度)

第二步:找出TOP 10(查询次数最多的10个)

这里我们采用堆结构,维护建立一个K(10)大小的小根堆,然后遍历300万的Query,分别和根元素对比,这步完成所需的时间复杂度O(nlogk)

综上所述:总的时间复杂度为:O(N)+O(nlogk);(N为1000万,n为300万)

如果无法一次全部读入内存,则:首先根据hash并求模,将文件分解成多个小文件,每个小文件采用上述方法处理,求出每个文件的中频率最高的10个查询串。然后归并处理,最终找出所有文件中10个最常见的查询串。   可参考:https://blog.csdn.net/lpjishu/article/details/52626891

详情可以移步:https://blog.csdn.net/lukas_sun/article/details/53747582

                        http://www.it610.com/article/5629205.htm

猜你喜欢

转载自blog.csdn.net/u012864854/article/details/79901279
今日推荐