Списки
Списки как массивы
Массивы
Удалить count элементов, начиная с i-того можно вызовом lst.splice(i, count). Эта же функция при вызове lst.splice(i, 0, it) вставляет элемент it на место индекса i (после lst[i-1]). Функция lst.reverse() обращает последовательность элементов списка. Ниже, справа от кода скрипта приведен результат вызова document.write(res); и тег <br> в html означает переход к следующей строке:
Однако, в ряде случаев, массив - не лучший выбор. В частности, при поиске в ширину необходимо часто добавлять элементы в начало списка. Эта операция для массива очень медленная, т.к. ему необходимо не только выделять дополнительную память, но и сдвигать все элементы массива вправо. Для таких случаев стоит использовать традиционные списки связанных между собой узлов ( = элементов) списка.Класс List
Перейдём теперь к созданию класса List, который будет оперировать со списками. Зададим список при помощи набора структур вида { nm, prv, nxt }, где nm - имя узла (может хранить любые данные), prv - указатель на предыдущий и nxt - на следующий узел. Если узлы не имеют указателя prv - то это односторонний (правосторонний) список List, иначе список двухсторонний и он реализован классом List2. Односторонний список чуть быстрее двухстороннего, однако, он заметно медленнее при удалении из конца (pop). Если используется эта функция, необходим List2.
Класс списка так же хранит указатели на первый - beg и последний - end его узел. Существует несколько способов интерпретации этих указателей. Мы подробнее рассмотрим более быстрый вариант, когда beg и end - это фиктивные узлы, а реальные находятся между ними.
Определим функцию, которая будет конструктором класса List:
function List(nm) { this .end = { nm: null , nxt: null }; // фиктивный последний элемент списка this .beg = { nm: null , nxt: this .end }; // фиктивный первый элементы списка this .length = 0; // число узлов в списке (пустой) if (nm !== undefined ) // если есть аргумент nm this .push(nm); // сразу вставляем первый элемент } |
В конструкторе класса List введен фиктивный указатель end, следующий за последним элементом списка. Фиктивный указатель beg через свойство beg.nxt ссылается на первый элемент списка. Так как список пока пустой, beg.nxt ссылается на end. Функцию (метод класса) push мы определим ниже.
Отметим, что поля nm и nxt инициализированы нулевым указателем null (напомним, что между undefined и null существует заметная разница). Фиктивный указатель end, при вставке в конец списка, будет превращаться в реальный последний узел. Поэтому, таким объявлением, мы предлагаем движку JavaScript сразу выделить память под объект с соответствующими полями, чтобы не пересоздавать его потом (при вставке). Это ускоряет работу с объектами (иногда в разы).
Методы класса
Кроме хранения данных, список должен выполнять определённые действия (иметь методы). Напишем, например, функцию, которая выводит список как строку в круглых скобках:
List. prototype .toString = function () { var n = this .beg.nxt; // сразу за beg стоит первый узел var st = "(" ; while (n !== this .end){ // пока нет фиктивного последнего st += n.nm + (n.nxt!== this .end ? "," : "" ); // выводим через запятую имена узлов n = n.nxt; // переходим к следующему узлу } return st+ ")" ; } |
Функция с именем toString может быть в любом классе. Если она определена, то экземпляр класса, когда этого требует соответствующий контекст, преобразуется в строку. Например, следующий скрипт:
var lst = new List(); document.write(lst); |
Вставка элементов
Напишем теперь функцию, вставляющую элемент с именем nm в начало списка:
List. prototype .unshift = function (nm) { this .length++; // увеличиваем число узлов this .beg.nxt = {nm: nm, nxt: this .beg.nxt}; // вставляемый элемент следует за beg } |
List. prototype .push = function (nm) { this .length++; // увеличиваем число узлов this .end.nm = nm; // фиктивный end становится реальным this .end = this .end.nxt = { nm: null , nxt: null }; // и затем снова фиктивным } |
Графически, последовательная вставка 0, 1, 2 в начало и конец списка выглядит следующим образом:
(первые картинки в каждой строке изображают пустые списки):
Удаление элементов
Обсудим алгоритмы удаления элементов из списка. Удаление в правосторонних списках (есть только ссылки nxt) из начала не представляет труда:
List. prototype .shift = function () { if ( this .length===0) return ; // список пуст - вернём undefined this .length--; // уменьшаем число узлов this .beg = this .beg.nxt; // фиктивный beg ссылается на второй элемент return this .beg.nm; } |
Удаление из конца одностороннего списка, существенно более затратная операция, так как необходимо получить последний элемент (реальный) о котором указатель end ничего не знает:
List. prototype .pop = function () { if ( this .length === 0) return ; // список пуст - вернём undefined this .length--; // уменьшаем число узлов var n = this .beg.nxt; // начиная с первого реального узла, while (n.nxt !== this .end) // ищем реальный последний узел n = n.nxt; // переходя каждый раз к следующему this .end = n; // фиктивный сдвигаем влево на один return n.nm; } |
Ещё немного функций
Приведём несколько функций класса List, эффективность которых не столь высока, по сравнению с вставкой в начало или конец списка. Так, получение узла под номером pos (начиная с нуля) выглядит следующим образом:
List. prototype .node = function (pos) { var n = this .beg.nxt; // бежим от начала while (n !== this .end && pos-- > 0) n = n.nxt; // переходим к следующему узлу return n; } |
i-- vs. --i
Ещё одна функция добавляет узел с именем nm на место pos в списке (начиная с нуля), сдвигая всё вправо:
List. prototype .insert = function (pos, nm) { if (pos <= 0) // добавляем в начало return this .unshift(nm); if (pos >= this .length) // добавляем в конец return this .push(nm); this .length++; // увеличиваем число узлов var n = this .node(pos-1); // перед вставляемым узлом n.nxt = { nm:nm, nxt:n.nxt}; } |
Аналогична функция удаления узла на месте pos в списке (начиная с нуля) и возвращения его поля nm:
List. prototype .remove = function (pos) { if (pos <= 0) // удаляем из начала return this .shift(); if (pos+1 >= this .length) // удаляем из конца return this .pop(); this .length--; // уменьшаем число узлов var n = this .node(pos-1); // перед удаляемым узлом var nm = n.nxt.nm; n.nxt = n.nxt.nxt; return nm; } |
Упорядоченная вставка
Добавим ещё функцию, которая вставляет элемент в начало списка, но при этом следит за упорядоченностью элементов:
List. prototype .add = function (nm) { var n = this .beg; // бежим от начала while (n !== this .end && n.nxt.nm !== null && this .lt(n.nxt.nm, nm) ) n = n.nxt; this .length++; // увеличиваем число узлов n.nxt = {nm: nm, nxt:n.nxt}; // вставляемый элемент следует за n } |
Нефиктивные указатели
Рассмотрим кратко ещё один способ работы со списком, когда указатели beg и end реально ссылаются на первый и последний элементы списка (напрямую, а не через поле nxt). Оформим его в виде класса List1
function List1(nm) { this .beg = null ; // указатель на первый элемент списка (пока в никуда) this .end = null ; // указатель на последний элемент списка (пока в никуда) this .length = 0; // число узлов в списке if (nm !== undefined ) // если есть аргумент nm this .push(nm); // сразу вставляем первый элемент с именем nm фу-ей push } |
Двухсторонние списки
Элементы (узлы) двухcторонних списков хранят в себе указатели, как на следующий элемент (nxt), так и на предыдущий (prv):
function List2(nm) { this .end = { }; // указатель на фиктивный последний элемент списка this .beg = { nxt: this .end }; // указатель на фиктивный первый элементы списка this .end.prv = this .beg; this .length = 0; // число узлов в списке if (nm !== undefined ) this .push(nm); } |
Тестирование быстродействия
Ниже тестируется вставка в начало и конец списка, реализованного массивом (медленный unshift) и правосторонними (List, List1) и двухсторонним списком (List2). Как организовать процесс тестирования описано в документе Speed.
добавлений
раз: (ms) ( цикл: )
push | unshift | |||
---|---|---|---|---|
Array | 13 | 9088 | Обычный массив JavaScript | |
List | 25 | 15 | Односторонний список List | |
List2 | 30 | 25 | Двухсторонний список List2 | |
List0 | 47 | 34 | Односторонний список List без инициализаций null | |
List1 | 49 | 33 | Односторонний список List с "нефиктивными" beg и end |
Ещё один тест связан с последовательной вставкой всех элементов в конец, а затем их удалением из конца и начала списка:
добавлений
раз: (ms) ( цикл: )
push,pop | push,shift | |||
---|---|---|---|---|
Array | 19 | 147 | Обычный массив JavaScript | |
List | 10693 | 13 | Односторонний список List | |
List2 | 22 | 30 | Двухсторонний список List2 |
Общий вывод такой: для операций push, unshift и shift, которые активно используются в алгоритмах поиска в ширину или глубину, стоит использовать класс List. Однако, если необходимо часто удалять элементы из конца списка pop, то List лучше не использовать.
Работа со списками
Подытожим. Методы работы со списками полностью аналогичны методам работы с массивами:
Чтобы классы List (односторонний список) и List2 (двухсторонний список) стали доступными, необходимо подключить модуль list.js. Приведём в справочных целях все методы этих классов:
- toString() - вывести список как строку (nm0, nm1, n2,...).
- push(nm) - добавить узел с именем nm в конец списка.
- unshift(nm) - добавить узел с именем nm в начало списка.
- add(nm) - добавить узел с именем nm с начала списка, выдерживая его упорядоченным (функция lt).
- pop() - получить поле nm последнего узла и убрать его из списка.
- shift() - получить поле nm первого узла и убрать его из списка.
- reverse() - перевернуть последовательность узлов в списке.
- node(pos) - получить узел под номером pos в списке (начиная с нуля).
- get(pos) - получить значение поля nm узла на месте pos в списке (начиная с нуля).
- put(pos, nm) - поменять поле nm узла месте pos в списке (начиная с нуля).
- insert(pos, nm) - добавить узел с именем nm на место pos в списке (начиная с нуля), сдвинув всё вправо.
- remove(pos) - удалить узел на месте pos в списке (начиная с нуля) и вернуть его nm.