堆排序与海量TopK问题

我的博客地址:https://rebornc.github.io/2018/11/15/%E5%A0%86%E6%8E%92%E5%BA%8F%E4%B8%8E%E6%B5%B7%E9%87%8FTopK%E9%97%AE%E9%A2%98/

排序算法是个老生常谈的问题,笔试要考,面试也问,不过翻来覆去也就那几个花样吧。大概理解一下各个算法的原理,记下表格里的数据,然后再试试手撕代码,基本上就没问题了。

从表格里可以看出,堆排序是一个时间和空间复杂度都比较优秀的算法,至于它的原理,看懂是肯定能轻易看懂的,但是我总觉得如果你不自己亲手写一遍,就很容易忘记。并且,用递归的话,代码也是很简短的,还没写过的同学,不妨自己试着敲一下吧hhh。

因为太久没写博客了觉得不能这么颓废下去,所以今天打算好好整理堆排序的相关知识点,同时讲一下面试时经常会被问到的TopK问题。

堆排序

1. 什么是堆

堆(heap)是一种数据结构,也被称为优先队列(priority queue)。队列中允许的操作是先进先出(FIFO),在队尾插入元素,在队头取出元素。而堆也是一样,在堆底插入元素,在堆顶取出元素,但是堆中元素的排列不是按照到来的先后顺序,而是按照一定的优先顺序排列的。这个优先顺序可以是元素的大小或者其他规则。
而二叉堆是一种特殊的堆,它是完全二元树(二叉树)或者是近似完全二元树(二叉树)。二叉堆有两种:最大堆和最小堆。最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值。如下图。

2. 堆排序的原理

堆排序(HeapSort)是指利用堆这种数据结构所设计的一种排序算法。它的关键在于建堆和调整堆。步骤主要如下:

  • 创建一个堆;
  • 把堆首(最大值)和堆尾互换;
  • 把堆的尺寸缩小1,并调整堆,把新的数组顶端数据调整到相应位置;
  • 重复步骤 2,直到堆的尺寸为1,此时排序结束。

当然,光看文字肯定不能很直观地理解,我们跟着图示来学习吧。
现在,我们有一个待排序的数组 {2, 4, 3, 7, 5, 8},我们通过构建最大堆的方法来排序。

  • 步骤说明如下:
    1. 将待排序的数组视作完全二叉树,按层次遍历。
    2. 找到二叉树的最后一个非叶子节点,也就是最后一个节点的父节点。即是 (len-1)/2 索引在的位置。如果其子节点的值大于其本身的值,则把它和较大子节点进行交换,即将数字3和8交换。如果并没有子节点大于它,则无需交换。
    3. 循环遍历,继续处理前一个节点,由于此时 4<7 ,因此再次交换。
    4. 循环遍历,继续处理前一个节点,由于此时 2<8 ,因此再次交换。注意:如果某个节点和它的某个子节点交换后,该子节点又有子节点,系统还需要再次对该子节点进行判断,做相同处理。
    5. 遍历完成后得到一个最大堆。将每次堆排序得到的最大元素与当前规模的数组最后一个元素(假设下标为i)交换,然后再继续调整前 i - 1 的数组。遍历终止之后,得到一个自小到大的排序数组。

C++代码实现如下

void adjust(vector<int> &arr, int index, int len) {
    int left = 2 * index + 1;
    int right = 2 * index + 2;
    int max_index = index;
    if (left < len && arr[left] > arr[max_index]) max_index = left;
    if (right < len && arr[right] > arr[max_index]) max_index = right;
    if (max_index != index) {
        swap(arr[max_index], arr[index]);
        adjust(arr, max_index, len); // 继续调整子节点
    }
}
void heapSort(vector<int> &arr, int len) {
    // 将数组进行堆排序
    for (int i = len / 2 - 1; i >= 0; i--) {
        adjust(arr, i, len);
    }
    // 将每次堆排序得到的最大元素与当前规模的数组最后一个元素交换
    for (int i = len - 1; i >= 1; i--) {
        swap(arr[0], arr[i]);
        adjust(arr, 0, i);
    }
}

海量TopK问题

剑指Offer有这样一道题,求最小的K个数,题目描述:输入n个整数,找出其中最小的K个数。例如输入 4,5,1,6,2,7,3,8 这8个数字,则最小的4个数字是 1,2,3,4。
而在面试的时候,我们也可能遇到这样的问题:有一亿个浮点数,如何找出其中最大的10000个?

这类问题我们把称为TopK问题:指从大量数据(源数据)中获取最大(或最小)的K个数据。

最容易想到的方法当然是全部排序再进行查找,然而时间复杂度怎么也要O(nlog₂n),当n极其大时,该算法占用的内存也emmm。而我们题目所要求返回的只是前K个数据,所以没必要全部排序,做那么多无用功。我们可以先取下标 0~k-1 的局部数组,用它来维护一个大小为K的数组,然后遍历后续的数字,进行比较后决定是否替换。这时候堆排序就派上用场了。我们可以将前K个数字建立为一个最小(大)堆,如果是要取最大的K个数,则在后续遍历中,将数字与最小堆的堆顶数字进行比较,若比它大,则进行替换,然后再重新调整为最大堆。整个过程直至所有数字遍历完为止。时间复杂度为O(n*log₂K),空间复杂度为K。

C++代码实现如下

class Solution {
public:
    void adjust(vector<int> &arr, int index, int len) {
        int left = 2 * index + 1;
        int right = 2 * index + 2;
        int max_index = index;
        if (left < len && arr[left] > arr[max_index]) max_index = left;
        if (right < len && arr[right] > arr[max_index]) max_index = right;
        if (max_index != index) {
            swap(arr[max_index], arr[index]);
            adjust(arr, max_index, len);
        }
    } 

    void heapSort(vector<int> &arr, int len) {
        for (int i = len / 2 - 1; i >= 0; i--) {
            adjust(arr, i, len);
        }
    //    for (int i = len - 1; i >= 1; i--) {
    //        swap(arr[0], arr[i]);
    //        adjust(arr, 0, i);
    //    }
    }

    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
        if (k <= 0 || k > input.size()) {
            vector<int> nullVec;
            return nullVec;
        }
        // 因为要取最小的k个数,所以取前k个数字构建一个最大堆
        // 相反,如果是取最大的k个数,则构建一个最小堆
        vector<int> sortedArray(input.begin(), input.begin() + k);
        heapSort(sortedArray, k);
        // 将后面的数字与这个构建好的二叉堆进行比较 
        for (int i = k; i < input.size(); i++) {
            if (input[i] < sortedArray[0]) {
                sortedArray[0] = input[i];
                adjust(sortedArray, 0, k);
            }
        }
        for (int i = k - 1; i >= 1; i--) {
            swap(sortedArray[0], sortedArray[i]);
            adjust(sortedArray, 0, i);
        }
        return sortedArray;
    }
};

相似的TopK问题还有:

  • 有10000000个记录,这些查询串的重复度比较高,如果除去重复后,不超过3000000个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。请统计最热门的10个查询串,要求使用的内存不能超过1GB。
  • 有10个文件,每个文件1GB,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。按照query的频度排序。
  • 有一个1GB大小的文件,里面的每一行是一个词,词的大小不超过16个字节,内存限制大小是1MB。返回频数最高的100个词。
  • 提取某日访问网站次数最多的那个IP。
  • 10亿个整数找出重复次数最多的100个整数。
  • 搜索的输入信息是一个字符串,统计300万条输入信息中最热门的前10条,每次输入的一个字符串为不超过255B,内存使用只有1GB。
  • 有1000万个身份证号以及他们对应的数据,身份证号可能重复,找出出现次数最多的身份证号。
  • 等等...

对于这类问题,比如上面第1个,可以先利用hash表将查询串存储并计数,然后再构建最小堆,将查询串的个数进行比较从而得到结果。核心思想都是一样的。

今天就先写到这里吧,困了睡觉去 Orz

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 算法思想贪心思想双指针排序快速选择堆排序桶排序荷兰国旗问题二分查找搜索BFSDFSBacktracking分治动态...
    第六象限阅读 3,065评论 0 0
  • 本文总结十大经典排序算法及变形,并提供Java实现。参考文章:十大经典排序算法总结(Java语言实现)快速排序算法...
    Steven1997阅读 17,594评论 3 186
  • 堆是一棵满足一定性质的二叉树,具体的讲堆具有如下性质:父节点的键值总是不大于它的孩子节点的键值(小顶堆), 堆可以...
    9527Roy阅读 618评论 0 0
  • 今天是星期六,天不亮,我就带着女儿去走亲戚了。吃完中午饭,我和女儿搭车就回家了,到了家里,我给女儿布置好作业,并告...
    肖琳的妈妈阅读 171评论 0 0
  • 这周要上交工作目标和计划,实际上对于VPE的工作还是有些手忙脚乱的感觉; 我现在制定的任期目标和计划: 1、pat...
    楚狂生阅读 143评论 0 0