算法问题,什么是堆?什么不是堆?好像觉得随便给个数组都说对?求教啊。。那就给一个7的后缀数组 倍增算法这也是堆吗?

君,已阅读到文档的结尾了呢~~
Top K 算法详解应用场景:
nbsp 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1 255字节。 nbsp
假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。),请你统计最热门的10个查询..
扫扫二维码,随身浏览文档
手机或平板扫扫即可继续访问
典型的Top K算法 找出一个数组里面前K个最大数
举报该文档为侵权文档。
举报该文档含有违规或不良信息。
反馈该文档无法正常浏览。
举报该文档为重复文档。
推荐理由:
将文档分享至:
分享完整地址
文档地址:
粘贴到BBS或博客
flash地址:
支持嵌入FLASH地址的网站使用
html代码:
&embed src='/DocinViewer-4.swf' width='100%' height='600' type=application/x-shockwave-flash ALLOWFULLSCREEN='true' ALLOWSCRIPTACCESS='always'&&/embed&
450px*300px480px*400px650px*490px
支持嵌入HTML代码的网站使用
您的内容已经提交成功
您所提交的内容需要审核后才能发布,请您等待!
3秒自动关闭窗口关于数据结构堆的二三事之第一话:二叉堆(上) - 窦小蹦(fairyroad) - CSD...
我的图书馆
关于数据结构堆的二三事之第一话:二叉堆(上) - 窦小蹦(fairyroad) - CSD...
关于数据结构堆的二三事之第一话:二叉堆(上) 收藏 此文于被推荐到CSDN首页如何被推荐?堆,很essential的数据结构,可惜严老大那本书没有怎么重视的感觉,但《算法导论》上有。可能严老大有自己的考虑。但从个人体会来看,应该还是非常有必要好好研究研究这个数据结构的,且不说堆是实现优先级队列的基础设施,更不说堆是众多图算法——如Dijkstra最短路径算法、Prim算法——的实现利器,单就堆排序及一些选择性算法——如寻找最大的K个值——而言,堆就不可忽视。
我们学习和研究一种数据结构,思维曲线一般可以遵从以下三步骤:
这里,我说的是针对学习和研究,在实际解决问题的过程中,往往是先分析需求,这里侧重的指复杂度需求,然后思考进行什么样的操作满足这种需求,再接下来,我们可能就要想,要实现这种操作,我们需要怎样组织信息呢?这个,就是数据模型了。另外,在学习与研究中,有时候并不能完全区分第二步和第三步,我想这个应该也不必多说为什么。在本系列文章中,一些明显的复杂度我就不分析了,一大坨数学符号看了头大,追求形式化理论化不免曲高和寡。但对于比较重要也比较复杂的复杂度分析,我将给出分析过程。
确定了思维曲线,接下来就是确定具体内容。堆其实有很多变种,都是在现实中遇到了麻烦,然后先辈牛人们就想出了新的变种堆。本文主要学习和研究基本二叉堆及其泛型扩展——d堆,二项堆,斐波那契堆,杨氏图表、笛卡尔树等等,Wikipedia上列出的堆的变种,我都争取进行分析和研究,这个过程,都是一个厚积薄发的过程。
学习和研究的目的,不应该仅仅满足于知识的掌握,这是常识,尽管这同样非常重要——这是我们在以后分析具体问题时的知识储备。我们还应该尽可能从一次学习和研究中提升自己建立模型的能力,这个过程可能很缓慢,不会像林志玲的脸蛋,望一眼就让人神魂颠倒灵魂出窍,但是意义却是不言自明的吧。所以这篇文章除了依据图1的思维曲线来学习研究堆,也希望可以总结出一些提升个人独立建立模型能力的感悟。最后我们上点干货,给出堆的C++、Python实现代码(文中以C++代码为解说,附件为C++与Python的完整实现),并具体解决几个具体问题。
1.&& 二叉堆与d堆1.1模型的建立我们认识堆基本上都是从堆排序算法开始,至少我是。根据《算法导论》,堆最早可能确实就是基于排序的需求,J. W. J.Williams发明了堆排序算法,但是Williams那篇论文我在网上搜不到(如果谁有,希望可以分享一下,嘿嘿),所以W爷爷到底是基于一个什么样具体的问题(需求)的“折磨”想出了这么好的办法,我也不得而知,所以只好猜猜了。
根据堆最经典的应用之一——堆排序的特点,我琢磨着应该还是基于对选择排序的不满。选择排序简单直观,每次从遍历序列从中找出最大值放入已排序序列中,显然,不平凡也不甘平凡的programmers发现了,这个遍历做的事情显然太少了,一个for(i=0; i&n; i++){foo(i);}都可以优化成for(i=0; i&n; i+=2){foo(i); foo(i+1);},凭什么选择排序这个大尾巴狼就没得优化空间?选择排序的基因思想是,每次从(剩余的)源序列中找出最大值。打个比方,这就像是比赛,如果你能从第一局打到最后一局,那你就是最强者(最大值),我们选出这个最强者(最大值)的过程就是选择的过程,选出来之后,将它标记,然后再从剩下的选手中以同样的方法选出第二强的,依次下去,直到只剩一个选手(边界)为止。从这个比方中,我们差不多也看出这种办法的毛病了:它浪费了中间很多次比赛的结果记录。还是举个例子,对于选手序列S={C, B, E, A, D},并设定强弱关系为A&B&C&D&E。
1)&&&&&&&& 第一轮,C和B比赛一场,B胜利了;于是B就继续和E赛一场,B胜了;B又和A赛一场,A胜利;A再和D赛一场,A再次胜出,A就被选出来。
2)&&&&&&&& 第二轮,C又和B赛一场,于是B就继续和E赛一场, B胜利,于是B继续和D赛一场;
3)&&&&&&&& ……
看出来了吧,这里C和B的比赛多余了,同样的,B和E的比赛又重复了。
问题找到了,怎么想办法克服呢?哈哈,you got it! 保存下这个结果!再继续往下想,怎么保存?就我不深的计算机学习经验,对于“大与小”、“胜与负”等等这种简明的二元关系,比较容易联想到的就是树了。我们知道,树的本质思想就是分治,是二元。对于树,父节点可以抽象为一个指定判断标准下的被比较对象,相应地,左子节点抽象为与父节点相比较后达该判断标准的对象,右子节点为未达标对象。在排序中,左子节点就是比俺嫩的,右子节点就是比俺熟的,基于这种思想,我们就有了二叉排序树(二叉搜索树);另外一种二元分治思想的具体化方式就是,凡是子节点就是比俺嫩的(或是比俺熟的),基于这种思想的就是我们的堆了。
言归正传,从这个具体的例子,我们可以抽象到选择排序,选择排序就是白白浪费了许多具有明显意义的中间结果,每次我们再去找下一个最大值的时候,都要重新老老实实重新遍历一遍整个剩余序列,programmers就开始琢磨着,如果再去找下一个最大值的时候,可以直接取出来就好了!有人就跳起来了,那不就是已排好序的序列了吗?好吧,那我退一步,我看能不能做到不用遍历整个剩余序列的办法就取出来呢?从O(N)降低到O(logN),行不?对此我们想到了要保存这些结果,再进一步地,我们想到了可以借助树的思想。对于树,除了前面的二元分治思想,另一种本质性思想是递归,递归性。这个应该很好理解,递归的本质又可以理解为任何一个小局部都服从某种规则,构成同样服从该规则的大局部,也就是整体。说了这么多,我们差不多可以引入堆的概念了。
(二叉)堆,如图2所示(本图摘自机械工业出版社《算法导论》第二版73页),它可以被视为一棵完全二叉树。完全二叉树的主要特点是,树的每一层都是满的,最后一层可能除外,而最后一层从一个节点的左子树开始填充。
再具体看堆的特点,堆之所以为堆的特点。对于根节点16的两个儿子,14和10都比根节点16小,这就是一种二元思想的体现了,存储在父节点与子节点之间的关系,就是在比较中获得的大小关系。再看递归思想,14的子节点8、7又比自己小,10的节点9、3也比自己小,以此类推。
堆从具体内存布局上看,是一种数组对象——完全二叉树嘛!显然,这棵完全二叉树中每个节点都与表示二叉堆的数组中元素存在某种对应关系,这在图2中也已体现出来。假设表示二叉堆的数组为A,那么A[0]表示树根,也就是堆顶,那么,对于某个数组下标为i的节点Ni与其父节点为Pi、左子节Li、右子节点Ri(假设左子节点或右子节点存在)之间的关系如下:
Pi = A[i/2], Li = A[2*i], Ri = A[2*i+1].
再次回到选择排序,堆排序的过程,就是在遍历数组时,将数组转换为具有图2中堆特性的数组:
A[i/2]&=A[2*i]
且A[i/2]&= A[2*i+1]
这样,最大值的时候,我们就知道是A[0]了,取出A[0]后,弹出来,修改一下数组的元素次序,下次最大值还是在A[0]处。这里的问题在于,取出A[0]后修改数组中的元素次序,这个复杂度会比老老实实再遍历一次剩余数组的开销低吗?
这就是对该模型的操作问题,以及操作的复杂性问题。带着这个问题,我们可以进入第二步了。
1.2针对模型的操作及复杂度分析1.2.1 构造——建堆任何数据结构模型,都无外乎构造、插入、删除、查找(读取)、修改等等。
从前述中我们得知,堆可以看作在“精化(refinement)”的完全二叉树,完全二叉树的数组表示中,数组后半段A[n/2]~A[n-1]都输树的叶子节点,因此,每个叶子节点都可以看做是只有一个元素的堆,建立堆的过程,就是从叶子节点的父节点开始,逐步上溯(递归思想),使得整个数组具备堆的特点,为此我们先引入一个辅助函数,它完成对指定的数组段得“堆化”的过程,这个函数就叫heapify。
//A[]为待“堆化”的数组,start和len确定了待“堆化”的子数组
template&typename T&
inline void heapify(T A[], int start, int len){
&& int son = start&&1+1; // 左子节点索引,因为下标从0开始
&& T item = A[start];
&& while(son &= len){
if(son & len-1 &&(A[son] & A[son+1])) ++ // 移至右子节点
&&&&& if(item &= A[son]) // (*)
&&&&& a[son&&1] = a[son]; //a[son&&1]就是父节点了,(*)行没有break出去,说明违反堆性质
&&&&& son &&=1; //孙子节点
&& a[son&&1] =
读懂代码往往是需要想象力的,尤其是算法性质的代码。而想象力最好的体现办法就是画图,请见图3(本图摘自《算法导论》第75页)。
每次循环中,从A[i], A[son], A[son+1]三者中选出最大的,将其“上移”。其实就这么简单。而这个复杂度应该也是很明显的O(logN)吧。
有了heapify,建堆的过程应该就很简单了,前面说了,从叶子节点的父节点开始,逐步上溯(递归思想),使得整个数组具备堆的特点,具体操作上,就是对从叶子节点的父节点开始的数组前半段,依次调用heapify。
template&typename T&
void make_heap(T A[], int len){
&& for(int i = len&&1; --i)
&&&&& heapify(A, i, len-1);
图我就不画了,请见《算法导论》第77页图6-3。
这个建堆的过程是一个典型的递归过程——是的,你懂的,递归可不一定就要一个劲自己个调用自己个,循环也可以。每次循环中都不断检查子树是否满足堆性质,不满足就调整,调整完就将循环下标前移。
1.2.2 插入与删除插入与删除其实也简单,主要是有一种情况要考虑到,就是新插入的元素和删除的元素可能改变现有的堆结构性质。所以要进行相关的验证,验证失败后就要再次heapify了。
template&typename T&
bool insert_heap(T A[], T item, int& heapsize)
&& int index = ++ // 初始时当然就是插入到数组尾部了
&& if(index == MAX_SIZE){
&&&&& std::cerr&&"heap size exceeded\n";
&& A[index] =
&& heapify(A, 0, heapsize-1);
下面给出的是不使用heapify的insert_heap()版本,基本上没什么区别,我写出来主要是为了扩展下思维和视野,意识到解决问题的方案永远不止一种,很非常好滴呀;另外,STL中使用的是这个版本的思路。不过我个人还是偏向于heapify版本的实现,简洁,复用性也体现了。
template&typename T&
bool insert_heap(T A[], T item, int& heapsize)
&& int& index = ++ // 初始时当然就是插入到数组尾部了
&& if(index == MAX_SIZE){
&&&&& std::cerr&&"heap size exceeded\n";
&& while(index & 0 && A[index &&1] & item){
&&&&& A[index] = A[index &&1];
&&&&& index &&=1;
&& A[index] =
初始时当然默认新插入的元素在数组尾部了,在侯捷老师《STL源码剖析》中将其抽象为一个hole,嗯,我觉得很好,下面是从这本书的繁体版174页图4-21。看着这个图,插入的过程应该是非常好理解的。
在这里想提一个小插曲,其实建堆的过程,我们也可以使用insert函数,就是每次从读取一个数据,然后插入到堆中,《算法导论》第六章习题就提到了这一点。
对于堆的删除操作,一般来说,删除的仅仅是堆顶元素,很少有指定一个索引值删除某元素或是指定元素值删除。所以正文部分就不讨论了,不过这个操作我们也实现一番,留到第三小节去,,下面是堆顶元素的删除函数。
template&typename T&
bool delete(T A[], int& heapsize){
&& if(!heapsize){
&&&&& std::cerr&&"heap is empty\n";
&& T tmp = A[0];
&& A[0] = A[heapsize];
&& A[heapsize--] =
&& heapify(A, 0, heapsize);
上面这段代码很简单,先将堆顶元素与堆尾元素交换,然后使堆的有效长度减1,然后重新对堆进行heapify。与插入操作的版本一样,下面给出的是仿STL的实现,如果您对STL源码没什么兴趣,可以不看。
template&typename T&
bool delete(T A[], int& heapsize){
if(!heapsize){
&&&&& std::cerr&&"heap is empty\n";
&& T item = A[heapsize - 1]; // 保存下最后一片叶子的值
&& A[heapsize - 1] = A[0]; //原来堆顶的值放到数组尾部
&& -- //因为原来的item已delete出去了,所以数组实际长度减1
&& int index = 0;
&& int child_index = index*2 + 2; //取右子节点
&& while(child_index & heapsize){
&&&&& if(A[child_index] & A[child_index - 1]) //取子节点数值最大者
&&&&&&&& --child_
&&&&& A[index] = A[child_index];
&&&&& index = child_
&&&&& child_index = index*2 + 2;
&& //处理边界情况,完全二叉树最后一片叶子为左子节点
&& if(child_index == heapsize){
&&&&& A[index] = A[child_index - 1];
&&&&& index = child_index - 1;
&& insert_heap(A, item, index);
1.2.3查找(读取)、修改与合并与删除操作一样,查找(读取)操作应该是读取堆顶元素,这个O(1)时间就可以实现了,也非常简单,直接返回A[0]就百事OK;对于修改操作,一般的也还是修改堆顶元素,变大或者变小,根据情况重新heapify一下就可以,如果是指定一个索引值修改某元素或是指定元素值进行修改,我想,现实中也不会完全没这种可能,所以我们在1.3节也给出相应的实现。下面先给出一般的读取与修改的代码。
template&typename T&
inline bool getTop(T A[], int heapsize, const T& res)
&& if(!heapsize){
&&&&& std::cerr&&"heap is empty\n";
&& res = A[0];
template&typename T&
bool modify(T A[], int heapsize, T dest)
&& if(!heapsize){
&&&&& std::cerr&&"heap is empty\n";
&& // 我们一直都假设堆为最大堆
&& if( dest & A[0])
&&&&& heapify(A, 0, heapsize-1);
对于合并(merge)操作,一般书上讲到二叉堆不会提到合并操作,因为这不是二叉堆所擅长的,在本系列的后面的部分中专门分析二项堆和斐波那契堆的时候还会提到,这里提到合并操作,既是考虑到思维的全面性,也是为了后面分析二项堆/斐波那契堆的时候有个对比。单独考虑二叉堆的合并,假设第一个堆的大小为N,第二个为M,主要有两个办法, 一种是依次取出第二个堆中的元素插入到第一个堆中,这种办法的复杂度为O(MlogN);另一种是先将第二个堆中的元素拷贝到第一个堆中,再执行一个heapify(),这种办法的复杂度为O(M+M+N),一般认为第二种情况下的复杂度优于第一种情况,事实上《算法导论》这是这么说的。下面是基本代码。
Merge操作的实现请见1.3节。
1.3二叉堆的测试限于篇幅,完整的C++实现只能上传到CSDN下载频道去,本节主要给出二叉堆的接口,当然,我们学习数据结构,其实学的就是实现,关于这一切,我都上传到了这里,里面包含了相应的测试文件。下面仅给出接口部分。
template&typename T, typename Compare = heap_aux::greater_equal&T& &
class binaryheap{
&&& binaryheap(const Compare& _comp = Compare());
&&& binaryheap(int size, const Compare& _comp = Compare());
&&& binaryheap(std::vector&T& _coll, const Compare& _comp = Compare());
&&& ~binaryheap(){}
&&& inline const int get_heapsize()
&&& inline const T& getTop()
&&& inline const T& get_elem(int index)
&&& void insert(T item);
&&& void delete_heap(int index = 0);
&&& void modify_top(T dest);
&&& void merge(const binaryheap& other);
&&& inline void make_heap();
&&& inline void heapify(int start, int len);
&&& std::vector&T&
在我的测试中,分为正确性测试与耗时测试两部分,都比较好懂。值得一提的是,与STL的make_heap相比较,binaryheap不知道为什么快了那么多,最快都快了50.4%,如下图所示。难道STL精深的封装与嵌套,竟要如此耗费了如此大的开销??
下期预告:以上基本上都还是一些理论性质的东西,下期我们将给出完整的二叉堆和D堆的C++实现,Python实现留待以后,因为我现在时间并不是非常充足。下期还将给出几个和堆关系紧密的现实算法问题及ACM训练题。所谓“古来学问无余力,少壮工夫老始成。纸上得来终觉浅,绝知此事要躬行”。
走,我们编程去!~
PS. 个人联系方式:
del.icio.us:
本文来自CSDN博客,转载请标明出处:
本文来自CSDN博客,转载请标明出处:
TA的最新馆藏[转]&[转]&[转]&[转]&[转]&[转]&
喜欢该文的人也喜欢2017年2月 总版技术专家分月排行榜第三
2017年5月 .NET技术大版内专家分月排行榜第一2017年4月 .NET技术大版内专家分月排行榜第一2017年3月 .NET技术大版内专家分月排行榜第一2017年2月 .NET技术大版内专家分月排行榜第一2016年10月 .NET技术大版内专家分月排行榜第一2016年8月 .NET技术大版内专家分月排行榜第一2016年7月 .NET技术大版内专家分月排行榜第一
2017年2月 总版技术专家分月排行榜第三
2017年5月 .NET技术大版内专家分月排行榜第一2017年4月 .NET技术大版内专家分月排行榜第一2017年3月 .NET技术大版内专家分月排行榜第一2017年2月 .NET技术大版内专家分月排行榜第一2016年10月 .NET技术大版内专家分月排行榜第一2016年8月 .NET技术大版内专家分月排行榜第一2016年7月 .NET技术大版内专家分月排行榜第一
2017年2月 总版技术专家分月排行榜第三
2017年5月 .NET技术大版内专家分月排行榜第一2017年4月 .NET技术大版内专家分月排行榜第一2017年3月 .NET技术大版内专家分月排行榜第一2017年2月 .NET技术大版内专家分月排行榜第一2016年10月 .NET技术大版内专家分月排行榜第一2016年8月 .NET技术大版内专家分月排行榜第一2016年7月 .NET技术大版内专家分月排行榜第一
本帖子已过去太久远了,不再提供回复功能。扫二维码下载作业帮
2亿+学生的选择
下载作业帮安装包
扫二维码下载作业帮
2亿+学生的选择
二叉堆的叶节点在数组中的下标为什么从[n/2]+1开始?《算法导论》里在堆排序那一章有一道证明题如下证明:数组表示存储n个元素的堆时,叶节点的下标分别是[n/2]+1,[n/2]+2,..n (此处[]指取下限[3/2] = 1)我的疑问如下:举例来讲,现在有8个节点,构成了一个二叉堆,(1)/ \(2) (3)/ \ / \(4) (5) (6) (7)/ (8)即使如算法导论中的惯例,数组下标从1开始,何以见得,leaf node (8)的下标为8,但是按照算法导轮上的这道证明题的结论,叶节点(8)在数组里德下标应该是[n/2]+1= [8/2] + 1 = 5 究竟是怎么回事?是我的理解有偏差吗?上述二叉堆并没有给出节点的值,括号里的数字是值数组里的index.
扫二维码下载作业帮
2亿+学生的选择
二叉堆一般用数组来表示.如果根节点在数组中的位置是1,第n个位置的子节点分别在2n和 2n+1.因此,第1个位置的子节点在2和3,第2个位置的子节点在4和5.以此类推.这种基于1的数组存储方式便于寻找父节点和子节点.-----------数组表示存储n个元素的堆时,叶节点的下标分别是[n/2]+1,[n/2]+2,..n (此处[]指取下限[3/2] = 1)是你的理解出了问题:leaf node (8)的下标为8,按照算法导轮上的这道证明题的结论,有8个节点的二叉堆,在数组里叶节点的下标应该是从 [n/2]+1= [8/2] + 1 = 5 起,一直到 8 .即5~8都是叶节点.
为您推荐:
扫描下载二维码求一个算法,把一组数据切成两组数据,使两组数据和之差的绝对值最小? 另外作为扩展,请考虑分成n组的情况。 - 知乎245被浏览14775分享邀请回答402 条评论分享收藏感谢收起151 条评论分享收藏感谢收起查看更多回答2 个回答被折叠()}

我要回帖

更多关于 二维数组 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信