算法专题之矩阵

223 阅读4分钟

前言

矩阵相关的算法在互联网世界有着广泛的应用,比如图片的像素修改(上一小结介绍过)、获取地图路径方案等.

在数学中,一个矩阵说穿了就是一个二维数组.矩阵相关的基础算法也都是基于二维数组的基础上完成各类数据操作.

本小节列举了前端面试中高频出现的leetcode真题,通过题目的思路讲解和代码实现系统的学习矩阵的基础知识.从宏观上建立起基本的认知,从而加深应用场景下使用矩阵解决实际问题的理解.

真题

图片旋转

一组由 N × N 矩阵表示的图像,其中每个像素点的大小为 4 字节.请你设计一种算法,将图像旋转90度.

示例:

给定 matrix = [
      [1,2,3],
      [4,5,6],
      [7,8,9]
    ]

旋转输入矩阵,使其变为:[
      [7,4,1],
      [8,5,2],
      [9,6,3]
    ]

思路分析.假设存在一个矩阵data = [[1,2],[3,4]](如下图).

2.png

如何将左图按顺时针旋转90度变成右图?

首先从下图中寻找规律,图中有左 - 中 - 右三种图片状态,为了从左图的1-2-3-4变成右图的3-1-4-2,可以通过以下两步实现.

  • 寻找矩阵的高度的中心轴线,上下两侧按照轴线进行数据交换.比如左图1 - 23 - 4之间可以画一条轴线,上下两侧围绕轴线交换数据,第一行变成了3 - 4,第二行变成了1 - 2.通过第一步操作变成了中图的样子.

  • 中图的对角线3 - 2和右图一致,剩下的将对角线两侧的数据对称交换就可以变成右图.比如将中图的14进行值交换.操作完后便实现了图片的旋转.值得注意的是4的数组索引是[0][1],而1的索引是[1][0],刚好索引顺序颠倒.

3.png

通过以上描述规律便可编写下面函数实现图片的旋转.

function rotate(matrix) {
  if(matrix.length == 0){
      return matrix;
  }
  
  const row = matrix.length; // 有多少行
  const column = matrix[0].length; // 有多少列

  const mid = row/2; // 找出中间行
  
  for(i = 0;i < mid;i++){
    const symmetric_hh = row - i -1; // 对称行的索引
    for(j = 0;j<column;j++){ // 以中间行为轴,上下对称行进行值交换
        let tmp = matrix[i][j];
        matrix[i][j] = matrix[symmetric_hh][j];
        matrix[symmetric_hh][j] = tmp;
    }
  }

  // 根据对角线进行值交换
  for(i = 0;i < row;i++){
    for(j = i+1;j<column;j++){
      let tmp = matrix[i][j];
      matrix[i][j] = matrix[j][i];
      matrix[j][i] = tmp;
    }
  }
 
  return matrix;

};

console.log(rotate([    // [ [ 7, 4, 1 ], [ 8, 5, 2 ], [ 9, 6, 3 ] ]
  [1,2,3],
  [4,5,6],
  [7,8,9]
]
));

矩阵搜索

请编写一个高效的算法来判断m x n 矩阵中,判断是否存在某一个目标值.该矩阵具有如下特性:

  • 每行中的整数从左到右按升序排列
  • 每行的第一个整数大于前一行的最后一个整数

示例:

输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true

从题意可知,题目是为了从二维数组寻找某个数据是否存在,存在返回true,不存在返回false.并且该二维数组装的都是整数,按升序排列.

既然是从升序的整数队列中寻找某个值,自然联想到二分查找法.比较简单的一条思路,是将二维数组先转化成一维数组,再使用二分法查找.

为了不修改矩阵的数据结构,本题采用另外一种方法解答(代码如下).先使用一次二分法确认目标值处于矩阵的哪一行,再使用一次二分法确认目标值处于哪一列.

function searchMatrix(matrix, target) {
  let end = matrix.length - 1; // 中止索引
  let start = 0; // 起始索引
  let mid;
  while(true){ // 第一层循环是为了确定目标值处于矩阵的哪一行
      if(start > end){
          return false;
      }
      mid = start + Math.floor((end - start)/2); //中间索引
      const start_value = matrix[mid][0];
      const end_value =  matrix[mid][matrix[mid].length - 1];
      if(target >= start_value && target <= end_value){
          if(target === start_value || target === end_value){
              return true;
          }
          break;
      }else if(target < start_value){
           end = mid - 1;
      }else{
           start = mid + 1;
      }     
  }
  const data = matrix[mid];
  end =  data.length - 1;
  start = 0;
  while(true){ // 第二层循环是为了确定目标值处于矩阵的哪一列
      if(start > end){
          return false;
      }
      mid = start + Math.floor((end-start)/2);
      if(data[mid] === target){
          return true;
      }else if(target < data[mid]){
          end = mid - 1; 
      }else{
          start = mid +1;
      }
  }

};

console.log(searchMatrix([[1,3,5,7],[10,11,16,20],[23,30,34,60]],60)); // true

矩阵赋值

给定一个 m x n 的矩阵,如果一个元素为0,则将其所在行和列的所有元素都设为0.

比如:

 0  1  2  0     0  0  0  0
 3  4  5  2  →  0  4  5  0
 1  3  1  5     0  3  1  0 

题目要将矩阵中的0元素所在的行和列都置为0.

整体思路用双重循环先遍历行,行中再遍历列.如果检测到某个元素等于0,就把这一列的序号缓存起来,而这一行的所有值就能全部填充为0了.

最后再根据缓存的列的序号,将矩阵的相应的列的数据全部置为0就是实现了目标(代码如下).

function setZeroes(matrix) {
  
  const column = new Set();

  for(let i = 0;i<matrix.length;i++){
      let flag = false; // 一行全部置0吗
      for(let j = 0;j<matrix[i].length;j++){
           if(matrix[i][j] == 0){
               column.add(j);
               flag = true;     
           }
           if(flag && j == matrix[i].length - 1){
               matrix[i].fill(0);
           }
      }
  }

  column.forEach((v)=>{
    for(let i = 0;i<matrix.length;i++){
        matrix[i][v] = 0;
    }
  })

  return matrix;
   
};

console.log(setZeroes([[1,1,1],[1,0,1],[1,1,1]])); // [[ 1, 0, 1 ], [ 0, 0, 0 ], [ 1, 0, 1 ] ]

螺旋矩阵

给你一个 mn 列的矩阵matrix,请按照顺时针螺旋顺序返回矩阵中的所有元素.

示例:

     1   2   3
     4   5   6
     7   8   9

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

题目要求按照顺时针螺旋顺序输出矩阵中的所有的元素,可按照以下几步操作.

  • 螺旋输出的第一步就是输出第一行的所有元素.第一行输出完毕后从矩阵中清除.

  • 第二步从上往下依次输出矩阵中每一行的最后一个元素,输出完毕后将矩阵的最后一列元素清除.

  • 第三步从右往左倒序输出最后一行的所有元素,输出完毕后将最后一行清除.

  • 第四步从下往上输出第一列的所有元素,输出完毕后清除.

上面一轮螺旋输出了最外圈的元素,继续对剩下的矩阵递归执行上述过程,最终就能输出所有元素(代码如下).

function spiralOrder(matrix) {
  
  const array = [];

  function spiralHandler(data){
      if(data.length > 0){
        // 输出第一行的元素  
        for(let j = 0;j<data[0].length;j++){
            array.push(data[0][j]);
        }
        data.shift();
        if(data.length == 0){
            return;
        }
        // 输出最后一列的元素
        const max_column = data[0].length - 1;
        if(max_column >= 0){
          for(let i = 0;i<data.length;i++){
            array.push(data[i][max_column]);
            data[i].pop();
            if(data[i].length == 0){ // 如果数组为空移除
                data.splice(i,1);
                i--;
            }
          }
        }
        // 倒序输出最后一行的元素
        const last_row = data.length - 1;
        if(last_row >= 0){
           for(let j = data[last_row].length - 1;j>=0;j--){
              array.push(data[last_row][j]);
           }
           data.pop();
        }
        // 输出第一列的元素     
        if(data.length > 0){
            for(let i = data.length -1;i>=0;i--){
                array.push(data[i][0]);
                data[i].shift();
                if(data[i].length == 0){  // 如果数组为空移除
                  data.splice(i,1);
                }
            }
        }    
      }else{
          return;
      }
      spiralHandler(data);
  }

  spiralHandler(matrix);
  
  return array;

};

console.log(spiralOrder([[1,2,3],[4,5,6],[7,8,9]])); // [1, 2, 3, 6, 9, 8, 7, 4, 5]

路径搜索

4.png

本题是要在矩阵中寻找一条符合要求的路径,如果存在返回true,不存在返回false.

矩阵中每一个点都有可能是起始点,因此首先要遍历矩阵中每一个点,让其作为起始点去判断路径是否存在.

整体的实现思路是采用递归和回溯的方式.例如上图中假如A为起始点,首先判断A的值是否等于word[index]的值(index默认为0,意为要寻找的第一个点).

如果不相等直接放弃当前路径,如果相等就要寻找第二个点,index自增1.以A为中心,按照上 - 右 - 下 - 左四个方向获取点依次判断.

而这四个点的判断方式又递归执行上述过程,寻找到了第二个点后再继续按上述方式寻找第三个点,直至将word的值全部找到(代码如下).

寻找路径的过程中,如果某条路经的点跑出了矩阵的边界或者与word[index]的值不相等,就要放弃该条路径回溯到上一步,继续探索其他的路径.

function exist(board, word) {
    //index是字符是索引,row是行数,column是列数
    function search(index,row,column){
        //如果出了边界或者值不相等说明当前这条路径不符合要求
        if(board[row] === undefined || board[row][column] === undefined || board[row][column] !== word[index]){
            return false;
        }
        //走到了这一步说明word的最后一个字符也在该条路经成功匹配,返回true
        if(index === word.length - 1){
            return true;
        }
        //将当前点的值缓存起来并置为false,那么在它的后续的路径寻找中该点一直为false不会被重复使用
        const tmp = board[row][column];
        board[row][column] = false;
        //按照上右下左的顺序寻找下一个点
        const result = search(index+1,row-1,column) || search(index+1,row,column+1) || search(index+1,row+1,column) || search(index+1,row,column - 1)
        // 递归结束后,利用闭包的特性将值还原
        board[row][column] = tmp;
        return result;
    } 
    for(let i = 0;i<board.length;i++){
        for(let j = 0;j<board[i].length;j++){
            if(search(0,i,j)){
                return true;
            }
        }
    }
    return false;
 };

 console.log(exist([["a","b"],["c","d"]],"abcd")); //  false