JS中樹形結構的平常操做

代碼量超過了文字,慎讀前端

前面部分是流水帳,不感興趣的同窗能夠直接略過😄node

記得剛入行前端的時候,我很是排斥使用框架,全部的功能,都要本身用原生寫,那時候好有激情啊......,固然也是到處碰壁,其中某個管理系統中的一個樹形表格折磨了我好久,後端的同窗直接甩給我一個相似這樣的結構:面試

const list = [
  { id: 3, name: 'cc', parentId: 1, sort: 2 },
  { id: 4, name: 'dd', parentId: 1, sort: 1 },
  { id: 5, name: 'ee', parentId: 2, sort: 4 },
  { id: 6, name: 'ff', parentId: 3, sort: 3 },
  { id: 7, name: 'gg', parentId: 2, sort: 0 },
  { id: 8, name: 'hh', parentId: 4, sort: 1 },
  { id: 9, name: 'ii', parentId: 1, sort: 3 },
  { id: 1, name: 'aa', parentId: 0, sort: 1 },
  { id: 2, name: 'bb', parentId: 0, sort: 0 },
]
複製代碼

而後需求大概是這個樣子算法

要達到這種效果,首先得將這個列表轉換成樹形結構,而後遍歷這個樹形結構生成表格,過程當中還須要算出子節點的深度等等後端

當時思考良久而答案始終不可得🤔,因而百度一番首次接觸了遞歸(竟然還能搜到😊):數組

function fn(data, pid) {
    var result = [], temp;
    for (var i = 0; i < data.length; i++) {
        if (data[i].pid == pid) {
            var obj = {"text": data[i].name,"id": data[i].id};
            temp = fn(data, data[i].id);
            if (temp.length > 0) {
                obj.children = temp;
            }
            result.push(obj);
        }
    }
    return result;
}
複製代碼

而後我根據個人狀況將上面的寫法改造了一番終於將功能實現了,當時實現完後是滿滿的成就感吶!框架

不久以後,我回頭再看這個轉換,能不能不使用遞歸進行轉換呢?當時瞭解遞歸時也知道遞歸的性能不怎麼樣,而且後端甩給個人列表也有些大,我測試每次轉換大概要消耗大幾十毫秒,因此這成了我內心的一道坎,得翻過去啊,因而🤔函數

function toTree (tree, topId) {
    tree.forEach(function (child) {
        child.children = tree.filter(function (cd) {
            return cd.parentId === child.id
        })
    })

    return tree.filter(child => child.parentId === topId)
}
複製代碼

太爽了,當時看到這樣的代碼,起碼得興奮一夜,況且仍是我本身想出來的😄。上面的原理就是,利用數組是一維的,同時利用引用類型的特性,直接改變節點的屬性值,而後根據topId過濾一下就能獲得轉換好的樹。這很好,避開了遞歸,而後我將先後2種方式比較了一下,效率差很少提高20幾倍左右(應該沒記錯,幾年了)。性能

又過了不久,印證了一句話,too young too simple,我偶然從某篇文章(不記得了)中瞭解到了深度優先和廣度優先、時間複雜度和空間複雜度這幾個詞,因而開啓了新世界。測試

上面的作法,看似簡潔,但在時間複雜度上的表現彷佛太弱了,每一次循環都要進行一次filter操做,返回時又是一次循環,這是不必的。

接下來我將以往的一些相關的經驗總結分享一下......,

列表轉樹

繼承上面的一丟丟思路,通過個人改良版本,原理仍是利用了引用類型的特性,用一個map經過節點的id保存每個節點的children引用,而後一邊遍歷一邊更新children,最後清空map。

function convertListToTree (list, topId = 0) {
  let result
  let map = {}

  list.forEach(child => {
    const { id, parentId } = child
    const children = map[parentId] = map[parentId] || []

    if (!result && parentId === topId) {
      result = children
    }

    children.push(child)

    child.children = map[id] || (map[id] = [])
  })
  map = null

  return result
}
複製代碼

上面的代碼量多了一些,但仔細看會發現,整個轉換過程只經歷了一次循環O(n),相比以前的方法,優化了不少倍。但僅僅是轉換彷佛不太夠,可能有時候除了轉換還須要排序等其它操做。以排序來講,若是能儘可能減小時間複雜度,那確定最好不過了,這裏能夠在拿到全部children以後直接利用map來完成,遍歷map,而後對每一個children數組sort操做,如今將代碼再改造一下

function convertListToTree(list, callBack = c => c, options = {}) {
      const {
        topId = 0,
        idKey = 'id',
        parentIdKey = 'parentId',
        sortKey
      } = options

      let result
      let map = {}

      list.forEach(child => {
        const { [idKey]: id, [parentIdKey]: parentId } = child
        const children = map[parentId] = map[parentId] || []

        if (!result && parentId === topId) {
          result = children
        }

        children.push( callBack(child) || child )

        child.children = map[id] || (map[id] = [])
      })

      sortKey && Object.keys(map).forEach(i => {
        if (map[i].length > 1) {
          map[i].sort((a, b) => a[sortKey] - b[sortKey])
        }
      })

      map = null

      return result
    }
複製代碼

如今對最開始的list進行轉換

const tree = convertListToTree(list, node => {
  node.someKey = 1
}, { sortKey: 'sort' })
複製代碼

搞定了列表轉樹,但若是遍歷樹呢?

深度優先搜索和廣度優先搜索

深度優先搜索算法(英語:Depth-First-Search,DFS)是一種用於遍歷或搜索樹或圖的算法。沿着樹的深度遍歷樹的節點,儘量深的搜索樹的分支。當節點v的所在邊都己被探尋過,搜索將回溯到發現節點v的那條邊的起始節點。 這一過程一直進行到已發現從源節點可達的全部節點爲止。-----維基百科

按照深度優先搜索,上圖的遍歷順序爲: a b e f c d g h

廣度優先搜索算法(英語:Breadth-First-Search,縮寫爲BFS),又譯做寬度優先搜索,或橫向優先搜索,是一種圖形搜索算法。簡單的說,BFS是從根節點開始,沿着樹的寬度遍歷樹的節點。若是全部節點均被訪問,則算法停止。-----維基百科

按照廣度優先搜索,上圖的遍歷順序爲: a b c d e f g h

都很容易理解,接下來,先將他們簡單實現

深度優先遞歸方式 原理就是每碰到有children的節點遞歸調用一下

function recursiveEachByDfs (tree, cb) {
  let i = 0
  let node
  while (node = tree[i++]) {
    cb(node)

    if (node.children && node.children.length) {
      recursiveEachByDfs(node.children, cb)
    }
  }
}
複製代碼

深度優先非遞歸方式 原理就是利用棧,後進先出,每次碰到children,就將children壓入棧頂

function eachByDfs([...stack], cb) {
  while (stack.length) {
    // 出棧
    const node = stack.shift()

    cb(node)

    if (node.children && node.children.length) {
      // 入棧
      stack.unshift(...node.children)
    }
  }
}
複製代碼

廣度優先遞歸方式 原理就是,遍歷當前層的節點時,將遇到children存入數組中,做爲tree傳入下一個調用,終止條件爲沒有下一層節點時

function recursiveEachByBfs(tree, cb) {
  const nextLevels = []

  // 遍歷當前層同時拿到下一層節點
  let i = 0
  let node
  while (node = tree[i++]) {
    cb(node)
    
    nextLevels.push(...node.children)
  }

  // 遞歸下一層全部節點
  if (nextLevels.length) {
    recursiveEachByBfs(nextLevels, cb)
  }
}
複製代碼

廣度優先非遞歸方式 原理就是利用隊列,先進先出,讓碰到children去排隊,就像食堂排隊打飯同樣

function eachByBfs([...queue], cb) {
  while (queue.length) {
    // 出隊
    const node = queue.shift()

    cb(node)

    if (node.children && node.children.length) {
      // 入隊
      queue.push(...node.children)
    }
  }
}
複製代碼

如今我們就能經過上面任意一種方式遍歷樹形結構了,但僅僅是這樣確定是不夠的。根據我以往碰到的需求,至少還可能須要

  1. 在遍歷時計算出當前節點的深度
  2. 在遍歷時計算出當前節點的全部父節點
  3. 更新當前節點的部分數據
  4. 根據id尋找節點
  5. 支持遍歷某一個分支,或者知足某些條件的遍歷

首先,我們先大概分析一下上述功能:

  • 第一、2點,能夠當作是相似的需求,不論是深度優先仍是廣度優先,都是從父到子搜索的過程,利用這一點,能夠將當前節點信息(也便是以後節點的父節點)層層傳遞下去,而後每通過一層,層層疊加。
  • 第三、4點,這就是個回調方法傳入當前節點搞定了,當查找到節點就不必繼續循環了
  • 第5點,和第4點相似,能夠在遍歷時接收一下回調函數的返回值,經過返回值決定循環是continue仍是break

那接下來,咱們將上面的4個基本方法都改造一下,所有支持上述功能,而後供君挑選。

  • relations[0]: 節點深度
  • relations[1]: 全部父節點,按深度順序排列

深度優先遞歸

只須要通過函數參數傳遞一下

function recursiveEachByDfs(tree, cb, relations = [0, []]) {
  let i = 0
  let node
  while (node = tree[i++]) {
    const behavior = cb(node, ...relations)

    if (behavior === 'break') {
      break
    }
    if (behavior === 'continue') {
      continue
    }

    if (node.children) {
      const [level, parents] = relations
      recursiveEachByDfs(node.children, cb, [level + 1, parents.concat(node)])
    }
  }
}

// 搜索根節點id爲0的分支,最深搜索3層
recursiveEachByDfs(tree, (node, level, parents) => {
  if (level > 2) {
    return 'continue'
  }
  if (level === 0 && node.id !== 0) {
    return 'continue'
  }
  console.log(node, level, parents)
})
複製代碼

深度優先非遞歸

既然用到了棧,那就能夠往棧中加點料,原理就是在每一次入棧時,將入棧子節點們的父節點和深度信息(就叫信息對象吧,不限於這些東西)等一塊兒放到棧頂,以後在每次出棧時判斷若是不是節點類型的值,就說明這是接下來遍歷的子節點們的父節點以及深度信息,用一張圖來描述(基於本章第二張圖):

function eachByDfs([...stack], cb) {
  let relations = [0, []]
  // 將信息對象壓入棧頂
  stack.unshift(relations)

  while (stack.length) {
    let node = stack.shift()

    if (Array.isArray(node)) {
      relations = node

      node = stack.shift()
    }

    const behavior = cb(node, ...relations)

    if (behavior === 'break') {
      break
    }
    if (behavior === 'continue') {
      continue
    }

    if (node.children && node.children.length) {
      // 若是此時棧頂是信息對象,說明當前節點是它的父節點中子節點的最後一個了
      if (stack.length && !Array.isArray(stack[0])) {
        stack.unshift(relations)
      }

      const [level, parents] = relations
      stack.unshift([level + 1, parents.concat(node)], ...node.children)
    }
  }
}

// 搜索根節點id爲0的分支,最深搜索3層
eachByDfs(tree, (node, level, parents) => {
  if (level > 2) {
    return 'continue'
  }
  if (level === 0 && node.id !== 0) {
    return 'continue'
  }
  console.log(node, level, parents)
})
複製代碼

廣度優先遞歸

首次進入函數時,初始化一個信息對象,以後,將每一層的節點被統一放到數組中(將問題統一,就像處理第一層節點同樣),除了第一層節點的其它層的節點可能有不一樣的父節點,因此須要像插隊同樣,在每一個節點的children以前插入一個信息對象

function recursiveEachByBfs(tree, cb) {
  const nextLevels = []
  let relations = [0, []]

  if (!Array.isArray(tree[0])) {
    // 首次進入,放入父節點和深度信息
    tree.unshift(relations)
  }

  let i = 0
  let node
  while (node = tree[i++]) {
    if (Array.isArray(node)) {
      relations = node
      continue
    }
    const behavior = cb(node, ...relations)

    if (behavior === 'break') {
      break
    }
    if (behavior === 'continue') {
      continue
    }

    const [level, parents] = relations
    // 在每一個節點的子節點以前插入信息對象
    nextLevels.push([level + 1, parents.concat(node)], ...node.children)
  }

  // 遞歸下一層全部節點
  if (nextLevels.length) {
    recursiveEachByBfs(nextLevels, cb)
  }
}

// 搜索根節點id爲0的分支,最深搜索3層
recursiveEachByBfs(tree, (node, level, parents) => {
  if (level > 2) {
    return 'continue'
  }
  if (level === 0 && node.id !== 0) {
    return 'continue'
  }
  console.log(node, level, parents)
})
複製代碼

廣度優先非遞歸

思路和深度優先非遞歸相似,只不過這裏換成來隊列,但處理起來比棧稍微簡單點,在每一個節點的children以前插入一個信息對象......

function eachByBfs([...queue], cb) {
  let relations = [0, []]
  queue.unshift(relations)

  while (queue.length) {
    let node = queue.shift()

    if (Array.isArray(node)) {
      relations = node

      node = queue.shift()
    }

    const behavior = cb(node, ...relations)
    if (behavior === 'break') {
      break
    }
    if (behavior === 'continue') {
      continue
    }

    if (node.children && node.children.length) {
      const [level, parents] = relations
      queue.push([level + 1, parents.concat(node)], ...node.children)
    }
  }
}

// 搜索根節點id爲0的分支,最深搜索3層
eachByBfs(tree, (node, level, parents) => {
  if (level > 2) {
    return 'continue'
  }
  if (level === 0 && node.id !== 0) {
    return 'continue'
  }
  console.log(node, level, parents)
})
複製代碼

如今,基本的功能都實現了,主要是以往作的項目都過小了,至今沒發現什麼更復雜的場景,若是有,那就繼續改造......,思路都差很少

對象的深度優先遍歷和廣度優先遍歷

這個我只在刷面試題的時候碰到......,這裏簡單實現一下,思路都是同樣的,知道怎麼遍歷就不是問題了😄,僅供參考,統一的思路就是將對象的每一個屬性拆解成一個信息對象,對象中包含key、value、parent等等等等,而後和處理數組的方式差很少,有以下粒子

const symbolName = Symbol()
    const obj = {
      a: {
        b: {
          c: 1
        },
        d: [{ j: 1 }, { [symbolName]: 222 }]
      },
      e: {
        f: 3
      },
      g: {
        h: {
          i: 4
        }
      }
    }
複製代碼

深度優先遞歸

const recursiveEachObjByDfs = (obj, cb) => {
  Reflect.ownKeys(obj).forEach(key => {
    const value = obj[key]

    cb({ key, value, parent: obj })

    if (typeof value === 'object') {
      recursiveEachObjByDfs(value, cb)
    }
  })
}
recursiveEachObjByDfs(obj, console.log)
複製代碼

深度優先非遞歸

const eachObjByDfs = (obj, cb) => {
  const stack = Reflect.ownKeys(obj).map(key => ({key, value: obj[key], parent: obj}))

  while (stack.length) {
    const options = stack.shift()

    cb(options)

    if (typeof options.value === 'object') {
      stack.unshift(...Reflect.ownKeys(options.value).map(key => {
        return {
          key,
          value: options.value[key],
          parent: options.value
        }
      }))
    }
  }
}
eachObjByDfs(obj, console.log)
複製代碼

廣度優先遞歸

const recursiveEachObjByBfs = (obj, cb) => {
  const currentLevels = Reflect.ownKeys(obj).map(key => ({key, value: obj[key], parent: obj}))

  const each = levels => {
    const nextLevels = []
    let i = 0
    let options
    while (options = levels[i++]) {
      cb(options)

      if (typeof options.value === 'object') {
        nextLevels.push(...Reflect.ownKeys(options.value).map(key => {
          return {
            key,
            value: options.value[key],
            parent: options.value
          }
        }))
      }
    }

    if (nextLevels.length) {
      each(nextLevels)
    }
  }

  each(currentLevels)
}
recursiveEachObjByBfs(obj, console.log)
複製代碼

廣度優先非遞歸

const eachObjByBfs = (obj, cb) => {
  const queue = Reflect.ownKeys(obj).map(key => ({key, value: obj[key], parent: obj}))

  while (queue.length) {
    const options = queue.shift()

    cb(options)

    if (typeof options.value === 'object') {
      queue.push(...Reflect.ownKeys(options.value).map(key => {
        return {
          key,
          value: options.value[key],
          parent: options.value
        }
      }))
    }
  }
}
eachObjByBfs(obj, console.log)
複製代碼

寫得很差,莫計較,獻給須要的同窗,若是文章中有什麼錯誤或者有什麼能夠改進的地方,能夠下方留言咯。

相關文章
相關標籤/搜索