列表變樹

需求背景

在需要存儲樹結構的情況下,一般由于使用的關系型數據庫(如 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;
}

注:來源于小哥的分享

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 2019.12-2020.02后端面試材料分享,算法篇。 拿到了字節offer,走完了Hello單車和達達的面試流...
    潤著閱讀 841評論 0 0
  • 1 多益網絡面試 Q:博客項目里面如何驗證賬號密碼的?有沒有做什么安全措施 A: 在登錄表單中填寫用戶名和密碼后,...
    全村希望gone閱讀 906評論 0 3
  • 前言 二叉樹的前序遍歷,中序遍歷,后序遍歷是面試中常常考察的基本算法,關于它的概念這里不再贅述了,還不了解的同學可...
    Jesse1995閱讀 16,591評論 0 3
  • 1.vector中resize() 和 reserve() 函數的區別? reserve 容器預留空間 ,但并不真...
    azubi閱讀 1,019評論 0 0
  • 公眾號 coding 筆記、點滴記錄,以后的文章也會同步到公眾號(Coding Insight)中,希望大家關注_...
    被稱為L的男人閱讀 1,847評論 0 5