Двумерные массивы

Объявление, ввод и вывод двумерного массива

Для хранения прямоугольных таблиц, состоящих из строк и столбцов, необходимо использовать массивы, каждый элемент которых является строкой  массивом чисел. Если считать, что одна строка — это vector<int>, то двумерный массив — это вектор элементов типа vector<int>, то есть его нужно объявлять как vector<vector<int> >. При этом по стандарту языка C++ до 2011 года, в конце определения между двумя символами “<” должен стоять пробел, начиная с C++11 этот пробел необязателен.

Если объявить массив a таким образом, то a[i] будет одномерным массивом, который обычно считают строкой. То есть a[i][j] будет j-м элементом i-й строки. Например, двумерный массив из 3 строк и 4 столбцов можно записать в виде:

a[0][0]  a[0][1]  a[0][2]  a[0][3]
a[1][0]  a[1][1]  a[1][2]  a[1][3]
a[2][0]  a[2][1]  a[2][2]  a[2][3]

Чтобы создать массив из n строк и m столбцов, можно объявить его указанным образом:

vector<vector<int> > a;

Затем необходмио размер “внешнего” массива изменить на n (сделать n строк в таблице):

a.resize(n);

Затем размер каждого массива-строки необходимо изменить на m. Это можно сделать циклом:

for (int i = 0; i < n; ++i)
    a[i].resize(m);

Заметим, что строки могут иметь разную длину, например, можно сделать массив в форме “лесенки”, где каждая строка будет содержать на один элемент больше предыдущей:

for (int i = 0; i < n; ++i)
    a[i].resize(i + 1);

Но если необходимо создать прямоугольный массив, то можно сразу же при объявлении задать его размер, если воспользоваться конструктором для вектора с параметрами. Первый параметр — размер вектора, второй необязательный параметр — значение, которым будут инциализированы элементы вектора. Тогда в качестве первого параметра можно передать n, а в качестве второго параметра можно явно указать конструктор, который создает вектор из m элементов типа int: vector<int>(m). Итак, создать прямоугольную таблицу размером n×m можно в одну строку:

vector<vector<int> > a(n, vector<int>(m));

Если вложенному вызову конструктора (для строки) передать второй параметр, то все элементы массива будут заполнены переданным значением вместо нуля.

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

for (int i = 0; i < n; ++i)
{   // Выводим на экран строку i
    for (int j = 0; j < m; ++j)
    {
        cout << a[i][j] << " ";
    }
    cout << endl;
    // Строка завершается символом перехода на новую строку
}

А считать двумерный массив с клавиатуры можно при помощи еще более простого алгоритма (массив вводится по строкам, то есть в порядке, соответствующему первому примеру):

for (i = 0; i < n; ++i)
{
    for (j = 0; j < m; ++j)
    {
        cin >> a[i][j];
    }
}

Обработка двумерного массива

Обработка двумерных массивов производится аналогичным образом. Например, если мы хотим записать в массив таблицу умножения, то есть присвоить элементу a[i][j] значение i * j, это можно сделать следующим образом при помощи вложенных циклов:

for (i = 0; i < n; ++i)
{
    for (j = 0; j < m; ++j)
    {
        a[i][j] = i * j;
    }
}

Рассмотрим более сложную задачу и несколько способов ее решения. Пусть дан квадратный двумерный массив размером n×n. Необходимо элементам, находящимся на главной диагонали проходящей из левого верхнего угла в правый нижний (то есть тем элементам a[i][j], для которых i == j) присвоить значение 1, элементам, находящимся выше главной диагонали – значение 0, элементам, нахощящимся ниже главной диагонали – значение 2. То есть получить такой массив (пример для n == 4):

1 0 0 0
2 1 0 0
2 2 1 0
2 2 2 1

Рассмотрим несколько способов решения этой задачи. Элементы, которые лежат выше главной диагонали – это элементы a[i][j], для которых i < j, а для элементов ниже главной диагонали i > j. Таким образом, мы можем сравнивать значения i и j и по ним определять значение a[i][j]. Получаем следующий алгоритм:

for (i = 0; i < n; ++i)
{
    for (j = 0; j < n; ++j)
    {
        if (i < j)
        {
            a[i][j] = 0;
        }
        else if (i > j)
        {
            a[i][j] = 2;
        }
        else
        {
            a[i][j] = 1;
        }
    }
}

Данный алгоритм плох, поскольку выполняет одну или две инструкции if для обработки каждого элемента. Если мы усложним алгоритм, то мы сможем обойтись вообще без условных инструкций.

Сначала заполним главную диагональ, для чего нам понадобится один цикл:

for (i = 0; i < n; ++i)
{
    a[i][i] = 1;
}

Затем заполним значением 0 все элементы выше главной диагонали, для чего нам понадобится в каждой из строк с номером i присвоить значение элементам a[i][j] для j=i+1, ..., n-1. Здесь нам понадобятся вложенные циклы:

for (i = 0; i < n; ++i)
{
    for (j = i + 1; j < n; ++j)
    {
        a[i][j] = 0;
    }
}

Аналогично присваиваем значение 2 элементам a[i][j] для j=0, ..., i-1:

for (i = 0; i < n; ++i)
{
    for (j = 0; j < i; ++j)
    {
        a[i][j] = 2;
    }
}

Можно также внешние циклы объединить в один и получить еще одно, более компактное решение:

for (i = 0; i < n; ++i)
{   // Заполняем строку с номером i
    for (j = 0; j < i; ++j)
    {
        a[i][j] = 2;    // Сначала пишем 2 ниже диагонали
    }
    a[i][j] = 1;        // После завершения предыдущего цикла i==j, пишем 1
    for (++j; j < n; ++j)  // Цикл начинаем с увеличения j на 1
    {
        a[i][j] = 0;    // Записываем 0 выше диагонали
    }
}

Многомерные массивы

Если необходимо хранить в массиве величину, зависящую от трёх параметров-индексов, например, a[i][j][k], то для представления такого массива нужно использовать вектор, элементами которого будут двумерные векторы, то есть

vector<vector<vector<int> > > a;

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

Их тоже можно создавать в одну строку, добавив ещё один уровень вложенности конструкторов. Например, пусть требуется создать массив размера x×y×z, то есть первый индекс будет принимать значения от 0 до x-1, второй индекс от 0 до y-1, третий индекс от 0 до z-1. Можно использовать такое объявление:

vector<vector<vector<int> > > a(x, vector<vector<int> >(y, vector<int>(z)));

Передача двумерных массивов в функцию

Передавать двумерные массивы в функцию всегда лучше по ссылке, т.е. добавляя знак “&” перед идентификатором параметра. Например:

void print(vector<vector<int> > & a)

Это обязательно делать в случае, когда функция должна модифицировать передаваемый массив, если же функция не модифицирует передаваемый массив, всё равно лучше массив передавать по ссылке, так как в этом случае не производится копирование массива (если массив передается по значению, то создается копия массива, что требует дополнительного времени).

Форматирование чисел при выводе

Допустим, мы заполним массив таблицей умножения: a[i][j] = i * j как в примере в начале раздела. Если мы теперь попробуем вывести этот массив на экран, разделяя элементы в строке одним пробелом, то из-за того, что числа имеют различную длину столбцы таблицы окажутся неровными:

0 0 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9
0 2 4 6 8 10 12 14 16 18
0 3 6 9 12 15 18 21 24 27

Для того, чтобы получить ровные столбцы необходимо, выводить числа так, чтобы одно выводимое число имело ширину, например, ровно в 3 символа, а “лишние” позиции были бы заполнены пробелами. Тогда получится следующая таблица:

0  0  0  0  0  0  0  0  0  0
0  1  2  3  4  5  6  7  8  9
0  2  4  6  8 10 12 14 16 18
0  3  6  9 12 15 18 21 24 27

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

for(int i = 0; i < n; ++i)
{
    for(int j = 0; j < m; ++j)
    {
        cout.width(3);
        cout << a[i][j];
    }
    cout << endl;
}

Заметим, что мы теперь не выводим пробел после каждого числа, поскольку мы добавили этот пробел к ширине выводимого поля. Функция width действует однократно, только на следующее выводимый в поток значение, поэтому ее нужно вызывать перед каждым выводом числа на экран.

Внимание! Если выводимое число или строка имеет большую длину, чем это было установлено функцией width, то это число или строка будут выведены полностью, а не будет обрезано до указанного значения. То есть предпочтительней вывести результат некрасиво, нежели неверно.