blog icon indicating copy to clipboard operation
blog copied to clipboard

算法之美

Open lmk123 opened this issue 10 years ago • 5 comments

现在是凌晨三点三十八分 :)

本来我一点钟的时候是准备睡的,然而正准备关灯的时候,我一不小心瞟到了角落的《数据结构与算法 JavaScript 描述》(下面简称《算法》)。

这本书刚买回来时,我简单翻了一下前两章,然后就后悔买这本书了:书上讲的都是很基础的内容,第一章完全就是给 JavaScript 新手看的,第二章讲了 Array 的一些方法,而这些内容我早在《JavaScript 权威指南》里面就看过了。

这也是这本书一直被我放在角落的原因。

为了避免自己去睡觉(明明很困但就是不想睡),我翻了翻这本书,发现第二章有四个习题,粗略看了看也不难,正好拿来清醒一下大脑。

前三题没什么难度,轻松完成。但是第四题却让我意识到,这本书可能没我想像中的那么简单:

创建这样一个对象,它将字母存储在数组中,并且用一个方法可以将字母连在一起,显示成一个单词。

我先试着理解了一下题目。它似乎是要实现这样一个函数,我们假设它叫 word

word( 'o', 'n' ); // 输出单词 no
word( 's' , 'y' , 'e' ); // 输出单词 yes
word( 'j', 'v', 'a', 'a' ); // 输出单词 java
word( 'c', 'i', 's', 't', 'p', 'r' ); // 输出单词 script

这道题跟前面三题、跟前两章给 js 新手看的内容根本不是一个级别啊!!

按照我的思路,我首先得列出输入的这几个字母所能组合成的所有字符串,然后再验证它是不是一个有效的单词。例如上面的 ['s','y','e'] 可以组合成:

sye sey
yse yes
esy eys

而其中只有 yes 是英文单词。

……好难!我只是一个前端,在工作中从来没碰到这样的场景啊!!但这却激起了我的兴趣,我的瞌睡一下子就没了。

为了方便我理解字母组合成单词的过程,我专门画了一张图: 思路 (我知道这图很 low,将就看一下吧 - -)

如图所示,单词的组建过程其实就是一个树状结构。即使我知道了这一点,可是我怎么知道它所有的路径呢?

我尝试从字母个数与组合结果这两个数据之间寻找联系。我注意到这么个规律:

字母的个数       组合结果
  1              1
  2              2
  3              6
  4             24

虽然我算不出其中的规律,但是我总觉得这种结构在哪见过,我记得《JavaScript 语言精粹》里面好像讲到过一个斐什么那什么的数列,而这本《算法》好像也在哪提到过。往书的前面翻了翻,果然,它在第 1.2.7 节里讲解“递归”的时候用到了这个函数(虽然书上没说这个函数跟一个数列有关,但函数的名字我还是记得的):

function factorial(number) {
  if (number == 1) {
    return number;
  } else {
    return number * factorial(number -1);
  }
}

由此可以算出,当字母的个数有 5 个时,最终组合出来的结果有 1 * 2 * 3 * 4 * 5 = 120 个。

可是知道这一点对我没有任何帮助,我需要一个遍历这颗“树”的方法,从而得到最终这 120 个组合结果。

我把这棵树从“分层”的角度来看,每一层都要比上一层少一个节点,而这一层就是由从上一层里面剩余的几个节点组成的(我知道很绕口)。根据这个想法,半小时后,我的第一版出来了:

// 复制(或将类数组转换为真)数组的辅助函数
function copyArr( arr ) {
    return [].slice.call( arr , 0 );
}

function list() {
    var args   = copyArr( arguments ) ,
        length = args.length ,
        tree   = [];

    for ( var i = 0 ; i < length ; i += 1 ) {
        var layer = [];
        layer.push( args[ i ] ); // 依次抽出元素
        args.forEach( function ( v , index ) {
            if ( i !== index ) {
                layer.push( v ); // 将剩余元素加到层中
            }
        } );
        tree.push( layer );
    }
    console.log( tree );
    return tree;
}

很明显这是不对的……执行 list( 's', 'y', 'e') 之后,控制台只输出了三个结果:

[ [ 's', 'y', 'e' ], [ 'y', 's', 'e' ], [ 'e', 's', 'y' ] ]

苦思冥想一番,发现真是没什么头绪,就像一头狼看到了一个刺猬,不知道从哪里下嘴。终于,半个小时过后我放弃了,把书往旁边一扔,关灯睡觉。

我躺下之后,脑海里仍然还想着上面那幅图,突然大脑里灵光一闪:当我躺下来的时候,那幅图也跟着逆时针旋转了 90 度,原本的树结构变成了很容易理解的“对象”。例如 [ 's', 'y', 'e' ] 的样子变成了下面这样:

{
  s:{
     y:{
        e:{} // sye
     },
     e:{
         y:{} // sey
      }
  }
  // ...下面类似,不再列出
}

从这个角度看的话,思路一下就明了了:我只需要一层层生成这棵树就可以了!就如上面所说,下一层比上一层少一个元素,所以我需要一个将某个元素从数组中抽离的方法,而一层层的生成树肯定要用到递归!

我一下子从床上弹了起来,赶紧重新开了电脑(关闭了还没半分钟),然后写下了第二版:

// 将 arr 中的 element 抽离,并返回抽离之后的新数组
function spliceFormArr( arr , element ) {
    var i = arr.indexOf( element ) ,
        c;
    if ( i >= 0 ) {
        c = copyArr( arr );
        c.splice( i , 1 );
    }
    return c;
}

function list() {
    var args = copyArr( arguments ) ,
        root = {};

    subTree( root , args );
    console.log( root );
    return root;

    // 生成子树的方法
    function subTree( root , arr ) {
        // 当子树里最终一个节点都没有的时候,这里就不会执行 forEach 了
        arr.forEach( function ( v , i , a ) {
            // 为子树上的每一个节点创建一个对象,并在下一层中抽离父节点
            subTree( root[ v ] = {} , spliceFormArr( a , v ) );
        } );
    }
}

信心满满的执行了 list( 's', 'y', 'e'),结果如下: 运算结果 真是太棒了!!关键点攻破之后,重复字母、性能优化、单词匹配等问题也就不难了——一步步解开一个题目的感觉真是太好了!!

(第二天我发现我高兴的太早了,即使我生成了这棵树,我还是不知道怎么拿到最终的组合结果……)

我一下子喜欢上了这本书。

这是我第一次深刻理解到“算法”两字的含义。

小插曲

解完题后,我意犹未尽,谷歌了一下“算法”,在知乎上看到一个人将《算法导论》里 99% 的题目做完了,结果却因为是本科生,所以被人认为只是纸上谈兵而找不到好工作的故事。

我真正被作者感动的是他在评论里留下的一句话:“我会变强的,哪天变得足够强,以至于有人愿意为我一个人破例,以至于这些规则不再成为束缚。"

lmk123 avatar Apr 30 '15 20:04 lmk123

你真的是专科生?太厉害了~!!!!我也是

punkmonday avatar Dec 12 '16 02:12 punkmonday

牛~

micookie avatar Apr 05 '17 11:04 micookie

全排列生成算法有兴趣了解一下

QwertyJack avatar Jul 26 '18 06:07 QwertyJack

动态规划

AsJoy avatar Aug 27 '18 03:08 AsJoy

👍

Donkey-Kurt avatar Nov 24 '23 03:11 Donkey-Kurt