Сообщений 0    Оценка 380        Оценить  
Система Orphus

Строки и столбцы

Автор: Stanky
Источник: RSDN Magazine #5-2004
Опубликовано: 03.04.2005
Исправлено: 10.12.2016
Версия текста: 1.0
Описание реализации

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

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

Если попытаться решить эту задачу "в лоб", то при вставке и удалении элементов часто придётся передвигать практически все элементы массива. При большом размере массива это может привести к катастрофическому падению производительности.

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

ПРИМЕЧАНИЕ

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

Описание реализации

Допустим, у нас имеется двумерный массив:


В памяти он выглядит следующим образом:


Самая тривиальная операция, которую мы можем сделать - это добавить строку к массиву.

Добавление строки {1, 2}:


Операция посложнее – вставка строки в массив. Здесь уже необходимо сдвинуть вправо элементы массива на длину строки, начиная с последнего и заканчивая первым элементом по индексу новой строки.

Общее количество сдвигов: КоличествоСтолбцов * (КоличествоСтрок - ИндексНовойСтроки).

Вставка строки {1, 2} по индексу 1:


И самая трудоёмкая операция – вставка столбца. Здесь уже необходимо сдвигать вправо элементы, начиная с последнего, причём сдвиг должен происходить так: элементы последней строки, начиная с ИндексаНовогоСтолбца, в количестве КоличествоСтолбцов – ИндексНовогоСтолбца, сдвигаются на КоличествоСтрок, а элементы, предшествующие этому индексу, в количестве ИндексНовогоСтолбца сдвигаются на КоличествоСтрок - 1. Элементы предпоследней строки, начиная с ИндексаНовогоСтолбца, в количестве КоличествоСтолбцов – ИндексНовогоСтолбца, сдвигаются на КоличествоСтрок - 1, а элементы, предшествующие этому индексу в количестве ИндексНовогоСтолбца сдвигаются на КоличествоСтрок - 2 и так далее.

Общее количество сдвигов: КоличествоСтолбцов * КоличествоСтрок - ИндексНовогоСтолбца.

Вставка столбца {1, 2} по индексу 1:


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

Пойдём так же по порядку:

Самое простое решение по вставке строк – хранить каждую строку в одномерном массиве, а адрес этого массива записывать в другой одномерный массив – массив базовых адресов строк.

Выглядит это примерно так:


При такой организации чтобы получить адрес элемента двумерного массива нужно: по ИндексуСтроки взять значение АдресаСтроки из массива базовых адресов и прибавить к нему смещение: ИндексКолонки * РазмерЭлемента.

Теперь операция вставки строки в массив сводится только к сдвигу вправо базовых адресов и количество сдвигов для этой операции: КоличествоСтрок - ИндексНовойСтроки.

Вставка строки {1, 2} по индексу 1:


Хоть это и не принципиально, но при таком подходе мы вдобавок избавились от операции умножения для получения индекса, что происходит при обычном двумерном массиве: чтобы в массиве NxM получить элемент [i, j] нужно обратиться по адресу Массив[(i * КоличествоСтолбцов + j) * РазмерЭлемента].

Адрес строки занимает 4 байта, а элемент массива может быть любого размера, так что мы не только минимизировали количество сдвигов, но и при размере элемента больше 4-х байт, уменьшили количество обращений к памяти. А так же мы получили возможность менять строки местами всего лишь поменяв базовые адреса строк местами.

Если мы оставим всё, как есть, то для вставки столбца нам потребуется в каждой строке сдвинуть вправо элементы, начинающиеся с ИндексаНовогоСтолбца в количестве КоличествоСтолбцов - ИндексНовогоСтолбца.

Общее количество сдвигов: КоличествоСтрок * (КоличествоСтолбцов – ИндексНовогоСтолбца).

Хоть алгоритм вставки столбца и упростился, и количество сдвигов уменьшилось, для нас это всё равно неприемлемо, так что идём дальше.

По аналогии со строками введём вспомогательный массив для столбцов – массив смещений в строках. Для определённости считаем, что каждый элемент массива занимает 2 байта.

Выглядит это примерно так:


При такой организации, чтобы получить адрес элемента двумерного массива, нужно: по ИндексуСтроки взять значение АдресаСтроки из массива базовых адресов и прибавить к нему смещение, взятое по ИндексуКолонки из массива смещений.

Теперь операция вставки столбца в массив сводится только к сдвигу вправо смещений в строках, и количество сдвигов для этой операции: КоличествоСтолбцов - ИндексНовойКолонки.

Вставка столбца {1, 2} по индексу 1:


ПРИМЕЧАНИЕ

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

Ну, вот вроде бы и всё со вставками, но резонно напрашивается мысль о том, что раз у нас есть операции вставки, то должны быть и операции удаления.

Ну со строками всё просто - получить адрес строки, освободить занимаемую ей память и убрать её адрес из массива базовых адресов, а вот со столбцами малость сложнее.

Чтобы при каждом удалении столбца не делать ненужных нам сдвигов, введём ещё один вспомогательный массив – карта блоков, показывающая, занят в данный момент определённый блок (элемент строки) или свободен.

Выглядит это примерно так:


Удаление столбца по индексу 2:


Если мы теперь захотим добавить столбец, то он просто займёт первый свободный блок.

ПРИМЕЧАНИЕ

Сравнение методов: я лишь приведу приблизительные цифры для своего метода и обычных сдвигов. Для сдвигов я сразу выделял необходимый объём памяти под массив 8192 * 8192 и считал, что в нём 8192 строки и соответственно 1 столбец. На добавление ещё 8191 столбца уходила примерно минута, а вот своим методом я создавал массив 16384 * 16384 на что уходило всего 6-7 секунд, причём объём памяти занимаемый массивом 8192 * 8192 = 256МБ, а вот массив 16384 * 16384 занимает 1ГБ, причём в моём методе не было никаких поблажек – вся память выделялась по мере требования. Но тут я хочу обратить внимание ещё на один нюанс – я пользовался памятью из кучи (malloc), а не виртуальной памятью, поэтому в моей реализации есть гранулярность выделения памяти – то есть даже если в массиве находится одна строка, то она занимает 1, 2, 4, 8, 16, 32 или 64КБ. Результат в 7 секунд получался при гранулярности в 64КБ – то есть каждая строка как раз вмещала 16384 элемента по 4 байта, а вот при гранулярности в 32КБ результат получился 12-15 секунд, что тоже очень неплохо, учитывая, что для таких вещей использовать виртуальную память должно быть обязательным требованием!!!

Применить это можно, например, к шифрованию спутниковых каналов, где вещание ведётся как раз с изменённым чередованием строк, или же в каком-нибудь графическом приложении например для изменения размера картинки или её зеркального отображения по вертикали/горизонтали ну и так далее.


Эта статья опубликована в журнале RSDN Magazine #5-2004. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 380        Оценить