平铺节点数组转嵌套树

Posted: 10.01.2019

#JS题目 #面试问题 - 算法

介绍

这道题是我碰见的,阿里笔试的原题。

当时没做出来,后来又花了一个多小时仔细钻研了一下,研究出了一个较为完美的答案。

然后把答案发给了面试官(这已经是笔试结束后的第二天了)。

面试官说“很好”。(但真的有多好我也不知道)

总之这道题的要求是这样的:

/**
 * 平铺节点数组转嵌套树
 * 说明:将一个包含深度信息的节点数组转换成一棵树,要求只能遍历一次该数组
 * 输入值:TreeNode数组 TreeNode为包含title, depth(正整数,深度不限)字段的Object
 * 输出值:组装好的嵌套树,子节点挂载在对应父节点的children字段上
 */

/*
举例 (title字段仅为便于理解,实际无固定规则)
输入:[
  { title: '1', depth: 1 },
  { title: '1-1', depth: 2 },
  { title: '1-1-1', depth: 3 },
  { title: '1-1-2', depth: 3 },
  { title: '1-2', depth: 2 },
  { title: '2', depth: 1 },
  { title: '2-1', depth: 2 },
]
输出:[
  {
    "title": "1",
    "depth": 1,
    "children": [
      {
        "title": "1-1",
        "depth": 2,
        "children": [
          {
            "title": "1-1-1",
            "depth": 3
          },
          {
            "title": "1-1-2",
            "depth": 3
          }
        ]
      },
      {
        "title": "1-2",
        "depth": 2
      }
    ]
  },
  {
    "title": "2",
    "depth": 1,
    "children": [
      {
        "title": "2-1",
        "depth": 2
      }
    ]
  }
]
*/

代码实现

我这个实现,可以应对任意顺序的 input。

就相当于是把输入的数组顺序打乱,瞎鸡儿排,我这算法都能给他nen出来。

并且我的算法可以处理任意属性名(例子中是 title,但我的算法啥都行)。

/**
 * 这道题的核心,就是把数组里的对象插入到合适的位置
 * 在这期间,要适时地添加 children 属性
 */
function arrayToTree(arr) {
  const ans = [];
  // 遍历数组
  for (let obj of arr) {
    // 对于每一个对象来说,拿到其除了 depth 以外的属性名
    let keyName = "";
    for (let key in obj) {
      if (key !== "depth") {
        keyName = key;
        break;
      }
    }

    /**
     * 通过递归来进行转换
     * 这里的 level 用来标记深度,格式就是 "title" 的内容
     */
    let level = obj[keyName];
    arrayToTreeHelper(ans, obj, keyName, level);
  }

  return ans;
}

/**
 * @params {Array} ans - 答案数组对应的对象里,应该包含当前 level 的 children
 * @params {Object} obj - input 数组里每一个单独的对象
 * @params {String} keyName - obj 里内容对应的属性名(相当于是"title")
 * @params {String} level - 用来记录当前的层数
 * @return {void}
 */
function arrayToTreeHelper(ans, obj, keyName, level) {
  /**
   * level 的长度如果是1,那么则意味着下面之一:
   * 1. 该 level 的 title 一开始就是 "1","2" 之类的
   * 2. 或者该 level 原来是 "1-2-3",然后传递到这一层,只剩下 "3"
   * 无论是那种情况,我们都需要插入到合适的位置
   */
  if (level.length === 1) {
    /**
     * 开始找插入的位置:
     * 这个时候我们已经在正确的 children 数组里了(递归到了这里)
     * 我们需要做的,就是找到我们这个 obj 在 children 数组里的位置
     * - pos 代表了 ans(children) 的 index
     * - currentTitle 代表了目前的 title
     */
    let pos = 0;
    let currentTitle = "";
    /**
     * 查看 ans(children)数组里,第一个元素的 keyName
     * 如果该 key 不是 depth,以及不是 children
     * 那么就把当前的 currentTitle 给设置成这个属性名
     * 这时候的 currentTitle 就相当于在 [2,3,4,6] 数组里按顺序插入5的时候
     * 第一个元素2,因为我们要设置一个在 [2,3,4,6] 里的标记
     * 这个 currentTitle 就相当于是记录了2,3,4,6,然后和当前的 title 比较
     */
    for (let key in ans[pos]) {
      if (key !== "depth" && key !== "children") {
        currentTitle = ans[pos][key];
        break;
      }
    }
    /**
     * 接下来开始遍历 ans,查看 要插入的元素是否比 currentTitle 大
     * 就相当于现在是 "1-2-2",在 ["1-2-1" "1-2-3"] 里找正确的插入位置
     * 可以直接用字符串来比较大小,因为 "1-2-1" < "1-2-2"
     */
    while (pos < ans.length && obj[keyName] >= currentTitle) {
      /**
       * 这个时候,假设我们所在的 children 是 ['1-2-1'],
       * 然后我们要插入 '1-2-2',我们遍历所在的 children,找到了位置 index = 1,
       * 然后就可以直接插入了,没有任何问题。
       * 
       * 但是还有一种情况,我们所在的 children 是 ['1-2-1', '1-2-2'],
       * 而我们要插入 '1-2-2',这个时候在 children 里的 '1-2-2' 就是
       * 我们在上一层新建的一个暂时的对象(这部分建议先看下面的代码,比较好理解)
       * 而这个暂时性的对象(ans[pos])是这样的
       * {
       *   children: [],
       *   title: '1-2-3'
       * }
       * 我们现在要做的,就是升级这个暂时性的对象为完整的对象
       * 然后把当前对象的属性解构出来,覆盖暂时的对象
       * children 属性则是继续沿用暂时对象的 children 属性
       */ 
      if (obj[keyName] === currentTitle) {
        ans[pos] = { children: ans[pos].children, ...obj };
        return;
      }

      // 遍历 ans, 继续比较
      pos++;
      for (let key in ans[pos]) {
        if (key !== "depth" && key !== "children") {
          currentTitle = ans[pos][key];
          break;
        }
      }
    }

    /**
     * 最后,把对象插入到正确的位置(pos 所在的位置)
     * 因为之前在暂时性对象的时候 return 了
     * 因此如果能到达这部分代码进行插入,就是正确的对象插入到正确的位置
     */
    ans.splice(pos, 0, obj);
    return;
  }

  /**
   * 刚才是已经抵达了底层的情况
   * 现在是还没到达底层的情况,就比如 "1-3-2" 剩下 "3-2"
   * 拿到当前的层数,"3-2" 的话就取3
   */
  const currentLevel = parseInt(level.charAt(0)) - 1;

  /**
   * 这个时候我们就面临了一个问题:
   * 正常的 ans 应该是这样一个数组:[
      {
        "title": "1-1-1",
        "depth": 3
      },
      {
        "title": "1-1-2",
        "depth": 3
      }
    ]
   * 但是我们很有可能先插入了 "1-1-1" 这一层,导致 "1-1-2" 还没有插入
   * ans[currentLevel] 是按照顺序排列的,如果数组里只有一个结果,
   * 而我们要插入到 index = 1 的位置上,那个位置上现在是什么东西都没有的
   * 因此我们需要创建一个暂时的对象,来占据 currentLevel 这个位置
   */
  if (ans[currentLevel] === null || ans[currentLevel] === undefined) {
    /**
     * 分配一个暂时的对象在 ans[currentLevel] 这个位置上
     * 给予这个空的对象一个 children 属性,一个空的数组(因为该路径没到底,必然有 children)
     * 并且把当前的 keyName 设置成当前的路径,例如 "1-3-2" 里的 "1-3"
     * (这里的 obj[keyName] 指向的就是完整的路径,例如 "1-3-2")
     * (而 level 则是当前的路径,例如 "1-3-2" 剩下了 "3-2")
     * 
     * 这个时候,我们的数组会变成这样:[
        {
          "title": "1-1-1",
          "depth": 3
        },
        {
          "mytitle": "1-1-2",
          "children": []
        }
      ]
     * 注意,这个时候还没有插入 depth 属性,因为这只是一个暂时的对象
     */
    ans[currentLevel] = { children: [] };
    ans[currentLevel][keyName] = obj[keyName].slice(
      0,
      obj[keyName].length - level.length + 1
    );
  }

  /**
   * 这个时候,当我们到这里的时候,我们可以保证:
   * ans[currentLevel] 有一个对象
   * 这个对象可能已经正确地存在,或者只是一个暂时的对象
   * 
   * 而在这里还要检查 children 属性,如果没有的话添加这个属性的目的
   * 是因为我刚才已经说了,如果我们执行了这段代码,就说明我们不在最后一层
   * 因此,children 是必然存在的
   * 对于刚刚创建的暂时的对象来说,children 属性已经设置好了
   * 但是对于那些普通的对象来说,children 属性还没有添加,因此要检查
   */
  if (ans[currentLevel].hasOwnProperty("children")) {
    arrayToTreeHelper(ans[currentLevel].children, obj, keyName, level.slice(2));
  } else {
    const children = [];
    ans[currentLevel]["children"] = children;
    /**
     * 递归给下一层的是这些东西:
     * children:这一个对象的 children
     * obj:input 数组里,包含了当前对象的那个,完整的对象(最外层的对象)
     * keyName:obj 里对应的属性名
     * level.slice(s):传递到下一层的 level,比如现在是 "1-3-2",那就传 "3-2"
     */
    arrayToTreeHelper(children, obj, keyName, level.slice(2));
  }
}

测试

const array = [
  { name: "1-1-1", depth: 3 },
  { hair: "2-1", depth: 2 },
  { hello: "1", depth: 1 },
  { with: "2", depth: 1 },
  { lynch: "1-2", depth: 2 },
  { is: "1-1-2", depth: 3 },
  { my: "1-1", depth: 2 }
];

const fs = require("fs");

fs.writeFile("test.json", JSON.stringify(arrayToTree(array), null, 2), () => {
  console.log("ok");
});

最终得到的文件

[
  {
    "children": [
      {
        "children": [
          {
            "name": "1-1-1",
            "depth": 3
          },
          {
            "is": "1-1-2",
            "depth": 3
          }
        ],
        "my": "1-1",
        "depth": 2
      },
      {
        "lynch": "1-2",
        "depth": 2
      }
    ],
    "hello": "1",
    "depth": 1
  },
  {
    "children": [
      {
        "hair": "2-1",
        "depth": 2
      }
    ],
    "with": "2",
    "depth": 1
  }
]

是不是贼牛逼!