Получить уровень иерархии

У меня есть массив объектов, где каждый объект имеет свойство id и ParentId (поэтому их можно расположить в деревьях). Они не в определенном порядке.

Обратите внимание, что id и parentId не будут целыми числами, они будут строками (просто хотелось, чтобы пример кода был чище..)

Есть только один корень: скажем, его id:1 Данные выглядят так:

 data = [
   {
        id:"id-2",
        parentId:"id-3"
    },
    {
        id:"id-4",
        parentId:"2"
    },
    {
        id:"id-3",
        parentId:"id-4"
    },
    {
        id:"id-5",
        parentId:"id-4"
    },
    {
        id:"id-6",
        parentId:"id-1"
    },
    {
        id:"id-7",
        parentId:"id-1"
    }
    // and so on...
]

Я ищу эффективный способ дать каждому объекту свойство level, которое должно указывать уровень вложенности, которым он является...

Тогда они должны выглядеть так:

data = [
    {
        id:"id-2",
        parentId:"id-1",
        level:2
    },
    {
        id:"id-3",
        parentId:"id-4",
        level:5

    },
    {
        id:"id-4",
        parentId:"id-2",
        level:3

    },
    {
        id:"id-5",
        parentId:"id-4",
        level:5
    },
    {
        id:"id-6",
        parentId:"id-1",
        level:2

    },
    {
        id:"id-7",
        parentId:"id-3",
        level:4
    }
    // and so on...
]

Вкратце:

Я хочу, чтобы этот level добавлялся динамически, проходя через массив и выясняя иерархию.

Кроме того, (если возможно) они должны быть отсортированы в соответствии с порядком, например, все объекты level:3 от одного и того же родителя должны быть рядом друг с другом, а не должны быть братья и сестры одного и того же родителя рядом друг с другом, а не два двоюродные братья 3 уровня рядом друг с другом.


person adardesign    schedule 03.01.2013    source источник
comment
как выглядит ваш корневой элемент?   -  person Joe Coder    schedule 03.01.2013
comment
Есть один корень, скажем, его parentId:1 (будет строкой)   -  person adardesign    schedule 03.01.2013
comment
данные уже отсортированы по id?   -  person Joe Coder    schedule 03.01.2013
comment
@JoeCoder, нет, это будет случайный порядок.   -  person adardesign    schedule 03.01.2013
comment
Каким будет идентификатор и parentId корневого элемента? Каким должен быть уровень этого элемента?   -  person ATOzTOA    schedule 03.01.2013
comment
@ATOzTOA корневой идентификатор: 1, это не часть данных ..   -  person adardesign    schedule 03.01.2013
comment
Я обновил описание, чтобы оно было более понятным.. спасибо за ваш вклад   -  person adardesign    schedule 03.01.2013


Ответы (4)


Рабочий пример приведенного ниже кода находится на jsFiddle.

Индексируйте дерево по идентификатору и просматривайте его вверх, от каждого узла, и считайте, пока не дойдете до корня. Сначала индексируя, мы приближаемся к временной сложности O (n) (в зависимости от плотности дерева). ****Обновлено, чтобы удовлетворить требования сортировки и разрешить исключение корневого узла***:

function levelAndSort(data, startingLevel) {
    // indexes
    var indexed = {};        // the original values
    var nodeIndex = {};      // tree nodes
    var i;
    for (i = 0; i < data.length; i++) {
        var id = data[i].id;
        var node = {
            id: id,
            level: startingLevel,
            children: [],
            sorted: false
        };
        indexed[id] = data[i];
        nodeIndex[id] = node;
    }

    // populate tree
    for (i = 0; i < data.length; i++) {
        var node = nodeIndex[data[i].id];
        var pNode = node;
        var j;
        var nextId = indexed[pNode.id].parentId;
        for (j = 0; nextId in nodeIndex; j++) {
            pNode = nodeIndex[nextId];
            if (j == 0) {
                pNode.children.push(node.id);
            }
            node.level++;
            nextId = indexed[pNode.id].parentId;
        }
    }

    // extract nodes and sort-by-level
    var nodes = [];
    for (var key in nodeIndex) {
        nodes.push(nodeIndex[key]);
    }
    nodes.sort(function(a, b) {
        return a.level - b.level;
    });

    // refine the sort: group-by-siblings
    var retval = [];
    for (i = 0; i < nodes.length; i++) {
        var node = nodes[i];
        var parentId = indexed[node.id].parentId;
        if (parentId in indexed) {
            var pNode = nodeIndex[parentId];
            var j;
            for (j = 0; j < pNode.children.length; j++) {
                var child = nodeIndex[pNode.children[j]];
                if (!child.sorted) {
                    indexed[child.id].level = child.level;
                    retval.push(indexed[child.id]);
                    child.sorted = true;
                }
            }
        }
        else if (!node.sorted) {
            indexed[node.id].level = node.level;
            retval.push(indexed[node.id]);
            node.sorted = true;
        }
    }
    return retval;
}

Пример:

// level 0 (root) excluded
var startingLevel = 1;
var someData = [
    {id : "id-1", parentId : "id-0"},
    {id : "id-2", parentId : "id-0"},
    {id : "id-3", parentId : "id-2"},
    {id : "id-4", parentId : "id-3"},
    {id : "id-5", parentId : "id-4"},
    {id : "id-6", parentId : "id-4"},
    {id : "id-7", parentId : "id-0"},
    {id : "id-8", parentId : "id-1"},
    {id : "id-9", parentId : "id-7"},
    {id : "id-10", parentId : "id-1"},
    {id : "id-11", parentId : "id-1"},
    {id : "id-12", parentId : "id-1"}
];
var outputArray = levelAndSort(someData, startingLevel);

Выход:

введите здесь описание изображения

Примечание

Если вы измените порядок ввода, сортировка получится немного другой, но все равно правильной (т. е. в порядке уровней, сгруппированных по родственным элементам).

person Joe Coder    schedule 03.01.2013
comment
Выглядит отлично! есть ли возможность опустить фактический корень? (в ваших данных это первый ..) - person adardesign; 04.01.2013

Я не уверен, где вы берете значение для level, поэтому я предполагаю, что это просто целое число. Кстати, вы можете добавить свойство уровня к каждому элементу вашего массива, перебирая его.

for (var i = 0, l = data.length; i < l; i++) { 
    data[i].level = i
}

что даст

data = [{id:"1", parentId:"3", level:0 }, {id:"2", parentId:"1", level:1 } ...]
person Mark Dee    schedule 03.01.2013
comment
Я хочу, чтобы level добавлялось динамически через цикл через массив и выяснение иерархии, а также, поскольку я указал, что id не будет целым числом, они будут строкой - person adardesign; 03.01.2013
comment
так что вы имеете в виду, выяснить, насколько глубоко элемент с этим идентификатором внутри этого родителя? и назначить его свойству уровня? - person Mark Dee; 03.01.2013
comment
используя jQuery в том же цикле for выше data[i].level = $(data[i].id).parentsUntil($(data[i].parentId)).length + 1; - person Mark Dee; 03.01.2013
comment
@Mark Dee Спасибо, но id и parentId не являются целыми числами, это будут строки .. - person adardesign; 03.01.2013

Вот ваш рабочий код. Уровень начинается со 2.

ПРЕДУПРЕЖДЕНИЕ. Если уровень не может быть рассчитан, приложение может войти в бесконечный цикл. Итак, убедитесь, что parentId действителен для всех объектов и хотя бы у одного из них есть parentId="id-1".

<script type="text/javascript">
    data = [
        {
            id:"id-2",
            parentId:"id-3"
        },
        {
            id:"id-4",
            parentId:"id-2"
        },
        {
            id:"id-3",
            parentId:"id-5"
        },
        {
            id:"id-5",
            parentId:"id-1"
        }
    ];

    function processData() {
        flag = true;

        while(flag) {
            flag = false;

            for(var i = 0; i < data.length; i++) {
                if(data[i].parentId == "id-1") {
                    data[i].level = 2;
                } else {
                    l = getLevel(data[i].parentId);
                    if(l > 0) {
                        data[i].level = l + 1;
                    } else {
                        flag = true;
                    }
                }
            }
        }
    }

    function getLevel(id) {
        for(var i = 0; i < data.length; i++) {
            if(data[i].id == id) {
                if(data[i].level) {
                    return data[i].level;
                } else {
                    return 0;
                }
            }
        }

        return 0;
    }

    processData();

    console.log(data);

</script>
person ATOzTOA    schedule 03.01.2013

Один из способов решить эту проблему без необходимости рекурсии — создать иерархию DOM. Для каждого элемента в массиве создайте div и прикрепите его к его родителю. затем пройдитесь по DOM и назначьте уровни (сверху — уровень 1, затем добавьте 1 для каждого дочернего элемента).

Я создал примерную демонстрацию здесь: http://jsfiddle.net/4AqgM/.

Выдержка из кода:

top.dataset.level="1";
var topChildren=top.getElementsByTagName("div");
for (var i=0;i<topChildren.length;i++) {
topChildren[i].dataset.level=parseInt(topChildren[i].parentNode.dataset.level)+1;
}
person Christophe    schedule 03.01.2013
comment
@adardesign извините, я не уверен, что понимаю. Дивы - это всего лишь промежуточный шаг, и они не предназначены для дальнейшего использования. В вашем вопросе не говорится, что вы хотите (вложенных или нет). - person Christophe; 03.01.2013
comment
Умный способ использовать DOM в качестве структуры данных, но избыточный, потому что дерево уже существует в виде серии parentId ссылок. - person Joe Coder; 03.01.2013
comment
@JoeCoder согласен с избыточностью. Мой подход был бы интересен, если бы целью уровней (не объясненных в вопросе) было построение дерева DOM. - person Christophe; 03.01.2013
comment
@Christophe, верно, цель состоит в том, чтобы в конечном итоге построить DOM, но не вложенный, я буду генерировать плоские родители DOM рядом с детьми ... - person adardesign; 04.01.2013
comment
@adardesign Понятно. Я немного изменил свою демонстрацию, чтобы убрать вложенные элементы div в конце, надеюсь, это поможет: jsfiddle.net/4AqgM /3 . Опять же, это грубый код с возможностью оптимизации... - person Christophe; 04.01.2013