В этой статье я хотел бы сделать краткий обзор того, как создавать, управлять и просматривать многомерные массивы в D.
Массивы
Массивы D можно разделить на обычные массивы, массивы фиксированной длины (статические) и динамические массивы. Обычные массивы представляют собой общую концепцию коллекции, в которой элементы расположены рядом. Статические массивы имеют фиксированные размеры во время создания и, следовательно, известны во время компиляции. Динамические массивы не имеют фиксированных размеров и могут увеличиваться или уменьшаться по запросу. Тип массива имеет свойства length
(длина) и ptr
(указатель на первый элемент) (подробнее о массивах D можно прочитать здесь).
int[] arr = [2, 0, -1, 3, 5]; arr.length; /* 5 */ arr.ptr; /* 7FE3FBD34000 */ *arr.ptr; /* 2 */
Динамические массивы также называются срезами в D. Специфика срезов и их реализации выходит за рамки статьи (о срезах можете прочитать здесь).
int[] arr = [2, 0, -1, 3, 5]; int[] arrSlice1 = arr[]; // создать срез "arr" assert(arrSlice1.length == arr.length); int[] arrSlice2 = arr[1 .. 3]; // создать срез "arr" arrSlice2.length; /* 2 */ *arrSlice2.ptr; /* 0 */ arrSlice2 ~= 5; // добавить новый элемент /* [0, -1, 5] */
Создание многомерных массивов
Многомерный массив в D может быть создан с использованием стандартных массивов. Создадим один.
int[][] jaggedArr1 = [[0, 1, 2], [3, 4, 5]]; /* [[0, 1, 2] [3, 4, 5]] */
Это создаст так называемый «зубчатый» массив, потому что количество элементов в каждом измерении не фиксировано и может быть произвольным.
int[][] jaggedArr2 = [[0, 1], [2, 3, 4], [5, 6]]; /* [[0, 1] [2, 3, 4] [5, 6]] */ int[][] dynamicJaggedArr = new int[][](2, 3); /* [[0, 0, 0] [0, 0, 0]] */
Такая схема массива не очень эффективна, потому что внешний массив строится как отдельный блок памяти со ссылками на внутренние массивы. Каждый поиск в массиве будет иметь небольшие накладные расходы.
D позволяет вам создать быструю и более эффективную с точки зрения памяти версию, создав плотный многомерный массив, если размеры массива указаны заранее.
int[2][3] denseArr = [[1, 2], [3, 4], [5, 6]]; /* [[1, 2] [3, 4] [5, 6]] */
или в случае, когда первое измерение известно, а второе должно быть переменной длины
int rows = 3; double[2][] dynamicDenseArr = double[2][](rows); /* [[0, 0, 0] [0, 0, 0]] */
Вы также можете использовать функцию iota
модуля std.range
для ленивого создания диапазона значений в заданном диапазоне и функцию chunks
для создания двумерного представления плоского буфера одномерного массива.
import std.range; import std.array; int[] arr = 20.iota.array; auto arr2dView = arr.chunks(5); /* [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19]] */
Стандартные массивы не обладают этим shape
свойством, но мы можем моделировать его с помощью двух D-шаблонов.
// рекурсивный шаблон void shape(T)(T arr, long[] dims = []) { dims ~= arr.length; shape(arr[0], dims); } // назначение шаблона в остановке рекурсии и печати результата void shape(T: int)(T val, long[] dims) { writeln(dims); } arr2dView.shape; /* [4, 5] */ auto arr3dView = arr2dView.chunks(2); arr3dView.shape; /* [2, 2, 5] */
Конечно, наше самодельное shape
свойство будет отображать правильные размеры только для правильно разбитых массивов.
Создание многомерных массивов с помощью Mir
Высокопроизводительные многомерные массивы могут быть созданы с помощью библиотеки mir-algorithm, являющейся частью более крупного пакета Mir, который включает в себя различные высокопроизводительные числовые библиотеки для D. Чтобы соответствовать терминологии Mir, мы называем массивы срезами, а матрицы — многомерными срезами.
Модульmir.ndslice предоставляет различные быстрые и эффективные с точки зрения памяти реализации многомерных срезов (не путайте их со стандартными D-срезами ). mir.ndslice Slice
— многомерный диапазон произвольного доступа, который также имеет shape
, strides
, structure
и т.д. свойства. Следует иметь в виду, что mir.ndslice поставляется со своими собственными Slice
-совместимыми реализациями многих функций std.range, поэтому по большей части вам не нужно явно импортировать функции из std.range.
Создадим срез.
import mir.ndslice; auto mirSlice = [1, 2, 3].slice!int; /* [[[0, 0, 0], [0, 0, 0]]] */
Ой, это не похоже на то, что мы ожидали. Если вы посмотрите на форму mirArr.shape
вышеупомянутого среза, вы увидите [1, 2, 3]
. Да, мы создали трехмерный срез нулей (потому что int.init == 0
) вместо трехэлементного среза. Что вам нужно сделать, так это использовать as
функцию модуля mir.ndslice.topology
, которая создает ленивое представление исходных элементов, преобразованных в желаемый тип.
import mir.ndslice.topology: as; auto mirSlice = [1, 2, 3].as!int.slice; /* [1, 2, 3] */
Создадим еще несколько многомерных массивов для примера.
auto a = slice!int([2, 3]); auto b = slice!int(2, 3); // тоже работает! /* [[0, 0], [0, 0], [0, 0]] */
Если вам нужно инициализировать срез с определенным значением, вы можете указать его после определения формы.
import mir.ndslice; auto a = slice([2, 3], 1); /* [[1, 1, 1], [1, 1, 1]], */ auto b = slice([2, 3], -0.1); /* [[-0.1, -0.1, -0.1], [-0.1, -0.1, -0.1]] */ auto c = slice!long([2, 3], 5); /* [[5, 5, 5], [5, 5, 5]] */
Создание срезов с помощью slice
кажется немного громоздким. Есть специальный метод sliced
, который значительно упрощает работу.
int[] arr = [1, 2, 3]; auto mirSlice = arr.sliced; /* [1, 2, 3] */ // однострочный вариант auto mirSlice = [1, 2, 3].sliced; /* [1, 2, 3] */
sliced
создает n-мерное представление над итератором. Итератор может быть простым массивом D, указателем или итератором, определяемым пользователем.
import std.array; import mir.ndslice; int[][] jaggedArr = [[0, 1, 2], [3, 4, 5]]; // стандартный D-массив // создает одномерный вид среза, состоящий из обычных D-массивов. `.shape` будет `[2]` auto arrSlice11 = jaggedArr.sliced; // выделенный 2D срез. `.shape` будет `[2, 3]` auto arrSlice12 = jaggedArr.fuse; /* arrSlice11 и arrSlice12: [[0, 1, 2], [3, 4, 5]] */ auto arrSlice2 = 100.iota.array.sliced; /* [0, 1, 2, 3, ..., 99] */ // функция `iota` также принимает необязательное начальное значение. // выделенный 1D срез, состоящий из ленивых 1D срезов поверх `IotaIterator`. // `.shape` будет `[2]` auto a1 = iota([2, 3], 10).array.sliced; // выделенный 2D срез. `.shape` будет `[2, 3]` auto a2 = iota([2, 3], 10).slice; /* a1 и a2: [[10, 11, 12], [13, 14, 15]] */
Вы можете пропустить вызов array
вашего объекта, если укажете размеры sliced
.
auto a = 20.iota.sliced(20); /* [0, 1, 2, 3, ..., 19] */ auto b = 10.iota.sliced(2, 5); /* [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]] */
Но это еще не все! Вы можете комбинировать iota
с, fuse
чтобы выделить новый срез следующим образом.
auto a = [2, 3].iota!int.fuse; /* [[0, 1, 2], [3, 4, 5]] */ auto b = [2, 3].iota!int(2).fuse; /* [[2, 3, 4], [5, 6, 7]] */
Хорошо, но как мне вернуться к обычным массивам D?
Чтобы вернуться к массиву D, используйте .field
свойство среза.
import mir.ndslice; auto mirSlice = [2, 3].slice!int; int[] arr = mirSlice.field; /* [0, 0, 0, 0, 0, 0] */
Подождите, но теперь это одномерный массив! Да, потому что то, что мы называем 2D-массивом в D, — это просто 2D-представление в 1D-массиве. Использование вложенных массивов int[][]
для представления многомерных массивов неэффективно, поскольку внешние массивы будут содержать ссылки на внутренние массивы, а поиск каждого элемента будет иметь дополнительные расходы.
Свойство .field
доступно только для непрерывных в памяти срезов.
Печать срезов
Печать массивов и срезов в D. Используйте writeln
с циклом foreach
.
import std.stdio; import mir.ndslice; auto m = 15.iota.sliced(3, 5); writeln(m); /* [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9], [10, 11, 12, 13, 14]] */ foreach (i; m) { writeln(i); } /* [0, 1, 2, 3, 4], [5, 6, 7, 8, 9], [10, 11, 12, 13, 14] */
Что ж, это работает, но как насчет красивой печати? Если вы хотите узнать, как красиво печатать многомерные массивы D, ознакомьтесь с этой записью.
Создание случайных N-мерных срезов
Что, если вы хотите создать срез случайных элементов? Мы будем использовать функции generate
, take
модуля std.range и функцию uniform
модуля std.random. Хотя mir.ndslice имеет множество собственных реализаций функций std.range, generate
и take
является эксклюзивным для std.range.
import std.range: generate, take; import std.random: uniform; import mir.ndslice; auto rndSlice = generate!(() => uniform(0, 0.99)).take(10).array.sliced; /* [0.184327, 0.741689, 0.296142, ..., 0.0102164] */
Обратите внимание, как мы явно конвертируем результат выражения в array
прежде, чем передать его в sliced
. Почему? Потому что вызов array
фактически выполняет предыдущее ленивое выражение и позволяет использовать sliced
его результат. Без этого преобразования sliced
не сработало бы, поскольку не знает, что делать с Take!(Generator!(function () @safe => uniform(0, 0.99)))
типом (типом generate!(() => uniform(0, 0.99)).take(10)
) выражения.
Теперь давайте изменим форму полученного среза с помощью функции reshape
, и мы получим срез из случайных элементов. Операция reshape
не выделяет новую память, но возвращает новый срез для одних и тех же данных.
int err; // сохраняет операционный вывод auto rndMatrix = rndSlice.reshape([5, 2], err); /* [[0.184327, 0.741689], [0.296142, 0.982893], [0.587764, 0.763811], [0.312337, 0.891162], [0.0886852, 0.0102164]] */
Вы также можете изменить форму с помощью, sliced
но тогда вам сначала придется использовать flattened
для сглаживания вашего массива.
auto a = [2, 4].iota.flattened.sliced(4, 2); /* [[0, 1], [2, 3], [4, 5], [6, 7]] */
Другой способ сгенерировать случайный многомерный срез с использованием стандартных функций D — это объединить предыдущее generate
выражение с fill
методом, доступным в std.algorithm. Вот, взгляните.
import mir.ndslice; import std.algorithm.mutation: fill; import std.range: generate; import std.random: uniform; double[] arr = new double[10]; // выделить массив double auto fun = generate!(() => uniform(0, .99)); // присвоить массиву значения arr.fill(fun); // fill принимает как отдельные значения, так и функции arr.sliced(5, 2); /* [[0.0228295, 0.267278], [0.224073, 0.962407], [0.475771, 0.317672], [0.966923, 0.886558], [0.758477, 0.854988]] */
Вышеупомянутые операции делают все на месте. Если это не является обязательным требованием, мы можем сделать немного иначе. Давайте создадим новый объект и предоставим 2D-представление в наш заполненный массив.
auto sl = slice!double(5, 2); auto fun = generate!(() => uniform(0, 99)); sl.field.fill(fun); /* [[0.273062, 0.59894], [0.358506, 0.784792], [0.634209, 0.906578], [0.0535602, 0.573161], [0.0746745, 0.537331]] */
Однако, если вы хотите использовать mir на полную мощность, вам следует использовать randomSlice
метод, предоставляемый пакетом mir.random.algorithm. Этот метод позволяет выполнить случайную выборку из нормального или равномерного распределения с использованием выбранной вами формы среза. Вот как это работает.
// этот импорт необходим import mir.ndslice; import mir.random : threadLocalPtr, Random; // генераторы случайных чисел import mir.random.variable : uniformVar, normalVar; // распределения import mir.random.algorithm : randomSlice; auto rndMatrix1 = uniformVar!int(0, 10).randomSlice([5, 2]); /* [[5, 0], [9, 3], [8, 3], [5, 9], [4, 8]] */ // или другой вариант с нестандартным rng семенем auto rng = Random(123); auto rndMatrix2 = rng.randomSlice(uniformVar(-1.0, 1.0), [5, 2]); // даже если тип является предполагаемым, вы можете определить его auto rndMatrix3 = rng.randomSlice(uniformVar!double(-1.0, 1.0), [5, 2]); /* [[-0.0660341, 0.290473], [0.215803, 0.975375], [-0.724666, 0.293703], [0.131249, 0.664371], [-0.0193379, 0.706641]] */ // или используйте указатель на генератор rng auto rndMatrix = threadLocalPtr!Random.randomSlice(uniformVar!double(-1.0, 1.0), [5, 2]); /* [[0.664374, -0.432451], [0.717084, 0.130015], [-0.0144875, -0.402825], [-0.741251, -0.116261], [0.918571, -0.530099]] */
Какой метод использовать — решать вам. Интуитивно понятно, что использование специальных функций mir было бы наиболее эффективным. Однако возможность генерировать массивы и матрицы также с использованием стандартных языковых методов без слишком большого ущерба для производительности — это здорово. И это верно не только для многомерных массивов. Когда язык позволяет вам плавно перемещаться по своей экосистеме, вы не чувствуете себя обремененным конкретной библиотекой. В конце концов, это означает, что вам не нужно знать значительную часть его API, чтобы добиться цели.
Посмотрите, как обновить элементы до нуля.
// работает для ndslices, arrays и ranges. import mir.algorithm.iteration: each; rndMatrix.each!((ref a){a = 0;}); // не выделяем /* [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]] */ rndMatrix.each!"a = 0"; // строковый миксин тоже работает, не выделяем /* [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]] */
Кроме того, вы можете использовать map
вместо each
для создания нового Slice
объекта. Однако имейте в виду, что вам придется вызвать slice
, потому что map
ленив.
auto zeroMatrix = rndMatrix.map!(i => 0).slice; // выделено /* [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]] */ auto zeroMatrix = rndMatrix.shape.slice!double(0); // выделено /* [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]] */
Если вам не хватает numpy с его zeros
, напишите простой шаблон D.
void zeros(T)(ref T obj) { obj.each!((ref a) {a = 0;}); // не выделяется }
Более короткий вариант с миксином.
void zeros(T)(ref T obj) { obj.each!"a = 0"; // не выделяется } rndMatrix.zeros; /* [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]] */
Также работает простое присвоение операционного индекса.
rndMatrix[] = 0; // или 0.0
Вариант с map
и выделением.
auto zeros(T)(T obj){ return obj.map!"0".slice; // выделяется новый срез } auto zeroMatrix = rndMatrix.zeros; /* [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]] */
Основные операции
Основные математические операции с одномерными срезами просты.
import mir.ndslice; auto a = 10.iota.sliced(10); /* лениво: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] */ auto b = a + 2; /* лениво: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11] */ auto c = 2 * b - 1; /* лениво: [3, 5, 7, 9, 11, 13, 15, 17, 19, 21] */ auto d = c^^2; // or c * c /* лениво: [9, 25, 49, 81, 121, 169, 225, 289, 361, 441] */ auto a = slice([3], 1); // [1, 1, 1] auto b = slice([3], 2); // [2, 2, 2] auto c = a + b; /* лениво: [3, 3, 3] */ auto d = b - a; /* лениво: [1, 1, 1] */ auto e = 10.iota.sliced(5, 2); auto f = e - 9; /* лениво: [[-9, -8], [-7, -6], [-5, -4], [-3, -2], [-1, 0]] */ auto d = 4.iota.sliced(2, 2); auto g = d * d.transposed.slice; // "g" не выделяется потому, что * лениво в Mir d[] *= d.transposed; // другой вариант с выделением /* [[0, 2], [2, 9]] */
Как вы могли заметить, операции применяются поэлементно. Мне удобнее переключаться с D-массива на срезы Mir с помощью sliced
, выполнять ряд основных операций, а затем снова переключаться с помощью .field
на D-массив. Нет необходимости злоупотреблять map
. Имейте в виду, что .field
требуется для того, чтобы Slice
был непрерывным (смотрите assumeContiguous
метод). Но будьте осторожны с assumeContiguous
, он отменяет некоторые операции без выделения памяти, такие как .transposed
.
auto a = 10.iota.sliced(5, 2); a.shape == a.transposed.assumeContiguous.shape; // true
Что, если я хочу изменить только одно или несколько определенных значений внутри многомерного среза? Давай попробуем.
auto a = 10.iota.sliced(5, 2); a[1, 0] *= 2; // ОШИБКА!
Вы не можете сделать это на ленивом представлении срезов. Вам нужен реальный Slice
объект, созданный с помощью slice
.
auto a = slice([5, 2], 1); a[1, 0] *= 2; /* [[1, 1], [2, 1], [1, 1], [1, 1], [1, 1]] */ // обновить всю третью строку a[2][] *= 3; /* [[1, 1], [2, 1], [3, 3], [1, 1], [1, 1]] */
Давайте посмотрим , как использовать некоторые универсальные функции, такие как exp
, sqrt
и sum
с Slice
. Мы можем применить exp
и sqrt
используя map
следующим образом:
auto a = slice!double([2, 3], 1.0); /* [[1, 1, 1], [1, 1, 1]] */ import mir.math.common: exp, sqrt; auto b = a.map!exp; /* лениво: [[2.71828, 2.71828, 2.71828], [2.71828, 2.71828, 2.71828]] */ auto c = b.map!sqrt; /* лениво: [[1.64872, 1.64872, 1.64872], [1.64872, 1.64872, 1.64872]] */
А как насчет суммы? mir поставляется с собственной реализацией sum
в модуле mir.math.sum. В зависимости от типа переменной доступны разные алгоритмы суммирования.
import mir.math.sum; auto a = 10.iota.slice; auto b = arr.sum; /* 45 */
Доступ к измерениям среза
Как выполнять операции с отдельными измерениями? Для этого у Mir есть функция byDim
, которая принимает измерение в качестве параметра для итерации. Посмотрим, как им пользоваться.
byDim
возвращает одномерный срез, состоящий из N-1
размерных срезов.
import mir.ndslice; auto a = [5, 2].iota.slice; /* [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]] */ a.byDim!1; // 1 по строкам, 0 по столбцам для 2D-среза /* [[0, 2, 4, 6, 8], [1, 3, 5, 7, 9]] */
Давайте посчитаем сумму каждого столбца в 2D-срезе.
import mir.math.sum; auto colsSum = a.byDim!1.map!sum; /* лениво: [20, 25] */
Мы можем проверить, какой столбец содержит нечетные числа.
import mir.algorithm.iteration: all; auto b = [5, 2].iota.slice; /* [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]] */ auto c = a.byDim!1.map!(a => a.all!(a => a % 2 == 1)) auto d = a.byDim!1.map!(all!"a % 2"); // или менее подробный миксин /* [false, true] */
А как насчет сортировки 2D-среза по размеру?
import mir.ndslice; import mir.ndslice.sorting; auto a = [5, 3, -1, 0, 10, 5, 6, 2, 7, 1].sliced(5, 2); /* [[5, 3], [-1, 0], [10, 5], [6, 2], [7, 1]] */ a.byDim!0.each!sort; // сортировка на месте /* [[3, 5], [-1, 0], [5, 10], [2, 6], [1, 7]] */
Индексирование, нарезка и итерация
mir.ndslice Slice
индексируется целыми числами без знака, идентично стандартным массивам D.
auto origSlice = 10.iota.slice; /* [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] */ origSlice[0]; /* 0 */ origSlice[8]; /* 8 */
Slice
точно так же нарезаются с использованием синтаксиса диапазона номеров [start .. end]
.
auto origSlice = 10.iota.slice; /* [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] */ auto newSlice = origSlice[0 .. 6]; /* [0, 1, 2, 3, 4, 5] */
Чтобы проиндексировать последний элемент фрагмента, вы можете использовать свойство .length
или его сокращение $
. Вы также можете использовать пустые скобки []
для выбора всех элементов массива от индекса 0
до последнего индекса $
. Однако отрицательные значения индекса не допускаются.
auto origSlice = 10.iota.slice; assert(origSlice == origSlice[0 .. origSlice.length]); assert(origSlice == origSlice[0 .. $]); assert(origSlice == origSlice[]);
Кроме того, вы можете выполнять основные математические операции с $
оператором, чтобы индексировать элементы с конца. Индексирование только с помощью $
не работает.
auto origSlice = 10.iota.slice; /* [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] */ origSlice[$-1]; /* 9 */ origSlice[2 .. $-3]; /* [2, 3, 4, 5, 6] */ origSlice[$-3 .. $-1] /* [7, 8] */
Многомерные срезы индексируются путем индексации сначала первого (строка), а затем второго (столбец) измерения.
auto matrix = iota([4, 2], 1); /* [[1, 2], [3, 4], [5, 6], [7, 8]] */ matrix[0 .. 2] /* [[1, 2], [3, 4]] */ matrix[0 .. 2, 1] /* [3, 4] */ matrix[0 .. 2][1][0] /* 3 */
Slice
также можно проиндексировать с другим Slice
подобным образом origSlice[newSlice]
. В таком случае newSlice
заменяется [0 .. newSlice.length]
индексным диапазоном.
auto origSlice = iota([4, 2], 1).slice; /* [[1, 2], [3, 4], [5, 6], [7, 8]] */ auto newSlice = 3.iota.slice; /* [0, 1, 2] */ origSlice[newSlice]; // origSlice[[0 .. 3]] /* [[1, 2], [3, 4], [5, 6]] */
Источник: tastyminerals — Multidimensional Arrays in D / Mar 22, 2020