Реализация структуры данных в виде массива с быстрыми операциями.

Заявление об ограничении ответственности ! Вы должны быть знакомы со сложностью времени и пространства.

Массив использует O (N) памяти, имеет O (1) доступ, вставку и удаление в конце. Однако вставка и удаление с самого начала - это O (N).

Unshift медленнее, чем Push, и Shift медленнее, чем Pop, потому что им также необходимо отменить смещение / сдвинуть все элементы влево после добавления первого элемента. Он растет с его входом (линейный рост).

Существует множество структур данных - связанные списки можно удалять и удалять быстро, но доступ - O (N). Стеки и очереди могут быть эффективными только на одном конце. Хеш-таблицы работают быстро, но требуют много памяти и не имеют индексации. Некоторые деревья большую часть времени работают довольно быстро, но для их эффективности требуется конкретный вариант использования.

Цель

Реализуйте массив, который имеет O (1) для доступа, вставки и удаления, использует как можно меньше памяти и в большинстве ситуаций будет неотличим от массива.

Я буду использовать JavaScipt и имитирую его реализацию массива. Обратите внимание, что реализация массива JavaScript - это реализация динамического массива, который несколько раз изменяет размер массива по мере того, как элементы вставляются / удаляются из него (известная как амортизированная константа).

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

Ссылка на репозиторий: https://github.com/AT-290690/binary-array

В конце концов, он должен действовать так же, как массив, за исключением того, что он сможет изменять как начало, так и конец коллекции из миллиона элементов, не беспокоясь.

NANOBENCH version 2
> node benchmark/benchmark.js
# regularArray.get middle 200.000 items
ok ~60 μs (0 s + 59833 ns)
# binaryArray.get middle 200.000 items
ok ~233 μs (0 s + 232709 ns)
# regularArray.push 200.000 times
ok ~4.62 ms (0 s + 4624250 ns)
# binaryArray.push 200.000 times
ok ~7.33 ms (0 s + 7330417 ns)
# regularArray.pop 200.000 times
ok ~2.04 ms (0 s + 2036458 ns)
# binaryArray.pop 200.000 times
ok ~5.53 ms (0 s + 5525167 ns)
# regularArray.shift 200.000 times
ok ~4.49 s (4 s + 489609209 ns)
# binaryArray.shift 200.000 times
ok ~7.96 ms (0 s + 7956125 ns)
# regularArray.unshift 200.000 times
ok ~4.43 s (4 s + 426783042 ns)
# binaryArray.unshift 200.000 times
ok ~10 ms (0 s + 10255167 ns)
all benchmarks completed
ok ~8.95 s (8 s + 954412377 ns)

Идея

Как добиться O (1) операций в начале? Ответ заключается в вопросе - Почему массив не сдвигается, а сдвигает O (N)? Потому что он меняет порядок своих элементов каждый раз он снимается и сдвигается.

Заключение - просто не меняйте порядок вещей! Просто сделайте все в конце! Push и Pop - это O (1), поэтому нам просто нужно использовать их вместо них.

Но… Как нам отслеживать индексы? Вздох ... На это уйдет больше времени. Короткий ответ - используйте два массива.

Реализация

Сначала мне нужны два массива - один левый и один правый.

left = [];
right = [];

Каждое добавленное значение в конце я поставлю справа.

Каждое значение, которое добавляется вначале, я помещаю слева.

Чтобы отслеживать смещения, мне понадобятся два свойства:

смещение влево и смещение вправо, которые увеличиваются / уменьшаются при каждой операции.

Время

Для достижения O (1) требуется простое управление состоянием и немного математики.

Я буду отслеживать вставки и соответственно увеличивать offsetRight и offsetLeft.

offsetRight = 0; // offsetRight++ when adding at end
offsetLeft = 0; // offsetLeft-- when adding at start

Я делаю то же самое, когда добавляю или удаляю элементы

Если индекс отрицательный - вставьте слева и наоборот.

Удаление и установка аналогичны. Но как получить товары по их соответствующим индексам?

В классе BinaryArray есть метод get.

По заданному индексу и вычисляет ключ, который затем соответствует фактическому индексу в массиве, содержащем значение.

index + this._offsetLeft

В действии:

const binArr = new BinaryArray();
binArr.addToRight(0);
binArr.addToRight(1);
binArr.addToRight(2);
binArr.addToRight(3);
binArr.addToRight(4);
binArr.addToLeft(-1);
binArr.addToLeft(-2);
binArr.get(0) // -2
binArr.get(1) // -1
binArr.get(2) // 0
binArr.get(3) // 1
...

Вот гифка, описывающая вышеизложенное (синий - справа, красный - слева).

Общий размер массива: слева * -1 + справа = -2 * -1 + 5 = 7

Независимо от операции индексы поддерживаются согласованными и упорядоченными.

  _offsetRight: 5,
  _offsetLeft: -2,
  left: [ <1 empty item>, -1, -2 ],
  right: [ 0, 1, 2, 3, 4 ],
  size: 7

* left всегда будет иметь пустое значение в индексе 0, поскольку индекс 0 не может иметь как положительное, так и отрицательное значение. Также это имеет смысл, поскольку в середине (точка поворота) может быть только один элемент.

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

binArray.toArray(); // [-2, -1, 0, 1, 2, 3, 4]
[...binArray]; // [-2, -1, 0, 1, 2, 3, 4]

Отлично, теперь у меня есть все операции в O (1), но как насчет пространства?

Космос

Давайте вставим больше элементов в конце.

Элементы массива Left и Right не сбалансированы: -2 в начале и 9 в конце (один массив больше другого). Это вообще не проблема, но иногда мне может понадобиться, чтобы на каждой стороне было одинаковое количество предметов.

Реализация метода балансировки:

Контрольный показатель

Вот очень простой тест «Массив против BinaryArray». BinaryArray выводит результат мгновенно, пока массив не распечатает свои первые результаты через несколько секунд. Будет только хуже, если количество элементов увеличится, в то время как BinaryArray останется неизменным.

Он действительно идет «бррр», но не похож на массив… пока. У массивов есть методы, которые упрощают их использование.

Подобно массиву

Если он выглядит как массив и действует как массив, значит, это объект!

В основном это будет специфично для JavaScript - массив имеет следующие методы:

push, pop, unshift, shift, map, filter, reduce, find, some, every….

.push, .pop, .shift и .unshift просты (просто реализуйте их как можно ближе к спектру).

Основа для итераторов такова:

for (let i = 0; i < this.size; i++) callback(this.get(i), i, this);

.concat мог бы быть лучше по пространственной сложности, но на данный момент это достаточно хороший чит:

concat(second) { 
return new BinaryArray([...this, ...second]) 
}

Теперь я должен признать, что обманул .sort, но я никогда не реализовал лучший алгоритм сортировки, чем стандартный.

sort(callback) {
 return new BinaryArray(this.toArray().sort(callback));
 }

В спецификации .reverse указано, что это нужно делать на месте (без создания каких-либо дополнительных массивов), так что здесь нет никакого обмана.

Будет только пропускать. заполнить

Проверка сходства с массивом

Если приведенный ниже код выводит те же результаты - мы массив.