Пошук уроків, статей та іншого контенту
Навчитеся працювати з вкладеними масивами, матрицями та координатами через індекси й вкладені цикли.
У JavaScript немає окремого типу «двовимірний масив». Матриця подається як масив, елементами якого є інші масиви:
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];У цьому прикладі:
matrix — зовнішній масив;
кожен вкладений масив — рядок;
matrix[0] — перший рядок;
matrix[0][1] — елемент у першому рядку та другому стовпці.
Індекси в JavaScript починаються з нуля:
console.log(matrix[0][1]); // 2
console.log(matrix[2][0]); // 7Зручно домовитися, що координати записуються як [рядок, стовпець]. Тоді координата [2, 0] означає третій рядок і перший стовпець.
Для невеликих структур найпростіше використовувати літерал масиву:
const board = [
["X", "O", "X"],
["O", "X", "O"],
["O", "O", "X"]
];Метод Array.from() дає змогу створити матрицю програмно:
const rows = 3;
const columns = 4;
const matrix = Array.from(
{ length: rows },
() => Array(columns).fill(0)
);
console.log(matrix);
// [
// [0, 0, 0, 0],
// [0, 0, 0, 0],
// [0, 0, 0, 0]
// ]Функція в другому аргументі Array.from() викликається окремо для кожного рядка.
Це важливо, оскільки кожен рядок має бути незалежним масивом.
fill()Такий код виглядає коротшим, але створює помилку:
const matrix = Array(3).fill(Array(4).fill(0));
matrix[0][0] = 1;
console.log(matrix);
// [
// [1, 0, 0, 0],
// [1, 0, 0, 0],
// [1, 0, 0, 0]
// ]Усі три рядки посилаються на один і той самий вкладений масив.
Правильний варіант:
const matrix = Array.from(
{ length: 3 },
() => Array(4).fill(0)
);
matrix[0][0] = 1;
console.log(matrix);
// [
// [1, 0, 0, 0],
// [0, 0, 0, 0],
// [0, 0, 0, 0]
// ]Для доступу до елемента потрібно послідовно вказати індекси всіх рівнів вкладеності:
const matrix = [
[10, 20, 30],
[40, 50, 60]
];
console.log(matrix[1][2]); // 60
matrix[0][1] = 99;
console.log(matrix);
// [
// [10, 99, 30],
// [40, 50, 60]
// ]Якщо індекс виходить за межі вкладеного масиву, результатом буде undefined:
console.log(matrix[5]); // undefined
console.log(matrix[0][10]); // undefinedПеред доступом до вкладеного елемента інколи потрібно перевірити, чи існує рядок:
function getCell(matrix, row, column) {
if (!Array.isArray(matrix[row])) {
return undefined;
}
return matrix[row][column];
}
console.log(getCell(matrix, 0, 1)); // 99
console.log(getCell(matrix, 10, 0)); // undefinedНайпоширеніший спосіб обробити кожен елемент — використати два вкладені цикли:
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
for (let row = 0; row < matrix.length; row++) {
for (let column = 0; column < matrix[row].length; column++) {
console.log(
`matrix[${row}][${column}] = ${matrix[row][column]}`
);
}
}Зовнішній цикл перебирає рядки, а внутрішній — елементи поточного рядка.
Умова внутрішнього циклу має використовувати довжину поточного рядка:
column < matrix[row].lengthЦе особливо важливо для нерегулярних, або «зубчастих», масивів.
for...of для значеньЯкщо індекси не потрібні, код можна зробити коротшим:
const matrix = [
[2, 4, 6],
[8, 10, 12]
];
for (const row of matrix) {
for (const value of row) {
console.log(value);
}
}entries() для значень та індексівМетод entries() повертає індекс і значення:
for (const [rowIndex, row] of matrix.entries()) {
for (const [columnIndex, value] of row.entries()) {
console.log({
row: rowIndex,
column: columnIndex,
value
});
}
}function sumMatrix(matrix) {
let sum = 0;
for (const row of matrix) {
for (const value of row) {
sum += value;
}
}
return sum;
}
const matrix = [
[3, 5],
[7, 9]
];
console.log(sumMatrix(matrix)); // 24function findMaximum(matrix) {
if (matrix.length === 0) {
return undefined;
}
let maximum = -Infinity;
let position = null;
for (let row = 0; row < matrix.length; row++) {
for (let column = 0; column < matrix[row].length; column++) {
const value = matrix[row][column];
if (value > maximum) {
maximum = value;
position = [row, column];
}
}
}
return {
value: maximum,
position
};
}
const matrix = [
[4, 12, 3],
[8, 5, 10]
];
console.log(findMaximum(matrix));
// { value: 12, position: [0, 1] }Якщо максимальних значень кілька, цей алгоритм поверне координати першого знайденого.
Методи map(), filter() та reduce() працюють із вкладеними масивами так само, як і зі звичайними. Однак для повного обходу матриці метод потрібно застосувати на кожному рівні.
const matrix = [
[1, 2],
[3, 4]
];
const increased = matrix.map(row =>
row.map(value => value + 10)
);
console.log(increased);
// [
// [11, 12],
// [13, 14]
// ]map() зовнішнього рівня обробляє рядки, а map() внутрішнього рівня — елементи рядка.
const matrix = [
[1, 8, 3],
[10, 2, 7]
];
const filteredRows = matrix.map(row =>
row.filter(value => value >= 5)
);
console.log(filteredRows);
// [
// [8],
// [10, 7]
// ]Результат може бути «зубчастим»: у різних рядках залишиться різна кількість елементів.
Якщо потрібно отримати один плоский масив:
const values = matrix.flat();
const largeValues = values.filter(value => value >= 5);
console.log(largeValues); // [8, 10, 7]Метод flat() об’єднує вкладені масиви:
const matrix = [
[1, 2],
[3, 4],
[5, 6]
];
console.log(matrix.flat());
// [1, 2, 3, 4, 5, 6]Для більшої глибини можна передати число рівнів:
const data = [
[[1, 2], [3]],
[[4], [5, 6]]
];
console.log(data.flat(2));
// [1, 2, 3, 4, 5, 6]Якщо рівень вкладеності невідомий, можна використати flat(Infinity), але це варто робити обережно: глибока структура може бути великою, а повне розгортання — дорогим за часом і пам’яттю.
Для пошуку елемента у вкладеній структурі можна застосовувати вкладені цикли. Якщо після знаходження потрібно завершити обидва цикли, зручно використовувати return у функції:
function findPosition(matrix, predicate) {
for (let row = 0; row < matrix.length; row++) {
for (let column = 0; column < matrix[row].length; column++) {
if (predicate(matrix[row][column], row, column)) {
return [row, column];
}
}
}
return null;
}
const matrix = [
[2, 4, 6],
[8, 11, 14]
];
const position = findPosition(
matrix,
value => value % 2 !== 0
);
console.log(position); // [1, 1]Такий підхід зупиняє пошук одразу після першого збігу.
Для пошуку всіх координат:
function findAllPositions(matrix, predicate) {
const positions = [];
for (let row = 0; row < matrix.length; row++) {
for (let column = 0; column < matrix[row].length; column++) {
if (predicate(matrix[row][column], row, column)) {
positions.push([row, column]);
}
}
}
return positions;
}
const matrix = [
[2, 4, 6],
[8, 4, 10]
];
console.log(
findAllPositions(matrix, value => value === 4)
);
// [[0, 1], [1, 1]]Транспонування міняє місцями рядки та стовпці.
Наприклад:
1 2 3 1 4
4 5 6 → 2 5
3 6Реалізація для прямокутної матриці:
function transpose(matrix) {
if (matrix.length === 0) {
return [];
}
const rowCount = matrix.length;
const columnCount = matrix[0].length;
const result = Array.from(
{ length: columnCount },
() => Array(rowCount)
);
for (let row = 0; row < rowCount; row++) {
for (let column = 0; column < columnCount; column++) {
result[column][row] = matrix[row][column];
}
}
return result;
}
const matrix = [
[1, 2, 3],
[4, 5, 6]
];
console.log(transpose(matrix));
// [
// [1, 4],
// [2, 5],
// [3, 6]
// ]Ця реалізація очікує, що всі рядки мають однакову довжину.
Перед операціями над матрицями часто потрібно перевірити, чи є вона прямокутною:
function isRectangular(matrix) {
if (matrix.length === 0) {
return true;
}
const columnCount = matrix[0].length;
return matrix.every(row =>
Array.isArray(row) && row.length === columnCount
);
}
console.log(isRectangular([
[1, 2],
[3, 4]
])); // true
console.log(isRectangular([
[1, 2],
[3]
])); // falseДля квадратної матриці кількість рядків і стовпців має бути однаковою:
function isSquareMatrix(matrix) {
return (
isRectangular(matrix) &&
matrix.length > 0 &&
matrix.length === matrix[0].length
);
}У реальних програмах інколи вважають порожню матрицю квадратною матрицею розміру 0 × 0. Важливо вибрати правило та послідовно його дотримуватися.
Для квадратної матриці елементи головної діагоналі мають однакові індекси рядка та стовпця:
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
for (let index = 0; index < matrix.length; index++) {
console.log(matrix[index][index]);
}
// 1
// 5
// 9Сума головної діагоналі:
function mainDiagonalSum(matrix) {
if (!isSquareMatrix(matrix)) {
throw new Error("Потрібна квадратна матриця");
}
let sum = 0;
for (let index = 0; index < matrix.length; index++) {
sum += matrix[index][index];
}
return sum;
}Для побічної діагоналі в матриці розміру n × n індекс стовпця обчислюється як n - 1 - row:
function secondaryDiagonalSum(matrix) {
if (!isSquareMatrix(matrix)) {
throw new Error("Потрібна квадратна матриця");
}
let sum = 0;
const size = matrix.length;
for (let row = 0; row < size; row++) {
const column = size - 1 - row;
sum += matrix[row][column];
}
return sum;
}
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
console.log(mainDiagonalSum(matrix)); // 15
console.log(secondaryDiagonalSum(matrix)); // 15У матриці непарного розміру центральний елемент належить обом діагоналям. Якщо потрібно підсумувати всі елементи двох діагоналей без подвійного врахування центру, це потрібно обробити окремо.
Матриці часто використовують для ігрових полів, карт, зображень і пошуку шляхів. Для клітинки можна визначити її сусідів через зміщення координат.
Для руху вгору, вправо, вниз і вліво використовують такі зміщення:
const directions = [
[-1, 0], // вгору
[0, 1], // вправо
[1, 0], // вниз
[0, -1] // вліво
];Функція отримання доступних сусідів:
function getNeighbors(matrix, row, column) {
const directions = [
[-1, 0], // вгору
[0, 1], // вправо
[1, 0], // вниз
[0, -1] // вліво
];
const neighbors = [];
for (const [rowOffset, columnOffset] of directions) {
const nextRow = row + rowOffset;
const nextColumn = column + columnOffset;
const isInside =
nextRow >= 0 &&
nextRow < matrix.length &&
nextColumn >= 0 &&
nextColumn < matrix[nextRow].length;
if (isInside) {
neighbors.push({
position: [nextRow, nextColumn],
value: matrix[nextRow][nextColumn]
});
}
}
return neighbors;
}
const matrix = [
["A", "B", "C"],
["D", "E", "F"],
["G", "H", "I"]
];
console.log(getNeighbors(matrix, 1, 1));
// [
// { position: [0, 1], value: "B" },
// { position: [1, 2], value: "F" },
// { position: [2, 1], value: "H" },
// { position: [1, 0], value: "D" }
// ]Перевірка меж є обов’язковою. Без неї спроба звернутися до клітинки за межами матриці поверне undefined або призведе до помилки під час наступного доступу.
Для множення матриць A та B кількість стовпців A має дорівнювати кількості рядків B.
Якщо A має розмір m × n, а B — n × p, результат матиме розмір m × p.
Кожен елемент результату обчислюється як сума добутків відповідних елементів рядка першої матриці та стовпця другої:
function multiplyMatrices(first, second) {
if (!isRectangular(first) || !isRectangular(second)) {
throw new Error("Матриці мають бути прямокутними");
}
if (first.length === 0 || second.length === 0) {
return [];
}
const firstColumns = first[0].length;
const secondRows = second.length;
if (firstColumns !== secondRows) {
throw new Error(
"Кількість стовпців першої матриці " +
"має дорівнювати кількості рядків другої"
);
}
const result = Array.from(
{ length: first.length },
() => Array(second[0].length).fill(0)
);
for (let row = 0; row < first.length; row++) {
for (let column = 0; column < second[0].length; column++) {
for (let index = 0; index < firstColumns; index++) {
result[row][column] +=
first[row][index] * second[index][column];
}
}
}
return result;
}
const first = [
[1, 2, 3],
[4, 5, 6]
];
const second = [
[7, 8],
[9, 10],
[11, 12]
];
console.log(multiplyMatrices(first, second));
// [
// [58, 64],
// [139, 154]
// ]Часова складність стандартного алгоритму для матриць розмірів m × n та n × p становить O(m × n × p).
Метод slice() або spread-оператор копіює лише зовнішній рівень:
const original = [
[1, 2],
[3, 4]
];
const copy = [...original];
copy[0][0] = 99;
console.log(original);
// [
// [99, 2],
// [3, 4]
// ]Вкладені рядки залишилися спільними.
Для двовимірної матриці можна скопіювати кожен рядок:
const copy = original.map(row => [...row]);
copy[0][0] = 50;
console.log(original);
// [
// [99, 2],
// [3, 4]
// ]
console.log(copy);
// [
// [50, 2],
// [3, 4]
// ]У цьому прикладі original уже був змінений попередньою операцією, але після map() зміни copy більше не впливають на його рядки.
structuredClone()Для глибокого копіювання структур, які підтримує цей API, можна використати structuredClone():
const original = [
[1, 2],
[3, 4]
];
const copy = structuredClone(original);
copy[0][0] = 100;
console.log(original[0][0]); // 1
console.log(copy[0][0]); // 100JSON.parse(JSON.stringify(value)) не є універсальним способом копіювання: він втрачає спеціальні значення та типи, зокрема undefined, Date, Map, Set, функції та інші об’єкти.
Іноді вкладеність невідома заздалегідь. Наприклад:
const nested = [
1,
[2, [3, 4]],
[[5], 6]
];Для рекурсивного обходу можна перевіряти, чи є поточне значення масивом:
function sumNestedArray(value) {
if (!Array.isArray(value)) {
return value;
}
let sum = 0;
for (const item of value) {
sum += sumNestedArray(item);
}
return sum;
}
const nested = [
1,
[2, [3, 4]],
[[5], 6]
];
console.log(sumNestedArray(nested)); // 21Аналогічно можна реалізувати власний варіант flat(Infinity):
function flattenDeep(value, result = []) {
for (const item of value) {
if (Array.isArray(item)) {
flattenDeep(item, result);
} else {
result.push(item);
}
}
return result;
}
console.log(flattenDeep([
[1, 2],
[3, [4, 5]]
]));
// [1, 2, 3, 4, 5]Рекурсія зручна для довільної глибини, але для дуже глибоких структур може спричинити переповнення стека викликів. У таких випадках краще використати власний стек ітеративно.
Не кожну таблицю даних варто зберігати як масив масивів.
Підходить, якщо:
дані природно поділені на рядки;
потрібен простий доступ за координатами;
розміри невеликі або помірні;
рядки можуть мати різну довжину.
Для прямокутної матриці іноді ефективніше зберігати всі значення в одному масиві:
const rows = 3;
const columns = 4;
const values = Array(rows * columns).fill(0);
function getIndex(row, column) {
return row * columns + column;
}
values[getIndex(1, 2)] = 7;
console.log(values);
// [0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0]Переваги такого представлення:
простіше зберігати дані в деяких форматах;
немає окремих об’єктів-рядків;
можна краще контролювати розподіл пам’яті.
Недолік — координати потрібно перетворювати на один індекс.
TypedArrayДля великих числових матриць можна використовувати Int32Array, Float64Array та інші типізовані масиви:
const values = new Float64Array(6);
values[0] = 1.5;
values[1] = 2.5;
console.log(values);
// Float64Array(6) [1.5, 2.5, 0, 0, 0, 0]Типізований масив не є багатовимірним сам по собі, тому координати також потрібно перетворювати на плоский індекс.
const copy = [...matrix];Це копіює лише зовнішній масив. Для незалежної копії рядків використовуйте:
const copy = matrix.map(row => [...row]);const matrix = Array(5).fill([]);Усі рядки тут є одним і тим самим масивом. Створюйте їх у функції:
const matrix = Array.from(
{ length: 5 },
() => []
);matrix[0].length для всіх рядківДля зубчастого масиву це може пропустити елементи або звернутися за межі:
for (let row = 0; row < matrix.length; row++) {
for (let column = 0; column < matrix[row].length; column++) {
// Обробка matrix[row][column]
}
}У домовленості [row, column] першим завжди йде індекс рядка:
matrix[row][column]Під час транспонування індекси міняються місцями:
result[column][row] = matrix[row][column];Під час обходу сусідів або роботи з координатами перевіряйте:
row >= 0 &&
row < matrix.length &&
column >= 0 &&
column < matrix[row].lengthПеред транспонуванням, множенням або роботою зі стовпцями перевіряйте прямокутність матриці.
Цей код змінює початкову матрицю:
for (let row = 0; row < matrix.length; row++) {
for (let column = 0; column < matrix[row].length; column++) {
matrix[row][column] *= 2;
}
}А цей створює нову:
const doubled = matrix.map(row =>
row.map(value => value * 2)
);Вибір залежить від контракту функції. Якщо функція не повинна змінювати вхідні дані, використовуйте немутуючі методи або копіювання.
Розглянемо поле, де 1 означає зайняту клітинку, а 0 — вільну. Потрібно порахувати зайняті клітинки, які мають усіх чотирьох сусідів також зайнятими:
function countSurroundedCells(matrix) {
if (!isRectangular(matrix) || matrix.length < 3) {
return 0;
}
const rows = matrix.length;
const columns = matrix[0].length;
if (columns < 3) {
return 0;
}
let count = 0;
for (let row = 1; row < rows - 1; row++) {
for (let column = 1; column < columns - 1; column++) {
const isSurrounded =
matrix[row][column] === 1 &&
matrix[row - 1][column] === 1 &&
matrix[row + 1][column] === 1 &&
matrix[row][column - 1] === 1 &&
matrix[row][column + 1] === 1;
if (isSurrounded) {
count++;
}
}
}
return count;
}
const field = [
[0, 1, 0, 0, 0],
[1, 1, 1, 0, 1],
[0, 1, 1, 1, 0],
[0, 0, 1, 0, 0]
];
console.log(countSurroundedCells(field)); // 2Ми починаємо обхід з 1 і завершуємо на rows - 2 та columns - 2, тому що крайові клітинки не можуть мати чотирьох сусідів.
Багатовимірний масив у JavaScript — це масив, що містить інші масиви.
Для доступу до елемента матриці використовують matrix[row][column].
Вкладені цикли дають змогу обійти всі рядки та стовпці.
Для створення незалежних рядків використовуйте Array.from(), а не Array(...).fill(...).
map() потрібно застосовувати на кожному рівні вкладеності.
flat() перетворює вкладені масиви на плоский масив.
Перед матричними операціями перевіряйте прямокутність і сумісність розмірів.
Координати особливо корисні під час роботи з сусідами, діагоналями, ігровими полями та пошуком шляхів.
Для великих числових матриць можна розглянути плоский масив або TypedArray.
Під час копіювання враховуйте, що вкладені масиви є посилальними значеннями.