巧妙利用引用,將數組轉換成樹形數組

前言

筆者所作的一個項目須要作一個前端的樹形菜單,後端返回的數據是一個平行的list,list中的每一個元素都是一個對象,例如list[0]的值爲{id: 1, fid: 0, name: 一級菜單},每一個元素都指定了父元素,生成的菜單能夠無限級嵌套。一開始找的插件須要手動將生成好的樹形數組傳進去才能使用(儘管後來找到了一個UI框架,能夠直接傳list進去,只須要指定id和fid),可是當時思索了很久都沒能正確寫出來,故在空下來的時候認真想了一下,整理成筆記,以便後期查閱。前端

準備工做

由於是前端處理,因此本文實現語言爲js。node

以下,有一個平行的list列表和一個不在list中的根節點root:程序員

var s = [
  { id: 1, fid: 0, name: "第一級菜單1" },
  { id: 2, fid: 0, name: "第一級菜單2" },
  { id: 3, fid: 1, name: "第二級菜單1.1" },
  { id: 4, fid: 1, name: "第二級菜單1.2" },
  { id: 5, fid: 2, name: "第二級菜單2.1" },
  { id: 6, fid: 3, name: "第三級菜單1.1.1" },
  { id: 7, fid: 3, name: "第三級菜單1.1.2" },
  { id: 8, fid: 4, name: "第三級菜單1.2.1" },
  { id: 9, fid: 4, name: "第三級菜單1.2.2" },
  { id: 10, fid: 6, name: "第四級菜單1.1.1.1" },
  { id: 11, fid: 6, name: "第四級菜單1.1.1.2" },
  { id: 12, fid: 9, name: "第四級菜單1.2.2.1" },
  { id: 13, fid: 9, name: "第四級菜單1.2.2.2" },
  { id: 14, fid: 0, name: "第一級菜單3" }
]

var root = { id: 0, fid: 0, name: "根菜單" };

須要整理成相似於下面的樣子,若是該節點沒有子節點,就沒有node屬性:算法

{
    id: xx,
    fid: xx,
    name: xx,
    node: [
        id: xx,
        fid: xx,
        name: xx,
        node: [...]
    ]
}

須要一個打亂list順序的shuffle算法,該算法會對原數組進行影響:後端

function shuffle(a) {
  var len = a.length;
  for (var i = 0; i < len; i++) {
    var end = len - 1;
    var index = (Math.random() * (end + 1)) >> 0;
    var t = a[end];
    a[end] = a[index];
    a[index] = t;
  }
};

使用JSON序列化來實現數組的深度拷貝:數組

function deepCopy(arr) {
  return JSON.parse(JSON.stringify(arr));
}

使用一個簡單的方式來初步判斷結果是否正確:框架

function check(node) {
    return JSON.stringify(node).match(/菜單/g).length;
}

使用遞歸

【思路】dom

對於這種問題,由於不知道到底要循環多少層,因此使用遞歸可以以一種很方便的方式來解決。函數

【步驟】插件

1. 遍歷當前列表,找出fid爲傳入的父元素的id的節點,並掛到父元素的node上;

2. 每找到一個節點就從當前列表刪除這個元素(否則遞歸怎麼終止);

3. 對於每個子節點,重複如上步驟,將子節點當成下一層的父節點繼續查找該節點的子節點。

能夠看到,時間複雜度最壞爲O(n!)

【實現】

function arr2tree(arr, father) {
  // 遍歷數組,找到當前father的全部子節點
  for (var i = 0; i < arr.length; i++) {   
    if (arr[i].fid == father.id) {
      // 這裏是有子節點才須要有node屬性(也就是說有node裏毫不會爲空list)
      if (!father.node) { 
        father.node = [];
      }
      var son = arr[i];
      father.node.push(son);
      arr.splice(i, 1); // 刪除該節點,當list爲空的時候就終止遞歸
      i--; // 因爲刪除了i節點,因此下次訪問的元素下標應該仍是i
    }
  }
  // 再對每個子節點進行如上操做
  if (father.node) { // 須要先判斷有沒有子節點
    var childs = father.node;
    for (var i=0; i<childs.length; i++) {
      arr2tree(arr, childs[i]); // 調用遞歸函數
    }
    // 用於按名稱進行排序,若是不強調順序能夠去掉
    father.node.sort(function (a, b) {
      return a.name > b.name;
    })
  }
}

【檢驗】

shuffle(s); // 打亂數組
var arr = deepCopy(s); // 拷貝一份,避免對原數組進行修改
arr2tree(arr, root);
console.log(check(root)); // 預期輸出15
console.log(root); // 手工檢查輸出是否正確

不使用遞歸

【思路】

當數據量大的時候,使用遞歸及其容易由於內存溢出而沒法運行,有沒有不使用遞歸的方式呢?能不可以直接就用循環來搞定呢?能不能邊遍歷這個元素,就直接把這個元素放到正確的位置上,這樣就能夠省好多事情。能夠用一個哈希表(字典/對象)來儲存這些元素,鍵(屬性名)就是元素的id,這樣就能夠直接判斷當前遍歷的元素的父元素在不在哈希表裏面了。

突然,筆者想到了一個特性——引用js中的對象都是引用的,哪怕我已經把a對象push進一個list中了,我在後面對a對象進行的任何修改都會在list中反映出來。也就是說,我把a元素掛到對應的父元素f上了,當我在後面找到a元素的子元素b時,我把該子元素b掛到a上,f中掛載的a也會同樣有b元素。

【步驟】

1. 新建一個對象temp用於存放臨時信息。遍歷列表,將當前訪問元素a加到temp中(屬性名爲對象id,屬性值爲該對象);

2. 在temp中查找是否有a的子節點,有的話就將子節點掛到a上;

3. 在temp中查找是否有a的父節點,有的話就將a掛到父節點上;

能夠看到,時間複雜度爲O(n^2^),空間複雜度也不會過高,該方法不會對原數組進行修改。

【實現】

function arr2tree2(arr, root) {
  var temp = {};
  temp[root.id] = root;
  for (var i = 0; i < arr.length; i++) {
    // 插入一個新節點,後面對該節點的修改都會同步到該節點的父節點上
    temp[arr[i].id] = arr[i];
    // 查找是否有子節點
    var keys = Object.keys(temp);
    for (var j = 0; j < keys.length; j++) {
      if (temp[keys[j]].fid == arr[i].id) {
        temp[arr[i].id].node ? "" : temp[arr[i].id].node = [];
        temp[arr[i].id].node.push(temp[keys[j]]); // 將該子節點掛到當前節點的node上
      }
    }
    // 查找是否有父節點
    if (temp[arr[i].fid]) {
      temp[arr[i].fid].node ? "" : temp[arr[i].fid].node = [];
      temp[arr[i].fid].node.push(arr[i]); // 將當前節點掛到父節點的node上
    }
  }
  return temp;
}

【檢驗】

shuffle(s); // 打亂數組
var result = arr2tree2(s, root);
console.log(check(result[root.id])); // 預期輸出15
console.log(result[root.id]); // 手工檢查輸出是否正確

總結

平時筆者所作的項目大多也不涉及到算法,平時秉承的也是能用循環解決的就用循環解決,能夠看到,算法對於程序員而言仍是很重要的,本文也只是我的的想法,歡迎一塊兒探討。

相關文章
相關標籤/搜索