Как построить дерево из плоского списка с отступом в JavaScript?

#javascript #algorithm #tree

#javascript #алгоритм #дерево

Вопрос:

Я всегда испытываю трудности, когда сталкиваюсь с этой проблемой, и это требует серьезной работы. В настоящее время я пытаюсь решить эту проблему, и это, вероятно, займет у меня несколько дней. Хотел посмотреть, есть ли у вас система или простой способ решения этой проблемы.

В принципе, скажем, у вас есть плоский список узлов DOM с отступом влево с шагом 15 пикселей. Визуально он формирует дерево, похожее на файловый браузер. Но структурно в DOM это реализовано как плоский список. Как вы можете затем выполнить итерацию по списку и построить дерево?

 <div style='padding-left: 0px'>A</div>
<div style='padding-left: 15px'>AA</div>
<div style='padding-left: 15px'>AB</div>
<div style='padding-left: 30px'>ABA</div>
<div style='padding-left: 30px'>ABB</div>
<div style='padding-left: 45px'>ABBA</div>
<div style='padding-left: 45px'>ABBB</div>
<div style='padding-left: 45px'>ABBC</div>
<div style='padding-left: 30px'>ABC</div>
<div style='padding-left: 15px'>AC</div>
<div style='padding-left: 0px'>B</div>
<div style='padding-left: 0px'>C</div>
...
  

Затем это должно превратиться в дерево JSON, подобное этому:

 [ 
  {
    title: 'A',
    children: [
      {
        title: 'AA',
        children: []
      },
      {
        title: 'AB',
        children: [
          {
            title: 'ABA',
            children: []
          },
          {
            title: 'ABB',
            children: [
              {
                title: 'ABBA',
                children: []
              },
              {
                title: 'ABBB',
                children: []
              },
              {
                title: 'ABBC',
                children: []
              }
            ]
          },
          {
            title: 'ABC',
            children: []
          }
        ]
      },
      {
        title: 'AC'
      }
    ]
  },
  {
    title: 'B',
    children: []
  },
  {
    title: 'C',
    children: []
  }
]
  

Как вы это делаете? Я заблудился:

 let tree = []
let path = [0]

let items = list('div')

items.forEach(item => {
  let left = parseInt(item.style[`padding-left`] || 0) % 15
  let set = tree
  let p = path.concat()
  while (left) {
    let x = p.shift()
    set[x] = set[x] || { children: [] }
    set = set[x].children
    left--
  }
})

function list(s) {
  return Array.prototype.slice.call(document.querySelectorAll(s))
}
  

Ответ №1:

Это стек, поскольку он последовательный. Что-то вроде этого?

Мы предполагаем, что структура папок полностью «расширена», поэтому родительский элемент каждой папки (кроме самого нижнего, для которого родительский элемент является корневым) должен быть проверен до текущей. Родительский элемент также должен иметь более низкое назначение «padding-left».

ptrs это стек, к которому мы добавляем ссылку на следующую проверенную папку. Папка наверху (в конце) стека — это последняя папка, которую мы исследовали. Если эти папки в верхней части стека имеют более высокое или равное «заполнению слева» присвоение, они не могут быть родительскими для текущей папки; и у нас не может быть больше дочерних элементов после текущей папки, поэтому мы удаляем (всплываем) их, пока не найдем последнююпомещена папка с более низким «отступом влево».

 function getData(s){
  const left =  s.match(/d /)[0];
  const title = s.match(/[A-Z] /)[0];
  return [left, title];
}

function f(divs){
  const tree = {
    title: 'root',
    children: []
  };
  const ptrs = [[0, tree]]; // stack
  for (let str of divs){
    const [left, title] = getData(str);
    while (ptrs.length amp;amp; ptrs[ptrs.length-1][0] >= left)
      ptrs.pop();
    parent = ptrs.length ? ptrs[ptrs.length-1][1] : tree;
    const obj = {title: title, children: []};
    parent.children.push(obj);
    ptrs.push([left, obj]);
  }
  return tree;
}

var divs = [
  "<div style='padding-left: 0px'>A</div>",
  "<div style='padding-left: 15px'>AA</div>",
  "<div style='padding-left: 15px'>AB</div>",
  "<div style='padding-left: 30px'>ABA</div>",
  "<div style='padding-left: 30px'>ABB</div>",
  "<div style='padding-left: 45px'>ABBA</div>",
  "<div style='padding-left: 45px'>ABBB</div>",
  "<div style='padding-left: 45px'>ABBC</div>",
  "<div style='padding-left: 30px'>ABC</div>",
  "<div style='padding-left: 15px'>AC</div>",
  "<div style='padding-left: 0px'>B</div>",
  "<div style='padding-left: 0px'>C</div>"
]

console.log(f(divs));  

Комментарии:

1. Работает как шарм! Не могли бы вы немного объяснить общую технику, мне это все еще кажется волшебством 🙂 Может быть, пройдитесь по строкам кода в цикле и их обоснованию.

2. @LancePollard Конечно. Я добавил объяснение. Пожалуйста, дайте мне знать, если что-то еще нуждается в уточнении.

Ответ №2:

Интересное упражнение. Вот еще один подход, который немного более подробный, чем предыдущее решение, но также работает с узлами dom, а не со строковым html

 const buildTree = (selector) => {
    const elems = [...document.querySelectorAll(selector)]
              .map((el,i)=>({el, title: el.textContent, idx:i, inset: parseInt(el.style.paddingLeft)}));

    const getChildren = ({inset:pInset, idx:start}) => {
      const nextParentIdx = elems.findIndex(({inset, idx}, i)=> inset <= pInset amp;amp; i >start);
      const desc = elems.slice(start, nextParentIdx 1 )
                  .filter(({inset})=>inset-pInset === 15);
      return desc.map(getItem); 
    }

    const getItem = (o)=>{
      return {title: o.title, children: getChildren(o)}
    }
    
    return elems.filter(({inset})=>!inset).map(getItem)
}

   
console.log(JSON.stringify(buildTree('div'),null, 4))  
 .as-console-wrapper {   max-height: 100%!important;top:0;}  
 <div style='padding-left: 0px'>A</div>
<div style='padding-left: 15px'>AA</div>
<div style='padding-left: 15px'>AB</div>
<div style='padding-left: 30px'>ABA</div>
<div style='padding-left: 30px'>ABB</div>
<div style='padding-left: 45px'>ABBA</div>
<div style='padding-left: 45px'>ABBB</div>
<div style='padding-left: 45px'>ABBC</div>
<div style='padding-left: 30px'>ABC</div>
<div style='padding-left: 15px'>AC</div>
<div style='padding-left: 0px'>B</div>
<div style='padding-left: 0px'>C</div>  

Комментарии:

1. Конечно, вы знаете, что я использовал только string HTML в качестве примера. Я был на мобильном телефоне, плюс я не хотел с ним связываться 🙂