[TDD磕算法] 排排队,吃果果(一)失败尝试

本文是[TDD磕算法] 我为什么尝试用TDD解算法题系列的一篇。

题目

原谅我标题党了,其实是这次解的题目很难一言两语概括出来。这里我尝试解释解释。

假定一队人排成一列
每个人向前看,如果看到前面有1个比自己高、或者和自己同样高的人。就举个牌子报数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上看到,编号406
https://leetcode.com/problems/queue-reconstruction-by-height

初步分析

初看起来题目非常适合TDD解决。排序的逻辑似乎有点复杂,但是应该比较容易一步步从简单到复杂演进。比如先从最简单的情况开始,队列里只有一个人,可以直接返回。两个人的话有两种情况……
另外这道题目不禁让我想起了Combined Number这道Kata。同样也是实现排序,排序的逻辑有些绕。最终可以很漂亮的把问题的几个方面分离开解决掉。

关于Combined Number,可以在这里看到更详细的练习和讨论。
http://cyber-dojo.org/kata/edit/FCDDF1?avatar=zebra
https://groups.google.com/forum/#!topic/agileshanghai/d6cy6tCXHA8

失败尝试

大致想想之后就开始信心满满的TDD起来了,果不其然失败了……
全过程见 http://cyber-dojo.org/review/show/8922CE128B?avatar=ray&was_tag=1&now_tag=1
大体历程如下

第一个测试

只有一个人的话,必然前面没有更高的人。

reconstruct_should_be([[3,0]], [[3,0]]);

reconstruct_should_be的第一个参数是输入,第二个是预期的输出。

实现代码,输入什么就返回什么。

function reconstrutQueue(people) {
  return people;
}
第二个测试

两个人,有一个前面有更高的人,另一个没有。

reconstruct_should_be([[2,1],[3,0]], [[3,0],[2,1]]);

实现代码,按前面人数排序。

function reconstructQueue(people) {
  return people.sort( (p1, p2) => p1[1] -p2[1]);
}

稍作重构,抽取常量

return people.sort( (p1, p2) => p1[PRE] -p2[PRE]);
第三个测试

两个人,前面都没有更高的人。也就是说是低的在前。

reconstruct_should_be([[2,0],[3,0]], [[2,0],[3,0]]);

直接通过了。

第四个测试

三个人,从低到高排。

reconstruct_should_be([[2,0],[3,0],[1,0]], [[1,0],[2,0],[3,0]]);

实现代码,如果前面人数一样,按高度排序。

  return people.sort( (p1, p2) => {
    if (p1[PRE] === p2[PRE]) return p1[0] - p2[0]
    return p1[PRE] -p2[PRE]
  });
第五个测试

三个人,中等高度的排在最高的后面。

reconstruct_should_be([[2,1],[3,0],[1,0]], [[1,0],[3,0],[2,1]]);

又一次直接通过了。

第六个测试

三个人,最矮的排在中间。

reconstruct_should_be([[2,0],[3,0],[1,1]], [[2,0],[1,1],[3,0]]);

实现代码,排序方式改为先按高度排列,相同高度的按前面人数排列。这样三个人就会排成[1,1],[2,0],[3,0]。

  let result = people.sort( (p1, p2) => {
    if (p1[VAL] === p2[VAL]) { 
      return p1[PRE] - p2[PRE];
    }
    return p1[VAL] - p2[VAL];
  });

在排序之后增加一个调整顺序的过程。硬编码快速通过,把前面有1个人的项目向后挪一位。也就是[1,1]挪到[2,0]后面。

  for (let i=0; i<result.length; i++) {
    if (result[i][PRE] === 1) {
      let temp = result[i];
      result[i] = result[i+1];
      result[i+1] = temp;
      i++;
    }
  }
第七个测试

三个人,最矮的排在最后。

reconstruct_should_be([[2,0],[3,0],[1,2]], [[2,0],[3,0],[1,2]]);

实现代码,增加第二个硬编码分支,处理有2个人在前面的情况。

  for (let i=0; i<result.length; i++) {
    if (result[i][PRE] === 1) {
    ...
    }
    if (result[i][PRE] === 2) {
      let temp = result.splice(i,1)[0];
      result.splice(i+2,0,temp);
      i+=2;
    }
  }

重构,把两个分支改写成相同的逻辑。

    if (result[i][PRE] === 1) {
      let temp = result.splice(i,1)[0];
      result.splice(i+1,0,temp);
      i+=1;
    }
    if (result[i][PRE] === 2) {
      let temp = result.splice(i,1)[0];
      result.splice(i+2,0,temp);
      i+=2;
    }

重构,统一为一个分支。

    if (sortedByVal[i][PRE] > 0) {
      let nLater = sortedByVal[i][PRE];
      let temp = sortedByVal.splice(i,1)[0];
      sortedByVal.splice(i+nLater,0,temp);
      i+=nLater;
    }
第八个测试

最高的人在前面,后面的两个按高度排。

reconstruct_should_be([[2,1],[3,0],[1,1]], [[3,0],[1,1],[2,1]]);

实现代码,呃…… 就这么掉坑里了。
首先是没有很容易的快速通过的思路。
反复几次后发现在排好的数组里挪动的方式,把一个人向后挪了之后,换来的人可能也需要后移。
所以决定不在原来的数组里挪动位置,而是全部插入一个新数组。插入时比较新数组里的位置和这个人前面人的个数,直到所有的人都插入新数组为止。

function reorder(sortedByVal) {
  let result = [];
  let length = sortedByVal.length;
  for (let i=0; i<length; i++) {
    for (let j=0; j<sortedByVal.length; j++) {
      if (sortedByVal[j][PRE] <= i) {
        result.push(sortedByVal[j]);
        sortedByVal.splice(j,1);
        break;
      }
    }
  }
  return result;
}

但是发现这时候[ [ 1, 0 ], [ 3, 0 ], [ 2, 1 ] ]这个测试又失败了。
经过一段思索无法继续后,我觉得就算通过苦思冥想把这个题解出来,也已经和TDD无关了。就此停止了这次练习。

感想

从来没有尝试过仔细描述一段失败的过程。不知道你读到这里是和感受?

  • “简直是浪费时间嘛”?
  • “早就知道这样做不靠谱”?
  • “看起来还有救啊,怎么就失败了”?
  • “太菜了,让我来”?

这里说说掉坑的心得。怎么知道自己掉坑了。

  • 最终爬不出来的点很容易判定。突然发现自己思考了10分钟,还是没有写代码。
  • 顾此失彼,写了一段代码快速通过当前测试,结果前面的测试挂了。
  • 感觉前面重构出的结构在阻碍新的用例快速通过。
  • 一个早期征兆是想不清楚下一个测试是什么?比如这个过程中两次直接通过的测试,都代表了对当前程序的行为没有理解清楚。
  • 测试想不清楚的背后是没有想出逐步分解问题的思路,进而加入测试的顺序可能不是适宜代码演进。导致重构时对实现进行重大调整。比如第六步和最后一步。

下一篇文章会总结我在反思之后重新解决的过程,敬请期待。

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

推荐阅读更多精彩内容