Transpozycja tablicy 2D w JavaScript


154

Mam tablicę tablic, coś w rodzaju:

[
    [1,2,3],
    [1,2,3],
    [1,2,3],
]

Chciałbym to przetransponować, aby uzyskać następującą tablicę:

[
    [1,1,1],
    [2,2,2],
    [3,3,3],
]

Programowo nie jest to trudne przy użyciu pętli:

function transposeArray(array, arrayLength){
    var newArray = [];
    for(var i = 0; i < array.length; i++){
        newArray.push([]);
    };

    for(var i = 0; i < array.length; i++){
        for(var j = 0; j < arrayLength; j++){
            newArray[j].push(array[i][j]);
        };
    };

    return newArray;
}

Wydaje się to jednak nieporęczne i czuję, że powinien istnieć łatwiejszy sposób na zrobienie tego. Jest tu?


4
Czy możesz zagwarantować, że te dwa wymiary zawsze będą takie same? 1x1, 2x2, 3x3 itd. Do czego arrayLengthdokładnie służy parametr? Aby upewnić się, że nie wykraczasz poza określoną liczbę elementów w tablicy?
zmiażdż

8
To nie ma nic wspólnego z JQuery, zmieniłem tytuł.
Joe,

4
Sprawdź to: stackoverflow.com/questions/4492678/… . To, co robisz, to transpozycja macierzy
stackErr

1
Tak, transpozycja. Odwracanie byłoby zupełnie inne i mnie to nie interesuje. Na razie.
ckersch,

3
Przekątna od lewej górnej do prawej dolnej pozostaje niezmieniona, więc istnieje możliwość optymalizacji.
S Meaden

Odpowiedzi:


205
array[0].map((_, colIndex) => array.map(row => row[colIndex]));

mapwywołuje podaną callbackfunkcję raz dla każdego elementu tablicy, po kolei, i konstruuje nową tablicę na podstawie wyników. callbackjest wywoływana tylko dla indeksów tablicy, które mają przypisane wartości; nie jest wywoływana dla indeksów, które zostały usunięte lub którym nigdy nie przypisano wartości.

callbackjest wywoływana z trzema argumentami: wartością elementu, indeksem elementu oraz przechodzonym obiektem Array. [źródło]


8
To jest dobre rozwiązanie. Jeśli jednak zależy Ci na wydajności, powinieneś użyć oryginalnego rozwiązania OP (z poprawką błędu do obsługi tablic M x N, gdzie M! = N). sprawdź to jsPerf
Billy McKee

3
Jeśli użyjesz go dwa razy na tej samej macierzy, wróci do pierwszego z rotacji o 90 '
Olivier Pons

3
dlaczego array[0].mapzamiast array.map?
John Vandivier,

4
array[0].mapponieważ chce iterować dowolną liczbę razy, że istnieją kolumny, array.mapsprawdziłby liczbę wierszy.
joeycozza

2
@BillyMcKee w roku 2019 i Chrome 75 loopssą 45% wolniejsze niż map. I tak, transponuje poprawnie, więc drugi bieg zwraca początkową macierz.
Ebuall


39

Możesz użyć underscore.js.

_.zip.apply(_, [[1,2,3], [1,2,3], [1,2,3]])

3
To było ładne - także podkreślenie jest dla mnie bardziej potrzebne niż jQuery.
John Strickler,

lub jeśli korzystasz z funkcjonalnej biblioteki, tak jak rambdamożesz to zrobićconst transpose = apply(zip)
Guy Who Knows Stuff

1
Dlaczego ta opcja miałaby być lepsza niż wybrana odpowiedź?
Alex Lenail

To pytanie ma lata i nie jestem pewien, czy teraz. Jest jednak czystszy niż oryginalna wersja zaakceptowanej odpowiedzi . Zauważysz, że od tego czasu był znacznie edytowany . Wygląda na to, że używa ES6, który, jak sądzę, nie był dostępny w 2013 roku, kiedy to pytanie zostało szeroko zadane.
Joe

24

najkrótsza droga z lodash/ underscorei es6:

_.zip(...matrix)

gdzie matrixmoże być:

const matrix = [[1,2,3], [1,2,3], [1,2,3]];

Lub bez ES6:_.zip.apply(_, matrix)
ach

9
Blisko, ale _.unzip (matrix) jest krótszy;)
Vigrant

1
Czy możesz to rozwinąć? Nie rozumiem, co tu mówisz. Ten krótki fragment ma rozwiązać problem? czy to tylko część czy co?
Julix,

1
mój Boże, to krótkie rozwiązanie. właśnie dowiedziałem się o operatorze ... - wykorzystałem go do rozbicia ciągu na tablicę liter ... dzięki za odpowiedź
Julix

21

Wiele dobrych odpowiedzi! Skonsolidowałem je w jedną odpowiedź i zaktualizowałem część kodu, aby uzyskać bardziej nowoczesną składnię:

One- liners inspirowane przez Fawad Ghafoor i Óscar Gómez Alcañiz

function transpose(matrix) {
  return matrix[0].map((col, i) => matrix.map(row => row[i]));
}

function transpose(matrix) {
  return matrix[0].map((col, c) => matrix.map((row, r) => matrix[r][c]));
}

Funkcjonalny styl podejścia z redukcją autorstwa Andrew Tatomyr

function transpose(matrix) {
  return matrix.reduce((prev, next) => next.map((item, i) =>
    (prev[i] || []).concat(next[i])
  ), []);
}

Lodash / Underscore użytkownika marcel

function tranpose(matrix) {
  return _.zip(...matrix);
}

// Without spread operator.
function transpose(matrix) {
  return _.zip.apply(_, [[1,2,3], [1,2,3], [1,2,3]])
}

Podejście waniliowe

function transpose(matrix) {
  const rows = matrix.length, cols = matrix[0].length;
  const grid = [];
  for (let j = 0; j < cols; j++) {
    grid[j] = Array(rows);
  }
  for (let i = 0; i < rows; i++) {
    for (let j = 0; j < cols; j++) {
      grid[j][i] = matrix[i][j];
    }
  }
  return grid;
}

Podejście Vanilla in-place ES6 inspirowane przez Emanuela Saringana

function transpose(matrix) {
  for (var i = 0; i < matrix.length; i++) {
    for (var j = 0; j < i; j++) {
      const temp = matrix[i][j];
      matrix[i][j] = matrix[j][i];
      matrix[j][i] = temp;
    }
  }
}

// Using destructing
function transpose(matrix) {
  for (var i = 0; i < matrix.length; i++) {
    for (var j = 0; j < i; j++) {
      [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
    }
  }
}

11

Schludny i czysty:

[[0, 1], [2, 3], [4, 5]].reduce((prev, next) => next.map((item, i) =>
    (prev[i] || []).concat(next[i])
), []); // [[0, 2, 4], [1, 3, 5]]

Poprzednie rozwiązania mogą prowadzić do niepowodzenia w przypadku podania pustej tablicy.

Tutaj jest to funkcja:

function transpose(array) {
    return array.reduce((prev, next) => next.map((item, i) =>
        (prev[i] || []).concat(next[i])
    ), []);
}

console.log(transpose([[0, 1], [2, 3], [4, 5]]));

Aktualizacja. Jeszcze lepiej można to napisać za pomocą operatora spreadu:

const transpose = matrix => matrix.reduce(
    ($, row) => row.map((_, i) => [...($[i] || []), row[i]]), 
    []
)

2
Nie wiem, czy lepiej nazwałbym tę aktualizację. To jest sprytne, jasne, ale to koszmar do przeczytania.
Marie

9

Możesz to zrobić na miejscu, wykonując tylko jeden przejazd:

function transpose(arr,arrLen) {
  for (var i = 0; i < arrLen; i++) {
    for (var j = 0; j <i; j++) {
      //swap element[i,j] and element[j,i]
      var temp = arr[i][j];
      arr[i][j] = arr[j][i];
      arr[j][i] = temp;
    }
  }
}

1
jeśli twoja tablica nie jest kwadratem (na przykład 2x8), to chyba nie działa
Olivier Pons

2
To rozwiązanie zmienia oryginalną tablicę. Jeśli nadal potrzebujesz oryginalnej macierzy, to rozwiązanie może nie być tym, czego chcesz. Inne rozwiązania zamiast tego tworzą nową tablicę.
Alex

Czy ktoś wie, jak to się robi w składni ES6? Próbowałem, [arr[j][j],arr[i][j]] = [arr[i][j],arr[j][j]]ale to nie działa, czy coś mi brakuje?
Nikasv

@Nikasv, którego prawdopodobnie chcesz [arr[j][i], arr[i][j]] = [arr[i][j], arr[j][i]]. Zwróć uwagę, że masz pewne arr[j][j]terminy, które zawsze będą odnosić się do komórek na przekątnej.
Algorithmic Canary

6

Po prostu kolejna odmiana użycia Array.map. Korzystanie z indeksów pozwala na transpozycję macierzy, w których M != N:

// Get just the first row to iterate columns first
var t = matrix[0].map(function (col, c) {
    // For each column, iterate all rows
    return matrix.map(function (row, r) { 
        return matrix[r][c]; 
    }); 
});

Wszystko, co trzeba zrobić, to odwzorowanie elementów najpierw w kolumnie, a następnie w wierszu.


5

Jeśli masz możliwość korzystania ze składni Ramda JS i ES6, możesz to zrobić w inny sposób:

const transpose = a => R.map(c => R.map(r => r[c], a), R.keys(a[0]));

console.log(transpose([
  [1, 2, 3, 4],
  [5, 6, 7, 8],
  [9, 10, 11, 12]
])); // =>  [[1,5,9],[2,6,10],[3,7,11],[4,8,12]]
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.22.1/ramda.min.js"></script>


2
Niesamowite użycie zarówno Ramdy, jak i ES6 do rozwiązania tego problemu
Ashwin Balamohan

1
Ramda faktycznie ma transpose-function teraz.
Jacob Lauritzen

5

Inne podejście polegające na iteracji tablicy od zewnątrz do wewnątrz i zredukowaniu macierzy poprzez mapowanie wartości wewnętrznych.

const
    transpose = array => array.reduce((r, a) => a.map((v, i) => [...(r[i] || []), v]), []),
    matrix = [[1, 2, 3], [1, 2, 3], [1, 2, 3]];

console.log(transpose(matrix));


W rzeczy samej! Zoptymalizowałeś odpowiedź @Andrew Tatomyr! (benchmarki działają na twoją korzyść
!;


2

Możesz to osiągnąć bez pętli, korzystając z następującego.

Wygląda bardzo elegancko i nie wymaga żadnych zależności, takich jak jQuery lub Underscore.js .

function transpose(matrix) {  
    return zeroFill(getMatrixWidth(matrix)).map(function(r, i) {
        return zeroFill(matrix.length).map(function(c, j) {
            return matrix[j][i];
        });
    });
}

function getMatrixWidth(matrix) {
    return matrix.reduce(function (result, row) {
        return Math.max(result, row.length);
    }, 0);
}

function zeroFill(n) {
    return new Array(n+1).join('0').split('').map(Number);
}

Zminimalizowane

function transpose(m){return zeroFill(m.reduce(function(m,r){return Math.max(m,r.length)},0)).map(function(r,i){return zeroFill(m.length).map(function(c,j){return m[j][i]})})}function zeroFill(n){return new Array(n+1).join("0").split("").map(Number)}

Oto demo, które wrzuciłem razem. Zwróć uwagę na brak pętli :-)

// Create a 5 row, by 9 column matrix.
var m = CoordinateMatrix(5, 9);

// Make the matrix an irregular shape.
m[2] = m[2].slice(0, 5);
m[4].pop();

// Transpose and print the matrix.
println(formatMatrix(transpose(m)));

function Matrix(rows, cols, defaultVal) {
    return AbstractMatrix(rows, cols, function(r, i) {
        return arrayFill(cols, defaultVal);
    });
}
function ZeroMatrix(rows, cols) {
    return AbstractMatrix(rows, cols, function(r, i) {
        return zeroFill(cols);
    });
}
function CoordinateMatrix(rows, cols) {
    return AbstractMatrix(rows, cols, function(r, i) {
        return zeroFill(cols).map(function(c, j) {
            return [i, j];
        });
    });
}
function AbstractMatrix(rows, cols, rowFn) {
    return zeroFill(rows).map(function(r, i) {
        return rowFn(r, i);
    });
}
/** Matrix functions. */
function formatMatrix(matrix) {
    return matrix.reduce(function (result, row) {
        return result + row.join('\t') + '\n';
    }, '');
}
function copy(matrix) {  
    return zeroFill(matrix.length).map(function(r, i) {
        return zeroFill(getMatrixWidth(matrix)).map(function(c, j) {
            return matrix[i][j];
        });
    });
}
function transpose(matrix) {  
    return zeroFill(getMatrixWidth(matrix)).map(function(r, i) {
        return zeroFill(matrix.length).map(function(c, j) {
            return matrix[j][i];
        });
    });
}
function getMatrixWidth(matrix) {
    return matrix.reduce(function (result, row) {
        return Math.max(result, row.length);
    }, 0);
}
/** Array fill functions. */
function zeroFill(n) {
  return new Array(n+1).join('0').split('').map(Number);
}
function arrayFill(n, defaultValue) {
    return zeroFill(n).map(function(value) {
        return defaultValue || value;
    });
}
/** Print functions. */
function print(str) {
    str = Array.isArray(str) ? str.join(' ') : str;
    return document.getElementById('out').innerHTML += str || '';
}
function println(str) {
    print.call(null, [].slice.call(arguments, 0).concat(['<br />']));
}
#out {
    white-space: pre;
}
<div id="out"></div>


Dlaczego nie chcesz tego robić bez pętli? Bez pętli jest wolno
Downgoat

5
Czy .map nie jest pętlą? Tylko jeden, którego nie widzisz? Chodzi mi o to, że przegląda wszystkie wejścia i robi z tym coś ...
Julix,

2

ES6 1linery jako:

let invert = a => a[0].map((col, c) => a.map((row, r) => a[r][c]))

tak samo jak Óscar, ale jak wolisz obrócić go w prawo:

let rotate = a => a[0].map((col, c) => a.map((row, r) => a[r][c]).reverse())

2

Edycja: ta odpowiedź nie transponuje macierzy, ale ją obróci. Przede wszystkim nie przeczytałem uważnie pytania: D

obrót w prawo i w lewo:

    function rotateCounterClockwise(a){
        var n=a.length;
        for (var i=0; i<n/2; i++) {
            for (var j=i; j<n-i-1; j++) {
                var tmp=a[i][j];
                a[i][j]=a[j][n-i-1];
                a[j][n-i-1]=a[n-i-1][n-j-1];
                a[n-i-1][n-j-1]=a[n-j-1][i];
                a[n-j-1][i]=tmp;
            }
        }
        return a;
    }

    function rotateClockwise(a) {
        var n=a.length;
        for (var i=0; i<n/2; i++) {
            for (var j=i; j<n-i-1; j++) {
                var tmp=a[i][j];
                a[i][j]=a[n-j-1][i];
                a[n-j-1][i]=a[n-i-1][n-j-1];
                a[n-i-1][n-j-1]=a[j][n-i-1];
                a[j][n-i-1]=tmp;
            }
        }
        return a;
    }

Nie odpowiada jednak na pytanie; Transpozycja jest jak ... odbicie lustrzane wzdłuż przekątnej (bez rotacji)
DerMike

@DerMike dzięki za wskazanie. Nie wiem, dlaczego popełniłem ten błąd :) Ale przynajmniej widzę, że przydało się to niektórym innym.
Aryan Firouzian

Oto mój jednoliniowy kod do obracania matrycy: stackoverflow.com/a/58668351/741251
Nitin Jadhav

1

Powyższe odpowiedzi okazały się trudne do odczytania lub zbyt szczegółowe, więc sam je piszę. Myślę, że jest to najbardziej intuicyjny sposób implementacji transpozycji w algebrze liniowej, nie wykonujesz wymiany wartości , ale po prostu wstaw każdy element we właściwe miejsce w nowej macierzy:

function transpose(matrix) {
  const rows = matrix.length
  const cols = matrix[0].length

  let grid = []
  for (let col = 0; col < cols; col++) {
    grid[col] = []
  }
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      grid[col][row] = matrix[row][col]
    }
  }
  return grid
}

1

Myślę, że jest to nieco bardziej czytelne. Używa Array.fromi logika jest identyczna z używaniem zagnieżdżonych pętli:

var arr = [
  [1, 2, 3, 4],
  [1, 2, 3, 4],
  [1, 2, 3, 4]
];

/*
 * arr[0].length = 4 = number of result rows
 * arr.length = 3 = number of result cols
 */

var result = Array.from({ length: arr[0].length }, function(x, row) {
  return Array.from({ length: arr.length }, function(x, col) {
    return arr[col][row];
  });
});

console.log(result);

Jeśli masz do czynienia z tablicami o nierównej długości, musisz zamienić na arr[0].lengthcoś innego:

var arr = [
  [1, 2],
  [1, 2, 3],
  [1, 2, 3, 4]
];

/*
 * arr[0].length = 4 = number of result rows
 * arr.length = 3 = number of result cols
 */

var result = Array.from({ length: arr.reduce(function(max, item) { return item.length > max ? item.length : max; }, 0) }, function(x, row) {
  return Array.from({ length: arr.length }, function(x, col) {
    return arr[col][row];
  });
});

console.log(result);


0
function invertArray(array,arrayWidth,arrayHeight) {
  var newArray = [];
  for (x=0;x<arrayWidth;x++) {
    newArray[x] = [];
    for (y=0;y<arrayHeight;y++) {
        newArray[x][y] = array[y][x];
    }
  }
  return newArray;
}

0

Bezbiblioteczna implementacja w języku TypeScript, która działa dla dowolnego kształtu macierzy, który nie obetnie twoich tablic:

const rotate2dArray = <T>(array2d: T[][]) => {
    const rotated2d: T[][] = []

    return array2d.reduce((acc, array1d, index2d) => {
        array1d.forEach((value, index1d) => {
            if (!acc[index1d]) acc[index1d] = []

            acc[index1d][index2d] = value
        })

        return acc
    }, rotated2d)
}

0

Jedna linijka, która nie zmienia podanej tablicy.

a[0].map((col, i) => a.map(([...row]) => row[i]))

0
reverseValues(values) {
        let maxLength = values.reduce((acc, val) => Math.max(val.length, acc), 0);
        return [...Array(maxLength)].map((val, index) => values.map((v) => v[index]));
}

3
Nie wysyłaj tylko kodu jako odpowiedzi, ale dołącz wyjaśnienie, co robi Twój kod i jak rozwiązuje problem. Odpowiedzi z wyjaśnieniem są na ogół wyższej jakości i częściej przyciągają głosy za.
Mark Rotteveel

0

const transpose = array => array[0].map((r, i) => array.map(c => c[i]));
console.log(transpose([[2, 3, 4], [5, 6, 7]]));


0

Nie znalazłem odpowiedzi, która mnie satysfakcjonowała, więc napisałem ją sam, myślę, że jest łatwa do zrozumienia i wdrożenia i nadaje się do wszystkich sytuacji.

    transposeArray: function (mat) {
        let newMat = [];
        for (let j = 0; j < mat[0].length; j++) {  // j are columns
            let temp = [];
            for (let i = 0; i < mat.length; i++) {  // i are rows
                temp.push(mat[i][j]);  // so temp will be the j(th) column in mat
            }
            newMat.push(temp);  // then just push every column in newMat
        }
        return newMat;
    }

0

Ponieważ nikt do tej pory nie wspomniał o funkcjonalnym podejściu rekurencyjnym, jest to moje podejście. Adaptacja Haskella Data.List.transpose.

var transpose = as => as.length ? as[0].length ? [ as.reduce( (rs,a) => a.length ? ( rs.push(a[0])
                                                                                   , rs
                                                                                   )
                                                                                 : rs
                                                            , []
                                                            )
                                                 , ...transpose(as.map(a => a.slice(1)))
                                                 ]
                                               : transpose(as.slice(1))
                                : [],
    mtx       = [[1], [1, 2], [1, 2, 3]];

console.log(transpose(mtx))
.as-console-wrapper {
  max-height: 100% !important
}

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.