问题:

1 、 为什么离开现在的公司

应届生。

2 、 你最满意和最不满意的任务是什么?为什么?

让人厌烦的机械化工作。(以及让人厌烦的写不出脚本自动化这些破事的自己)

机械化,就是说,花了时间就有结果。不能体现我的聪明才智。==

3 、 你的爱好是什么?

把不错的电影一遍一遍看,提取出音频一遍遍听。

4 、 有 github / bitbucket 等账号优先考虑

GitHub:district10

技术问题:

以思路为主,简洁易懂。

1 、 八皇后问题(你要真从网上抄,我也没治不是么)

才复习了这个。

首先不能同行,那就直接用 C[i]i=[0,7),每个 i 都不一样)来存储每一行的列的位置。比如 C[2]=5 代表皇后点位为 (3, 6)

不能同列,可以用循环检测是否和其他皇后冲突。

不能同斜线,可以用循环检测是否和其他皇后冲突。两种情况:

  • dx == dy
  • dx == -dy

优化 1:把 8 皇后的放置看成一个过程,第一个皇后,随意放,第二个皇后,只要和第一个皇后不冲突,随意放,以此,八个皇后都放上了,就是一个 valid 的情况。

这样冲突的判断不必要和全部其他皇后检测,只需要看之前的皇后。

优化 2:列、主对角线、副对角线编码,存起来,占了一个坑,就把那个坑标记好。

这样,就不用通过循环来判断是否冲突了。只要检测一个标志位。

这个标志位设计方案怎么都可以,比如像这样:

    主对角线                                            副对角线

  y - x    ?dx == dy                                y + x    ? dx == -dy (dx+dy==0)
      +----------------------------------> y            +-----------------------------------> y
      |  0   1   2   3   4   5   6   7                  |  0   1   2   3   4   5   6   7
      | -1   0   1   2   3   4   5   6                  |  1   2   3   4   5   6   7   8
      | -2  -1   0   1   2   3   4   5                  |  2   3   4   5   6   7   8   9
      | -3  -2  -1   0   1   2   3   4                  |  3   4   5   6   7   8   9  10
      | -4  -3  -2  -1   0   1   2   3                  |  4   5   6   7   8   9  10  11
      | -5  -4  -3  -2  -1   0   1   2                  |  5   6   7   8   9  10  11  12
    x | -6  -5  -4  -3  -2  -1   0   1               x  |  6   7   8   9  10  11  12  13
      | -7  -6  -5  -4  -3  -2  -1   0                  |  7   8   9  10  11  12  13  14
      V                                                 V

代码如下:

void search( int cur ) {
    if( cur == n ) {
        ++tot;
        // 打印结果的代码放在这里
    } else {
        for( int i = 0; i < n; ++i ) {
            // vis: visited, vis[0] -> col, vis[1] -> minor diag, vis[2] -> major diag
            if( !vis[0][i] && !vis[1][cur+i] && !vis[2][cur-i+n] ) {
                C[cur] = i;
                // col         y-x=0           y+x=0
                vis[0][i] = vis[1][cur+i] = vis[2][cur-i+n] = 1;
                search( cur+1 );
                vis[0][i] = vis[1][cur+i] = vis[2][cur-i+n] = 0;    // 改回来
            }
        }
    }
}

其中 vis 是一个二维数组,vis[0][i] 表示第 i 列是否被占坑(1 是有人,0 是没人), vis[1][i] 表示副对角线(上面右图), vis[2][i] 表示主对角线(上面左图,因为 cur-i 可能为负(图中下三角部分),所以,要加上 n=8 )。

如果想看看每个可能情况的放置,在上面 // 打印结果的代码放在这里 处加上:

printf( "solve #%d:", tot );
for( int i = 0; i < n; ++i ) {
    printf( " %d", C[i] );
}
printf( "\n" );

输出如下:

solve #1: 0 4 7 5 2 6 1 3
solve #2: 0 5 7 2 6 3 1 4
solve #3: 0 6 3 5 7 1 4 2
solve #4: 0 6 4 7 1 3 5 2
...
solve #89: 7 1 3 0 6 4 2 5
solve #90: 7 1 4 2 0 6 3 5
solve #91: 7 2 0 5 1 4 6 3
solve #92: 7 3 0 2 5 1 6 4

或者,你要一个视觉化的表示,上面的代码可以这么来:

printf( "-- Solve #%03d --\n", tot );
for( int i = 0; i < n; ++i ) {
    printf( "+-+-+-+-+-+-+-+-+\n" );
    for( int j = 0; j < n; ++j ) {
        printf( "|%c", C[i] == j ? 'X' : ' ' );
    }
    printf( "|\n" );
}
printf( "-----------------\n\n" );

打印出来就是这样:(如果看得不够“方”,那是字宽和行高的问题,就怪不得我了)

-- Solve #092 --
+-+-+-+-+-+-+-+-+
| | | | | | | |X|
+-+-+-+-+-+-+-+-+
| | | |X| | | | |
+-+-+-+-+-+-+-+-+
|X| | | | | | | |
+-+-+-+-+-+-+-+-+
| | |X| | | | | |
+-+-+-+-+-+-+-+-+
| | | | | |X| | |
+-+-+-+-+-+-+-+-+
| |X| | | | | | |
+-+-+-+-+-+-+-+-+
| | | | | | |X| |
+-+-+-+-+-+-+-+-+
| | | | |X| | | |
-----------------
2 、 给定一个正整数,判断这个正整数是否是质数。请给出代码或者伪代码

质数只能靠筛选(它就这么定义的,也没有人发现什么别的特性可以替代这个定义,还能加速判断)。

唯一的优化就是,你要判断 n 是否是质数,你不用从 1 到 n-1 一个一个判断他们是否互质,只要判断到 sqrt(n) 就可以了。这里到底是 sqrt(n) 还是 sqrt(n)-1,sqrt(n)+1,sqrt(n-1),sqrt(n) + 1?对于这种 off-by-one 的潜在 bug,我们分析一下:

看看 n = 9,sqrt(9) = 3:

  • 首先,我们要判断 3,所以 sqrt(n)-1、sqrt(n-1) 错;
  • 其次,我们不用判断 4,因为 32 == 9,4 比 3 大,如果有个数乘以 4 等于 9,那它小于 3,那我们已经判断过了啊!所以没必要 sqrt(n)+1;

这么分析起来是不是有点意思,但很累?实际情况下,我都是 trail-and-error 的。只有这么几种情况,一个个试呗。而且,我们把代码放松点。不优化那么“严肃”,问题就简单多了。代码如下

bool isPrime( int n ) {
    int ceil = sqrt(n);
    for( int i = 2; i <= ceil; ++i ) {
        if( 0 == n%i ) { return false; }
    }
    return true;
}

可能的优化:

  1. 用一个表,直接从里面读取结果(是不是质数)。适合于需要判断很多次,又不担心内存的情况;
  2. 每次计算后把结果保存起来。适合于可能对一些数字要经常判断,又不想占用很多内存的情况;

如果你计算能力绰绰有余,要什么优化。

3 、 现在有一个双向链表,每一个 node 的结构如下,{ name: 唯一标识, left: leftNode, right: rightNode }

请用代码或者伪代码描述,给定一个 name ,和一个双向链表 dlink ,找到这个对应的结点并删除。

我还没有复习到双向链表。假设你这么定义的 dlink:

typedef node dlink; // 然后用 leftNode 指向头,rightNode 指向尾

我画个示意图,大致如下(天知道我为什么这么喜欢画 ascii 示意图):

首节点的 left 指向自己,末节点的 right 指向自己。
dlink 的 left 指向首节点,如果没有 node,就指向自己。right 情况类似。

                                           +-----------+
                                           |           |
     +-------+       +--------+      +-----v--+        |
+----> node  +-------> node   +------>  node  +--------+
^ +--+       <-------+        <------+        <--------+
| |  +-^-----+       +--------+      +--------+        |
| |    |                                               |
| +----+                                               |
|                                                      |
|                                                      |
|   +---------+                                        |
+---+ dlink   +----------------------------------------+
    |         |
    +---------+

那我们的函数接口为 bool delNode( dlink *dl, char *name ),大概可以写成这样:

bool delNode( dlink *dl, char *name ) {

    if( !dl || !name ) { return false; }

    node *n = dl->left;
    while( n && strcmp(n->name, name) != 0 ) {
        if( n->right == n ) { return false; }
        n = n->right;
    }
    if( !n ) { return false; }

    // 现在 n 就是我们要删除的节点了,那就把它删掉吧
    node *pre = n->left;
    node *next = n->right;
    // 根据我们的定义,left 和 right 都不可能为 null
    // 但实际编码的过程中,我还是 assert 一下
    assert( pre && next && "this line won't fail.\n" );

    if( pre == n ) {    // 这是第一个节点
        dl->left = next;
        next->left = next;
        free( n ); // free( pre )
        return true;
    }

    if( next == n ) {   // 这是最后一个节点
        pre->right = pre;
        dl->right = pre;
        free( n ); // free( next )
        return true;
    }

    // 走到这一步,说明这是一个中庸的节点
    pre->right = next;
    next->left = pre;
    free( n );
    return true;
}
4 、 请简述用户从浏览器浏览一个网页背后数据流转的流程

既然说数据流转,我就不说什么 DNS 查询的部分了。

对于用户而言,数据主要是网页的 html、js 以及 css 文件。

访问一个 url,如果以 blahblah.html 结尾,那就会

  • 如果 cache 里有这个网页而且没有过期,那就直接用本地的;
  • 否则,请求这个网页。数据便从服务器那段传过来,可能是一个静态页面,服务器直接回传;也可能是一个动态生成的页面,需要临时生成一个页面。

现在用户拿到了 html,然后浏览器解析它,发现里面有引入别的 css 文件,js 文件,以及图片等等。于是浏览器再看,是否已经下载了?是否过期?

能用本地的就用本地的,不能就发出请求去获取。

于是浏览器一边获取数据,一边显示了出来。

5 、 请简述 git 多人协作的流程

首先明确 git 是一个非中心的版本控制系统。所以每个 repo(repository)的副本(姑且这么称呼),都是包含完整的历史信息(除非你 clone 的时候用了 --depth 1 之类的东西)。

但非中心的方式协作起来不方便。因为你要知道别人的副本在哪个服务器(当然也可以是本机),哪个目录。所以通常都有一个中心化的位置,大家一起在这里存取。

以 GitHub 为例,新建一个 repo 后,克隆到本地,本地的 master 分支和 GitHub 上的 origin/master 联系了起来。

每次修改前,要先 pull(或者你喜欢手动 fetch 然后 merge),然后本地修改,然后提交到 origin/master,以和其他人同步。 pull 了之后可能有冲突,要解决 conflicts。解决好了没问题,再 push 到 GitHub。

如果大家都往 master 分支同步,可能会把这个分支搞得很乱。所以我们都是新建一个 dev 分支,搞定了一些事情,再切到 master 分支,把 dev 分支的内容 merge 进来。

对于一些小的 bug fix,或者新的 feature,也最好从 dev 分支 checkout 一个新的小分支,处理完了再合并到 dev。

git 是没有权限控制的。所以 GitHub 上只有一个团队的人才能直接修改 origin 上的 branches,其他人要提交 PR(Pull Requests)。


[北京][15k-18k] 创业公司求 Java 开发工程师,基于微信平台。我们可以给到的:广阔的发展空间,独当一面的机会,我全部的技术相关经验。 - V2EX

写完了发现你们要的是 Java 工程师……好吧,我只能说:毕业之前我可以学得不错的。

我也曾瞟过一本英文的 Pro Java8。