字串陣列轉為樹形結構

pardon110發表於2020-07-05

基於路徑串,分類目錄串,以及資料庫查詢記錄源,分別用js物件導向,函式,及過程的方式構建樹形資料結構

題面

寫一個演算法實現字串陣列按照一定格式轉為樹形結構

const arr = [
  'root/lufei.jpg',
  'root/font/VarelaRound.ttf',
  'root/font/SourceSansPro.ttf',
  'root/img/avator.jpg',
  'root/img/favicon.jpg',
  'root/img/bg5.jpg',
  'root/img/bg4.jpg',
  'root/img/upload/a.png',
  'root/img/nt/mvvm.png',
  'root/img/nt/android.png',
  'root/img/nt/upload.png',
  'root/img/nt/js.png',
  'root/img/mvvm/a.png',
];

轉為類似如下樹形

      "name": "root",
      "children": [
        {
          "name": "lufei.jpg"
        },
        {
          "name": "font",
          "children": [
            {
              "name": "VarelaRound.ttf"
            },
            {
              "name": "SourceSansPro.ttf"
            }
          ]
        },
        ...

思路

/分割,每段示為一個節點,節點有兩個屬性,其中children 是節點型別的陣列,其節點資料結構描述如下

type Node {
    name string
    children []*Node
}

顯然還需要一個節點查詢方法,子節點遞迴插入

類方案

用js原型+建構函式實現程式碼

function O() {
  this.name = 'Trie'
  this.children = []
}

// 第一個匹配成功節點,用於插入
O.prototype.matchChild = function (part) {
  for (let i = 0; i < this.children.length; i++) {
    if (this.children[i].name == part) {
      return this.children[i]
    }
  }
  return null
}

/**
 * 插入節點
 * @param {[]string} parts 
 * @param {Number} height 
 */
O.prototype.insert = function (parts, height) {
  if (parts.length == height) {
    // 清除子節點
    delete this.children
    // 必須有退出機制
    return
  }
  part = parts[height]
  // 查詢第一個同級同名節點
  child = this.matchChild(part)
  if (child == null) {
    // 新建子節點
    child = new O()
    child.name = part
    this.children.push(child)
  }
  // 進入下一層繼續搜尋插入
  child.insert(parts, height + 1)
}

測試

o = new O()
arr.map(v => {
  t = v.split('/')
  o.insert(t, 0)
})
console.log(JSON.stringify(o,null,2))

效果

{
  "name": "Trie",
  "children": [
    {
      "name": "root",
      "children": [
        {
          "name": "lufei.jpg"
        },
        {
          "name": "font",
          "children": [
            {
              "name": "VarelaRound.ttf"
            },
            {
              "name": "SourceSansPro.ttf"
            }
          ]
        },
        {
          "name": "img",
          "children": [
            {
              "name": "avator.jpg"
            },
            {
              "name": "favicon.jpg"
            },
            {
              "name": "bg5.jpg"
            },
            {
              "name": "bg4.jpg"
            },
            {
              "name": "upload",
              "children": [
                {
                  "name": "a.png"
                }
              ]
            },
            {
              "name": "nt",
              "children": [
                {
                  "name": "mvvm.png"
                },
                {
                  "name": "android.png"
                },
                {
                  "name": "upload.png"
                },
                {
                  "name": "js.png"
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "name": "abc",
      "children": [
        {
          "name": "img",
          "children": [
            {
              "name": "mvvm",
              "children": [
                {
                  "name": "a.png"
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

函式方案

已知如下陣列

let srcList = ['動物-昆蟲-螞蟻', '動物-昆蟲', '植物-草-綠色', '植物-花-紅色','植物-花-黃色']

達成結構

[
  {
    "name": "動物",
    "children": [
      {
        "name": "昆蟲",
        "children": [
          {
            "name": "螞蟻"
          }
        ]
      }
    ]
  },
  {
    "name": "植物",
    "children": [
      {
        "name": "草",
        "children": [
          {
            "name": "綠色"
          }
        ]
      },
      {
        "name": "花",
        "children": [
          {
            "name": "紅色"
          },
          {
            "name": "黃色"
          }
        ]
      }
    ]
  }
]

解法詳情

function listToTree(srcList) {
  // 1.記錄根位置
  let destList = []
  srcList.forEach(path => {
    // 2.待匹配項
    let pathList = path.split('-')
    // 3.將移動指標重置頂層,確保每次從根檢索匹配(必須!!!)
    let levelList = destList
    // 4.遍歷待詢節點
    for (let name of pathList) {
      // 5.同層同名節點查詢匹配
      let obj = levelList.find(item => item.name == name)
      // 6.若不存在則建立該節點
      if (!obj) {
        obj = { name, children: [] }
        levelList.push(obj)
        // 7.若當前被增節點是葉子節點,則裁剪該節點子節點屬性
        if (name == pathList[pathList.length - 1]) {
          delete obj.children
        }
      }
      // 8.已有則進入下一層,繼續尋找
      levelList = obj.children
    }
  })
  return destList
}

測試

let srcList = ['動物-昆蟲-螞蟻', '動物-昆蟲', '植物-草-綠色', '植物-花-紅色','植物-花-黃色']
let result = listToTree(srcList)
console.log(JSON.stringify(result, null, 2))

資料庫變樹形

通常從資料庫查詢的樹形條源如下

arr = [
  { id: 1, pid: 0 },
  { id: 2, pid: 1 },
  { id: 3, pid: 2 },
  { id: 4, pid: 3 },
  { id: 5, pid: 3 },
  { id: 6, pid: 2 },
  { id: 7, pid: 1 }
]

要轉的樹形結構

0
|
1
/ \
2   7
/
3

3->2->1->0
7->1->0

辦法簡單,粗暴將源資料轉為節點陣列,然後填充建樹

// 轉為節點
nda = arr.map(v => {
  return { ...v, children: [] }
})

// 建立父子節點鏈路
nda.forEach(v => {
  let parent = nda.find(item => item.id == v.pid)
  if (parent) parent.children.push(v)
  let child = nda.find(item => item.pid == v.id)
  if (child) v.children.push(child)
});

列印id為1的資料樹形結構

let obj = nda.find(item => item.id == 1)
console.log(JSON.stringify(obj, null, 2))

{
  "id": 1,
  "pid": 0,
  "children": [
    {
      "id": 2,
      "pid": 1,
      "children": [
        {
          "id": 3,
          "pid": 2,
          "children": [
            {
              "id": 4,
              "pid": 3,
              "children": []
            },
            {
              "id": 4,
              "pid": 3,
              "children": []
            },
            {
              "id": 5,
              "pid": 3,
              "children": []
            }
          ]
        },
        {
          "id": 3,
          "pid": 2,
          "children": [
            {
              "id": 4,
              "pid": 3,
              "children": []
            },
            {
              "id": 4,
              "pid": 3,
              "children": []
            },
            {
              "id": 5,
              "pid": 3,
              "children": []
            }
          ]
        },
        {
          "id": 6,
          "pid": 2,
          "children": []
        }
      ]
    },
    {
      "id": 2,
      "pid": 1,
      "children": [
        {
          "id": 3,
          "pid": 2,
          "children": [
            {
              "id": 4,
              "pid": 3,
              "children": []
            },
            {
              "id": 4,
              "pid": 3,
              "children": []
            },
            {
              "id": 5,
              "pid": 3,
              "children": []
            }
          ]
        },
        {
          "id": 3,
          "pid": 2,
          "children": [
            {
              "id": 4,
              "pid": 3,
              "children": []
            },
            {
              "id": 4,
              "pid": 3,
              "children": []
            },
            {
              "id": 5,
              "pid": 3,
              "children": []
            }
          ]
        },
        {
          "id": 6,
          "pid": 2,
          "children": []
        }
      ]
    },
    {
      "id": 7,
      "pid": 1,
      "children": []
    }
  ]
}

小結

最後一種看起來簡單,實際上取巧了,資料庫取的條目通常是確定,所以可先將所有節點構建出來,後兩兩建立當前節點與上下級之關係。事實上,慣用法是遞迴建立。遞迴建立通常要求有出路,終止遞迴條件。而資料記錄,又恰巧只記錄一個節點的情況。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章