Велосипедим связанный список на Wolfram

—

от автора

Георгий Вицин пытается освободиться за O(1)

Георгий Вицин пытается освободиться за O(1)

Возможно 11 подписчиков моего блога обратили внимание на тот факт, что все мои статьи касаются языка Wolfram, а несколько последних статей вышли довольно громоздкими. Одна из последних статей была помечена Хабром как требующая в среднем 32 минуты на прочтение. Я посчитал, что это может отпугнуть пользователей (все 11 человек и не факт, что все они до сих пор читают Хабр) и решил попробовать писать более короткие статьи. Плюс я сам перестану теряться в большом количестве текста.

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

Ниже реализация структуры данных LinkedList на Wolfram Language.

О связанном списке

Не думаю, что это требует длинных объяснений, но если совсем коротко, то такая структура данных ценна тем, что:

  • Добавление в конец/начало за O(1)

  • Удаление из начала за O(1)

  • Поиск или получение по индексу за O(n) — это не очень ценно

  • Вставка в середину за O(1), но поиск-то долгий

  • Получение всех элементов за O(n) — как у большинства структур

Эта структура данных используется для реализации стека и очереди, так как она оптимизированно добавляет элементы в начало и конец. В Wolfram Language начиная с версии 12.1 есть такие структуры данных как связанный список и двунаправленный связанный список, а также стек, очередь, кольцевой буфер и некоторые другие. Но нас не интересует встроенная реализация, так как здесь клуб вело-любителей. Но если что, вот так создаются связанные списки в WL:

linkedList = CreateDataStructure["LinkedList"]; Do[linkedList["Append", i], {i, 1000}] linkedList["Visualization"]
doublyLinkedList = CreateDataStructure["DoublyLinkedList"]; Do[doublyLinkedList["Append", i], {i, 1000}] doublyLinkedList["Visualization"]
Визуализация связанного списка и двунаправленного связанного списка

Визуализация связанного списка и двунаправленного связанного списка

Узел списка

Что ж, приступим! В первую очередь реализуем не сам список целиком, а только один узел этого списка. Сначала легкая подготовка:

$HistoryLength = 0;  ClearAll[LinkedListNode];  SetAttributes[LinkedListNode, HoldFirst]; 

HoldFirst — позволит держать первый аргумент невыполненным. В WL — это один из способов хранения «ссылки» на выражение, вместо самого выражения, т.е. значения. Теперь создаем «конструктор» для узла списка (на самом деле это просто функция):

LinkedListNode[ref_Symbol, value_, next_] :=  With[{uuid = CreateUUID["LINKEDLISTNODE-"]},  If[Not[AssociationQ[ref]], ref = <|"Nodes" -> <||>|>];  ref["Nodes", uuid] = <| "Value" -> value,  "Next" -> next |>;  LinkedListNode[ref, uuid] ]; 
  • ref — та самая «ссылка», в которой хранятся данные списка и узла. Если эта ссылка не является ассоциацией, то ей задается новое значение.

  • uuid — уникальный номер узла

  • value — значение

  • next — следующий узел

Вот пример узла:

ClearAll[ref];  node1 = LinkedListNode[ref, 1, Null] (*LinkedListNode[ref, "LINKEDLISTNODE-ad484b44-803b-436f-966a-b359ac961235"]*)

Вот какое значение будет хранить ссылка-символ, после вызова конструктора:

ref (*<|   "Nodes" -> <|     "LINKEDLISTNODE-ad484b44-803b-436f-966a-b359ac961235" -> <|       "Value" -> 1, "Next" -> Null     |>   |> |>*)

Обратите внимание, что символ ref имеет значение, но внутри выражения с заголовком LinkedListNode благодаря атрибуту оно все еще остается в виде невычисленного символа. Лучше понять смысл «удерживания» можно на простом примере:

x = 1;   f[x]  SetAttributes[g, HoldFirst]  g[x]
Как работает HoldFirst

Как работает HoldFirst

Теперь добавим узлу методы. Извлечение его свойств:

LinkedListNode[ref_Symbol, uuid_String][key_] :=  ref["Nodes", uuid, key]

Уникального номера:

LinkedListNode[ref_Symbol, uuid_String]["UUID"] :=  uuid; 

И изменение значений свойств:

LinkedListNode /: Set[LinkedListNode[ref_Symbol, uuid_String][key_], value_] :=  ref["Nodes", uuid, key] = value; 

Выше мы по сути переопределяем «оператор присваивания», т.е. «=». Сделано это при помощи так называемого UpSet (obj /: func[obj] := expr[obj]), когда определение связывается не с функцией, а с самим объектом, на котором вызывается функция. Еще добавим метод связывания двух узлов:

LinkedListNode /: link[node_LinkedListNode, next_LinkedListNode] := ( If[node["Next"] =!= Null, next["Next"] = node["Next"]];  node["Next"] = next ); 

Сделаем так, что этот метод может объединить целый список узлов:

link[list_List] := Fold[link, First[list], Rest[list]]; 

Как это работает:

node2 = LinkedListNode[ref, 2, Null];  node3 = LinkedListNode[ref, 3, Null];  link[{node1, node2, node3}] (* => node3 *)  (* node1 -> node2 -> node3 *)

Пока что реализованных методов достаточно. Перейдем к реализации самого списка.

Реализация списка

Как обычно подготовка:

ClearAll[LinkedList];  SetAttributes[LinkedList, HoldFirst]; 

И конструктор списка. Особенность этой функции в том, что она проверяет «ссылку» и если она неподходящая, то меняет ее значение, а если подходящая то возвращает выражение как есть:

LinkedList[ref: _Symbol?(Not@*AssociationQ): Automatic] :=  Which[ ref === Automatic,  With[{$ref = Unique["LINKEDLIST`$"]}, LinkedList[$ref]],  True,  Clear[ref];  ref = <|"Nodes" -> <||>, "Size" -> 0, "Head" -> Null, "Tail" -> Null|>;  LinkedList[ref] ]; 

ref: _Symbol?(Not@*AssociationQ): Automatic слева направо делает следующее:

  • ref: _Symbol — значит аргумент должен иметь тип Symbol (т.е. ссылка)

  • ?(Not@*AssociationQ) — проверяет аргумент применяя функцию справа от знака вопроса

  • Not@*AssociationQ — композиция двух функций

  • : Automatic — после второго двоеточия значение по умолчанию. Интересно, что оно может не проходить проверку слева

Теперь метод добавления в конец. Мы создадим функцию append. Она будет брать хвост списка и если его значение это Null, то добавлять и хвост и голову. Но если хвост уже указывает на существующий узел, то к этому узлу привязывается новый узел, а хвост указывает на новый узел. Все тоже самое, но с другой стороны сделаем для prepend:

LinkedList /: append[LinkedList[ref_Symbol], item_] :=  With[{node = LinkedListNode[ref, item, Null]},  ref["Size"] = ref["Size"] + 1;  If[ref["Tail"] === Null,  ref["Head"] = node;  ref["Tail"] = node; ,  (*Else*) link[ref["Tail"], node];  ref["Tail"] = node;  ];  ]; 
LinkedList /: prepend[LinkedList[ref_Symbol], item_] :=  With[{node = LinkedListNode[ref, item, Null]},  ref["Size"] = ref["Size"] + 1;  If[ref["Head"] === Null,  ref["Head"] = node;  ref["Tail"] = node; ,  (*Else*) link[node, ref["Head"]];  ref["Head"] = node;  ];  ]; 

Для обычных списков в WL есть функции First и Last, которые получают первый и последний элемент. Но мы для списка определим отдельные «свойства», которые получают голову и хвост:

LinkedList[ref_Symbol]["Head"] :=  If[ref["Size"] > 0, ref["Head"]["Value"]]; 
LinkedList[ref_Symbol]["Tail"] :=  If[ref["Size"] > 0, ref["Tail"]["Value"]]; 

Так же переопределим функцию, которая получает элемент по индексу (тот самый метод, который занимает O(n)):

LinkedList /: Part[LinkedList[ref_Symbol], i_Integer] /; Abs[i] <= ref["Size"]:=  Block[{$node},  Which[i === 0, LinkedList,  i === 1, ref["Head"],  i === -1, ref["Tail"],  i > 0,  $node = ref["Head"];  Do[$node = $node["Next"], {j, 2, i}];  $node,  i < 0,  $node = ref["Head"];  Do[$node = $node["Next"], {j, -ref["Size"] + 1, i, 1}];  $node ]["Value"] ]; 

И удаление головы с возвращением узла. Здесь я создам функцию pop :

LinkedList /: pop[LinkedList[ref_Symbol]] :=  With[{head = ref["Head"]},  ref["Head"] = head["Next"];  head["Next"] = Null;  head["Value"] ]; 

И последнее, что нам потребуется — функция, которая возвращает все элементы связанного списка в виде обычного списка. Обычно для этих целей используют встроенную функцию Normal:

LinkedList /: Normal[LinkedList[ref_Symbol]] :=  Block[{node = ref["Head"], value},      Table[         value = node["Value"];          node = node["Next"];          value,          {i, 1, ref["Size"]}     ] ]; 

Примеры

Минимальная реализация связанного списка готова. Создать его можно вот так:

linkedList = LinkedList[]

Затем его можно заполнить:

Do[append[linkedList, i], {i, 10}] Do[prepend[linkedList, i], {i, 10}]

Удалить часть из начала:

Table[pop[linkedList], {5}] (* {10, 9, 8, 7, 6} *)

Посмотреть содержимое:

Normal[linkedList] (* {5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} *)

Производительность

Моя реализация не самая оптимальная, плюс WL — это интерпретируемый язык и каждую команду он отправляет в ядро по отдельности. На самом деле можно намного лучше, но это будет более громоздко и малоинформативно. Но в конце концов нам важна не сама скорость выполнения операций, а их временная сложность. Что ж, докажем, что добавление в начало и конец не зависят от размера списка:

linkedList = LinkedList[];  appendTime = Table[{i, AbsoluteTiming[append[linkedList, i]][[1]]}, {i, 1, 10000}]; 
linkedList = LinkedList[];  prependTime = Table[{i, AbsoluteTiming[prepend[linkedList, i]][[1]]}, {i, 1, 10000}]; 
linkedList = LinkedList[];  popTime = Table[{i,  append[linkedList, i];  prepend[linkedList, i];  AbsoluteTiming[pop[linkedList]][[1]] }, {i, 1, 10000}]; 

И построим график зависимости времени операции от размера списка:

ListPlot[{appendTime, prependTime, popTime},  ImageSize -> Large,  PlotTheme -> "Detailed",  PlotLegends -> {"append", "prepend", "pop"} ]
Время операции в зависимости от размера связанного списка

Время операции в зависимости от размера связанного списка

Ну и докажем, что время получения по индексу и извлечения всех элементов линейно растет с ростом числа элементов:

linkedList = LinkedList[];  partTime = Table[{i,  append[linkedList, i];  index = RandomInteger[{1, First[linkedList]["Size"]}]; AbsoluteTiming[linkedList[[index]]][[1]] }, {i, 1, 2000}]; 
linkedList = LinkedList[];  normalTime = Table[{i,  append[linkedList, i];  AbsoluteTiming[Normal[linkedList]][[1]] }, {i, 1, 2000}]; 

И построим график:

ListPlot[{partTime, normalTime},  ImageSize -> Large,  PlotTheme -> "Detailed",  PlotLegends -> {"part", "Normal"} ]
Время получения элемента по индексу и получения всех элементов в зависимости от размера списка

Время получения элемента по индексу и получения всех элементов в зависимости от размера списка

Собственно на этом на сегодня все! Связанный список готов, а реализация двунаправленного связанно списка и дополнительные методы списков, например, вставка в середину и получение самого узла вместо значения нам для демонстрации создания этой структуры е потребовались.

Красивая плашка

И последняя совершенно необязательная часть. Сделаем так, чтобы в интерфейсе Mathematica наша структура данных красиво отображалась. Т.е. не в виде LinkedList[ref], а так, как отображаются другие сложные объекты. Например разреженный массив:

SparseArray[{{1, 1} -> 2, {100, 100} -> 101, {5, 66} -> 1243}]
Разреженный массив с тремя ненулевыми элементами

Разреженный массив с тремя ненулевыми элементами

Чтобы сделать нечто похожее — надо использовать внутреннюю функцию для SummaryBox :

$linkedListIcon = ImageResize[Import["https://wolfr.am/1jBx5PyAQ"], 1920 / 5];    LinkedList /: MakeBoxes[linkedList: LinkedList[ref_Symbol?AssociationQ],  form: (StandardForm | TraditionalForm)] :=  Module[{above, below},  above = { {BoxForm`SummaryItem[{"Ref: ", Defer[ref]}], SpanFromLeft},  {BoxForm`SummaryItem[{"Size: ", ref["Size"]}], SpanFromLeft},  {BoxForm`SummaryItem[{"Head: ", linkedList["Head"]}], SpanFromLeft},  {BoxForm`SummaryItem[{"Tail: ", linkedList["Tail"]}], SpanFromLeft} };  below = {};   (*Return*) BoxForm`ArrangeSummaryBox[ LinkedList,  linkedList,  $linkedListIcon,  above,  below,  form,  "Interpretable" -> Automatic ] ];
  • MakeBoxes — функция, которая переопределяет отображение некоторого выражения в интерфейсе Wolfram Mathematica

  • (StandardForm | TraditionalForm) — два альтернативных формата на которых сработает это отображение. Есть еще например FullForm — он не переопределен и покажет список «как есть»

  • BoxForm`ArrangeSummaryBox и BoxForm`SummaryItem — те самые внутренние функция для создания «плашек»

  • $linkedListIcon — иконка, которую я скачал прямо из интернета по короткой ссылке

Визуальное отображение связанного списка в блокноте

Визуальное отображение связанного списка в блокноте

Заключение

В этой статье я постарался показать каким образом можно самостоятельно реализовать некую структуру данных на Wolrfam Language. Я особенно хочу отметить то, как я использовал удерживающий атрибут HoldFirst, а так же конструкцию With для того чтобы в функции с HoldFirst передавать не ссылку а значение. Надеюсь вам статья показалась интересной! В следующий раз мы реализуем еще какую-нибудь структуру данных с использованием особенностей WL.

Всем спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/articles/787784/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *