- Оригинальный адрес:Golang Datastructures: Trees
- Оригинальный автор:Ilija Eftimov
- Перевод с:Программа перевода самородков
- Постоянная ссылка на эту статью:GitHub.com/rare earth/gold-no…
- Переводчик:steinliber
- Корректор:Endone,LeoooY
Деревья — это структуры данных, с которыми вам не приходится иметь дело на протяжении большей части вашей карьеры программиста, или вы можете легко избежать их использования, даже если вы их не понимаете (это то, что я делал в прошлом).
Не поймите меня неправильно: массивы, списки, стеки и очереди — это очень мощные структуры данных, которые могут помочь вам продвинуться в программировании, но они не решают всех проблем, независимо от того, как их использовать. и насколько они эффективны. Когда вы добавляете хеш-таблицу в эту комбинацию, вы можете решить довольно много проблем, но для многих проблем это мощный (и, возможно, уникальный) инструмент.
Итак, давайте взглянем на древовидные структуры, а затем научимся их использовать, немного потренировавшись.
маленькая теория
Массивы, списки, очереди и стеки хранят данные в наборах с вершинами и хвостами, поэтому они называются «линейными структурами». Но когда дело доходит до структур данных, таких как деревья и графики, это сбивает с толку, потому что данные не хранятся в структуре линейным образом.
Деревья называются нелинейными структурами. На самом деле вы также можете сказать, что дерево является иерархической структурой данных, потому что его данные хранятся иерархическим образом.
Для вашего удовольствия, вот определение древовидной структуры из Википедии:
Дерево — это структура данных, состоящая из узлов (или вершин) и ребер, не содержащая циклов. Дерево без узлов называется пустым деревом. Непустое дерево состоит из корневого узла и иерархии, которая может быть образована несколькими уровнями дополнительных узлов.
Это определение означает, что дерево — это просто набор узлов (или вершин) и ребер (или соединений между узлами), и оно не содержит циклов.
Например, структура данных, представленная в графе, представляет собой комбинацию узлов, названных от A до F, с шестью ребрами. Хотя все его элементы создают впечатление, что они строят дерево, все узлы A, D, F имеют цикл, поэтому эта структура данных не является деревом.
Если мы разорвем ребро между узлами F и E и добавим узел G, соединяющий G и F ребром, мы получим такую структуру:
Теперь, поскольку мы устранили циклы в графе, мы можем сказать, что теперь у нас есть правильная древовидная структура. У него есть буква А, называемаякорневой узел, всего 7узел. Узел А имеет 3дочерний узел(B, D и F) и узлы на один слой ниже этих узлов (C, E и G соответственно). Следовательно, узел А имеет 6потомок узла. Кроме того, это дерево имеет 3 листовых узла (C, E и G), или они называются узлами без потомков.
Что общего у узлов B, D и F? Поскольку у них один и тот же родительский узел (узел A), ониродственный узел. Все они находятся на первом уровне, потому что каждому из них требуется только один шаг, чтобы достичь корневого узла. Например, узел G находится на втором уровне, потому что путь от G до AдорожкаДля: G -> F -> A нам нужно пройти два ребра, чтобы достичь узла A.
Теперь, когда мы немного познакомились с теорией деревьев, давайте посмотрим, как можно использовать деревья для решения некоторых задач.
Моделирование HTML-документов
Если вы разработчик программного обеспечения, который никогда не писал HTML, я предполагаю, что вы видели (или знаете), как выглядит HTML. Если вы все еще не знаете, я предлагаю вам щелкнуть правой кнопкой мыши на странице, которую вы сейчас читаете, и нажать «Просмотр исходного кода», чтобы увидеть ее.
Серьезно, иди проверь, я буду ждать здесь. . .
Браузеры имеют нечто встроенное, называемое DOM — кросс-платформенный, независимый от языка интерфейс прикладного программирования, который обрабатывает эти веб-документы как древовидную структуру, где каждый узел представляет собой объект, представляющий часть документа. Это означает, что когда браузер читает код HTML в вашем документе, он загружает документ и создает на его основе модель DOM.
Итак, давайте на мгновение представим, что мы разработчики браузеров Chrome или Firefox и нам нужно смоделировать DOM. Что ж, чтобы немного облегчить это упражнение, давайте посмотрим на небольшой HTML-документ:
<html>
<h1>Hello, World!</h1>
<p>This is a simple HTML document.</p>
</html>
Итак, если бы мы смоделировали этот документ как древовидную структуру, он выглядел бы так:
Теперь мы можем рассматривать текстовые узлы как отдельныеNode
, но для простоты можно предположить, что любой элемент HTML может содержать текст.
html
У узла будет два дочерних узла,h1
иp
узлы, которые содержат поляtag
,text
иchildren
. Давайте поместим это в код:
type Node struct {
tag string
text string
children []*Node
}
ОдинNode
Необязательными будут только имя метки и дочерние узлы. Давайте пройдемся по тому, что мы видим вышеNode
tree, чтобы попробовать создать этот HTML-документ самостоятельно:
func main() {
p := Node{
tag: "p",
text: "This is a simple HTML document.",
id: "foo",
}
h1 := Node{
tag: "h1",
text: "Hello, World!",
}
html := Node{
tag: "html",
children: []*Node{&p, &h1},
}
}
Это выглядит нормально, у нас есть базовая древовидная структура и она работает.
Build MyDOM - простая замена DOM 😂
Теперь, когда у нас есть древовидная структура, давайте вернемся назад и посмотрим, что делает DOM. Например, если бы MyDOM(TM) использовался вместо DOM в реальной среде, мы должны были бы иметь доступ к его узлам и модифицировать их с помощью JavaScript.
Самый простой способ сделать это с помощью JavaScript — использовать следующий код
document.getElementById('foo')
Эта функция будет вdocument
Найдите в дереве сfoo
Узел как идентификатор. Давайте обновим нашNode
структуру для большей функциональности, затем напишите функцию запроса для нашей древовидной структуры:
type Node struct {
tag string
id string
class string
children []*Node
}
Теперь каждый из нашихNode
структура будет иметьtag
,children
, что указывает наNode
срез указателей на дочерние узлы,id
представляет идентификатор в этом узле DOM,class
Относится к классу, который можно применить к этому узлу DOM.
Теперь вернемся к нашему предыдущемуgetElementById
функция запроса. как это сделать. Во-первых, давайте построим древовидную структуру, которую можно использовать для проверки нашего алгоритма запроса:
<html>
<body>
<h1>This is a H1</h1>
<p>
And this is some text in a paragraph. And next to it there's an image.
<img src="http://example.com/logo.svg" alt="Example's Logo"/>
</p>
<div class='footer'>
This is the footer of the page.
<span id='copyright'>2019 © Ilija Eftimov</span>
</div>
</body>
</html>
Это очень сложный HTML-документ. давайте использоватьNode
Представьте его структуру в виде структуры на языке Go:
image := Node{
tag: "img",
src: "http://example.com/logo.svg",
alt: "Example's Logo",
}
p := Node{
tag: "p",
text: "And this is some text in a paragraph. And next to it there's an image.",
children: []*Node{&image},
}
span := Node{
tag: "span",
id: "copyright",
text: "2019 © Ilija Eftimov",
}
div := Node{
tag: "div",
class: "footer",
text: "This is the footer of the page.",
children: []*Node{&span},
}
h1 := Node{
tag: "h1",
text: "This is a H1",
}
body := Node{
tag: "body",
children: []*Node{&h1, &p, &div},
}
html := Node{
tag: "html",
children: []*Node{&body},
}
Мы начинаем строить эту древовидную структуру снизу вверх. Это означает построение структуры от самой глубоко вложенной структуры доbody
иhtml
узел. Давайте посмотрим на график этой древовидной структуры:
Реализовать запрос узла 🔎
Давайте перейдем к нашей цели сделать JavaScript доступным в нашемdocument
вызыватьgetElementById
и найти то, что он хочет найтиNode
.
Для этого нам нужно реализовать алгоритм запроса дерева. Наиболее популярными методами поиска (или обхода) графовых и древовидных структур являются поиск в ширину (BFS) и поиск в глубину (DFS).
Поиск в ширину ⬅➡
Как следует из названия, метод обхода, принятый BFS, сначала будет учитывать «ширину» исследовательского узла, а затем «глубину». Ниже приведена визуальная диаграмма алгоритма BFS, проходящего через всю древовидную структуру:
Как видите, сначала алгоритм выполняет два шага в глубину (черезhtml
иbody
узел), затем он проходитbody
Все детские узлы, наконец, перейдите к следующему слою для доступаspan
иimg
узел.
Если вам нужны пошаговые инструкции, это будет:
- мы начинаем с корня
html
Узел начинается - мы подталкиваем его к
queue
- Мы начинаем входить в цикл, если
queue
Не пустой, этот цикл будет работать вечно - мы проверяем
queue
Соответствует ли следующий элемент запросу. Если есть совпадение, мы возвращаемся к узлу, и все кончено. - Когда совпадений не найдено, мы помещаем всех дочерних элементов проверяемого узла в очередь, чтобы их можно было проверить позже.
-
GOTO
четвертый шаг
Давайте взглянем на простую реализацию этого алгоритма в Go, и я поделюсь некоторыми советами о том, как легко запомнить алгоритм.
func findById(root *Node, id string) *Node {
queue := make([]*Node, 0)
queue = append(queue, root)
for len(queue) > 0 {
nextUp := queue[0]
queue = queue[1:]
if nextUp.id == id {
return nextUp
}
if len(nextUp.children) > 0 {
for _, child := range nextUp.children {
queue = append(queue, child)
}
}
}
return nil
}
Этот алгоритм имеет 3 ключевых момента:
-
queue
- он будет содержать все узлы, посещенные алгоритмом - Получать
queue
первый элемент в , проверьте, соответствует ли он, если узел не соответствует, перейдите к следующему узлу - viewing
queue
поместите все дочерние элементы узла перед следующим элементомпоставить в очередь.
По сути, весь алгоритм реализован вокруг добавления дочерних узлов в очередь и обнаружения узлов, которые уже находятся в очереди. Конечно, если мы все еще не находим совпадения в конце очереди, мы возвращаемсяnil
вместо того, чтобы указывать наNode
указатель.
Поиск в глубину ⬇
Для полноты картины давайте посмотрим, как работает DFS.
Как упоминалось ранее, поиск в глубину сначала посещает как можно больше узлов в глубину, пока не достигнет конечного узла в древовидной структуре. Когда это происходит, он возвращается к узлу выше, находит другую ветвь в древовидной структуре и продолжает движение вниз.
Давайте посмотрим, как это выглядит:
Не волнуйтесь, если это вас смущает — я добавил больше деталей, чтобы поддержать мое объяснение в пошаговом руководстве.
Алгоритм начинается точно так же, как BFS — он начинается сhtml
прибытьbody
сноваdiv
узел. Тогда, напротив, алгоритм не продолжает обход до тех пор, покаh1
узел, он переходит к листовому узлуspan
Один шаг вперед. как только он найдетspan
является листовым узлом, он вернетdiv
узел, чтобы найти другие ветки для изучения. Так какdiv
тоже не может быть найден, поэтому он возвращаетсяbody
узел, в этом узле он находит новую ветку, он посетит ветку вh1
узел. Затем он продолжит те же шаги, что и раньше — возвратbody
Затем узел обнаруживает, что есть еще одна ветвь для изучения — в конечном итоге посещаяp
иimg
узел.
Если вам интересно, "как мы вернемся к родителю без указателя на родителя", то вы забыли один из старейших приемов в книге - рекурсию. Давайте посмотрим на простую рекурсивную реализацию этого алгоритма в Go:
func findByIdDFS(node *Node, id string) *Node {
if node.id == id {
return node
}
if len(node.children) > 0 {
for _, child := range node.children {
findByIdDFS(child, id)
}
}
return nil
}
Поиск по названию класса 🔎
Еще одна функция, которой должен обладать MyDOM™, — это поиск узлов по имени класса. По сути, когда скрипт JavaScript выполняетсяgetElementsByClassName
, MyDOM должен уметь собирать все узлы с определенным именем класса.
Как вы понимаете, это также алгоритм, который должен исследовать все дерево структуры MyDOM(TM) для получения узлов, удовлетворяющих определенным условиям.
Для простоты давайте сначала реализуемNode
метод построения, называемыйhasClass
:
func (n *Node) hasClass(className string) bool {
classes := strings.Fields(n.classes)
for _, class := range classes {
if class == className {
return true
}
}
return false
}
hasClass
ПолучатьNode
Поле классов структуры, разбивая их по символу пробела, затем перебирая этот фрагмент классов и пытаясь найти имя класса, которое мы хотим. Давайте напишем несколько тестовых примеров для проверки этой функции:
type testcase struct {
className string
node Node
expectedResult bool
}
func TestHasClass(t *testing.T) {
cases := []testcase{
testcase{
className: "foo",
node: Node{classes: "foo bar"},
expectedResult: true,
},
testcase{
className: "foo",
node: Node{classes: "bar baz qux"},
expectedResult: false,
},
testcase{
className: "bar",
node: Node{classes: ""},
expectedResult: false,
},
}
for _, case := range cases {
result := case.node.hasClass(test.className)
if result != case.expectedResult {
t.Error(
"For node", case.node,
"and class", case.className,
"expected", case.expectedResult,
"got", result,
)
}
}
}
Как вы видете,hasClass
функция проверитNode
Находится ли имя класса в списке имен классов. Теперь давайте перейдем к завершению реализации MyDOM, найдя все совпадения по имени класса.Node
.
func findAllByClassName(root *Node, className string) []*Node {
result := make([]*Node, 0)
queue := make([]*Node, 0)
queue = append(queue, root)
for len(queue) > 0 {
nextUp := queue[0]
queue = queue[1:]
if nextUp.hasClass(className) {
result = append(result, nextUp)
}
if len(nextUp.children) > 0 {
for _, child := range nextUp.children {
queue = append(queue, child)
}
}
}
return result
}
Алгоритм кажется знакомым? Это потому, что вы смотрите на модифицированныйfindById
функция.findAllByClassName
как это работает иfindById
Аналогично, но вместо возврата сразу после того, как совпадение найдено, он возвращает совпавшееNode
добавить вresult
в ломтиках. Это будет продолжаться до тех пор, пока всеNode
.
Если совпадений не найдено, тоresult
Срез будет пуст. Если какие-либо из них совпадают, они будут использоваться какresult
Часть возвращенной.
Последнее, что следует здесь отметить, это то, что мы используем подход в ширину для обхода древовидной структуры — этот алгоритм использует очередь для хранения каждогоNode
структуру, перебирая эту очередь и добавляя совпадения, если они найденыresult
в ломтиках.
удалить узел 🗑
Еще одна функция, которая часто используется в Dom, — это удаление узлов. Точно так же, как это может сделать DOM, наша MyDOM(TM) тоже должна это делать.
Самый простой способ сделать это в Javascript:
var el = document.getElementById('foo');
el.remove();
Хотя нашdocument
знать, как с этим боротьсяgetElementById
(позднее по телефонуfindById
), но нашNode
не знаю, как обращаться сremove
функция. Удалено из MyDOM(TM)Node
Потребуются два шага:
- мы нашли
Node
родительский узел, а затем удалить его из набора дочерних узлов родительского узла; - Если вы хотите удалить
Node
Есть дочерние узлы, и мы должны удалить эти дочерние узлы из DOM. Это означает, что мы должны удалить все указатели на эти дочерние узлы и их родительские узлы (то есть удаляемый узел), чтобы сборщик мусора в Go мог освободить занятую память.
Вот простой способ добиться вышеизложенного:
func (node *Node) remove() {
// Remove the node from it's parents children collection
for idx, sibling := range n.parent.children {
if sibling == node {
node.parent.children = append(
node.parent.children[:idx],
node.parent.children[idx+1:]...,
)
}
}
// If the node has any children, set their parent to nil and set the node's children collection to nil
if len(node.children) != 0 {
for _, child := range node.children {
child.parent = nil
}
node.children = nil
}
}
Один*Node
будет иметьremove
функция, которая выполняет два шага, описанных выше, для достиженияNode
удалить операцию.
На первом этапе мы преобразуем этот узел изparent
Узел вынимается из списка дочерних узлов узла, и узел удаляется путем обхода этих дочерних узлов, слияния элементов до и после узла для формирования нового списка.
На втором шаге, проверив, есть ли у этого узла дети, мыparent
Цитата удалена, тогда ставьте этуNode
Поле дочернего узла имеет значениеnil
.
что дальше?
Очевидно, что наша реализация MyDOM(TM) никогда не сможет заменить DOM. Тем не менее, я уверен, что это интересный пример, который поможет вам учиться, и это также интересный вопрос. Мы взаимодействуем с браузерами каждый день, поэтому размышления о том, как они работают внутри, могут быть интересным упражнением.
Если вы хотите использовать нашу древовидную структуру и написать для нее дополнительные функции, вы можете посетить JavaScript HTML DOM WC3.ДокументацияЗатем рассмотрите возможность добавления дополнительных функций в MyDOM.
Очевидно, что цель этой статьи — дать вам больше узнать о древовидной (графовой) структуре и понять современные популярные алгоритмы поиска/обхода. Тем не менее, пожалуйста, продолжайте изучать и практиковаться в любом случае, и оставьте комментарий под статьей, если есть какие-либо улучшения в вашей реализации MyDOM.
Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.
Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из ИнтернетаНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.