列表变树

需求背景

在需要存储树结构的情况下,一般由于使用的关系型数据库(如 MySQL),是以类似表格的扁平化方式存储数据。因此不会直接将树结构存储在数据库中


image.png

代表了如下的树状结构:

{
  id: 1,
  pid: 0,
  data: 'a',
  children: [
    {id: 2, pid: 1, data: 'b'},
    {id: 3, pid: 1, data: 'c'},
  ]
}

🌰

const list = [
  { pid: null, id: 1, data: "1" },
  { pid: 1, id: 2, data: "2-1" },
  { pid: 1, id: 3, data: "2-2" },
  { pid: 2, id: 4, data: "3-1" },
  { pid: 3, id: 5, data: "3-2" },
  { pid: 4, id: 6, data: "4-1" },
];

方法一

递归解法:该方法简单易懂,从根节点出发,每一轮迭代找到 pid 为当前节点 id 的节点,作为当前节点的 children,递归进行。

function listToTree(
  list,
  pid = null,
  { idName = "id", pidName = "pid", childName = "children" } = {}
) {
  return list.reduce((root, item) => {
    // 遍历每一项,如果该项与当前 pid 匹配,则递归构建该项的子树
    if (item[pidName] === pid) {
      const children = listToTree(list, item[idName]);
      if (children.length) {
        item[childName] = children;
      }
      return [...root, item];
    }
    return root;
  }, []);
}
//时间复杂度为 O(n^2)

方法二

迭代法:利用对象在 js 中是引用类型的原理。第一轮遍历将所有的项,将项的 id 与项自身在字典中建立映射,为后面的立即访问做好准备。 由于操作的每一项都是对象,结果集 root 中的每一项和字典中相同 id 对应的项实际上指向的是同一块数据。后续的遍历中,直接对字典进行操作,操作同时会反应到 root 中。

function listToTree(
  list,
  rootId = null,
  { idName = "id", pidName = "pid", childName = "children" } = {}
) {
  const record = {}; // 用空间换时间,用于将所有项的 id 及自身记录到字典中
  const root = [];

  list.forEach((item) => {
    record[item[idName]] = item; // 记录 id 与项的映射
    item[childName] = [];
  });

  list.forEach((item) => {
    if (item[pidName] === rootId) {
      root.push(item);
    } else {
      // 由于持有的是引用,record 中相关元素的修改,会在反映在 root 中。
      record[item[pidName]][childName].push(item);
    }
  });

  return root;
}
//时间复杂度为 O(n)

方法二变体一

在解法二的基础上,将两轮迭代合并成一轮迭代。采用边迭代边构建的方式:

function listToTree(
  list,
  rootId = null,
  { idName = "id", pidName = "pid", childName = "children" } = {}
) {
  const record = {}; // 用空间换时间,用于将所有项的 id 及自身记录到字典中
  const root = [];

  list.forEach((item) => {
    const id = item[idName];
    const parentId = item[pidName];

    // 如果该项不在 record 中,则放入 record。如果该项已存在 (可能由别的项构建 pid 加入),则合并该项和已存在的数据
    record[id] = !record[id] ? item : { ...item, ...record[id] };

    const treeItem = record[id];

    if (parentId === rootId) {
      // 如果是根元素,则加入结果集
      root.push(treeItem);
    } else {
      // 如果父元素不存在,则初始化父元素
      if (!record[parentId]) {
        record[parentId] = {};
      }
      // 如果父元素没有 children, 则初始化
      if (!record[parentId][childName]) {
        record[parentId][childName] = [];
      }

      record[parentId][childName].push(treeItem);
    }
  });

  return root;
}
//时间复杂度为 O(n)

方法二变体二

record 字典仅记录 id 与 children 的映射关系,代码更精简:

function listToTree(
  list,
  rootId = null,
  { idName = "id", pidName = "pid", childName = "children" } = {}
) {
  const record = {}; // 用空间换时间,仅用于记录 children
  const root = [];

  list.forEach((item) => {
    const newItem = Object.assign({}, item); // 如有需要,可以复制 item ,可以不影响 list 中原有的元素。
    const id = newItem[idName];
    const parentId = newItem[pidName];

    // 如果当前 id 的 children 已存在,则加入 children 字段中,否则,初始化 children
    // item 与 record[id] 引用同一份 children,后续迭代中更新 record[parendId] 就会反映到 item 中
    newItem[childName] = record[id] ? record[id] : (record[id] = []);

    if (parentId === rootId) {
      root.push(newItem);
    } else {
      if (!record[parentId]) {
        record[parentId] = [];
      }
      record[parentId].push(newItem);
    }
  });

  return root;
}
//时间复杂度为 O(n)

总结

● 递归法:在数据量增大的时候,性能会急剧下降。好处是可以在构建树的过程中,给节点添加层级信息。
● 迭代法:速度快。但如果想要不影响源数据,需要在 record 中存储一份复制的数据,且无法在构建的过程中得知节点的层级信息,需要构建完后再次深度优先遍历获取。
● 迭代法变体一:按需创建 children,可以避免空的 children 列表。
时间复杂度
代码的执行时间 T(n)与每行代码的执行次数 n 成正比,人们把这个规律总结成这么一个公式:T(n) = O(f(n));
大O时间复杂度并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度,简称时间复杂度。
● 只关注循环执行次数最多的一段代码

function total(n) { // 1
  var sum = 0; // 2
  for (var i = 0; i < n; i++) { // 3
    sum += i; // 4
  } //5 
} //6
 //只有第 3 行和第 4 行是执行次数最多的,分别执行了 n 次,那么忽略常数项,所以此段代码的时间复杂度就是 O(n)。

● 加法法则:总复杂度等于量级最大的那段代码的复杂度。

function total(n) { 
  // 第一个 for 循环
  var sum1 = 0; 
  for (var i = 0; i < n; i++) {
    for (var j = 0; j < n; j++) {
      sum1 = sum1 + i + j; 
    }
  }
  // 第二个 for 循环
  var sum2 = 0;
  for(var i=0;i<1000;i++) {
    sum2 = sum2 + i;
  }
  // 第三个 for 循环
  var sum3 = 0;
  for (var i = 0; i < n; i++) {
    sum3 = sum3 + i;
  }
}
  //取最大量级,所以整段代码的时间复杂度为 O(n2)。

● 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

function f(i) {
  var sum = 0;
  for (var j = 0; j < i; j++) {
    sum += i;
  }
  return sum;
}
function total(n) {
  var res = 0;
  for (var i = 0; i < n; i++) {
    res = res + f(i); // 调用 f 函数
  }
}
 // O(n2)。
function total1(n) {
  var sum = 0;
  var i = 1;
  while (i <= n) {
    sum += i;
    i = i * 2;
  }
}
function total2(n) {
  var sum = 0;
  for (var i = 1; i <= n; i = i * 2) {
    sum += i;
  }
}
//2x = n => x = log2n => O(logn) 

🌰

function total(n) {
  var sum = 0;
  for (var i = 1; i <= n; i++) {
    sum += i;
  }
  return sum;
}
function total(n) {
  var sum = n * (n + 1) / 2
  return sum;
}

注:来源于小哥的分享

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 2019.12-2020.02后端面试材料分享,算法篇。 拿到了字节offer,走完了Hello单车和达达的面试流...
    润着阅读 807评论 0 0
  • 1 多益网络面试 Q:博客项目里面如何验证账号密码的?有没有做什么安全措施 A: 在登录表单中填写用户名和密码后,...
    全村希望gone阅读 883评论 0 3
  • 前言 二叉树的前序遍历,中序遍历,后序遍历是面试中常常考察的基本算法,关于它的概念这里不再赘述了,还不了解的同学可...
    Jesse1995阅读 16,565评论 0 3
  • 1.vector中resize() 和 reserve() 函数的区别? reserve 容器预留空间 ,但并不真...
    azubi阅读 991评论 0 0
  • 公众号 coding 笔记、点滴记录,以后的文章也会同步到公众号(Coding Insight)中,希望大家关注_...
    被称为L的男人阅读 1,790评论 0 5