Skip to content

算法之美 #12

Open
Open
@lmk123

Description

@lmk123

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

本来我一点钟的时候是准备睡的,然而正准备关灯的时候,我一不小心瞟到了角落的《数据结构与算法 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% 的题目做完了,结果却因为是本科生,所以被人认为只是纸上谈兵而找不到好工作的故事。

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

Activity

micookie

micookie commented on Apr 5, 2017

@micookie

牛~

QwertyJack

QwertyJack commented on Jul 26, 2018

@QwertyJack

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

AsJoy

AsJoy commented on Aug 27, 2018

@AsJoy

动态规划

Donkey-Kurt

Donkey-Kurt commented on Nov 24, 2023

@Donkey-Kurt

👍

Repository owner deleted a comment from punkmonday on Nov 24, 2023
SoftwareEngineerPalace

SoftwareEngineerPalace commented on Feb 16, 2024

@SoftwareEngineerPalace

用回溯来实现, leetcode 有原题

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @lmk123@SoftwareEngineerPalace@QwertyJack@AsJoy@micookie

        Issue actions

          算法之美 · Issue #12 · lmk123/blog