赌5毛钱,你解不出这道Google面试题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/dQCFKyQDXYm3F8rB0/article/details/90185792

640?wx_fmt=png


作者 | Kevin Ghadyani

译者 | 清儿爸

编辑 | Rachel

出品 | AI科技大本营(ID:rgznai100)


为了更了解其他人对软件工程的看法,我开始疯狂在 YouTube 上追 TechLead 的视频。在接下来的几天里,我为他在 Google 工作时提出的一道面试题想出了各种解决方案。


通过 TechLead 模拟 Google 面试(软件工程师职位)


TechLead 在 Google 的 100 多次面试中都提出了一个问题,这引起了我对 RxJS 的兴趣。本文会讨论解决该问题的所有传统方法。


他问这个问题的真正目的是从应聘者得到下列信息:在编码之前,他们会问正确的问题吗?提出的解决方案是否符合项目指南?他甚至指出,是否得到正确的答案一点都不重要,重要的是应聘者的思考方式,以及应聘者是否能够理解这个问题。


他谈到了一些解决方案,包括递归方法(受堆栈大小限制)和迭代方法(受内存大小限制)。本文将对这两个解决方案进行详细讨论。


TechLead 的问题


在 TechLead 的问题中,他要求应聘者在如下网格中,计算出所有颜色相同的最大连续块的数量。


640?wx_fmt=jpeg

640?wx_fmt=jpeg       


当看到这个问题时,我的第一反应是,必须做一些 2D 图像建模才能解决这个问题。听起来这道题在面试中几乎不可能回答出来。


但在听完他的详细解释之后,我方知情况并非如此。在这个问题中,我们需要处理的是已经捕获的数据,而不是解析图像。


数据建模


在编写任何代码之前都需要定义数据模型。对于任何问题,首先要弄清楚我们在处理什么,并收集业务需求。


在我们案例中,TechLead 为我们定义了许多具体的需求,例如:


  • 彩色方块或“节点”的概念

  • 数据集中包含 1 万个节点

  • 节点被组织成行和列,即二维数据

  • 列数和行数可能不同

  • 节点有颜色信息,并具有对“邻接”这一概念的表示方式


我们还可以从数据中获得更多信息:


  • 节点不会重叠

  • 节点不会和其自身邻接

  • 节点不会有重复的邻接

  • 位于边角的节点会比其他节点少一个或两个邻接


还有一些未知信息,例如:


  • 行数与列数的比

  • 可能的颜色数量

  • 只有一种颜色的可能性

  • 颜色的大致分布


开发人员的水平越高,其需要问的问题越多。虽然这有所帮助,但如果不能找出未知信息,问题的实际解决还是会存在阻碍。


大部分人并不会想到询问这些未知信息。在开始研究这个算法之前,我也不知道这些未知信息是什么。要找到所有的未知信息,需要与业务人员进行反复的讨论才行。


对于 TechLead 的这张照片来说,颜色的分布似乎是随机的。他只用了三种颜色,并且没有提到其他限制,因此我们暂时也做这种假设。另外我们还假设,这些颜色可能是相同的。


为了保证算法的有效性,因此我假设我们使用的是 100x100 的网格,以避免处理1行10000列这样的极端情况。


在一般情况下,我会在查看数据的最初几个小时内询问所有这些问题。这也是 TechLead 真正关心之处。应聘者需要思考,是要从编写一个随机解决方案开始,还是要首先找出问题所在。如果提前计划的话,这些问题将更容易处理。在解决这些问题之后,我们最终只需重写代码的一小部分即可。


创建数据模型


我们需要知道数据是如何输入的,以及我们希望以何种形式来处理这些数据。由于没有处理数据的系统,因此我们需要自己设计一个可视化的方法。


数据的基本结构如下:

  • Color

  • ID

  • X

  • Y


需要 ID 的原因在于,我们可能不止一次碰到同一个图片格。要想防止无限循环的话,就必须标记在这些情况下该图片格所处的位置。


此外,像这样的数据通常会分配某些 ID、哈希值或其他值。它是一个唯一的标识符,因此,我们可以通过某种方式来标识特定的节点。如果我们想知道最大的连续块,就需要知道该块中有哪些节点。


由于 TechLead 使用网格对数据进标识,我假设我们会得到 X 和 Y 的值。依靠这些属性,我就能够生成一些 HTML,并确保生成的内容与他给我们的内容相类似。


这是使用绝对定位来完成的,就像他的例子一样:


640?wx_fmt=jpeg      

答案:3


这种方法也可以处理更大一些的数据集,如下图:


640?wx_fmt=jpeg

答案:18


下面是生成节点的代码:


 
  
 1const generateNodes = ({ 2  numberOfColumns, 3  numberOfRows, 4}) => ( 5  Array( 6    numberOfColumns 7    * numberOfRows 8  ) 9  .fill()10  .map((11    item,12    index,13  ) => ({14    colorId: (15      Math16      .floor(17        Math.random() * 318      )19    ),20    id: index,21    x: index % numberOfColumns,22    y: Math.floor(index / numberOfColumns),23  }))24)const generateNodes = ({
2  numberOfColumns,
3  numberOfRows,
4}) => (
5  Array(
6    numberOfColumns
7    * numberOfRows
8  )
9  .fill()
10  .map((
11    item,
12    index,
13  ) => ({
14    colorId: (
15      Math
16      .floor(
17        Math.random() * 3
18      )
19    ),
20    id: index,
21    x: index % numberOfColumns,
22    yMath.floor(index / numberOfColumns),
23  }))
24)


我们使用行列信息创建一个一维数组,然后根据这些数据生成节点。


我用的是 colorId 而不是 color 。这样做有两个原因,一是随机化更为简洁,二是我们通常必须自己查找颜色值。


虽然 TechLead 没有明确说明,但该题目只用了 3 个颜色值,因此,我将数据集限制为 3 种颜色。我们只需知道它可能有数百种颜色,最终的算法就不需要改变了。


下面是一个更简单的例子,这是一个 2x2 的节点列表:


 
  
1[2  { colorId: 2, id: 0, x: 0, y: 0 },3  { colorId: 1, id: 1, x: 1, y: 0 },4  { colorId: 0, id: 2, x: 0, y: 1 },5  { colorId: 1, id: 3, x: 1, y: 1 },6]
2  { colorId: 2id0, x: 0, y: 0 },
3  { colorId: 1id1, x: 1, y: 0 },
4  { colorId: 0id2, x: 0, y: 1 },
5  { colorId: 1id3, x: 1, y: 1 },
6]


数据处理


我们希望知道每个节点的邻接关系,但仅靠 X 和 Y 的值无法做到。所以,给定 X 和 Y,我们还需要找出如何找出相邻的 X 和 Y 值。其实很简单,我们只需在 X 和 Y 上找到 +1 和 -1 的节点即可。


我为此写了一个函数:


 
  
 1const getNodeAtLocation = ({ 2  nodes, 3  x: requiredX, 4  y: requiredY, 5}) => ( 6  ( 7    nodes 8    .find(({ 9      x,10      y,11    }) => (12      x === requiredX13      && y === requiredY14    ))15    || {}16  )17  .id18)const getNodeAtLocation = ({
2  nodes,
3  x: requiredX,
4  y: requiredY,
5}) => (
6  (
7    nodes
8    .find(({
9      x,
10      y,
11    }) => (
12      x === requiredX
13      && y === requiredY
14    ))
15    || {}
16  )
17  .id
18)


我们用来生成节点的方式,实际上是一种计算相邻节点 ID 的数学方法。而在这一步中,我将采取一个与之相反的思路,即假设节点将以随机顺序输入。


我通过再次遍历所有节点来添加邻接关系:


 
  
 1const addAdjacencies = ( 2  nodes, 3) => ( 4  nodes 5  .map(({ 6    colorId, 7    id, 8    x, 9    y,10  }) => ({11    color: colors[colorId],12    eastId: (13      getNodeAtLocation({14        nodes,15        x: x + 1,16        y,17      })18    ),19    id,20    northId: (21      getNodeAtLocation({22        nodes,23        x,24        y: y - 1,25      })26    ),27    southId: (28      getNodeAtLocation({29        nodes,30        x,31        y: y + 1,32      })33    ),34    westId: (35      getNodeAtLocation({36        nodes,37        x: x - 1,38        y,39      })40    ),41  }))42  .map(({43    color,44    id,45    eastId,46    northId,47    southId,48    westId,49  }) => ({50    adjacentIds: (51      [52        eastId,53        northId,54        southId,55        westId,56      ]57      .filter((58        adjacentId,59      ) => (60        adjacentId !== undefined61      ))62    ),63    color,64    id,65  }))66)const addAdjacencies = (
2  nodes,
3) => (
4  nodes
5  .map(({
6    colorId,
7    id,
8    x,
9    y,
10  }) => ({
11    color: colors[colorId],
12    eastId: (
13      getNodeAtLocation({
14        nodes,
15        x: x + 1,
16        y,
17      })
18    ),
19    id,
20    northId: (
21      getNodeAtLocation({
22        nodes,
23        x,
24        y: y - 1,
25      })
26    ),
27    southId: (
28      getNodeAtLocation({
29        nodes,
30        x,
31        y: y + 1,
32      })
33    ),
34    westId: (
35      getNodeAtLocation({
36        nodes,
37        x: x - 1,
38        y,
39      })
40    ),
41  }))
42  .map(({
43    color,
44    id,
45    eastId,
46    northId,
47    southId,
48    westId,
49  }) => ({
50    adjacentIds: (
51      [
52        eastId,
53        northId,
54        southId,
55        westId,
56      ]
57      .filter((
58        adjacentId,
59      ) => (
60        adjacentId !== undefined
61      ))
62    ),
63    color,
64    id,
65  }))
66)


这个预处理代码中,我尽量避免了任何不必要的优化。它不会影响算法的最终性能,只会有助于简化我们的算法。


接下来,我将 colorId 换成 color 。这对于我们的算法而言其实没有必要,这一步只是为了更好的可视化。


我们为每组相邻的 X 和 Y 值调用 getNodeAtLocation 函数,并找到我们的 northId 、  eastId 、 southId 和 westId 。在此步骤中,我们不会对 X 和 Y 的值进行参数传递。


获取基本 ID 之后,再将它们转换为一个 adjacentIds 数组,这个数组只包含那些具有值的邻接数组。如此一来,如果我们有边角的话,就不用担心检查这些 ID 是不是为空。它还允许我们对数组进行循环,而无需在算法中手工记录每个基本 ID。


下面是另一个 2x2 网格的示例,这里我们使用了一组新的节点,并通过 addAdjacencies 来运行:


 
  
1[2  { adjacentIds: [ 1, 2 ], color: 'red',  id: 0 },3  { adjacentIds: [ 3, 0 ], color: 'grey', id: 1 },4  { adjacentIds: [ 3, 0 ], color: 'blue', id: 2 },5  { adjacentIds: [ 1, 2 ], color: 'blue', id: 3 },6]
2  { adjacentIds: [ 1, 2 
], color: 'red',  id: 0 },
3  { adjacentIds: [ 30 ], color: 'grey', id: 1 },
4  { adjacentIds: [ 30 ], color: 'blue', id: 2 },
5  { adjacentIds: [ 12 ], color: 'blue', id: 3 },
6]


优化预处理过程


为了简化本文的算法,我添加了另一个优化过程。该算法将删除与当前节点颜色不匹配的相邻 ID。


重写 addAdjacencies 函数,如下:


 
  
 1const addAdjacencies = ( 2  nodes, 3) => ( 4  nodes 5  .map(({ 6    colorId, 7    id, 8    x, 9    y,10  }) => ({11    adjacentIds: (12      nodes13      .filter(({14        x: adjacentX,15        y: adjacentY,16      }) => (17        adjacentX === x + 118        && adjacentY === y19        || (20          adjacentX === x - 121          && adjacentY === y22        )23        || (24          adjacentX === x25          && adjacentY === y + 126        )27        || (28          adjacentX === x29          && adjacentY === y - 130        )31      ))32      .filter(({33        colorId: adjacentColorId,34      }) => (35        adjacentColorId36        === colorId37      ))38      .map(({39        id,40      }) => (41        id42      ))43    ),44    color: colors[colorId],45    id,46  }))47  .filter(({48    adjacentIds,49  }) => (50    adjacentIds51    .length > 052  ))53)
2  nodes,
3) => (
4  nodes
5  .map(({
6    colorId,
7    id,
8    x,
9    y,
10  }) => ({
11    adjacentIds: (
12      nodes
13      .filter(({
14        x: adjacentX,
15        y: adjacentY,
16      }) => (
17        adjacentX === x + 1
18        && adjacentY === y
19        || (
20          adjacentX === x - 1
21          && adjacentY === y
22        )
23        || (
24          adjacentX === x
25          && adjacentY === y + 1
26        )
27        || (
28          adjacentX === x
29          && adjacentY === y - 1
30        )
31      ))
32      .filter(({
33        colorId: adjacentColorId,
34      }) => (
35        adjacentColorId
36        === colorId
37      ))
38      .map(({
39        id,
40      }) => (
41        id
42      ))
43    ),
44    color: colors[colorId],
45    id,
46  }))
47  .filter(({
48    adjacentIds,
49  }) => (
50    adjacentIds
51    .length > 0
52  ))
53)


我在添加更多功能的同时简化了 addAdjacencies 。


通过删除颜色不匹配的节点,我们的算法可以 100% 确定 adjacentIds 属性中的任何 ID 都是邻接的节点。


最后,我删除了所有不具有相同颜色邻接的节点,这进一步简化了我们的算法。这样,我们就将节点缩减为只有我们关心的那些节点。


错误的方式:递归


TechLead 指出,我们无法递归地执行这个算法,因为我们会遇到堆栈溢出的问题。


虽然在一定程度上,他这么说是对的,但有几种方法可以缓解这个问题。我们可以使用迭代或者尾递归(tail recursion),但 JavaScript 不再将尾递归作为自带功能。


尽管我们仍然可以用 JavaScript 来写一个尾递归函数,但为使得算法更加简单,我仍然选择了创建一个典型的递归函数。


在编写代码之前,我们需要先找到算法。对于递归,使用深度优先搜索是合理的。“不要担心别人不明白计算机科学术语。”在我向一位同事展示我想出的不同解决方案时,他如此说道。


算法


我们将从一个节点开始,尽可能向下搜索,直到到达一个端点。然后我们将返回并采取下一个分支路径,直到我们扫描完整个连续块为止。在此过程中,我们还必须记录我们搜索过的部分,以及最大的连续块的长度。


我将函数分成了两部分。其中一个函数将保存最大列表和先前扫描的 ID,同时至少循环每个节点一次。另一个函数则将从未扫描的根节点开始,进行深度优先遍历。


代码如下所示:


 
  
 1const getContiguousIds = ({ 2  contiguousIds = [], 3  node, 4  nodes, 5}) => ( 6  node 7  .adjacentIds 8  .reduce( 9    (10      contiguousIds,11      adjacentId,12    ) => (13      contiguousIds14      .includes(adjacentId)15      ? contiguousIds16      : (17        getContiguousIds({18          contiguousIds,19          node: (20            nodes21            .find(({22              id,23            }) => (24              id25              === adjacentId26            ))27          ),28          nodes,29        })30      )31    ),32    (33      contiguousIds34      .concat(35        node36        .id37      )38    ),39  )40)const getContiguousIds = ({
2  contiguousIds = [],
3  node,
4  nodes,
5}) => (
6  node
7  .adjacentIds
8  .reduce(
9    (
10      contiguousIds,
11      adjacentId,
12    ) => (
13      contiguousIds
14      .includes(adjacentId)
15      ? contiguousIds
16      : (
17        getContiguousIds({
18          contiguousIds,
19          node: (
20            nodes
21            .find(({
22              id,
23            }) => (
24              id
25              === adjacentId
26            ))
27          ),
28          nodes,
29        })
30      )
31    ),
32    (
33      contiguousIds
34      .concat(
35        node
36        .id
37      )
38    ),
39  )
40)


 
  
41const getLargestContiguousNodes = (42  nodes,43) => (44  nodes45  .reduce(46    (47      prevState,48      node,49    ) => {50      if (51        prevState52        .scannedIds53        .includes(node.id)54      ) {55        return prevState56      }575859      const contiguousIds = (60        getContiguousIds({61          node,62          nodes,63        })64      )656667      const {68        largestContiguousIds,69        scannedIds,70      } = prevState717273      return {74        largestContiguousIds: (75          contiguousIds.length76          > largestContiguousIds.length77          ? contiguousIds78          : largestContiguousIds79        ),80        scannedIds: (81          scannedIds82          .concat(contiguousIds)83        ),84      }85    },86    {87      largestContiguousIds: [],88      scannedIds: [],89    },90  )91  .largestContiguousIdsconst getLargestContiguousNodes = (
42  nodes,
43) => (
44  nodes
45  .reduce(
46    (
47      prevState,
48      node,
49    ) => {
50      if (
51        prevState
52        .scannedIds
53        .includes(node.id)
54      ) {
55        return prevState
56      }
57
58
59      const contiguousIds = (
60        getContiguousIds({
61          node,
62          nodes,
63        })
64      )
65
66
67      const {
68        largestContiguousIds,
69        scannedIds,
70      } = prevState
71
72
73      return {
74        largestContiguousIds: (
75          contiguousIds.length
76          > largestContiguousIds.length
77          ? contiguousIds
78          : largestContiguousIds
79        ),
80        scannedIds: (
81          scannedIds
82          .concat(contiguousIds)
83        ),
84      }
85    },
86    {
87      largestContiguousIds: [],
88      scannedIds: [],
89    },
90  )
91  .largestContiguousIds


下面,我们将逐步进行分析。


递归函数


getContiguousIds 是递归函数,在每个节点调用一次。在该函数每次返回结果时,我们都会得到一个连续节点的更新列表。


这个函数只有一个判断条件:节点是否已在列表中?如果没有,则再次调用getContiguousIds 。当该函数返回结果时,我们会获得一个更新的连续节点列表,该列表会被返回到 reducer ,并用作下一个 adjacentId 的状态。


每当我们用 concat 将当前节点连接到 contiguousIds 时,都要向 contiguousIds 传入值。每次进一步递归时,我们都要确保在循环执行 adjacentIds 之前,当前节点已经被添加到 contiguousIds 列表中。这可以确保我们不会无限地递归。


循环


该函数的后半部分也会遍历每个节点一次。递归函数使用 reducer来检查代码是否已被扫描。若已被扫描,就继续循环,直到找到一个没有循环的节点,或者直到退出循环为止。


如果我们的节点尚未被扫描,则调用 getContiguousIds,并继续遍历,直到扫描完成。这是同步的,但可能需要一些时间。


每当函数返回一个 contignousIds 列表,都对照 largestContiguousIds 进行检查,如果该列表的返回值更大的话,就存储返回值。


同时,我们将把这些 contiguousIds 添加到我们的 scannedIds 列表中,以标记我们搜索的节点。


执行


就算我们有 10000 个项目,这个算法也不会遇到 3 种随机颜色的堆栈溢出问题。如果我把所有的都改成单一颜色,就可能会遇到堆栈溢出的问题,这是因为我们的递归函数经历了 10000 次的递归。


顺序迭代


由于内存比函数调用的堆栈要大,所以我的下一个想法是在一个循环中完成整个事情。我们将跟踪节点列表的列表。我们将不断添加它们,并将它们链接在一起,直到退出循环。


这个方法要求在完成循环之前,将所有可能的节点列表保存在内存中。在递归示例中,我们只将最大的列表保存在内存中。


 
  
 1const getLargestContiguousNodes = ( 2  nodes, 3) => ( 4  nodes 5  .reduce( 6    ( 7      contiguousIdsList, 8      { 9        adjacentIds,10        id,11      },12    ) => {13      const linkedContiguousIds = (14        contiguousIdsList15        .reduce(16          (17            linkedContiguousIds,18            contiguousIds,19          ) => (20            contiguousIds21            .has(id)22            ? (23              linkedContiguousIds24              .add(contiguousIds)25            )26            : linkedContiguousIds27          ),28          new Set(),29        )30      )313233      return (34        linkedContiguousIds35        .size > 036        ? (37          contiguousIdsList38          .filter((39            contiguousIds,40          ) => (41            !(42              linkedContiguousIds43              .has(contiguousIds)44            )45          ))46          .concat(47            Array48            .from(linkedContiguousIds)49            .reduce(50              (51                linkedContiguousIds,52                contiguousIds,53              ) => (54                new Set([55                  ...linkedContiguousIds,56                  ...contiguousIds,57                ])58              ),59              new Set(adjacentIds),60            )61          )62        )63        : (64          contiguousIdsList65          .concat(66            new Set([67              ...adjacentIds,68              id,69            ])70          )71        )72      )73    },74    [new Set()],75  )76  .reduce((77    largestContiguousIds = [],78    contiguousIds,79  ) => (80    contiguousIds.size81    > largestContiguousIds.size82    ? contiguousIds83    : largestContiguousIds84  ))85)const getLargestContiguousNodes = (
2  nodes,
3) => (
4  nodes
5  .reduce(
6    (
7      contiguousIdsList,
8      {
9        adjacentIds,
10        id,
11      },
12    ) => {
13      const linkedContiguousIds = (
14        contiguousIdsList
15        .reduce(
16          (
17            linkedContiguousIds,
18            contiguousIds,
19          ) => (
20            contiguousIds
21            .has(id)
22            ? (
23              linkedContiguousIds
24              .add(contiguousIds)
25            )
26            : linkedContiguousIds
27          ),
28          new Set(),
29        )
30      )
31
32
33      return (
34        linkedContiguousIds
35        .size > 0
36        ? (
37          contiguousIdsList
38          .filter((
39            contiguousIds,
40          ) => (
41            !(
42              linkedContiguousIds
43              .has(contiguousIds)
44            )
45          ))
46          .concat(
47            Array
48            .from(linkedContiguousIds)
49            .reduce(
50              (
51                linkedContiguousIds,
52                contiguousIds,
53              ) => (
54                new Set([
55                  ...linkedContiguousIds,
56                  ...contiguousIds,
57                ])
58              ),
59              new Set(adjacentIds),
60            )
61          )
62        )
63        : (
64          contiguousIdsList
65          .concat(
66            new Set([
67              ...adjacentIds,
68              id,
69            ])
70          )
71        )
72      )
73    },
74    [new Set()],
75  )
76  .reduce((
77    largestContiguousIds = [],
78    contiguousIds,
79  ) => (
80    contiguousIds.size
81    > largestContiguousIds.size
82    ? contiguousIds
83    : largestContiguousIds
84  ))
85)


另一个想法是,从顶部开始遍历,并将每个节点循环一次。到在此过程总,我们必须检查 ID 是否存在于节点列表的列表 contiguousIdsList 中。


如果它不存在于任何 contiguousIds 列表中,我们就将添加该列表和 adjacenIds 。这样,在循环时,就会有其他的内容链接到它。


如果我们的节点在其中一个列表之中,那么节点就可能也存在于其中相当多的列表中。我们想要把所有这些都链接在一起,并从 contiguousIdsList 中删除未链接的那些节点。在我们得到节点列表的列表之后,检查哪个列表是最大的,这个算法就完成了。


执行


与递归版本不同的是,当所有 10000 个项目都是相同的颜色时,这个算法能够完成任务。但该算法的一个缺陷是,它执行得相当慢。在上述代码的性能评估中,我没有考虑到循环列表的列表的情况,这显然对性能有很大的影响。


随机迭代


我想采用递归方法背后的思路,并以迭代方式进行应用。这一算法的目标是精确命中每个节点一次,并且只存储最大的连续块:


 
  
 1const getLargestContiguousNodes = ( 2  nodes, 3) => { 4  let contiguousIds = [] 5  let largestContiguousIds = [] 6  let queuedIds = [] 7  let remainingNodesIndex = 0 8 910  let remainingNodes = (11    nodes12    .slice()13  )141516  while (remainingNodesIndex < remainingNodes.length) {17    const [node] = (18      remainingNodes19      .splice(20        remainingNodesIndex,21        1,22      )23    )242526    const {27      adjacentIds,28      id,29    } = node303132    contiguousIds33    .push(id)343536    if (37      adjacentIds38      .length > 039    ) {40      queuedIds41      .push(...adjacentIds)42    }434445    if (46      queuedIds47      .length > 048    ) {49      do {50        const queuedId = (51          queuedIds52          .shift()53        )545556        remainingNodesIndex = (57          remainingNodes58          .findIndex(({59            id,60          }) => (61            id === queuedId62          ))63        )64      }65      while (66        queuedIds.length > 067        && remainingNodesIndex === -168      )69    }7071    if (72      queuedIds.length === 073      && remainingNodesIndex === -174    ) {75      if (76        contiguousIds.length77        > largestContiguousIds.length78      ) {79        largestContiguousIds = contiguousIds80      }8182      contiguousIds = []83      remainingNodesIndex = 08485      if (86        remainingNodes87        .length === 088      ) {89        break90      }91    }92  }9394  return largestContiguousIds95}9697module.exports = getLargestContiguousNodeconst getLargestContiguousNodes = (
2  nodes,
3) => {
4  let contiguousIds = []
5  let largestContiguousIds = []
6  let queuedIds = []
7  let remainingNodesIndex = 0
8
9
10  let remainingNodes = (
11    nodes
12    .slice()
13  )
14
15
16  while (remainingNodesIndex < remainingNodes.length) {
17    const [node] = (
18      remainingNodes
19      .splice(
20        remainingNodesIndex,
21        1,
22      )
23    )
24
25
26    const {
27      adjacentIds,
28      id,
29    } = node
30
31
32    contiguousIds
33    .push(id)
34
35
36    if (
37      adjacentIds
38      .length > 0
39    ) {
40      queuedIds
41      .push(...adjacentIds)
42    }
43
44
45    if (
46      queuedIds
47      .length > 0
48    ) {
49      do {
50        const queuedId = (
51          queuedIds
52          .shift()
53        )
54
55
56        remainingNodesIndex = (
57          remainingNodes
58          .findIndex(({
59            id,
60          }) => (
61            id === queuedId
62          ))
63        )
64      }
65      while (
66        queuedIds.length > 0
67        && remainingNodesIndex === -1
68      )
69    }
70
71    if (
72      queuedIds.length === 0
73      && remainingNodesIndex === -1
74    ) {
75      if (
76        contiguousIds.length
77        > largestContiguousIds.length
78      ) {
79        largestContiguousIds = contiguousIds
80      }
81
82      contiguousIds = []
83      remainingNodesIndex = 0
84
85      if (
86        remainingNodes
87        .length === 0
88      ) {
89        break
90      }
91    }
92  }
93
94  return largestContiguousIds
95}
96
97module.exports = getLargestContiguousNode


这里,我们没有将节点添加到先前扫描的 ID 列表,而是从 remainingNodes 数组中拼接出值来,但是我不建议大家这样做。


分解


我把上述代码分成 3 个部分,用 if 语句分开。


让我们从中间部分开始。首先查看 queuedIds 。如果该对象有值,就对队列中的内容进行循环,看看它们是否存在于 remainingNodes 中。


第三部分的内容取决于第二部分的结果。如果 queuedIds 对象为空,并且 remainingNodesIndex 是 -1 的话,那么我们就已经完成了这个节点列表,并需要从一个新的根节点开始。新的根节点始终位于索引 0 处,因为我们正在对 remaininigNodes 进行拼接。


现在再来看循环的顶部。我可以使用 while (true) ,但是需要留一个跳出条件,以防止出错。这在调试时很有用,因为要弄清楚无限循环可能是件痛苦的事情。


之后,我们将拼接节点。我们将节点添加到 contiguousIds 列表中,并将 adjacentIds 添加到队列中。


执行


这一算法几乎和递归版本一样快。当所有节点都是相同颜色时,它是所有算法中速度最快的。


针对数据的优化


对相似的颜色进行分组


由于我们只知道有两种蓝色,所以我们可以将类似颜色的节点分组在一起,用于顺序迭代版本。


通过将节点拆分成 3 个更小的数组,我们可以减少内存占用,以及需要在列表的列表中执行的循环次数。尽管如此,这并不能解决所有颜色都相同的情况下会出现的问题,因此我们并不会使用此方法修改递归版本。这也意味着我们可以对操作进行多线程处理,将执行时间缩短近三分之一。


如果我们按顺序执行这些命令,只需先运行三个中最大的一个。如果最大值比另外两个值大,就无需检查它们。


可能存在的最大数据集的大小


我们可以检查每一次迭代,而不是在特定时间间隔检查是否有最大的列表。如果最大节点集合的规模大于或等于可用节点的一半(5000 或更高),那么,很显然我们已经有了最大的列表。


若使用随机迭代版本的话,我们可以找到迄今为止最大的列表大小,并查看剩余的节点数量,如果没有比最大的节点集合大小还小的数值,那么就可以说明,我们已经有最大的列表了。


使用递归


虽然递归有其局限性,但我们仍可以使用它。我们需要做的事情就是检查剩余节点的数量。如果它没有超出堆栈的限制,我们就可以使用更快的递归版本。这么做的风险是很大,但随着循环的深入,这一方法会缩短执行时间。


使用 for 循环


在知道节点最大数量的情况下,我们可以使用 for 循环编写 reduce 函数。无论何时,与 for 循环相比, Aray.prototype 方法都非常慢。


使用尾递归


我没有在本文中讨论相关算法,因为我认为尾递归需要一篇单独的文章来阐述。这是一个很大的主题,很多地方都需要解释。另外,虽然它使用了递归结构,但它可能并不会想你所期望的那样比while循环还快。


RxJS:可维护性与性能


有一些方法可以重写这些函数,这样你就可以更轻松地理解并维护它们。我想出的主要解决方案是使用 Redux-Observable 风格的 RxJS,但并不使用 Redux。


接下来,我想以常规的方式来编写代码,然后使用 RxJS 流式传输数据,看看能将算法性能提升多少。


我使用 RxJS 做了 3 个版本的算法,并做了一些修改来加快执行速度。与我之前的文章不同的是,即使增加了行和列,所有的三个版本都会变慢。


我本来可以做很多优化,但要以代码的可读性为代价,这不是我想要的。


最终,我终于找到了一个可行的解决方案,该方案目前是最快的,只需一半的执行时间。这已经是总体上最好的改进了。


只有当每个节点都是相同的颜色时,我才能用可观察到的数据击败内存占用较多的顺序迭代。从技术上来讲,这一算法也优于递归方法,因为在这种情况下,递归算法会出现堆栈溢出的问题。


在研究如何使用 RxJS 流数据之后,我意识到该方法对本文来说实在过于复杂了。希望以后会有文章详细介绍这些代码示例。


如果希望查看详细代码,可以查看如下 GitHub 项目地址:


https://github.com/Sawtaytoes/JavaScript-Performance-Interview-Question


最终统计数据


一般来说,最大的连续块平均有 30~80 个节点。


下面展示了相关算法的评估数据:


随机颜色


执行时间

方法

229.481ms

递归

272.303ms

迭代随机

323.011ms

迭代序列

391.582ms

Redux-Observable 并发

686.198ms

Redux-Observable 随机

807.839ms

Redux-Observable 顺序

一种颜色

执行时间

方法

1061.028ms

迭代随机

1462.143ms

Redux-Observable 随机

1840.668ms

Redux-Observable 顺序

2541.227ms

迭代序列


无论我进行了多少次测试,每种方法的相对排名位置都保持不变。


当所有节点颜色都相同时,Redux-Observable 并发方法受到了影响,我试过很多方法尝试提高这个方法的运行速度,但是没有成功。


游戏制作


在我的职业程序员生涯中,我曾两次遇到过这段代码。其中一次是我在开发独立游戏《Pulsen》时使用 Lua 编写的代码,代码长度要小得多。


还有一次是在我绘制一张世界地图的时候,该地区有一个预定义的节点列表,我对其进行了实时处理。这使得使用者可以通过键盘上的方向键来移动世界地图。


我还为具有 X 和 Y 值的未知项列表编写了一个节点生成器。听起来是不是很熟悉?我同样需要使网格位居屏幕中央。不过,要做到这点,在 HTML 中比在游戏引擎中要更容易实现。尽管如此,将一堆绝对定位的 div 放在中央位置也并不容易。


在这个案例中,实时执行时间并不怎么很重要,因为我在加载游戏时就进行了大量的预处理。


我想强调的是,TechLead 的问题可能是你会在职业生涯中遇到的问题,但在典型的 JavaScript 应用程序中,往往不太需要考虑程序的速度。


TechLead 在 Google 使用的是 Java ,我猜他面试的职位都很关心执行速度。他们有可能有一堆工作任务要处理大量的数据,因此像这样的解决方案可能是必要的。


但是,这个视频也有可能是关于 HTML 和 CSS 的职位的,谁知道呢!


结语


正如你在最终统计数据中所看到的那样,读起来最槽糕的代码几乎是最快的,并且还完成了我们所有的要求。


据我自己的经验,我花了更长的时间来开发非 RxJS 版本的代码。我认为,这是因为更快的版本需要全面的思考。Redux-Observable 能够让你以化整为零的方式进行思考。


这是一道非常有趣的问题。它起初看起来似乎很难,但是将它分解成几块之后,问题就迎刃而解了。


原文链接:

https://medium.freecodecamp.org/bet-you-cant-solve-this-google-interview-question-4a6e5a4dc8ee


(*本文由AI科技大本营编译,转载请联系微信1092722531)


CTA核心技术及应用峰会

5月25-27日,由中国IT社区CSDN与数字经济人才发展中心联合主办的第一届CTA核心技术及应用峰会将在杭州国际博览中心隆重召开,峰会将围绕人工智能领域,邀请技术领航者,与开发者共同探讨机器学习和知识图谱的前沿研究及应用。


更多重磅嘉宾请识别海报二维码查看,目前会议预售票抢购中,点击阅读原文即刻抢购。添加小助手微信15101014297,备注“CTA”,了解票务以及会务详情。


640?wx_fmt=jpeg


推荐阅读



640?wx_fmt=png


点击阅读原文,了解CTA核心技术及应用峰会

猜你喜欢

转载自blog.csdn.net/dQCFKyQDXYm3F8rB0/article/details/90185792