本文讨论算法题中常见的二维数组、矩阵的打印问题

1·矩阵从外到内螺旋打印

顺时针从外到内螺旋打印矩阵,例如输入如下的矩阵:

1 2 3
4 5 6
7 8 9

打印1 2 3 6 9 8 7 4 5
对于这样的问题,如果我们每个点逐个判断下一个打印哪个点,会比较麻烦。因此可以考虑每次确定一个范围,例如左上角(记为start)和右下角(记为end)的位置坐标,那么每次打印这两个点确定的矩阵的最外面那一圈,接着左上角的点向右下移动,右下角的点向左上移动,直到start的坐标比end“更右”或“更下”。
还是以上面的矩阵为例,首先左上角位于[0][0]也就是数字1的位置,右下角对应[2][2]也就是9的位置,那么打印二者确定的这一圈元素。接着左上角向右下移动到[1][1],右下角向左上移动到[1][1]位置,打印此时确定的一圈元素,实际上只有5这个数字。之后再移动,左上[2][2],右下[0][0],显然此时二者已经交错,说明打印完成。
因此,终止的条件就是:

startR >endR || startC >endC

左上角点的行号大于右下角点行号,或左上角点列号大于右下角点列号。
在达到这个条件之前,我们打印有这两个点(或者四个变量)确定的最外圈元素,接着移动这两个点。
对于一圈元素,一般情况,对应一个行列数都大于1的矩阵,此时可以分成四部分(四条边)分别打印即可。但是对于最里面一圈的元素,观察可知,对于列数多于行数的矩阵,最后会剩下1行;行数大于列数的矩阵,最后会剩下一列;行列数一样且为奇数的方阵,最后剩下一个(也可以看做一行)。因此,打印一圈元素这个函数需要分类讨论,避免出错。
C++代码实现如下:

void printOneCircle(vector<vector<int>>&matrix, int sr, int sc, int er, int ec) {
  int r, c;
  if (sr == er) {
    r = sr;
    for(c=sc;c<=ec;++c) cout<< matrix[r][c] << " ";
  }
  else if (sc == ec) {
    c = sc;
    for (r = sr; r <= er; ++r) cout << matrix[r][c] << " ";
  }
  else {
    r = sr;
    for (c = sc; c < ec; ++c) cout << matrix[r][c] << " ";
    c = ec;
    for (r = sr; r < er; ++r) cout << matrix[r][c] << " ";
    r = er;
    for (c = ec; c > sc; --c) cout << matrix[r][c] << " ";
    c = sc;
    for (r = er; r > sr; --r) cout << matrix[r][c] << " ";
  }
  //cout << endl;
}
void printMatSpiral(vector<vector<int>>&matrix) {
  if (matrix.empty())return;
  int rows = matrix.size(), cols = matrix[0].size();
  int startR = 0, startC = 0, endR = rows-1, endC = cols-1;
  while (startR <=endR && startC <=endC) {
    printOneCircle(matrix, startR, startC, endR, endC);
    ++startR;
    ++startC;
    --endC;
    --endR;
    //cout << "startR: "<< startR<<" startC: " << startC << endl;
    //cout << "endR: " << endR << " endC: " << endC << endl;
  }
}

函数printOneCircle就是打印一圈的函数,参数包括矩阵本身,左上角行、列号,右下角行、列号。具体实现时,先判断是否为一行或一列,是的话打印完这一行或一列即可,否则打印四条边,从左到右、上到下、右到左、下到上。这么做主要是为了方便处理四个角上的点。

2·矩阵Zig-Zag打印

同样例如如下矩阵:

1 2 3
4 5 6
7 8 9

要求按照1 4 2 3 5 7 8 6 9的顺序打印,延副对角线方向,先右上到左下,再左下到右上交替的顺序打印。这道题思路与上题类似,都是要先确定每一轮打印的起止点或范围,打印后确定下一个范围,重复直到全部打印完成。
那么观察可知,这种打印方式无论方向,起止点都是左下和右上的点,左下的点先向下移动,到达最后一行后向右,右上的点先向右移动,到达最后一列后向下移动,直到二者都到达矩阵右下角的点终止。
对于上面的矩阵,第一次起止点都位于[0][0],也就是包含数字1;接着是 4,2这条线,然后3,7这条线,然后是6,8,最后9,打印结束。
C++代码如下:

void printOneLine(vector<vector<int>>&matrix, int sr, int sc, int er, int ec,bool stoe) {
  if (stoe) {
    int r = sr, c = sc;
    while (r <= er && c >= ec) {
      cout << matrix[r][c] << " ";
      ++r;
      --c;
    }
  }
  else {
    int r = er, c = ec;
    while (r >= sr && c <= sc) {
      cout << matrix[r][c] << " ";
      --r;
      ++c;
    }
  }
  //cout << endl;
}
void printMatDiagonalZigzag(vector<vector<int>>&matrix) {
  if (matrix.empty())return;
  int rows = matrix.size(), cols = matrix[0].size();
  int startR = 0, startC = 0, endR = 0, endC = 0;
  bool stoe = true;
  while (startR < rows && startC < cols && endR < rows && endC < cols) {
    printOneLine(matrix, startR, startC, endR, endC, stoe);
    startR = (startC == cols - 1 ? startR + 1 : startR);
    startC = (startC == cols - 1 ? startC : startC+1);
    endC = (endR == rows - 1 ? endC + 1 : endC);
    endR = (endR == rows - 1 ? endR : endR + 1);
    
    stoe = !stoe;
    //cout << "startR: "<< startR<<" startC: " << startC << endl;
    //cout << "endR: " << endR << " endC: " << endC << endl;
  }
}

注意这里我们在更新下一行的行列位置时,对于右上角点,由于是先向右移动后向下移动,以当前列数是否在最后一列,作为向下还是向右的标志,所以先更新行,后更新列,否则如果先更新列,在倒数第二列时,列数先增加,在更新行号时就会认为当前已在最后一列因此行数也要加一,这样就会出错。而对于左下角点,类似的,由于是先向下后向右,以当前行数是否为最后一行作为标志,要先更新列数,再更新行数。
由于要求Zig-Zag方式,这里对每一行传入一个bool型变量判断打印的方向。

总结来说,这一类问题不适合逐点判断,而是定义一组宏观的参数,每次对这组参数确定的部分打印,然后更新这组参数。要注意参数的更新策略以及终止条件是否正确,还有打印每一部分时有无特殊情况等。