Algorithm Of Array&Matrix Intro


一、基础题解

1、顺时针打印二维数组

题目很基础但是也比较注重细节,具体思路:

  • 给矩阵定一个左上角和右下角
  • 打印的时候设置一个遍历坐标从左上角开始顺时针走
  • 顺时针走完一圈后就要更新左上角和右下角坐标,两者不能交错

关键点:不同矩阵最后左上角和右下角的情况不一样,比如说方阵的最后一步左上角和右下角重合,非方阵最后有两种情况,左上角和右下角在同一行上,或同一列上,此时要判断一下。

代码实现:

@Test
public void solution_2_顺时针打印矩阵() {

    int[][] matrix = RandomUtils.getRandomTwoDimensionArray(4, 3, 1, 10);
    printMatrix(matrix);
    System.out.println("顺时针打印:");

    // 左上角
    int leftUpRow = 0;
    int leftUpCol = 0;
    // 右下角
    int rightDownRow = matrix.length - 1;
    int rightDownCol = matrix[rightDownRow - 1].length - 1;

    while (leftUpRow <= rightDownRow && leftUpCol <= rightDownCol) {

        int row = leftUpRow;
        int col = leftUpCol;

        while (col <= rightDownCol) {
            System.out.print(matrix[leftUpRow][col++] + "\t");
        }
        col = rightDownCol;
        System.out.println();
        row++;
        while (row <= rightDownRow) {
            System.out.print(matrix[row++][rightDownCol] + "\t");
        }
        row = rightDownRow;
        System.out.println();
        col--;
        while (row > leftUpRow && col >= leftUpCol) {
            System.out.print(matrix[rightDownRow][col--] + "\t");
        }
        System.out.println();
        row--;
        while (row > leftUpRow) {
            System.out.print(matrix[row--][leftUpCol] + "\t");
        }
        System.out.println();
        leftUpRow++;
        leftUpCol++;
        rightDownRow--;
        rightDownCol--;
    }
}

2、矩阵中0值所在行列置0

将一个矩阵中 0 所在的行和列都置为 0。

这一题比较巧妙的一点就是并不是在遍历矩阵的时候对 0 值所在行列置 0,而是使用了 2 个数组记录 0 值的行列坐标,最后形成这样的结果:

代码实现:

@Test
public void solution_3_矩阵中0值所在的行列一并置0() {

    int[][] matrix = RandomUtils.getRandomTwoDimensionArray(3, 3, 0, 10);

    int M = matrix.length;
    int N = matrix[0].length;

    int[] rowRecord = new int[M];
    int[] colRecord = new int[N];

    for (int i = 0; i < M; i++) {
        for (int j = 0; j < N; j++) {
            if (matrix[i][j] == 0) {
                rowRecord[i] = 1;
                colRecord[j] = 1;
            }
        }
    }

    printMatrix(matrix);
    System.out.println();

    // 这种方式比较巧妙
    for (int row = 0; row < M; row++) {
        for (int col = 0; col < N; col++) {
            if (rowRecord[row] == 1 || colRecord[col] == 1)
                matrix[row][col] = 0;
        }
    }

    printMatrix(matrix);
}

3、Z 型打印矩阵

按照 Z 型走法去打印一个矩阵中的元素。

解题思路:

  • 设置一个坐标去按 Z 型走法遍历矩阵
  • 注意该坐标的走向:左下到右上、右上到左下、向右、向下

代码实现:

@Test
public void solution_4_Z型打印矩阵() {

    int[][] matrix = RandomUtils.getRandomTwoDimensionArray(4, 5, 1, 10);
    System.out.println("原矩阵: ");
    printMatrix(matrix);
    System.out.println("================================");

    // 打印元素的坐标
    int r = 0, c = 0;
    // 记录矩阵的行列最大值
    int m = matrix.length;
    int n = matrix[0].length;
    // 标记: 控制坐标斜向移动 左下到右上 还是 右上到左下
    boolean l2r = true; // true 表示左下到右上

    while (r < m && c < n) {

        // 从左下到右上的斜线
        if (l2r) {
            System.out.print(matrix[r][c] + "\t");
            // (左->右斜线) 假如现在在第一行 列未到边界,此时只能向右走
            if (r == 0 && c < n -1) {
                // 向右走一格后需要转向: 右上到左下
                l2r = !l2r;
                c++;
                continue;
            } else if (r > 0 && c < n - 1) {
                // 此时正在向右上角前进
                r--;
                c++;
                continue;
            } else {
                // 最后一列, 只能向下走
                r++;
                l2r = !l2r;
                continue;
            }
        } else {
            // 从右上到左下的斜线
            System.out.print(matrix[r][c] + "\t");
            if (r < m - 1 && c > 0) {
                r++;
                c--;
                continue;
            } else if (r < m - 1 && c == 0) {
                r++;
                l2r = !l2r;
                continue;
            } else {
                c++;
                l2r = !l2r;
                continue;
            }
        }
    }
}

4、边界为 1 的最大子方阵

给定一个 NxN 的矩阵 matrix,在这个矩阵中,只有 0 和 1 两种值,返回边框全是 1 最大正方形的边长长度。

方法一:暴力法

给定的是 NxN 方阵,可知符合条件的最大子方阵的边长长度最大是 N,最小是 1。

从最大的情况向内部推进:

@Test
public void solution_5_边界为1的最大子方阵() {

    int[][] matrix = {
        {0, 1, 1, 1, 1},
        {0, 1, 0, 0, 1},
        {0, 1, 0, 0, 1},
        {0, 1, 1, 1, 1},
        {0, 1, 0, 1, 1}
    };

    int maxRow = matrix.length;
    int maxCol = matrix[0].length;
    int maxSide = 0;

    // 暴力破解
    maxSide = s5_brute_force(matrix, maxRow, maxCol);
    System.out.println(maxSide);
}
private int s5_brute_force(int[][] matrix, int maxRow, int maxCol) {

    // matrix 是方阵,可以所求结果最大为 maxRow, 最小为 0
    for (int i = maxRow; i > 0; i--) {
        int maxR = maxRow - i;
        int maxC = maxCol - i;
        // j 和 k 用于确认左上角元素的范围
        for (int j = 0; j <= maxR; j++) {
            loop3:for (int k = 0; k <= maxC; k++) {
                // l 和 m 用于确认右下角元素的范围
                int l = j + i;
                int m = k + i;
                // 遍历方阵的四条边是否全部为 1
                for (int top = k; top < m; top++) {
                    if (matrix[j][top] == 0)
                        continue loop3;
                }
                for (int right = j + 1; right < l; right++) {
                    if (matrix[right][m - 1] == 0)
                        continue loop3;
                }
                for (int left = j + 1; left < l - 1; left++) {
                    if (matrix[left][k] == 0)
                        continue loop3;
                }
                for (int down = k; down < m - 1; down++) {
                    if (matrix[l - 1][k] == 0)
                        continue loop3;
                }
                // 走完循环,证明出现了四周全为 1 的方阵
                return i;
            }
        }
    }
    return 0;
}

时间复杂度:

  • 假设最大长度:from N to 1
  • 遍历矩阵:N * N
  • 内部遍历四条边:4n 约等于 N

总的 $T(n) = O(N^4)$

方法二:预处理优化

对于该题能够优化的地方就是在内部遍历四条边的时候,我们可以构建一个辅助三维数组,通过预处理数据,得到一些特征数据。

该三维数组前两维和 matrix 矩阵一样,只是最后一维是一个长度为 2 的数组,第一个元素记录前两维锚定的元素的右方有多少个 1(包括自身),第二个元素记录前两维锚定的元素的下方有多少个 1(包括自身)。

代码如下:

// 方法二:预处理优化
private int s5_preprocess_brute_force(int[][] matrix, int maxRow, int maxCol) {

    // 预处理
    preprocessing(matrix);

    // matrix 是方阵,可以所求结果最大为 maxRow, 最小为 0
    for (int i = maxRow; i > 0; i--) {
        int maxR = maxRow - i;
        int maxC = maxCol - i;
        // j 和 k 用于确认左上角元素的范围
        for (int j = 0; j <= maxR; j++) {
            for (int k = 0; k <= maxC; k++) {
                if (check(j, k, i))
                    return i;
            }
        }
    }
    return 0;
}

// 辅助数组
static int[][][] help = null;

// 预处理
private void preprocessing(int[][] matrix) {
    int n = matrix.length;
    // 预处理数组是三维的,前两维和 matrix 一样,但是第三维记录着该元素的右方和下方的 1 的个数
    help = new int[n][n][2];

    // 先处理 matrix 的最后一行
    int lastRow = n - 1;
    for (int lastCol = n - 1; lastCol >= 0; lastCol--) {
        int val = matrix[lastRow][lastCol];
        if (lastCol == n - 1 && val == 1) {
            help[lastRow][lastCol][0] = 1;
            help[lastRow][lastCol][1] = 1;
        } else if (val == 1) {
            help[lastRow][lastCol][0] = help[lastRow][lastCol + 1][0] + 1;
            help[lastRow][lastCol][1] = 1;
        } else {
            help[lastRow][lastCol][0] = 0;
            help[lastRow][lastCol][1] = 0;
        }
    }

    // 再处理 matrix 的最后一列
    int lastCol = n - 1;
    for (int row = n - 2; row >= 0; row--) {
        int val = matrix[row][lastCol];
        if (val == 1) {
            help[row][lastCol][0] = 1;
            help[row][lastCol][1] = help[row + 1][lastCol][1] + 1;
        } else {
            help[row][lastCol][0] = 0;
            help[row][lastCol][1] = 0;
        }
    }

    // 最后处理其他行和列
    for (int i = n - 2; i >= 0; i--) {
        for (int j = n - 2; j >= 0; j--) {
            int val = matrix[i][j];
            if (val == 1) {
                help[i][j][0] = help[i][j + 1][0] + 1;
                help[i][j][1] = help[i + 1][j][1] + 1;
            } else {
                help[i][j][0] = 0;
                help[i][j][1] = 0;
            }
        }
    }
}

// 通过辅助数组进行判断
private boolean check(int j, int k, int n) {
    if (help[j][k][0] >= n && help[j][k][1] >= n) {
        if (help[j][k + n - 1][1] >= n && help[j + n - 1][k][0] >= n)
            return true;
    }
    return false;
}

这道题引入了预处理的概念,和动态规划中打表法有些类似,要注意题中我们是如何进行预处理的,先从最后一行和最后一列处理比较方便。

5、子数组最大累加和

  • 给定一个数组 arr,返回子数组的最大累加和

例如:arr = [1, -2, 3, 5, -2, 6, -1]; 所有的子数组中 [3, 5, -2, 6] 可以累加出最大的和 12

方法一:暴力法

子数组必定是连续的,一个数组拥有的子数组数量和其长度 N 是一个平方级的关系。

$T(N) = O(N²)$

@Test
public void solution_6_子数组最大累加和() {

    int[] arr = {1, -2, 3, 5, -2, 6, -1};

    // 暴力法
    int maxSum = solution_6_brute_force(arr);
    System.out.println(maxSum);
}
// 方法一: 暴力破解法
private int solution_6_brute_force(int[] arr) {

    int maxSum = -1;

    for (int i = 0; i < arr.length; i++) {
        int sum = arr[i];
        int maxOfI = sum;

        for (int j = i + 1; j < arr.length; j++) {
            sum += arr[j];
            if (sum > maxOfI)
                maxOfI = sum;
        }

        if (maxOfI > maxSum)
            maxSum = maxOfI;
    }

    return maxSum;
}

方法二:优化

对于连续子数组的和,如果其中出现了某一连续部分的和为负数,那么可以认为这一部分对于求和是没有意义的,可以直接越过。

// 方法二:优化
private int solution_6_optimize(int[] arr) {

    int maxSum = -1;
    for (int i = 0; i < arr.length; i++) {
        int sum = arr[i];
        int maxOfI = sum;
        for (int j = i + 1; j < arr.length; j++) {
            sum += arr[j];
            // 如果这部分的和为负数,则可以越过
            if (sum < 0) {
                i = j;
                break;
            } else {
                if (sum > maxOfI)
                    maxOfI = sum;
            }
        }
        if (maxOfI > maxSum)
            maxSum = maxOfI;
    }

    return maxSum;
}

6、子矩阵的最大累加和

  • 给定一个矩阵 matrix,其中的值有正、有负、有0,返回子矩阵的最大累加和

这一题和上一题的不同之处就在于,上一题是数组,这一题是矩阵,但是我们可以利用上一题的思路来求解。

如果直接使用暴力法时间复杂度太高,因为要找出所有子矩阵,太不划算。

  • 解题思路:

先对矩阵的每一行求解当前行的最大累加和,接着将 2 行合并为 1 行,进行求解最大累加和,以此类推。

以 3 * 3 矩阵为例:

  • 情况一:对每一行求解最大累加和,共 3 种情况
  • 情况二:将两行合并为一行求解最大累加和,共 2 种情况
  • 情况三:将三行合并为一行求解最大累加和,共 1 种情况
  • 从 6 种情况中找到最大值即可求解

代码实现:

@Test
public void solution_7_子矩阵的最大累加和() {

    int[][] matrix = {
        {-1, -1, -1},
        {-1, 2, 2},
        {-1, -1, -1}
    };

    // 利用上一题的思想求解此题
    int maxSum = maxSum(matrix);
    System.out.println(maxSum); // 4
}

private int maxSum(int[][] matrix) {

    int beginRow = 0;
    final int M = matrix.length;
    final int N = matrix[0].length;

    int[] sums = new int[N];
    int max = -1;

    while (beginRow < M) {
        for (int i = beginRow; i < M; i++) {
            for (int j = 0; j < N; j++) {
                sums[j] += matrix[i][j];
            }

            int t = findMaxSum(sums);
            if (t > max)
                max = t;
        }
        Arrays.fill(sums, 0);
        beginRow++;
    }
    return max;
}

private int findMaxSum(int[] sums) {
    
    if (sums.length == 0)
        return 0;
    
    int maxSum = -1;
    for (int i = 0; i < sums.length; i++) {
        int tmp = sums[i];
        int sum = tmp;
        for (int j = i + 1; j < sums.length; j++) {
            tmp += sums[j];
            if (tmp < 0) {
                i = j;
                break;
            }
            if (tmp > sum)
                sum = tmp;
        }
        if (sum > maxSum)
            maxSum = sum;
    }

    return maxSum;
}

二、矩阵运算

1、矩阵加减

运算规则

  • 矩阵的加减运算要求:
    • 参于运算的矩阵必须为同型矩阵(例如 Amxn 和 Bmxn),即行数和列数都相等。
  • 实质:
    • 相同位置上的元素相加减

运算性质

  • 满足交换律和结合律
    • 交换律:A + B = B + A
    • 结合律:(A + B)+ C = A + (B + C)

2、矩阵与数的乘法

运算规则

数 $\lambda$ 乘矩阵 A,就是将数 $\lambda$ 乘矩阵 A 中的每一个元素,记为 $\lambda{A}$ 或 ${A}\lambda$ 。

特别的,称 $- A$ 称为 $A = ({a_{ij}})_{m \times s}$ 的负矩阵。

运算性质

满足结合律和分配律

  • 结合律:$(\lambda\mu)A = \lambda({\mu}A)\quad (\lambda + \mu)A = \lambda A + \mu A$
  • 分配律:$\lambda (A + B) = \lambda A + \lambda B$

例题:

已知两个矩阵:
$$
A = \begin{bmatrix} 3 & -1 & 2 \\ 1 & 5 & 7 \\ 2 & 4 & 5 \end{bmatrix},
\quad
B = \begin{bmatrix} 7 & 5 & -2 \\ 5 & 1 & 9 \\ 4 & 2 & 1 \end{bmatrix}
$$
满足矩阵方程 $A + 2X = B$,求未知矩阵 $X$ 。

解:由已知条件可知:
$$
X = \frac12(B - A) = \frac12
\left(
\begin{bmatrix} 7&5&-2 \\ 5&1&9 \\ 4&2&1\end{bmatrix}
-
\begin{bmatrix}3&-1&2 \\ 1&5&7 \\ 2&4&5\end{bmatrix}
\right)
= \frac12 \begin{bmatrix}4&6&-4 \\ 4&-4&2 \\ 2&-2&-4\end{bmatrix}
= \begin{bmatrix}2&3&-2 \\ 2&-2&1 \\ 1&-1&-2\end{bmatrix}
$$

代码实现:

@Test
public void solution_8_矩阵与数的运算() {

    // 注意:这里只举简单的情况,运算双方都是整数
    int[][] A = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };

    int n = 2;
    matrixMULInteger(A, n);

    printMatrix(A);

    // 2	4	6
    // 8	10	12
    // 14	16	18
}
private void matrixMULInteger(int[][] matrix, int n) {

    int M = matrix.length;
    int N = matrix[0].length;

    for (int i = 0; i < M; i++) {
        for (int j = 0; j < N; j++) {
            matrix[i][j] *= n;
        }
    }
}

3、矩阵与矩阵的乘法

运算规则

设 $A$ 为 $m \times p$ 的矩阵,$B$ 为 $p \times n$ 的矩阵,那么称 $m \times n$ 的矩阵 $C$ 为矩阵 $A$ 与 $B$ 的乘积,记作 $C = AB$,其中矩阵 $C$ 的第 $i$ 行第 $j$ 列元素可以表示为:

$$
(AB)_ {ij} = \sum_ {k = 1} ^ {p} a_ {ik} b_ {kj} = a_ {i1} b_ {1j} + a_ {i2} b_ {2j} + {\cdots} + a_ {ip} b_ {pj}
$$

矩阵 $A$ ,$B$ 表示为:

$$
A = \begin{bmatrix}a_{1,1}&a_{1,2}&a_{1,3} \\ a_{2,1}&a_{2,2}&a_{2,3}\end{bmatrix}
\qquad
B = \begin{bmatrix}b_{1,1}&b_{1,2} \\ b_{2,1}&b_{2,2} \\ b_{3,1}&b_{3,2}\end{bmatrix}
$$

注意:

  • 当矩阵 $A$ 的列数(column)等于矩阵 $B$ 的行数(row)时,$A,B$ 才可以相乘;
  • 矩阵 $C$ 的行数等于矩阵 $A$ 的行数,$C$ 的列数等于 $B$ 的列数;
  • 乘积 $C$ 的第 m 行第 n 列元素等于矩阵 $A$ 的第 m 行元素与矩阵 $B$ 的第 n 列对应元素乘积之和。

基本性质

  • 乘法结合律:$(AB)C = A(BC)$
  • 乘法左分配律:$(A + B)C = AC + BC$
  • 乘法右分配律:$C(A + B) = CA + CB$
  • 对数乘的结合性:$k(AB) = (kA)B = A(kB)$
  • 转置:$(AB)^T = B^TA^T$
  • 矩阵乘法在以下两种情况下满足交换律:
    • $AA^* = A^*A$,$A$ 和伴随矩阵相乘满足交换律
    • $AE = EA$,$A$ 和单位矩阵或数量矩阵满足交换律

方阵的幂

定义:设 $A$ 是方阵,$k$ 是一个正整数,规定 $A^0 = E,{\quad}A^k = \underbrace{A{\cdot}A{\cdot}A{\cdot}\;{\cdots}\;{\cdot}A}_k$

代码示例: 常规矩阵乘法

例如:
$$
C = AB =
\begin{bmatrix}7&4&5 \\ 2&7&3\end{bmatrix}
\times
\begin{bmatrix}5&1 \\ 1&6 \\ 7&6\end{bmatrix}
=
\begin{bmatrix}74&61 \\ 38&62\end{bmatrix}
$$

@Test
public void solution_9_矩阵与矩阵乘法() {

    int[][] A = RandomUtils.getRandomTwoDimensionArray(2, 3, 1, 7);
    int[][] B = RandomUtils.getRandomTwoDimensionArray(3, 2, 1, 7);

    System.out.println("matrix A :");
    printMatrix(A);

    System.out.println("matrix B :");
    printMatrix(B);

    System.out.println("A x B = ");
    int[][] res = matrixMULmatrix(A, B);
    printMatrix(res);

}

private int[][] matrixMULmatrix(int[][] A, int[][] B) {

    if (A[0].length != B.length) {
        throw new IllegalArgumentException("运算双方不符合矩阵乘法规则!");
    }

    int M = A.length;
    int N = B[0].length;

    int[][] res = new int[M][N];

    for (int i = 0; i < M; i++) {
        for (int j = 0; j < N; j++) {
            int sum = 0;
            for (int l = 0; l < B.length; l++) {
                sum += A[i][l] * B[l][j];
            }
            res[i][j] = sum;
        }
    }

    return res;
}

Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC