
算法导论第三版 求读后感
你猜你猜你猜猜猜
如何从最大的N个数中选出最大或者最小的n个数
首先,我们假设n和N都是内存可容纳的,也就是说N个数可以一次load到内存里存放在数组里(如果非要存在链表估计又是另一个challenging的问题了)。
从最简单的情况开始,如果n=1,那么没有任何疑惑,必须要进行N-1次的比较才能得到最大的那个数,直接遍历N个数就可以了。
如果n=2呢
当然,可以直接遍历2遍N数组,第一遍得到最大数max1,但是在遍历第二遍求第二大数max2的时候,每次都要判断从N所取的元素的下标不等于max1的下标,这样会大大增加比较次数。
对此有一个解决办法,可以以max1为分割点将N数组分成前后两部分,然后分别遍历这两部分得到两个最大数,然后二者取一得到max2。
也可以遍历一遍就解决此问题,首先维护两个元素max1,max2(max1=max2),取到N中的一个数以后,先和max1比,如果比max1大(则肯定比max2大),直接替换max1,否则再和max2比较确定是否替换max2。
采用类似的方法,对于n=2,3,4一样可以处理。
这样的算法时间复杂度为O(nN)。
当n越来越大的时候(不可能超过N\\\/2,否则可以变成是找N-n个最小的数的对偶问题),这个算法的效率会越来越差。
但是在n比较小的时候(具体多小不好说),这个算法由于简单,不存在递归调用等系统损耗,实际效率应该很不错. 堆:当n较大的时候采用什么算法呢
首先我们分析上面的算法,当从N中取出一个新的数m的时候,它需要依次和max1,max2,max3max n比较,一直找到一个比m小的max x,就用m来替换max x,平均比较次数是n\\\/2。
可不可以用更少的比较次数来实现替换呢
最直观的方法是,也就是网上文章比较推崇的堆。
堆有这么一些好处:1.它是一个完全二叉树,树的深度是相同节点的二叉树中最少的,维护效率较高;2.它可以通过数组来实现,而且父节点p与左右子节l,r点的数组下标的关系是s[l] = 2*s[p]+1和s[r] = 2*s[p]+2。
在计算机中2*s[p]这样的运算可以用一个左移1位操作来实现,十分高效。
再加上数组可以随机存取,效率也很高。
3.堆的Extract操作,也就是将堆顶拿走并重新维护堆的时间复杂度是O(logn),这里n是堆的大小。
具体到我们的问题,如何具体实现呢
首先开辟一个大小为n的数组区A,从N中读入n个数填入到A中,然后将A维护成一个小顶堆(即堆顶A[0]中存放的是A中最小的数)。
然后从N中取出下一个数,即第n+1个数m,将m与堆顶A[0]比较,如果m<=A[0],直接丢弃m。
否则应该用m替换A[0]。
但此时A的堆特性可能已被破坏,应该重新维护堆:从A[0]开始,将A[0]与左右子节点分别比较(特别注意,这里需要比较两次才能确定最大数,在后面我会根据这个来和败者树比较),如果A[0]比左右子节点都小,则堆特性能够保证,勿需继续,否则如左(右)节点最大,则将A[0]与左(右)节点交换,并继续维护左(右)子树。
依次执行,直到遍历完N,堆中保留的n个数就是N中最大的n个数。
这都是堆排序的基本知识,唯一的trick就是维护一个小顶堆,而不是大顶堆。
不明白的稍微想一下。
维护一次堆的时间复杂度为O(logn),总体的复杂度是O(Nlogn)这样一来,比起上面的O(nN),当n足够大时,堆的效率肯定是要高一些的。
当然,直接对N数组建堆,然后提取n次堆顶就能得到结果,而且其复杂度是O(nlogN),当n不是特别小的时候这样会快很多。
但是对于online数据就没办法了,比如N不能一次load进内存,甚至是一个流,根本不知道N是多少。
败者树:有没有别的算法呢
我先来说一说败者树(loser tree)。
也许有些人对loser tree不是很了解,其实它是一个比较经典的外部排序方法,也就是有x个已经排序好的文件,将其归并为一个有序序列。
败者树的思想咋一看有些绕,其实是为了减小比较次数。
首先简单介绍一下败者树:败者树的叶子节点是数据节点,然后两两分组(如果节点总数不是2的幂,可以用类似完全树的结构构成树),内部节点用来记录左右子树的优胜者中的败者(注意记录的是输的那一方),而优胜者则往上传递继续比较,一直到根节点。
如果我们的优胜者是两个数中较小的数,则根节点记录的是最后一次比较中的败者,也就是所有叶子节点中第二小的那个数,而最小的那个数记录在一个独立的变量中。
这里要注意,内部节点不但要记录败者的数值,还要记录对应的叶子节点。
如果是用链表构成的树,则内部节点需要有指针指向叶子节点。
这里可以有一个trick,就是内部节点只记录败者对应的叶子节点,具体的数值可以在需要的时候间接访问(这一方法在用数组来实现败者树时十分有用,后面我会讲到)。
关键的来了,当把最小值输出后,最小值所对应的叶子节点需要变成一个新的数(或者改为无穷大,在文件归并的时候表示文件已读完)。
接下来维护败者树,从更新的叶子节点网上,依次与内部节点比较,将败者更新,胜者往上继续比较。
由于更新节点占用的是之前的最小值的叶子节点,它往上一直到根节点的路径与之前的最小值的路径是完全相同的。
内部节点记录的败者虽然称为败者,但却是其所在子树中最小的数。
也就是说,只要与败者比较得到的胜者,就是该子树中最小的那个数(这里讲得有点绕了,看不明白的还是找本书看吧,对照着图比较容易理解)。
注:也可以直接对N构建败者树,但是败者树用数组实现时不能像堆一样进行增量维护,当叶子节点的个数变动时需要完全重新构建整棵树。
为了方便比较堆和败者树的性能,后面的分析都是对n个数构建的堆和败者树来分析的。
总而言之,败者树在进行维护的时候,比较次数是logn+1。
与堆不同的是,败者树是从下往上维护,每上一层,只需要和败者节点比较一次即可。
而堆在维护的时候是从上往下,每下一层,需要和左右子节点都比较,需要比较两次。
从这个角度,败者树比堆更优一些。
但是,请注意但是,败者树每一次维护必定需要从叶子节点一直走到根节点,不可能中间停止;而堆维护时,有可能会在中间的某个层停止,不需要继续往下。
这样一来,虽然每一层败者树需要的比较次数比堆少一倍,但是走的层数堆会比败者树少。
具体少多少,从平均意义上到底哪一个的效率会更好一些
那我就不知道了,这个分析起来有点麻烦。
感兴趣的人可以尝试一下,讨论讨论。
但是至少说明了,也许堆并非是最优的。
具体到我们的问题。
类似的方法,先构建一棵有n个叶子节点的败者树,胜出者w是n个中最小的那一个。
从N中读入一个新的数m后,和w比较,如果比w小,直接丢弃,否则用m替换w所在的叶子节点的值,然后维护该败者树。
依次执行,直到遍历完N,败者树中保留的n个数就是N中最大的n个数。
时间复杂度也是O(Nlogn) 类快速排序方法: 快速排序大家大家都不陌生了。
主要思想是找一个轴节点,将数列交换变成两部分,一部分全都小于等于轴,另一部分全都大于等于轴,然后对两部分递归处理。
其平均时间复杂度是O(NlogN)。
从中可以受到启发,如果我们选择的轴使得交换完的较大那一部分的数的个数j正好是n,不也就完成了在N个数中寻找n个最大的数的任务吗
当然,轴也许不能选得这么恰好。
可以这么分析,如果jn,则最大的n个数肯定在这j个数中,则问题变成在这j个数中找出n个最大的数;否则如果j 需要注意的是,这里的时间复杂度是平均意义上的,在最坏情况下,每次分割都分割成1:N-2,这种情况下的时间复杂度为O(n)。 但是我们还有杀手锏,可以有一个在最坏情况下时间复杂度为O(N)的算法,这个算法是在分割数列的时候保证会按照比较均匀的比例分割,at least 3n\\\/10-6。 具体细节我就不再说了,感兴趣的人参考算法导论(Introduction to Algorithms 第二版第九章 Medians and Orders Statistics)。 还是那个结论,堆不见得会是最优的。 本文快要结束了,但是还有一个问题:如果N非常大,存放在磁盘上,不能一次装载进内存呢 怎么办 对于介绍的Naive方法,堆,败者树等等,依然适用,需要注意的就是每次从磁盘上尽量多读一些数到内存区,然后处理完之后再读入一批。 减少IO次数,自然能够提高效率。 而对于类快速排序方法,稍微要麻烦一些:分批读入,假设是M个数,然后从这M个数中选出n个最大的数缓存起来,直到所有的N个数都分批处理完之后,再将各批次缓存的n个数合并起来再进行一次类快速排序得到最终的n个最大的数就可以了。 在运行过程中,如果缓存数太多,可以不断地将多个缓存合并,保留这些缓存中最大的n个数即可。 由于类快速排序的时间复杂度是O(N),这样分批处理再合并的办法,依然有极大的可能会比堆和败者树更优。 当然,在空间上会占用较多的内存。 总结:对于这个问题,我想了很多,但是觉得还有一些地方可以继续深挖:1. 堆和败者树到底哪一个更优 可以通过理论分析,也可以通过实验来比较。 也许会有人觉得这个很无聊;2. 有没有近似的算法或者概率算法来解决这个问题 我对这方面实在不熟悉,如果有人有想法的话可以一块交流。 如果有分析错误或遗漏的地方,请告知,我不怕丢人,呵呵 最后请时刻谨记,时间复杂度不等于实际的运行时间,一个常数因子很大的O(logN)算法也许会比常数因子小的O(N)算法慢很多。 所以说,n和N的具体值,以及编程实现的质量,都会影响到实际效率。 一:阶段C语言-------《C语言设计》 作者:谭浩大多数人都推荐谭这本书浩强的C语言程序设计,我觉得更适合当教科书,主要是篇幅没有那么大,书本的内容不是很全,由老师带入门是最好的。 《C Primer Plus》第五版 作者:Stephen Prata这本书比较厚,内容也比较基础,扩展的也比较广,自学容易上手,唯一的缺陷在于它是外国人编写,而国内的翻译往往让人难以满意,如果你有较好的英语水平,最好阅读英文版的。 《C语言入门经典》第四版 作者:Ivor Horton二:第二阶段这一阶段看个人主攻的方向了,安卓一般就JAVA,PC游戏、软件C++,也有推荐C#,这些都是主流语言,其他语言要看个人爱好与发展。 面向对象的语言,一般懂一门,之后要转其他语言也很容易的,其实思想都差不多,主要的不同在于语法。 C#容易上手,一个月入门,一年精通,而C++却有点难,一年入门,二十年也未必敢说自己精通。 如果有时间,可以了解一下汇编语言。 C++ Primer, 4rd Edition(入门类:C++ Primer, 4rd EditionThinking in C++, 2nd EditionThe C++ Standard Library: A Tutorial and Reference进阶类:The C++ Programming Language, Special EditionThe Design and Evolution of C++Inside C++ Object ModelC++ Templates: The Complete GuideSTL 源码剖析Generic Programming and the STLModern C++ Design: Generic Programming and Design Patterns Applied应用技巧类:Effective C++, 2nd EditonMore Effective C++Exceptional C++More Exceptional C++Effective STLRuminations on C++)三:第三阶段API\\\/SDK------------- 《windows程序设计》(Jeff Prosise)这书主要是开发Windows软件要深入研读的,毕竟是WindowsAPI。 如果是主攻C++,那么这本书你可以大概略读一遍,不用过多的深入追究,QT才是C++主要深入思虑的关键。 四:第四阶段MFC----《VC++技术内幕》、《深入浅出MFC》及视频教程孙鑫 VC++6.0如果是C++,QT是要重点学习的对象。 五:第五阶段COM\\\/DCOM\\\/ATL\\\/COM+---------《COM技术内幕》未知阶段:《数据结构》,《算法导论》这些基本是编程的核心了,编程的很大情况都依赖于算法的实现,这个两门基本要深入研究,这是决定编程能力的核心标准。 如果不弄底层的话,《操作系统原理》可以简单地了解。 要分清ios和android哪个开源组件多,看看github搜一下就一目了然搜ios得到40938个项目搜android得到112895个项目搜android得到112895个项目作为最大的代码托管网站,github的数据绝对能体现这两个阵营的代码开源热度作为最大的代码托管网站,github的数据绝对能体现这两个阵营的代码开源热度所以楼主的问题不成立【锟斤拷的回答(33票)】:----------谢邀,晚上来答了----------我做Android做了大约3年,做的虽然都是描绘Application层的小玩意,但是对整套生态圈应该感觉还是有点小了解;赶脚题主这个问题就有点黑安卓的意思,在我看来,这样的问题和“为什么C语言要比JAVA快”一样没有非常大的实际意义。 所以感觉题主从主观上在做安卓的时候受到了挫折,哈哈。 首先我想说的是,Android上的开源组件并不少。 具体项目我就不多说了,各位同仁也在分享,而且git和group上多的一笔,像UIL(Universal Image Loader)这种神器,还有SherlockActionBar(已经被归入support v7,并且在最新版本的ADT中已经被强制填入),很强大的Zxing这个一维码、二维码通吃的好玩意;还有什么pull-to-refresh的lib等等,同样我也希望在这个主题里面能看到更多同僚的分享。 相比来说,就以一二维码为例,zxing对iOS的支持就不怎么样,同事在搞iOS上的条码扫描和生成的时候,费了很大的功夫,且效果不理想,条码二维码这个近几年很火吧 可是iOS上没有一套完整的东西,zbar什么的,看了几个,都不理想,或多或少有欠缺。 所以我的观点的话,就这件事上,我是不同意楼主的观点的,首先jdk&android都是开源的,所以不考虑难度和质量来说,更容易开发出来一套工具lib或者框架;其次Android有Google这个爹,本着Google精神,有很多人愿意去分享自己的东西;再者我们从实际开发来讲,又或许是我做的项目都很小、很白痴,我没有觉得Android的第三方开源小玩意少,很多玩意都能找到相关的解决办法(现成代码)。 于是我想了解下题主在什么契机下觉得Android的开源项目少呢 Android本身就是个开源项目呀。 我再试图研究研究其它同仁的看法:回报的问题细讲讲工资,我姑且当大伙儿都跟我一样是给人打工的。 我现在的工资不比同组做iOS的人低,我是做app开发的,感觉现在不知道谁传出来的都有这样一种误解,(不算适配的情况下)android开发简单,iOS开发困难。 难道就因为oc入门比java削微复杂点么 或者说你以为会点java就随随便便地说Android一样,都能搞 别逗了。 Android里面玩法很多,跑到实际运营上就能给产品提供各种各样的玩法,像运营商定制,流氓功能(当然我们不提倡),都是能给公司和团队带来其它方面的获益的;就拿互联网app来讲,关键网络层代码用NDK写好,UI实现Android和iOS同样都是随便交给一个成手就能搞定,又哪里来的优越感 在你灵活运用23甚至更多种设计模式,通读并精通算法导论和架构之美后,开发语言难道还有不同之处 这个是我觉得很不满的;会java并不代表你会安卓,同样,你会用java更不代表你懂java,别用你从不知道哪里看到的资料说java烂,dalvik卡,我们讲纯技术,为何oc不做跨平台 如果是做Android BSP这种相关的工程师,也可以叫Android工程师,这种价钱我就不用多说了吧,不是和做app一个级别的,至少在北方是。 其次是广告收入,你们不要再看个排名就说iOS收入多多牛逼,应用内支付收益多少多少了好么 前两天一个小伙计拿俩图片当杀毒软件挣好几万的事情你们当真是装傻屏蔽了么 放垃圾广告山寨app这种事App store会让你上么 你要是邢山虎拿着我叫MT来喷我我一点都不带还嘴,有多少人一个自己运营的APP都没搞过就开始大谈广告收入和排名算法了 我认为大部分人花几天做一个app扔在Google play上,一次性也不用运营,咱们先不要把自己考虑成拯救世界的人,一个月挣个几百刀广告费不够诸位加个油吃个饭的嘛 万一点子投机了当真辞了工作去36kr觅个投资也好。 我说的都是以我这种屌丝来说的大实话,没接触过在BAT中做的经理。 碎片化的问题这个的确是一个大问题且无法回避,硬要牵扯的话,和所谓“开源”项目也有点关系:不够精:某些开源组件在某些特定机型上会报错,因为其可能改了原生的sdk;不够多:现在对于Android程序员来讲“兼容到2.1(更有甚者到1.6)”已经和“兼容到IE6”一个级别地让人痛苦不堪;这也导致了最起码我就有时候不愿意写一个库给小组用,你要做的东西特别多,很恶心;不够全:iOS其实一直贯彻的都是某些种特定的风格,至少和Android相比基本就可以看成只有一两种;我可以针对你的一两种来做一套工具框架,但是我肯定无法为你的未知种类做点什么(这里主要还是指UI,因为毕竟app开发的主要工作量还是在UI上)生态圈的问题一个是市场占有率,有说Android市场占有率占有高,开源项目就应该多的,这种纯属敷衍答案,此处不表;其次是我们所说的应用商店,换句话说是应用个数,再换个词儿叫有效应用个数,这个就不用我多说了吧,上文也有提到,像我这样就图个广告费还个贷款的人大有人在,做做rom放点系统apk流氓捆绑的也大有人在,觉得不爽的同僚也没必要喷我,谁不为了活着;从审核机制来看,我忘了在哪个资料上看到了,iOS帮开发者做好了XXX事情,是为了让他们开发出更好、更优良的点子;而我们做Android的,甚至是Android本身,仍然在摸索。 我突然想到一个,也是我经常碰见的,不知道cocoachina什么的iOS论坛怎么样,反正Android这边网上资料都烂了,各种瞎胡转载,我们不要求你们转载、爬虫的时候署名了,你好歹把代码格式给帖过来;你格式没有也没关系,你能不能把代码给帖全了 反正我在国内论坛上的时候,至少百度的时候,经常会心烦气躁,不知道题主是不是在搜某个实现的时候也碰见了这个问题 还是去stackoverflow上看看吧。 我个人做Android APP开发,却用OSX,包括最近开始看iOS相关的开发内容,有时候拿着手里的iPhone玩一玩,静音、音量、home、power键都是我的刚需,玩游戏的时候不用像nexus一样怕碰到虚拟键,我承认苹果做的非常非常棒,但是我决不承认安卓比苹果差,实在没有可比性。 【万宵的回答(13票)】:泻药个人理解题主说的开源组件是所谓的开源工程。 其实如果说到开源,真的就和钱啥的扯不上关系了。 生态,再过几年等4.0以下的都灭绝了,你们就不会说生态烂了其实Android有很多非常不错的开源工程,我这里说的开源工程是指那种作为依赖工程导入的的工程,下面介绍一下比较知名的android开源项目,都是造好的轮子,免去了我们很多麻烦android-pulltorefresh 一个强大的拉动刷新开源项目,支持各种控件下拉刷新ListView、ViewPager、WevView、ExpandableListView、GridView、(Horizontal)ScrollView、Fragment上下左右拉动刷新,比下面johannilsson那个只支持ListView的强大的多。 并且他实现的下拉刷新ListView在item不足一屏情况下也不会显示刷新提示,体验更好。 项目地址:ActionBarSherlock 为Android所有版本提供统一的ActionBar,解决4.0以下ActionBar的适配问题项目地址:MenuDrawer 滑出式菜单,通过拖动屏幕边缘滑出菜单,支持屏幕上下左右划出,支持当前View处于上下层,支持Windows边缘、ListView边缘、ViewPager变化划出菜单等。 项目地址:Android-ViewPagerIndicator 配合ViewPager使用的Indicator,支持各种位置和样式项目地址:SwipeBackLayout 左右或向上滑动返回的Activity项目地址:这个在知乎的手机app里面有,我说的是安卓版,苹果的没用过,向右滑动屏幕退出当前页面就是这个玩意儿做的还有一个异步图片加载的universal image loader,也是很猛的...题主要找的话可以找到很多,我只是列举一些用的比较多的 家可能觉得SF比较鸡肋或者难上手,所以很多就放弃了SF或者只打22,其实这样着实少了很多乐趣o(∩_∩)o... 当然了,其实要用SF死磕强队难度真的满大的,但在小频道打打其实问题并不大。 我会就打法,技能使用,衣服配置等方面为大家作一些参考性意见。 【打法】 其实就打法而言,这真是泛泛之谈,而且就SF而言,本就无固定打法可言。 不像PF,就是挡扣C扫地板。 我就来说下我个人的想法。 其实现实中打球我们都知道miss match,也就是错位进攻。 大家也都知道有时候挡拆把C拉出来后,自己的C可以强吃对方的G。 其实对SF来说,基本上都是miss match,当然了,就是因为不像C对G那么明显,而且也不够稳定,所以这就是死磕强队的难度。 进攻 如果你面对G的话,可以考虑反跑后中投或者虚实等技能。 因为G一般都是死死贴住SF的,本身弹跳和盖帽能力也欠缺,防守的话反应稍慢就盖不到了。 中投后虚实,W强吃然后举球上篮,反拉SS上篮都是不错的选择。 但一定要注意C的补防。 建议自己C拉出去给G挡的时候。 和G1V1的时候使用。 如果面对PF的话,建议可以考虑A。 有的人说A不是找死么 就SF那A的技能和FS还有运球能力。 当然,如果你被贴死了还A,这肯定是不明智的。 但如果你跑出空挡了,并且记住了自己的A的FS的顺序,这未尝不是SF对面PF的一个miss match呢 而且SF16及就有JAMES运球了。 这是个不错的技能。 下面会讲到 还有种进攻就是和自己队友配合。 SF的后撤拉人,在小频道打打可以,那天用SG刚试过。 PG的叠A,自己往PG身后走,PG利用背2来拉倒换防你的人。 当然不能纯这样打,否则不如打2G了。 防守 打2G的话就是防好2G,拿出你2V2的防守功力吧。 思想集中点,尽量别被挡了。 防PG超级运球的话尽量往弱侧防。 防SG后撤2的话靠反应吧。 (饿,我也防不住撒,一般都是打2局知道了对方的习惯才防得住)。 面对PG PF C的组合时,要随时注意补防对方的PG。 PF大力的话,顶上去。 如果等级低的时候劲量猜对方会使用普通大力,因为技能大力会往外翻,对于远扣可能弹跳不够。 如果对方喜欢W一直推的话,你就掏吧。 千万别抢C的冒位啊,卡好对方C的地板位才是真的。 【技能和各FS的使用和建议装配】 首先SF有个原始技能叫中投。 (这不是废话吗.。 。 。 汗)可是大家好像并不喜欢用。 但是我想如果你各个位置的反跑中投就很纯熟了,那我想你的实力不是提高一个档次的问题。 比如你45°回跑,看到球传过来,再往底线方向跑,很容易形成错身位的中投。 0°和负角度也很明显。 当然中投可以侧盖和追冒,这就要你自己把握了。 一开始练习出扭麻花和人才是正常的,多加练习会好很多。 尤其45°附近,你觉得困难的地方,对手盖起来也困难。 最好的方法还是反跑自己C挡后中投。 因为挡2的要求没挡3那么高,可以随时打。 当然要注意对方C的补防 反跑3。 会了反跑2,反跑3就比较轻松了。 虚实。 可以反跑后直接使用。 也可以中投后接虚实(再中投也可以,但命中会稍低),虚实后中投。 很多比较新手的朋友可能不知道。 虚实有2种用法。 一种是死球虚实。 一种是活球虚实。 第一种虚实的按法是死球后先按篮筐反方向再按W,第2种是接球后不动先W再按篮筐反方向,这种虚实后可以走动。 死球后想虚实接中投的话,虚实要逆时针使用,因为顺时针的话虚实后人物还要转身。 DS。 骗跳技能。 突破一人防守后使用不错,但对手2个人都在篮下时不建议时候。 可以结合虚实DS。 死球后虚实DS,顺时针是往外跳的,逆时针是向里跳的。 强吃G时也可以W举球然后DS,骗跳对方的C。 JAY。 在身位优于对手的时候(既比对手更靠近篮筐)可以将对手挤到。 如果乱用很容易被卡倒。 JAY后可以中投,不会被追冒,也可以虚实(用法就是死球虚实),要注意对方C的补防。 对方C扑过来,你就传自己的C吧。 凌波微步。 偶尔使用。 先不说是否难盖和会被抓,首先你使用的时候得面对2次防守。 但在45°使用不像反跑中投会出人才。 在小频道偶尔使用问题不大。 快速补防。 一个很好的防外线技能。 使用时自己要注意落点。 后撤。 可以拉开投篮空间,但要注意PF的弹跳和C的盖帽范围。 建议跑动拉出空挡后使用,也可以左右A一下后接后撤。 3分线附近使用成功可以直接投3。 后撤2.。 装了后撤后才可使用。 此技能大大加强了SF的运动能力。 让SF有了自己创造得分空间的能力。 配合普通过人和31及的运球FS,效果不错。 但比较难控制。 说2个例子,在对方底线没有卡死的情况下,可以接球后撤接普A溜底线。 如果对方死卡底线,而你又有31及的背后运球的话,可以后撤接背后运球(底线反方向)。 一个2或3的空挡就出来了。 左右倒手。 几乎没怎么 这个问题我前前后后考虑了有快一年了,也和不少人讨论过。 据我得到的消息,Google和微软都面过这道题。 这道题可能很多人都听说过,或者知道答案(所谓的堆),不过我想把我的答案写出来。 我的分析也许存有漏洞,以交流为目的。 但这是一个满复杂的问题,蛮有趣的。 看完本文,也许会启发你一些没有想过的解决方案(我一直认为堆也许不是最高效的算法)。 在本文中,将会一直以寻找n个最大的数为分析例子,以便统一。 注:本文写得会比较细节一些,以便于绝大多数人都能看懂,别嫌我罗嗦:) 我很不确定多少人有耐心看完本文 \ Naive 方法:\ 首先,我们假设n和N都是内存可容纳的,也就是说N个数可以一次load到内存里存放在数组里(如果非要存在链表估计又是另一个challenging的问题了)。 从最简单的情况开始,如果n=1,那么没有任何疑惑,必须要进行N-1次的比较才能得到最大的那个数,直接遍历N个数就可以了。 如果n=2呢 当然,可以直接遍历2遍N数组,第一遍得到最大数max1,但是在遍历第二遍求第二大数max2的时候,每次都要判断从N所取的元素的下标不等于max1的下标,这样会大大增加比较次数。 对此有一个解决办法,可以以max1为分割点将N数组分成前后两部分,然后分别遍历这两部分得到两个最大数,然后二者取一得到max2。 \ 也可以遍历一遍就解决此问题,首先维护两个元素max1,max2(max1=max2),取到N中的一个数以后,先和max1比,如果比max1大(则肯定比max2大),直接替换max1,否则再和max2比较确定是否替换max2。 采用类似的方法,对于n=2,3,4一样可以处理。 这样的算法时间复杂度为O(nN)。 当n越来越大的时候(不可能超过N\\\/2,否则可以变成是找N-n个最小的数的对偶问题),这个算法的效率会越来越差。 但是在n比较小的时候(具体多小不好说),这个算法由于简单,不存在递归调用等系统损耗,实际效率应该很不错.\ 堆:当n较大的时候采用什么算法呢 首先我们分析上面的算法,当从N中取出一个新的数m的时候,它需要依次和max1,max2,max3max n比较,一直找到一个比m小的max x,就用m来替换max x,平均比较次数是n\\\/2。 可不可以用更少的比较次数来实现替换呢 最直观的方法是,也就是网上文章比较推崇的堆。 堆有这么一些好处:1.它是一个完全二叉树,树的深度是相同节点的二叉树中最少的,维护效率较高;2.它可以通过数组来实现,而且父节点p与左右子节l,r点的数组下标的关系是s[l] = 2*s[p]+1和s[r] = 2*s[p]+2。 在计算机中2*s[p]这样的运算可以用一个左移1位操作来实现,十分高效。 再加上数组可以随机存取,效率也很高。 3.堆的Extract操作,也就是将堆顶拿走并重新维护堆的时间复杂度是O(logn),这里n是堆的大小。 \ 具体到我们的问题,如何具体实现呢 首先开辟一个大小为n的数组区A,从N中读入n个数填入到A中,然后将A维护成一个小顶堆(即堆顶A[0]中存放的是A中最小的数)。 然后从N中取出下一个数,即第n+1个数m,将m与堆顶A[0]比较,如果m<=A[0],直接丢弃m。 否则应该用m替换A[0]。 但此时A的堆特性可能已被破坏,应该重新维护堆:从A[0]开始,将A[0]与左右子节点分别比较(特别注意,这里需要比较两次才能确定最大数,在后面我会根据这个来和败者树比较),如果A[0]比左右子节点都小,则堆特性能够保证,勿需继续,否则如左(右)节点最大,则将A[0]与左(右)节点交换,并继续维护左(右)子树。 依次执行,直到遍历完N,堆中保留的n个数就是N中最大的n个数。 \ 这都是堆排序的基本知识,唯一的trick就是维护一个小顶堆,而不是大顶堆。 不明白的稍微想一下。 维护一次堆的时间复杂度为O(logn),总体的复杂度是O(Nlogn)这样一来,比起上面的O(nN),当n足够大时,堆的效率肯定是要高一些的。 当然,直接对N数组建堆,然后提取n次堆顶就能得到结果,而且其复杂度是O(nlogN),当n不是特别小的时候这样会快很多。 但是对于online数据就没办法了,比如N不能一次load进内存,甚至是一个流,根本不知道N是多少。 \ 败者树:有没有别的算法呢 我先来说一说败者树(loser tree)。 也许有些人对loser tree不是很了解,其实它是一个比较经典的外部排序方法,也就是有x个已经排序好的文件,将其归并为一个有序序列。 败者树的思想咋一看有些绕,其实是为了减小比较次数。 首先简单介绍一下败者树:败者树的叶子节点是数据节点,然后两两分组(如果节点总数不是2的幂,可以用类似完全树的结构构成树),内部节点用来记录左右子树的优胜者中的败者(注意记录的是输的那一方),而优胜者则往上传递继续比较,一直到根节点。 如果我们的优胜者是两个数中较小的数,则根节点记录的是最后一次比较中的败者,也就是所有叶子节点中第二小的那个数,而最小的那个数记录在一个独立的变量中。 这里要注意,内部节点不但要记录败者的数值,还要记录对应的叶子节点。 如果是用链表构成的树,则内部节点需要有指针指向叶子节点。 这里可以有一个trick,就是内部节点只记录败者对应的叶子节点,具体的数值可以在需要的时候间接访问(这一方法在用数组来实现败者树时十分有用,后面我会讲到)。 关键的来了,当把最小值输出后,最小值所对应的叶子节点需要变成一个新的数(或者改为无穷大,在文件归并的时候表示文件已读完)。 接下来维护败者树,从更新的叶子节点网上,依次与内部节点比较,将败者更新,胜者往上继续比较。 由于更新节点占用的是之前的最小值的叶子节点,它往上一直到根节点的路径与之前的最小值的路径是完全相同的。 内部节点记录的败者虽然称为败者,但却是其所在子树中最小的数。 也就是说,只要与败者比较得到的胜者,就是该子树中最小的那个数(这里讲得有点绕了,看不明白的还是找本书看吧,对照着图比较容易理解)。 \ 注:也可以直接对N构建败者树,但是败者树用数组实现时不能像堆一样进行增量维护,当叶子节点的个数变动时需要完全重新构建整棵树。 为了方便比较堆和败者树的性能,后面的分析都是对n个数构建的堆和败者树来分析的。 \ 总而言之,败者树在进行维护的时候,比较次数是logn+1。 与堆不同的是,败者树是从下往上维护,每上一层,只需要和败者节点比较一次即可。 而堆在维护的时候是从上往下,每下一层,需要和左右子节点都比较,需要比较两次。 从这个角度,败者树比堆更优一些。 但是,请注意但是,败者树每一次维护必定需要从叶子节点一直走到根节点,不可能中间停止;而堆维护时,有可能会在中间的某个层停止,不需要继续往下。 这样一来,虽然每一层败者树需要的比较次数比堆少一倍,但是走的层数堆会比败者树少。 具体少多少,从平均意义上到底哪一个的效率会更好一些 那我就不知道了,这个分析起来有点麻烦。 感兴趣的人可以尝试一下,讨论讨论。 但是至少说明了,也许堆并非是最优的。 \ 具体到我们的问题。 类似的方法,先构建一棵有n个叶子节点的败者树,胜出者w是n个中最小的那一个。 从N中读入一个新的数m后,和w比较,如果比w小,直接丢弃,否则用m替换w所在的叶子节点的值,然后维护该败者树。 依次执行,直到遍历完N,败者树中保留的n个数就是N中最大的n个数。 时间复杂度也是O(Nlogn)\ 类快速排序方法:\ 快速排序大家大家都不陌生了。 主要思想是找一个轴节点,将数列交换变成两部分,一部分全都小于等于轴,另一部分全都大于等于轴,然后对两部分递归处理。 其平均时间复杂度是O(NlogN)。 从中可以受到启发,如果我们选择的轴使得交换完的较大那一部分的数的个数j正好是n,不也就完成了在N个数中寻找n个最大的数的任务吗 当然,轴也许不能选得这么恰好。 可以这么分析,如果jn,则最大的n个数肯定在这j个数中,则问题变成在这j个数中找出n个最大的数;否则如果j \ 需要注意的是,这里的时间复杂度是平均意义上的,在最坏情况下,每次分割都分割成1:N-2,这种情况下的时间复杂度为O(n)。 但是我们还有杀手锏,可以有一个在最坏情况下时间复杂度为O(N)的算法,这个算法是在分割数列的时候保证会按照比较均匀的比例分割,at least 3n\\\/10-6。 具体细节我就不再说了,感兴趣的人参考算法导论(Introduction to Algorithms 第二版第九章 Medians and Orders Statistics)。 \ 还是那个结论,堆不见得会是最优的。 \ 本文快要结束了,但是还有一个问题:如果N非常大,存放在磁盘上,不能一次装载进内存呢 怎么办 对于介绍的Naive方法,堆,败者树等等,依然适用,需要注意的就是每次从磁盘上尽量多读一些数到内存区,然后处理完之后再读入一批。 减少IO次数,自然能够提高效率。 而对于类快速排序方法,稍微要麻烦一些:分批读入,假设是M个数,然后从这M个数中选出n个最大的数缓存起来,直到所有的N个数都分批处理完之后,再将各批次缓存的n个数合并起来再进行一次类快速排序得到最终的n个最大的数就可以了。 在运行过程中,如果缓存数太多,可以不断地将多个缓存合并,保留这些缓存中最大的n个数即可。 由于类快速排序的时间复杂度是O(N),这样分批处理再合并的办法,依然有极大的可能会比堆和败者树更优。 当然,在空间上会占用较多的内存。 \ 总结:对于这个问题,我想了很多,但是觉得还有一些地方可以继续深挖:1. 堆和败者树到底哪一个更优 可以通过理论分析,也可以通过实验来比较。 也许会有人觉得这个很无聊;2. 有没有近似的算法或者概率算法来解决这个问题 我对这方面实在不熟悉,如果有人有想法的话可以一块交流。 如果有分析错误或遗漏的地方,请告知,我不怕丢人,呵呵 最后请时刻谨记,时间复杂度不等于实际的运行时间,一个常数因子很大的O(logN)算法也许会比常数因子小的O(N)算法慢很多。 所以说,n和N的具体值,以及编程实现的质量,都会影响到实际效率。 我从别处看人的一些经验谈,给你粘希望对你有所帮助:嵌入式软件的好处是: (1)目前国内外这方面的人都很稀缺。 这一领域入门门槛较高,所以非专业IT人员很难切入这一领域;另一方面,是因为这一领域较新,目前发展太快,大多数人无条件接触。 (2)与企业计算等应用软件不同,嵌入式领域人才的工作强度通常低一些(但收入不低)。 (3)哪天若想创业,搞自已的产品,嵌入式不像应用软件那样容易被盗版。 硬件设计一般都是请其它公司给订做(这叫“贴牌”:OEM),都是通用的硬件,我们只管设计软件就变成自己的产品了。 (4)兴趣所在,这是最主要的。 从事嵌入式软件开发的缺点是: (1)入门起点较高,所用到的技术往往都有一定难度,若软硬件基础不好,特别是操作系统级软件功底不深,则可能不适于此行。 (2)这方面的企业数量要远少于企业计算类企业。 (3)有少数公司经常要硕士以上的人搞嵌入式,主要是基于嵌入式的难度。 但大多数公司也并无此要求,只要有经验即可。 (4)平台依托强,换平台比较辛苦。 兴趣的由来: 1、成功观念不同,不虚度此生,就是我的成功。 2、喜欢思考,挑战逻辑思维。 3、喜欢C C是一种能发挥思维极限的语言。 关于C的精神的一些方面可以被概述成短句如下: 相信程序员。 不要阻止程序员做那些需要去做的。 保持语言短小精干。 一种方法做一个操作。 使得它运行的够快,尽管它并不能保证将是可移植的。 4、喜欢底层开发,讨厌vb类开发工具(并不是说vb不好)。 5、发展前景好,适合创业,不想自己要死了的时候还是一个工程师。 方法步骤: 1、基础知识: 目的:能看懂硬件工作原理,但重点在嵌入式软件,特别是操作系统级软件,那将是我的优势。 科目:数字电路、计算机组成原理、嵌入式微处理器结构。 汇编语言、C\\\/C++、编译原理、离散数学。 数据结构和算法、操作系统、软件工程、网络、数据库。 方法:虽科目众多,但都是较简单的基础,且大部分已掌握。 不一定全学,可根据需要选修。 主攻书籍:the c++ programming language(一直没时间读)、数据结构-C2。 2、学习linux: 目的:深入掌握linux系统。 方法:使用linux—〉linxu系统编程开发—〉驱动开发和分析linux内核。 先看深,那主讲原理。 看几遍后,看情景分析,对照深看,两本交叉,深是纲,情是目。 剖析则是0.11版,适合学习。 最后深入代码。 主攻书籍:linux内核完全剖析、unix环境高级编程、深入理解linux内核、情景分析和源代。 3、学习嵌入式linux: 目的:掌握嵌入式处理器其及系统。 方法:(1)嵌入式微处理器结构与应用:直接arm原理及汇编即可,不要重复x86。 (2)嵌入式操作系统类:ucOS\\\/II简单,开源,可供入门。 而后深入研究uClinux。 (3)必须有块开发板(arm9以上),有条件可参加培训(进步快,能认识些朋友)。 主攻书籍:毛德操的《嵌入式系统》及其他arm9手册与arm汇编指令等。 4、深入学习: A、数字图像压缩技术:主要是应掌握MPEG、mp3等编解码算法和技术。 B、通信协议及编程技术:TCP\\\/IP协议、802.11,Bluetooth,GPRS、GSM、CDMA等。 C、网络与信息安全技术:如加密技术,数字证书CA等。 D、DSP技术:Digital Signal Process,DSP处理器通过硬件实现数字信号处理算法。 说明:太多细节未说明,可根据实际情况调整。 重点在于1、3,不必完全按照顺序作。 对于学习c++,理由是c++不只是一种语言,一种工具,她还是一种艺术,一种文化,一种哲学理念、但不是拿来炫耀得东西。 对于linux内核,学习编程,读一些优秀代码也是有必要的。 注意: 要学会举一反多,有强大的基础,很多东西简单看看就能会。 想成为合格的程序员,前提是必须熟练至少一种编程语言,并具有良好的逻辑思维。 一定要理论结合实践。 不要一味钻研技术,虽然挤出时间是很难做到的,但还是要留点余地去完善其他的爱好,比如宇宙,素描、机械、管理,心理学、游戏、科幻电影。 还有一些不愿意做但必须要做的 技术是通过编程编程在编程编出来的。 永远不要梦想一步登天,不要做浮躁的人,不要觉得路途漫上。 而是要编程编程在编程,完了在编程,在编程 等机会来了在创业(不要相信有奇迹发生,盲目创业很难成功,即便成功了发展空间也不一定很大)。 嵌入式书籍推荐 Linux基础 1、《Linux与Unix Shell 编程指南》 C语言基础 1、《C Primer Plus,5th Edition》【美】Stephen Prata着 2、《The C Programming Language, 2nd Edition》【美】Brian W. Kernighan David M. Rithie(K & R)着 3、《Advanced Programming in the UNIX Environment,2nd Edition》(APUE) 4、《嵌入式Linux应用程序开发详解》 Linux内核 1、《深入理解Linux内核》(第三版) 2、《Linux内核源代码情景分析》毛德操 胡希明著 研发方向 1、《UNIX Network Programming》(UNP) 2、《TCP\\\/IP详解》 3、《Linux内核编程》 4、《Linux设备驱动开发》(LDD) 5、《Linux高级程序设计》 杨宗德著 硬件基础 1、《ARM体系结构与编程》杜春雷着 2、S3C2410 Datasheet 英语基础 1、《计算机与通信专业英语》 系统教程 1、《嵌入式系统――体系结构、编程与设计》 2、《嵌入式系统――采用公开源代码和StrongARM\\\/Xscale处理器》毛德操 胡希明着 3、《Building Embedded Linux Systems》 4、《嵌入式ARM系统原理与实例开发》 杨宗德著 理论基础 1、《算法导论》 2、《数据结构(C语言版)》 3、《计算机组织与体系结构?性能分析》 4、《深入理解计算机系统》【美】Randal E. Bryant David O''Hallaron着 5、《操作系统:精髓与设计原理》 6、《编译原理》 7、《数据通信与计算机网络》 8、《数据压缩原理与应用》 C语言书籍推荐 1. The C programming language 《C程序设计语言》 2. Pointers on C 《C和指针》 3. C traps and pitfalls 《C陷阱与缺陷》 4. Expert C Lanuage 《专家C编程》 5. Writing Clean Code -----Microsoft Techiniques for Developing Bug-free C Programs 《编程精粹--Microsoft 编写优质无错C程序秘诀》 6. Programming Embedded Systems in C and C++ 《嵌入式系统编程》 7.《C语言嵌入式系统编程修炼》 8.《高质量C++\\\/C编程指南》林锐 尽可能多的编码,要学好C,不能只注重C本身。 算法,架构方式等都很重要。自学计算机编程应该看些什么书
为什么 iOS 有那么多优秀的开源组件,而 Android 反而很少
大家玩C遇到那种喜欢又来走去吸人的pf和C怎么防
一道经典的面试题:如何从N个数中选出最大(小)的n个数
如何自学成为数据分析师



