`
wocsok
  • 浏览: 10785 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

从一道笔试题谈算法优化(上)(转载)

阅读更多
因为受到经济危机的影响,我在 bokee.com 的博客可能随时出现无法访问的情况;因此将2005年到2006年间在 bokee.com 撰写的博客文章全部迁移到 csdn 博客中来,本文正是其中一篇迁移的文章。



声明:本文最初发表于《电脑编程技巧与维护》2006年第5期,版本所有,如蒙转载,敬请连此声明一起转载,否则追究侵权责任。

从一道笔试题谈算法优化(上)
作者:赖勇浩(http://blog.csdn.net/lanphaday)

引子
每年十一月各大IT公司都不约而同、争后恐后地到各大高校进行全国巡回招聘。与此同时,网上也开始出现大量笔试面试题;网上流传的题目往往都很精巧,既能让考查基础知识,又在平淡中隐含了广阔的天地供优秀学生驰骋。

这两天在网上淘到一道笔试题目(注1),虽然真假未知,但的确是道好题,题目如下:

从10亿个浮点数中找出最大的1万个。

这是一道似易实难的题目,一般同学最容易中的陷阱就是没有重视这个“亿”字。因为有10亿个单精度浮点数元素的数组在32位平台上已经达到3.7GB之巨,在常见计算机平台(如Win32)上声明一个这样的数组将导致堆栈溢出。正确的解决方法是分治法,比如每次处理100万个数,然后再综合起来。不过这不是本文要讨论的主旨,所以本文把上题的10亿改为1亿,把浮点数改为整数,这样可以直接地完成这个问题,有利于清晰地讨论相关算法的优化(注2)。

不假思索
拿到这道题,马上就会想到的方法是建立一个数组把1亿个数装起来,然后用for循环遍历这个数组,找出最大的1万个数来。原因很简单,因为如果要找出最大的那个数,就是这样解决的;而找最大的1万个数,只是重复1万遍而已。

template< class T >

void solution_1( T BigArr[], T ResArr[] )

{

for( int i = 0; i < RES_ARR_SIZE; ++i )

{

int idx = i;

for( int j = i+1; j < BIG_ARR_SIZE; ++j )

{

if( BigArr[j] > BigArr[idx] )

idx = j;

}

ResArr[i] = BigArr[idx];

std::swap( BigArr[idx], BigArr[i] );

}

}

设BIG_ARR_SIZE = 1亿,RES_ARR_SIZE = 1万,运行以上算法已经超过40分钟(注3),远远超过我们的可接受范围。

稍作思考
从上面的代码可以看出跟SelectSort算法的核心代码是一样的。因为SelectSort是一个O(n^2)的算法(solution_1的时间复杂度为O(n*m),因为solution_1没有将整个大数组全部排序),而我们又知道排序算法可以优化到O(nlogn),那们是否可以从这方面入手使用更快的排序算法如MergeSor、QuickSort呢?但这些算法都不具备从大至小选择最大的N个数的功能,因此只有将1亿个数按从大到小用QuickSort排序,然后提取最前面的1万个。

template< class T, class I >

void solution_2( T BigArr[], T ResArr[] )

{

std::sort( BigArr, BigArr + BIG_ARR_SIZE, std::greater_equal() );

memcpy( ResArr, BigArr, sizeof(T) * RES_ARR_SIZE );

}

因为STL里的sort算法使用的是QuickSort,在这里直接拿来用了,是因为不想写一个写一个众人皆知的QuickSort代码来占篇幅(而且STL的sort高度优化、速度快)。

对solution_2进行测试,运行时间是32秒,约为solution_1的1.5%的时间,已经取得了几何数量级的进展。

深入思考
压抑住兴奋回头再仔细看看solution_2,你将发现一个大问题,那就是在solution_2里所有的元素都排序了!而事实上只需找出最大的1万个即可,我们不是做了很多无用功吗?应该怎么样来消除这些无用功?

如果你一时没有头绪,那就让我慢慢引导你。首先,发掘一个事实:如果这个大数组本身已经按从大到小有序,那么数组的前1万个元素就是结果;然后,可以假设这个大数组已经从大到小有序,并将前1万个元素放到结果数组;再次,事实上这结果数组里放的未必是最大的一万个,因此需要将前1万个数字后续的元素跟结果数组的最小的元素比较,如果所有后续的元素都比结果数组的最小元素还小,那结果数组就是想要的结果,如果某一后续的元素比结果数组的最小元素大,那就用它替换结果数组里最小的数字;最后,遍历完大数组,得到的结果数组就是想要的结果了。

template< class T >

void solution_3( T BigArr[], T ResArr[] )

{

//取最前面的一万个

memcpy( ResArr, BigArr, sizeof(T) * RES_ARR_SIZE );

//标记是否发生过交换

bool bExchanged = true;

//遍历后续的元素

for( int i = RES_ARR_SIZE; i < BIG_ARR_SIZE; ++i )

{

int idx;

//如果上一轮发生过交换

if( bExchanged )

{

//找出ResArr中最小的元素

int j;

for( idx = 0, j = 1; j < RES_ARR_SIZE; ++j )

{

if( ResArr[idx] > ResArr[j] )

idx = j;

}

}

//这个后续元素比ResArr中最小的元素大,则替换。

if( BigArr[i] > ResArr[idx] )

{

bExchanged = true;

ResArr[idx] = BigArr[i];

}

else

bExchanged = false;

}

}

上面的代码使用了一个布尔变量bExchanged标记是否发生过交换,这是一个前文没有谈到的优化手段——用以标记元素交换的状态,可以大大减少查找ResArr中最小元素的次数。也对solution_3进行测试一下,结果用时2.0秒左右(不使用bExchanged则高达32分钟),远小于solution_2的用时。

深思熟虑
在进入下一步优化之前,分析一下solution_3的成功之处。第一、solution_3的算法只遍历大数组一次,即它是一个O(n)的算法,而solution_1是O(n*m)的算法,solution_2是O(nlogn)的算法,可见它在本质上有着天然的优越性;第二、在solution_3中引入了bExchanged这一标志变量,从测试数据可见引入bExchanged减少了约99.99%的时间,这是一个非常大的成功。

上面这段话绝非仅仅说明了solution_3的优点,更重要的是把solution_3的主要矛盾摆上了桌面——为什么一个O(n)的算法效率会跟O(n*m)的算法差不多(不使用bExchanged)?为什么使用了bExchanged能够减少99.99%的时间?带着这两个问题再次审视solution_3的代码,发现bExchanged的引入实际上减少了如下代码段的执行次数:

for( idx = 0, j = 1; j < RES_ARR_SIZE; ++j )

{

if( ResArr[idx] > ResArr[j] )

idx = j;

}

上面的代码段即是查找ResArr中最小元素的算法,分析它可知这是一个O(n)的算法,到此时就水落石出了!原来虽然solution_3是一个O(n)的算法,但因为内部使用的查找最小元素的算法也是O(n)的算法,所以就退化为O(n*m)的算法了。难怪不使用bExchanged使用的时间跟solution_1差不多;这也从反面证明了solution_3被上面的这一代码段导致性能退化。使用了bExchanged之后因为减少了很多查找最小元素的代码段执行,所以能够节省99.99%的时间!

至此可知元凶就是查找最小元素的代码段,但查找最小元素是必不可少的操作,在这个两难的情况下该怎么去优化呢?答案就是保持结果数组(即ResArr)有序,那样的话最小的元素总是最后一个,从而省去查找最小元素的时间,解决上面的问题。但这也引入了一个新的问题:保持数组有序的插入算法的时间复杂度是O(n)的,虽然在这个问题里插入的数次比例较小,但因为基数太大(1亿),这一开销仍然会令本方案得不偿失。

难道就没有办法了吗?记得小学解应用题时老师教导过我们如果解题没有思路,那就多读几遍题目。再次审题,注意到题目并没有要求找到的最大的1万个数要有序(注4),这意味着可以通过如下算法来解决:

1) 将BigArr的前1万个元素复制到ResArr并用QuickSort使ResArr有序,并定义变量MinElemIdx保存最小元素的索引,并定义变量ZoneBeginIdx保存可能发生交换的区域的最小索引;

2) 遍历BigArr其它的元素,如果某一元素比ResArr最小元素小,则将ResArr中MinElemIdx指向的元素替换,如果ZoneBeginIdx == MinElemIdx则扩展ZoneBeginIdx;

3) 重新在ZoneBeginIdx至RES_ARR_SIZE元素段中寻找最小元素,并用MinElemIdx保存其它索引;

4) 重复2)直至遍历完所有BigArr的元素。

依上算法,写代码如下:

template< class T, class I >

void solution_4( T BigArr[], T ResArr[] )

{

//取最前面的一万个

memcpy( ResArr, BigArr, sizeof(T) * RES_ARR_SIZE );

//排序

std::sort( ResArr, ResArr + RES_ARR_SIZE, std::greater_equal() );

//最小元素索引

unsigned int MinElemIdx = RES_ARR_SIZE - 1;

//可能产生交换的区域的最小索引

unsigned int ZoneBeginIdx = MinElemIdx;

//遍历后续的元素

for( unsigned int i = RES_ARR_SIZE; i < BIG_ARR_SIZE; ++i )

{

//这个后续元素比ResArr中最小的元素大,则替换。

if( BigArr[i] > ResArr[MinElemIdx] )

{

ResArr[MinElemIdx] = BigArr[i];

if( MinElemIdx == ZoneBeginIdx )

--ZoneBeginIdx;

//查找最小元素

unsigned int idx = ZoneBeginIdx;

unsigned int j = idx + 1;

for( ; j < RES_ARR_SIZE; ++j )

{

if( ResArr[idx] > ResArr[j] )

idx = j;

}

MinElemIdx = idx;

}

}

}

经过测试,同样情况下solution_4用时约1.8秒,较solution_3效率略高,总算不负一番努力。



待续……
分享到:
评论

相关推荐

    C++面试题笔试题C++ 数据结构算法笔试题资料合集.zip

    C++面试题笔试题C++ 数据结构算法笔试题资料合集: 50个C、C++面试题.pdf C++ 数据结构、算法笔试题.docx C++基础面试题.docx C++开发工程师面试题库.docx C++技能测试试卷一及答案.docx C++技能测试试卷二及答案....

    2019雷达算法工程师笔试题

    2019年的这份笔试题涵盖了多个相关的知识点,下面我会详细解释。 首先,电磁场与电磁波的知识在这个笔试题中占比不多,但这是雷达技术的基础。电磁波的传播特性、极化效应以及电磁波与物体的相互作用等基础概念,对...

    海康威视笔试视音频算法笔试题

    不骗人,整理的一套试卷的全部题目~2017年哒

    java笔试常见的算法题

    全排序、二分查找、冒泡排序、阶乘、最大公约数、最小公倍数、...这是里面包含的算法,本人在准备笔试的时候找的,算法尽量采用最优的。 所有的代码均经过测试,个人觉得没有问题,如果哪位大牛找到错误,欢迎批评指正

    2015-9-虹软校招内推笔试题-算法岗

    【虹软公司与算法工程师岗位】 ...综上所述,虹软公司的算法工程师笔试题不仅测试了候选人的基础知识,还考察了他们的实际应用能力和创新能力。对于准备此类考试的求职者来说,全面复习并不断实践是提升竞争力的关键。

    大华股份2014年嵌入式软件、算法类笔试题

    【大华股份2014年嵌入式软件、算法类笔试题】是针对该公司当年招聘过程中的一个重要环节,这是一场集技术性与专业性于一体的考试,旨在考察应聘者在嵌入式系统开发和算法设计方面的综合能力。2014年的笔试题目相比...

    DJI大疆2019年8月雷达算法工程师笔试题B卷

    以下是大疆2019年8月雷达算法工程师笔试题的知识点详细解读。 首先,“DJI大疆2019年8月雷达算法工程师笔试题B卷”这一标题说明这是一次面向特定职位(雷达算法工程师)的招聘考试。大疆(DJI)是一家专门从事民用...

    华为OD、大厂笔试算法题

    华为OD、大厂笔试算法题; 一共87题,每一题附答案(java语言),笔试时频繁出现的原题,想进大厂的小伙伴,欢迎下载; eg: 1、5键键盘的输出 有一个特殊的5键键盘,上面有a,ctrl-c,ctrl-x,ctrl-v,ctrl-a五个键...

    java笔试算法题40道

    - **问题描述**:一对兔子从第三个月开始每个月都会产下一对新兔子,而新生的兔子会在第四个月开始也每个月产下一对兔子。假设所有兔子都不会死亡,求每个月的兔子总数。 - **解决方法**:此问题可以用递归或迭代的...

    常用算法笔试题软件工程师

    常用算法笔试题软件工程师 在软件开发中,算法笔试题是一个非常重要的部分。今天,我们将讨论一些常用的算法笔试题,包括选择排序、直接插入排序和冒泡排序等。 稳定排序和非稳定排序 在排序算法中,稳定排序和非...

    2015虹软校招笔试题-算法岗

    2015年9月虹软校招内推笔试题-算法工程师岗位

    阿里巴巴2018人工智能算法岗笔试题

    新鲜出炉的阿里巴巴笔试题,今年人工智能两个答题全部都是NLP

    BAT iOS 算法笔试题集合

    在准备BAT(百度、阿里巴巴、腾讯)这样的中国顶级互联网公司的...总之,这个“BAT iOS算法笔试题集合”是一个宝贵的资源,它涵盖了面试中可能遇到的各种问题,通过深入学习和实践,你将更有信心面对大厂的面试挑战。

    web前端笔试题面试题汇总+前端优化总结

    web前端笔试题面试题汇总+前端优化总结 web前端笔试题面试题汇总+前端优化总结 web前端笔试题面试题汇总+前端优化总结 web前端笔试题面试题汇总+前端优化总结 web前端笔试题面试题汇总+前端优化总结 web前端笔试题...

    嵌入式软件笔试题合集.zip

    嵌入式软件笔试题合集嵌入式软件笔试题合集嵌入式软件笔试题合集嵌入式软件笔试题合集嵌入式软件笔试题合集嵌入式软件笔试题合集嵌入式软件笔试题合集嵌入式软件笔试题合集嵌入式软件笔试题合集嵌入式软件笔试题合集...

    一道网易笔试算法题

    12-02-28网易笔试一道算法题,附件代码是我自己的解题

    阿里巴巴最新算法工程师笔试题.pdf

    阿里巴巴最新算法工程师笔试题.pdf 本资源包含了阿里巴巴最新的算法工程师笔试题,涵盖了算法、数据结构、计算机系统、概率论、统计学等多个领域。以下是对每道题目的解释和知识点总结: 1. 程序输出结果: 知识...

    笔试面试算法题文档.zip

    笔试面试算法题文档.zip 笔试面试算法题文档.zip 笔试面试算法题文档.zip 笔试面试算法题文档.zip 笔试面试算法题文档.zip 笔试面试算法题文档.zip 笔试面试算法题文档.zip 笔试面试算法题文档.zip 笔试面试算法题...

Global site tag (gtag.js) - Google Analytics