有關數組轉樹形結構的算法探索

背景

由於項目緣故,須要實現一個數組轉樹形結構的數據,主要是用於樹組件的渲染。一開始沒有去網絡上找相關的成熟的算法,本身思考本身寫了一個,能夠用,也用了一段時間。可是有一次,數據量特別大,數據又有些特別(非特殊的樹形結構,子節點判斷一部分是經過 parentId 判斷,一部分又是經過其餘節點判斷),致使須要頻繁調用數組轉樹的方法,結果就是出現性能問題。就 1000+ 的節點,結果數據處理用了 50+s 的時間。 算法

通過一段時間調研和測試,作出如下總結。segmentfault

數組轉樹的算法

方法 1:我本身的實現辦法數組

/**
 * 數組結構轉換爲樹結構數據
 * 注意: 數組裏面的對象必定是排序過的,也就是說父級必定在前面,它的子級必定在後面
 * @param arrayData 源數據
 * @param parentId 父節點字段
 * @param parentKey 父節點key值
 * @param childrenKey 子節點的key值
 * @param idKey id的key值
 * @returns {Array}
 */
function arrayToTree1(arrayData, parentId = '', parentKey = 'pid', childrenKey = 'children', idKey = 'id') {
  const treeObject = [] // 輸出的結果
  const remainderArrayData = [] // 遍歷剩餘的數組

  arrayData = cloneDeep(arrayData)

  arrayData.forEach(function (item) {
    // 服務端返回的數據, parent沒有的狀況可能返回 null 值, 統一轉換成 ''
    if (item[parentKey] === null) {
      item[parentKey] = ''
    }
    if (item[parentKey] === parentId) {
      treeObject.push(item)
    } else {
      remainderArrayData.push(item)
    }
  })
  treeObject.forEach(function (item) {
    // 子節點遍歷,性能問題也主要在這裏,即便沒有子節點,也會遍歷一次
    const _childrenData = arrayToTree(remainderArrayData, item[idKey], parentKey, childrenKey, idKey)
    _childrenData && _childrenData.length > 0 && (item[childrenKey] = _childrenData)
  })
  return treeObject
}

方法 2:網絡

/**
 * 數組結構轉換爲樹結構數據
 * 注意: 數組裏面的對象必定是排序過的,也就是說父級必定在前面,它的子級必定在後面
 * @param arrayData 源數據
 * @param parentId 父節點字段
 * @param parentKey 父節點key值
 * @param childrenKey 子節點的key值
 * @param idKey id的key值
 * @returns {Array}
 */
export function arrayToTree2(arrayData, parentId = '', parentKey = 'pid', childrenKey = 'children', idKey = 'id') {
  const result = []
  const _arrayData = cloneDeep(arrayData) // 深拷貝

  _arrayData.forEach(item => {
    // 服務端返回的數據, parent沒有的狀況可能返回 null 值, 統一轉換成 ''
    if (item[parentKey] === null) {
      item[parentKey] = ''
    }
    if (item[parentKey] === parentId) {
      result.push(item)
    }
  })

  function recursionFn(treeData, arrayItem) {
    treeData.forEach(item => {
      // 找到父節點, 並添加到到 children 中. 若是當前節點不是目標節點的父節點, 就遞歸判斷當前節點的子節點
      if (item[idKey] === arrayItem[parentKey]) {
        if (!item[childrenKey]) {
          item[childrenKey] = []
        }
        item.children.push(arrayItem)
      } else if (item[childrenKey] && item[childrenKey].length) {
        recursionFn(item[childrenKey], arrayItem)
      }
    })
  }

  // 經過 reduce 遍歷數組. 經過遍歷每一個元素去構建目標樹數據
  return _arrayData.reduce((previousValue, currentValue) => {
    if (currentValue[parentKey]) { // 該節點是子節點
      recursionFn(previousValue, currentValue)
    }
    return previousValue
  }, result)
}

方法 3:app

/**
 * 數組結構轉換爲樹結構數據
 * @param arrayData 源數據
 * @param parentId 父節點字段
 * @param parentKey 父節點key值
 * @param childrenKey 子節點的key值
 * @param idKey id的key值
 * @returns {Array}
 */
function arrayToTree3(array, parentId = '', parentKey = 'parentId', childrenKey = 'children', idKey = 'id') {
  let arr = [...array]

  let rootList = arr.filter((item) => {
    // 服務端返回的數據, parent沒有的狀況可能返回 null 值, 統一轉換成 ''
    if (item[parentKey] === null) {
      item[parentKey] = ''
    }
    if (item[parentKey] === parentId) {
      return item
    }
  })

  function listToTreeData(rootItem, arr, parentKey, idKey) {
    rootItem.children = []
    arr.forEach((item) => {
      if (item[parentKey] === rootItem[idKey]) {
        rootItem.children.push(item)
      }
    })

    if (rootItem.children.length > 0) {
      rootItem.children.forEach((item) => {
        listToTreeData(item, arr, parentKey, idKey)
      })
    } else {
      return false
    }
  }

  rootList.map((rootItem) => {
    listToTreeData(rootItem, arr, parentKey, idKey)
    return rootItem
  })

  return rootList
}

測試

測試相關的代碼dom

// 生成樹數據的方法
function generateTreeData(size = 5, deep = 4) {
  var result = []

  var fn = function (parentId, size, level) {
    for (var i = 0; i < size; i++) {
      var _id = 'id_' + Math.floor(Math.random() * 10000000000)
      result.push({
        id: _id,
        parentId: parentId
      })
      if (level < deep) {
        fn(_id, size, level + 1)
      }
    }
  }

  fn('', size, 1)
  return result
}

// 測試方法
function testMethod(fun, args, tag) {
  args = cloneDeep(args)
  console.time(tag)
  var result = fun.apply(this, args)
  console.log(result);
  console.timeEnd(tag)
}

測試結果性能

var testData = generateTreeData(7, 5) // 共 19607 條數據, 每一個節點下的子節點都是 7 個, 5 層的深度

// 正序(子節點一定在父節點後面)
testMethod(arrayToTree1, [testData, ''], 'arrayToTree1') // 14917.079833984375ms
testMethod(arrayToTree2, [testData, ''], 'arrayToTree2') // 3157.8740234375ms
testMethod(arrayToTree3, [testData, ''], 'arrayToTree3') // 5573.368896484375ms

// 亂序排序
testData = testData.sort(function randomSort() { return Math.random() > 0.5 ? -1 : 1 })

// 亂序
testMethod(arrayToTree1, [_arr, ''], 'arrayToTree1') // 22853.781982421875ms
testMethod(arrayToTree2, [_arr, ''], 'arrayToTree2') // 100 ms ~ 300 ms 之間, 看亂序的結果. 但結果是錯的, 子節點有的有缺失
testMethod(arrayToTree3, [_arr, ''], 'arrayToTree3') // 11802.785888671875ms

從結果看,固然我本身寫的算法效率慢的離譜,數據量小還好,數據量一大就災難了。
方法 2,效率是很高,可是對數據有要求,必須是通過排序後的數據纔有用,但實際業務場景中,樹會有跨級排序的場景,因此很難保證是順序的。
比較合適的方法是方法 3測試

我用方法 3 的算法,在個人項目中,時間從原來的 50+ 縮短到 5s+, 提高的仍是很直觀的。this

我想應該是有更好的方法,若是你有更好的算法貼出來一塊兒分享下?code

其餘

JS數組reduce()方法詳解及高級技巧

相關文章
相關標籤/搜索