高效算法实践:基于TDD的排队问题解析与优化技巧(第三部分)

更新:10-28 神话故事 我要投稿 纠错 投诉

大家好,今天来为大家解答高效算法实践:基于TDD的排队问题解析与优化技巧(第三部分)这个问题的一些问题点,包括也一样很多人还不知道,因此呢,今天就来为大家分析分析,现在让我们一起来看看吧!如果解决了您的问题,还望您关注下本站哦,谢谢~

假设一群人排队

每个人看到比自己高或者和自己一样高的人都会期待。只要举起一个牌子,数1。同样,如果有3,则报告3。

如果列出每个人的身高和人数,就会形成两对的队列,如:

[(172, 0), (160, 1), (182, 0), (170, 2)]

意思是第一人身高172cm,前面没有人比他高或者等于他;第二个人160cm,前面还有一个;……

这道题是,给定一个已打乱的序列,将它们按原来的顺序排列。

例如:

输入:[ (7,2), (4,3), (8,0), (7,1), (6,0)]

输出:[ (6,0), (8,0), (7,1), (4,3), (7,2)]

性能分析

leetcode上的题不仅会测试函数是否正确,还会专门准备几个比较大的测试数据来测试性能。虽然不是很严格的性能测试,但是足以说明时间复杂度的高低。

虽然上一篇文章解决了这个问题,但是该程序的性能非常差。有多糟糕?

击败所有JavaScript 提交的0%。换句话说,没有比它更慢的解决方案了。我很惊讶它没有被称为暂停。

我们简单分析一下慢在哪里。

这个方案如果用文字来形容的话,就是按照前面个子高的人的数量来排列这些人。从前面0 人的队列开始,加入新队列,然后是前面1 人的队列,然后是前面2 人的队列,依此类推.

每次添加某人时,从头开始数队列中的人数不少于他,直到他前面的人数超过他应有的人数,然后加入到你数的人前面。

下面以[ (7,2), (2,0), (4,3), (3,2), (8,0), (7,1), (6,0) ] 为例显示添加的进程, |指示插入位置。

(2,0)=[ |]

(8,0)=[ (2,0) |]

(6,0)=[ (2,0), |(8,0) ]

(7,1)=[ (2,0), (6,0), (8,0) |]

(7,2)=[ (2,0), (6,0), (8,0), (7,1) |]

(3,2)=[ (2,0), (6,0), (8,0), |(7,1), (7,2)]

(4,3)=[ (2,0), (6,0), (8,0), (3,2), (7,1), |(7,2)]

[(2,0), (6,0), (8,0), (3,2), (7,1), (4,3), (7,2)] 可以看出时间消费有两个方面:

每次向队列中添加某人时,都需要遍历队列中已有的人。这应该是O(n2) 复杂度。我的算法基础不好,如有错误请指正。重排后的队列长度其实是已知的,但这个程序并不知道。不断插入新元素会导致数组容量调整,插入点之后的所有现有元素都会更新。有一个明显的优化。查找插入点时,不需要数到[x,n]的第n+1个位置。只要能保证插入顺序,算n个人就够了。

经验证,这种改变可以提高一些性能,但仍然比大多数解决方案慢。因为它只是让每次遍历搜索更快,但遍历次数不变,原始时间复杂度不变。

如果想要有更好的性能,就需要从头开始实现这个调整顺序的过程。

优化解求解过程

根据前面的分析,要让整个过程更快,就要避免每次都遍历队列中的人。另外,最好一次性加人到最终的位置,而不是每次加人都要调整很多人的位置。

基于这两点以及前两次尝试的经验。总体思路如下:

首先,将它们从低到高逐一添加。这确保了已经加入队列的人低于即将加入队列的人。其次,根据入队者前面个子高的人数,调整队列的位置,为后面个子高的人留出几个位置。

我还没有想清楚怎么做,我打算在做的过程中慢慢弄清楚。打磨和实现的整个过程参见:http://cyber-dojo.org/review/show/8922CE128B?avatar=toucanwas_tag=1now_tag=1

第一个和第二个测试与最后一个练习相同。前面有一个人,也有两个人。这里跳过它。

第三个测试

根据上次的经验,主要区别是前面的人数较多。排队的总人数并不是最重要的。

因此这次测试的选择:三个人,中间最低的一个。引入一项需要1人向后调整1个位置的改变。

reconstruct_should_be([[8,0],[2,1],[3,0]],[[3,0],[2,1],[8,0]]);还是先通过硬编码的测试。

if (已排序[0][1]==1) {

返回[[3,0],[2,1],[8,0]];

之后和上一个练习类似,通过一大段重构建立了处理[x,1]的逻辑(步骤17)。这里就跳过这个过程了。

最终代码如下。

函数重新排序(已排序){

让结果=[];

令电流=0;

让toBeFilled=0;

for (让i=0; i 排序.length; i ++) {

如果(待填充0){

让inFrontIndex=当前- toBeFilled - 1;

结果[inFrontIndex]=排序[i];

待填充——;

} 别的{

toBeFilled=排序[i][IN_FRONT];

如果(待填充0){

当前+=待填充;

}

结果[当前++]=排序[i];

}

}

返回结果;

}总体思路是,如果是[x,0],就依次添加新数组即可。

如果遇到[x,1],则将下一个位置添加到当前位置,注意有1个位置需要由后面的人填写。这是代码中的else 分支。

添加下一个人时,如果发现有空位需要填补,请将其添加到之前空出的座位中。这就是if 分支。

第四个测试

三人,最高的在前,后面的两人按身高顺序。

reconstruct_should_be([[3,1],[8,0],[2,1]],[[8,0],[2,1],[3,1]]);运行测试,[2,1]留有一个位置空着,但是下面的[3,1]被错误地添加到第一个位置。所以需要添加if分支。

实现代码

这时我突然发现,通过测试并不容易,再次感受到了第一次失败尝试时代码结构与测试方向的不匹配。

仔细想了想,我发现之前重构的时候写的代码太多了。当时的测试只有[x,1]的例子,也就是说一次最多需要留下一个位置。代码中使用了一个整数toBeFilled来记录需要填充的数量,因为猜测是要处理多个位置需要填充的情况。而且这个逻辑还没有经过检验。

因此,通过重构,用一个变量来记录这个保留位置。去掉多个位置的逻辑。

让toBeFilled=null;

for (让p 排序) {

if (toBeFilled !==null p[IN_FRONT]===0) {

结果[待填充]=p;

待填充=null;

} 别的{

if (toBeFilled===null p[IN_FRONT]===1 ) {

待填充=当前;

当前+=p[IN_FRONT];

}

结果[当前++]=p;

}

}

第五个测试

这一步我犹豫了。

首先,我尝试了[4,0]、[2,1]、[8,0]、[5,1]的序列,这是两个[x,1]没有排列在一起的情况。发现现有的逻辑已经支持了。

本来想后面加上[x, 2]的测试,但是想了想,觉得先测试两个人身高相等的情况会更小。

一共有三个人,两个身高一样的人在前面,最高的一个在后面。

reconstruct_should_be([[3,0],[8,0],[3,1]],[[3,0],[3,1],[8,0]]);实现代码

[3,1]需要在[3,0]之前添加到队列中,这样它所保留的空间就会被[3,0]填满。否则,根据代码逻辑,后面的[8,0]将会被填充到[3,1]之前的位置。

因此修改一开始对数组进行排序的比较方法。相反,首先按高度排序。如果身高相等,则按照前面的人数降序排列。

让排序=peoples.sort(compare_in_height_and_reverse_infront);

.

函数compare_in_height_and_reverse_infront(p1,p2){

if (p1[高度]===p2[高度]) {

返回p2[IN_FRONT] - p1[IN_FRONT];

} 别的{

返回p1[高度] - p2[高度];

}

}

第六个测试

添加前面两个人的示例。

一队三人,最低的人排在最后,前面的两个人按身高顺序排列。

reconstruct_should_be([[2,2],[8,0],[3,0]],[[3,0],[8,0],[2,2]]);实现代码

现在您可能需要记录2个保留位置并将toBeFilled更改为数组。

让toBeFilled=[];

for (让p 排序) {

if (toBeFilled.length 0 p[IN_FRONT]===0) {

让fillIndex=toBeFilled.pop();

结果[填充索引]=p;

} 别的{

if (toBeFilled.length===0 p[IN_FRONT]===1 ) {

toBeFilled=[当前];

当前+=p[IN_FRONT];

}

if (toBeFilled.length===0 p[IN_FRONT]===2 ) {

toBeFilled=[当前+1, 当前];

当前+=p[IN_FRONT];

}

结果[当前++]=p;

}

}重构并统一两个分支[x,1]和[x,2]的代码。

.

} 别的{

if (toBeFilled.length===0 p[IN_FRONT] 0 ) {

toBeFilled=reverseRange(当前,p[IN_FRONT]);

当前+=p[IN_FRONT];

}

结果[当前++]=p;

其中,reverseRange返回以当前索引为最后一个元素的反向数组,如reverseRange(3,1)===[3]、reverseRange(3,2)===[4,3]。

倒序的原因是我觉得在填充保留空间时从数组末尾删除元素成本更低。

这个功能的实现很简单,这里就不贴出来了。

在第七次失败的测试之前,添加了另一个测试来测试两个[x, 2]连接在一起的情况。发现测试直接成功,和之前处理连续[x,1]的情况一样。

第七个测试

reconstruct_should_be([[2,2],[8,0],[4,2],[3,0],[9,0]],

[[3,0],[8,0],[2,2],[9,0],[4,2]]);本次测试与上一次测试的不同之处在于,添加[2,2]时保留了2个空格。空,空,[2,2]

之后[3,0]就填满了1。 [3,0],空,[2,2]

下一个是[4,2]。这时,除了[2,2]前面剩余的空间外,还需要再留一个空间。 [3,0],空,[2,2],空,[4,2]

实现代码

最好先添加一个特殊情况分支。添加[x, 2]时,如果当前只有一个保留位置,则将当前位置添加到保留位置上。

if (toBeFilled.length===0 p[IN_FRONT] 0 ) {

. }

if (toBeFilled.length===1 p[IN_FRONT]===2) {

toBeFilled.unshift(当前);

当前+=1;

}重构以统一添加保留位置的两个分支。

让skip=p[IN_FRONT] - toBeFilled.length;

如果(跳过0){

toBeFilled=reverseRange(当前,跳过).concat(toBeFilled);

当前+=跳过;

}

结果[当前++]=p;

}

第八个测试

添加有[x,2]和[x,1]的情况

reconstruct_should_be([[2,2],[8,0],[3,1]],[[8,0],[3,1],[2,2]]);为了实现代码,首先将其硬编码,当[2,2]保留1,2个位置时,[3,1]应该选择第二个位置而不是第一个位置。

if (toBeFilled.length 0 p[IN_FRONT]===0) {

让fillIndex=toBeFilled.pop();

结果[填充索引]=p;

} else if (toBeFilled.length===2 p[IN_FRONT]===1){

让fillIndex=toBeFilled.shift();

结果[填充索引]=p;

} else { .重构以统一填充保留位置的两个分支。

if (toBeFilled.length p[IN_FRONT]) {

让skipInFill=-1 - p[IN_FRONT];

让fillIndex=toBeFilled.splice(skipInFill, 1)[0];

结果[填充索引]=p;

} else { .

重构,简化程序

我认为在填充保留位置时,我们还需要处理更多的情况,比如生成保留位置的分支。

结果添加了一些测试后,确认已经完成了。

此时可以推断,对于任意队列,比如

[8,0],[3,1],[2,2],

我可以在它后面添加一个0 高度的元素,然后变成

[8,0]、[3,1]、[2,2]、[0,3]。

那么程序排列的时候,除了附加的最小的[0,3]之外,其余元素按照填充保留位置的逻辑,也能排列出正确的顺序。换句话说,生成保留位置的分支是多余的。

重构后,调整顺序函数变为

函数重新排序(已排序){

让结果=[];

让toBeFilled=range(sorted.length);

for (让p 排序) {

结果[popAt(toBeFilled, p[IN_FRONT])]=p;

}

返回结果;

}这种重构会稍微增加操作toBeFilled 数组的空间复杂度和开销。但相对于程序逻辑的简化来说,这是非常值得的。

同样,我也将toBeFilled 数组从倒序更改为正序。性能没有太大变化,但更容易理解。

您还可以稍后进行重构以将toBeFilled 包装到类中。提供popAt(n)函数。效果如下。

保留.popAt(0) //0

保留.popAt(0) //1

保留.popAt(2) //4

保留.popAt(0) //2

保留.popAt(0) //3

served.popAt(0) //5类内部不能使用数组,而是使用一些内部状态变量来生成相应的序列。这样使用数组的性能损失也是可以弥补的。

不过这和主线剧情关系不大,所以本次练习到此结束。

这次求解时间减少到原来的1/3。 leetcode上的提交率超过66%,终于算是一个比较正常的解决方案了。

关于高效算法实践:基于TDD的排队问题解析与优化技巧(第三部分)的内容到此结束,希望对大家有所帮助。

用户评论

服从

这个标题看得我热血沸腾!终于要来优化效率了!

    有20位网友表示赞同!

清原

TDD磕算法总是那么神奇!每次都能够带来新的惊喜。

    有14位网友表示赞同!

我绝版了i

终于可以告别那些低效的算法了!

    有17位网友表示赞同!

巷口酒肆

排队吃果果?这听起来很有趣呀,一定很能体现效率提升的效果吧。

    有7位网友表示赞同!

◆残留德花瓣

期待看看作者是如何用TDD来优化算法的。这部分我最感兴趣!

    有10位网友表示赞同!

水波映月

三部曲完结篇? 好期待!

    有6位网友表示赞同!

余温散尽ぺ

我一直在关注这个系列文章,终于等到更新了!

    有6位网友表示赞同!

ゞ香草可樂ゞ草莓布丁

作者的文笔总是很清晰易懂,相信这次仍然会是一篇很棒的文章。

    有7位网友表示赞同!

在哪跌倒こ就在哪躺下

效率是所有程序员共同追求的目标吧!这篇文章让我充满了期待。

    有14位网友表示赞同!

心安i

希望这篇博客能够教会我一些提升算法效率的小技巧。

    有10位网友表示赞同!

落花忆梦

TDD简直就是算法开发的福音!

    有5位网友表示赞同!

↘▂_倥絔

吃果果的场景很有意思,不知道作者会怎么用它来解释算法优化?

    有13位网友表示赞同!

不要冷战i

学习算法本来就很枯燥,但这个标题看起来却充满趣味性。

    有11位网友表示赞同!

花花世界总是那么虚伪﹌

希望能看到一些具体的代码案例,这样更容易理解。

    有15位网友表示赞同!

灬一抹丶苍白

对TDD磕算法的应用还有很多疑问,希望作者能够详细解答。

    有12位网友表示赞同!

不浪漫罪名

效率优化是一个非常重要的课题,期待看到这篇文章的深入解析。

    有12位网友表示赞同!

百合的盛世恋

希望文章能涵盖多种类型的算法优化方法。

    有14位网友表示赞同!

淡抹丶悲伤

学习新技术需要不断地实践和应用,期待本文能够带来新的启发。

    有5位网友表示赞同!

最迷人的危险

算法优化是一个循序渐进的过程,相信这篇博客能够帮助我更好地理解这个过程。

    有13位网友表示赞同!

無極卍盜

分享这种优化思路很有益处,希望能看到更多优秀的案例分享!

    有7位网友表示赞同!

【高效算法实践:基于TDD的排队问题解析与优化技巧(第三部分)】相关文章:

1.蛤蟆讨媳妇【哈尼族民间故事】

2.米颠拜石

3.王羲之临池学书

4.清代敢于创新的“浓墨宰相”——刘墉

5.“巧取豪夺”的由来--米芾逸事

6.荒唐洁癖 惜砚如身(米芾逸事)

7.拜石为兄--米芾逸事

8.郑板桥轶事十则

9.王献之被公主抢亲后的悲惨人生

10.史上真实张三丰:在棺材中竟神奇复活

上一篇:《孤独深处:成长的印记,独自行走的旅程》 下一篇:经典名著《月牙儿》分析:深入探讨对比与意象的艺术魅力