LeetCode刷题之数组与矩阵

833 阅读7分钟

283. 移动零(Easy)

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]

说明:

  1. 必须在原数组上操作,不能拷贝额外的数组。
  2. 尽量减少操作次数。
两遍遍历

思路:题目要求把所有非0数移动到数组左边,那么可以采用两次遍历的方法,第一次遍历,将所有的非0元素移动到数组左边(不是采用交换位置的方式),第二次遍历就可以把右边的所有元素置为0了。

public void moveZeroes(int[] nums) {
    if (nums.length == 0) {
        return;
    }

    //准备两个指针
    int index = 0;
    //将所有的非0数移动到数组左边
    for (int num : nums) {
        if (num != 0) {
            nums[index++] = num;
        }
    }

    //将剩下的所有元素全部置为0
    while (index < nums.length) {
        nums[index++] = 0;
    }
}
一遍遍历

借鉴了快排的思想,用0当做中间点,把不等于0(注意题目没说不能有负数)的放到中间点的左边,等于0的放到其右边。

public void moveZeroes(int[] nums) {
    if (nums.length == 0) {
        return;
    }

    //两个指针i和j,j表示非零那部分的右边界
    int j = 0;
    for (int i = 0; i < nums.length; i++) {
        //元素为0的话,j记住,i继续往下找
        //当前元素不为0,就将他移动到左边,等于0的移动到右边
        if (nums[i] != 0) {
            int tmp = nums[i];
            nums[i] = nums[j];
            nums[j++] = tmp;
        }
    }
}

566. 重塑矩阵(Easy)

在MATLAB中,有一个非常有用的函数 reshape,它可以将一个矩阵重塑为另一个大小不同的新矩阵,但保留其原始数据。

给出一个由二维数组表示的矩阵,以及两个正整数r和c,分别表示想要的重构的矩阵的行数和列数。

重构后的矩阵需要将原始矩阵的所有元素以相同的行遍历顺序填充。

如果具有给定参数的reshape操作是可行且合理的,则输出新的重塑矩阵;否则,输出原始矩阵。

示例 1:

输入: 
nums = 
[[1,2],
 [3,4]]
r = 1, c = 4
输出: 
[[1,2,3,4]]

解释:
行遍历nums的结果是 [1,2,3,4]。新的矩阵是 1 * 4 矩阵, 用之前的元素值一行一行填充新矩阵。

示例 2:

输入: 
nums = 
[[1,2],
 [3,4]]
r = 2, c = 4
输出: 
[[1,2],
 [3,4]]

解释:
没有办法将 2 * 2 矩阵转化为 2 * 4 矩阵。 所以输出原矩阵。

注意:

  • 给定矩阵的宽和高范围在 [1, 100]。
  • 给定的 r 和 c 都是正数。
逐个填充

只需要这个遍历填充即可,注意边界条件。

public int[][] matrixReshape(int[][] nums, int r, int c) {
    if (nums.length == 0 || nums == null) {
        return nums;
    }


    if (nums.length * nums[0].length < r * c) {
        return nums;
    }

    //初始化新矩阵
    int[][] result = new int[r][c];
    int row = 0;
    int col = 0;
    for (int i = 0; i < nums.length; i++) {
        for (int j = 0; j < nums[0].length; j++) {
            result[row][col] = nums[i][j];
            //如果当前行没有填充完就继续填充
            if (col < c - 1) {
                col++;
            } else {
                //当前行填充完了就跳到下一行的末尾。
                col = 0;
                row++;
            }
        }
    }
    return result;
}



//找出新数组和旧数组坐标的规律可简化判断条件
public int[][] matrixReshape(int[][] nums, int r, int c) {
    int m = nums.length, n = nums[0].length;
    if (m * n != r * c) {
        return nums;
    }
    int[][] reshapedNums = new int[r][c];
    int index = 0;
    for (int i = 0; i < r; i++) {
        for (int j = 0; j < c; j++) {
            reshapedNums[i][j] = nums[index / n][index % n];
            index++;
        }
    }
    return reshapedNums;
}

485. 最大连续1的个数(Easy)

给定一个二进制数组, 计算其中最大连续1的个数。

示例 1:

输入: [1,1,0,1,1,1]
输出: 3
解释: 开头的两位和最后的三位都是连续1,所以最大连续1的个数是 3.

注意:

  • 输入的数组只包含 0 和1。
  • 输入数组的长度是正整数,且不超过 10,000。
直接遍历

一开始想到的是下面的解法,这种解法适合求连续相同的字符的个数,求连续1的话太繁杂,因为数组nums只有0和1。

public int findMaxConsecutiveOnes(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }

    int result = 0;
    int curCount = 0;
    int index = 0;

    //遍历数组
    while (index < nums.length) {
        //当前下班元素为1,求出以这个1开头的连续1的个数
        if (nums[index] == 1) {
            while (index < nums.length && nums[index] == 1) {
                curCount++;
                index++;
            }
            result = Math.max(curCount, result);
            curCount = 0;
        }else {
            index++;
        }
    }
    return result;
}

240. 搜索二维矩阵 II(Medium)

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:

每行的元素从左到右升序排列。 每列的元素从上到下升序排列。 示例:

现有矩阵 matrix 如下:

[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。

给定 target = 20,返回 false
起始点的选择

这是一种有点类似二分查找的方法,矩阵从左到右和从上到下都是升序的,如果换个起点,选择右上角和左下角,就会发现左边的数比当前数小,右边数比当前数大。所以可以把当前值和target进行比较

  • 如果 target 的值大于当前值,那么就向下走。
  • 如果 target 的值小于当前值,那么就向左走。
  • 如果相等的话,result改为true,跳出循环。
public boolean searchMatrix(int[][] matrix, int target) {
    if (matrix == null || matrix.length == 0) {
        return false;
    }

    boolean result = false;

    //初始化一个元素的索引
    int i = 0, j = matrix[0].length - 1;
    //从右上角开始搜索
    while (i >= 0 && j >= 0 && i < matrix.length && j < matrix[0].length) {
        if (matrix[i][j] == target) {
            result = true;
            break;
        } else if (matrix[i][j] > target) {
            j--;
        } else {
            i++;
        }
    }

    return result;
}

378. 有序矩阵中第K小的元素(Medium)

给定一个 n * n 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。 请注意,它是排序后的第 k 小元素,而不是第 k 个不同的元素。

示例:

matrix = [
   [ 1,  5,  9],
   [10, 11, 13],
   [12, 13, 15]
],
k = 8,

返回 13。

提示:

  • 你可以假设 k 的值永远是有效的,1 ≤ k ≤ n2 。
优先级队列

这种找第k个大小的元素可以想到用优先级队列来解决,这道题求第k小,那么可以采用大根堆。

public int kthSmallest(int[][] matrix, int k) {
    //初始化大根堆
    PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(k, new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    });

    //把数组中的元素传入大根堆
    for (int i = 0; i < matrix.length; i++) {
        for (int j = 0; j < matrix[0].length; j++) {
            //如果大根堆里已经满了,且最大值小于当前值,直接跳到下一行
            if (maxHeap.size() == k && maxHeap.peek() < matrix[i][j]) {
                break;
            }
            maxHeap.add(matrix[i][j]);
            //确保大根堆中只有k个元素
            if (maxHeap.size() > k) {
                maxHeap.poll();
            }
        }
    }

    return maxHeap.peek();
}

645. 错误的集合(Easy)

集合 S 包含从1到 n 的整数。不幸的是,因为数据错误,导致集合里面某一个元素复制了成了集合里面的另外一个元素的值,导致集合丢失了一个整数并且有一个元素重复。

给定一个数组 nums 代表了集合 S 发生错误后的结果。你的任务是首先寻找到重复出现的整数,再找到丢失的整数,将它们以数组的形式返回。

示例 1:

输入: nums = [1,2,2,4]
输出: [2,3]

注意:

给定数组的长度范围是 [2, 10000]。 给定的数组是无序的。

排序

对数组进行排序,然后进行遍历,如果当前元素等于前一个元素,那么当前元素就是重复的,且index+1就是丢失的元素,这个方法比较简单,不展开。

交换

数组包含1~n,那么说明原来的元素和下标是一一对应的num=index+1,第一次遍历数组,将元素放到正确的位置;第二次遍历,找到重复和丢失的数。

public int[] findErrorNums(int[] nums) {

    // 遍历数组,将数组交换到“正确”的位置
    for (int i = 0; i < nums.length; i++) {
        while (i + 1 != nums[i] && nums[i] != nums[nums[i] - 1]) {
            swap(nums, i, nums[i] - 1);
        }
    }

    // 找出重复元素和丢失的元素
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] != i + 1) {
            return new int[]{nums[i], i + 1};
        }
    }
    return null;
}

private void swap(int[] nums, int i, int j) {
    int tmp = nums[j];
    nums[j] = nums[i];
    nums[i] = tmp;
}

287. 寻找重复数(Meidum)

给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

示例 1:

输入: [1,3,4,2,2]
输出: 2

示例 2:

输入: [3,1,3,4,2]
输出: 3

说明:

  • 不能更改原数组(假设数组是只读的)。
  • 只能使用额外的 O(1) 的空间。
  • 时间复杂度小于 O(n2) 。
  • 数组中只有一个重复的数字,但它可能不止重复出现一次。
二分法(抽屉原理)

我们知道二分法的使用条件是数组有序,但是给定的数组是无序的,那怎么使用呢?

我们可以自己在有序的1~n进行二分,先取mid=(1+n)/2,判断原数组中小于等于mid的数有多少个,如果大于mid,那么根据抽屉原理,重复的数一定在[left,mid]内。

public int findDuplicate(int[] nums) {
      int len = nums.length;
      int left = 1;
      int right = len;

      while (left < right) {
          int mid = left + (right - left) / 2;

          int count = 0;
          for (int num : nums) {
              if (num <= nums[mid]) {
                  count++;
              }
          }

          // 根据抽屉原理,如果小于4的个数严格大于4
          // 说明重复元素一定在[1,4]区间
          if (count > mid) {
              right = mid;
          } else {
              left = mid + 1;
          }
      }
      return left;
  }

667. 优美的排列 II(Medium)

给定两个整数 n 和 k,你需要实现一个数组,这个数组包含从 1 到 n 的 n 个不同整数,同时满足以下条件:

① 如果这个数组是 [a1, a2, a3, ... , an] ,那么数组 [|a1 - a2|, |a2 - a3|, |a3 - a4|, ... , |an-1 - an|] 中应该有且仅有 k 个不同整数;.

② 如果存在多种答案,你只需实现并返回其中任意一种.

示例 1:

输入: n = 3, k = 1
输出: [1, 2, 3]
解释: [1, 2, 3] 包含 3 个范围在 1-3 的不同整数, 并且 [1, 1] 中有且仅有 1 个不同整数 : 1

示例 2:

输入: n = 3, k = 2
输出: [1, 3, 2]
解释: [1, 3, 2] 包含 3 个范围在 1-3 的不同整数, 并且 [2, 1] 中有且仅有 2 个不同整数: 1 和 2

提示:

 n 和 k 满足条件 1 <= k < n <= 10^4.

找规律

1~n顺序排序的数组的不同整数为1,当其中一个数字和另一个数字交换一次位置之后,不同整数就变为2。我们需要做的是构建数字之间不同的间隔。找规律。

当n = 50, k = 20时: [1,21,2,20,3,19,4,18,5,17,6,16,7,15,8,14,9,13,10,12,11,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50]
可以得出,下标从[0, k]中,偶数下标填充[1,2,3…],奇数下标填充[k + 1, k, k - 1…],后面[k + 1, n - 1]都是顺序填充

public int[] constructArray(int n, int k) {
    int[] ret = new int[n];
    int numK = k + 1;
    int numTemp = 1;
    //下标段[0, k]中,偶数下标填充[1,2,3..]
    for (int i = 0; i <= k; i += 2) {
    	ret[i] = numTemp++;
    }
    
    //下标段[0, k]中,奇数下标填充[k + 1, k, k - 1...]
    for (int i = 1; i <= k; i += 2) {
    	ret[i] = numK--;
    }
    
    //下标段[k + 1, n - 1]都是顺序填充
    for (int i = k + 1; i < n; i++) {
    	ret[i] = i + 1;
    }
    
    return ret;
}

697. 数组的度(Easy)

给定一个非空且只包含非负数的整数数组 nums, 数组的度的定义是指数组里任一元素出现频数的最大值。

你的任务是找到与 nums 拥有相同大小的度的最短连续子数组,返回其长度。

示例 1:

输入: [1, 2, 2, 3, 1]
输出: 2

解释:
输入数组的度是2,因为元素1和2的出现频数最大,均为2.

连续子数组里面拥有相同度的有如下所示: [1, 2, 2, 3, 1], [1, 2, 2, 3], [2, 2, 3, 1], [1, 2, 2], [2, 2, 3], [2, 2] 最短连续子数组[2, 2]的长度为2,所以返回2.

示例 2:

输入: [1,2,2,3,1,4,2]
输出: 6

注意:

nums.length 在1到50,000区间范围内。
nums[i] 是一个在0到49,999范围内的整数。

分析

求数组的度,就是求出现频率最大的数字的 index 距离的最小值。

  • 首先需要求出所有数字出现的频率,以及最大的频率是多少
  • 求出所有数字的 firstIndexlastIndex
  • 接着遍历数组,找出最小的 index
public int findShortestSubArray(int[] nums) {
      Map<Integer, Integer> numsCnt = new HashMap<>(); // 数字出现的频率
      Map<Integer, Integer> numsLastIndex = new HashMap<>();
      Map<Integer, Integer> numsFirstIndex = new HashMap<>();

      for (int i = 0; i < nums.length; i++) {
          int num = nums[i];
          numsCnt.put(num, numsCnt.getOrDefault(num, 0) + 1);
          numsLastIndex.put(num, i);
          if (!numsFirstIndex.containsKey(num)) {
              numsFirstIndex.put(num, i);
          }
      }

      // 求数字出现的最大频率
      int maxCnt = 0;
      for (int num : nums) {
          maxCnt = Math.max(maxCnt, numsCnt.get(num));
      }

      int ret = nums.length;
      for (int i = 0; i < nums.length; i++) {
          int num = nums[i];
          int cnt = numsCnt.get(num);
          if (cnt != maxCnt) {
              continue;
          }
          ret = Math.min(ret, numsLastIndex.get(num) - numsFirstIndex.get(num) + 1);
      }
      return ret;
}

766. 托普利茨矩阵(Easy)

如果矩阵上每一条由左上到右下的对角线上的元素都相同,那么这个矩阵是 托普利茨矩阵 。

给定一个 M x N 的矩阵,当且仅当它是托普利茨矩阵时返回 True。

示例 1:

输入: 
matrix = [
  [1,2,3,4],
  [5,1,2,3],
  [9,5,1,2]
]
输出: True

解释:   
在上述矩阵中, 其对角线为:
"[9]", "[5, 5]", "[1, 1, 1]", "[2, 2, 2]", "[3, 3]", "[4]"。
各条对角线上的所有元素均相同, 因此答案是True。

示例 2:

输入:
matrix = [
  [1,2],
  [2,2]
]
输出: False

解释:    
对角线"[1, 2]"上的元素不同。

说明:

  • matrix 是一个包含整数的二维数组。
  • matrix 的行数和列数均在 [1, 20]范围内。
  • matrix[i][j] 包含的整数在 [0, 99]范围内。

进阶:

  • 如果矩阵存储在磁盘上,并且磁盘内存是有限的,因此一次最多只能将一行矩阵加载到内存中,该怎么办?
  • 如果矩阵太大以至于只能一次将部分行加载到内存中,该怎么办?
原始的情况
public boolean isToeplitzMatrix(int[][] matrix) {
    // 遍历第一列为开头的对角线
    for (int i = 0; i < matrix.length; i++) {
        if (!check(matrix, matrix[i][0], i, 0)) {
            return false;
        }
    }

    // 遍历第一行为开头的对角线
    for (int i = 1; i < matrix[0].length; i++) {
        if (!check(matrix, matrix[0][i], 0, i)) {
            return false;
        }
    }

    return true;
}

private boolean check(int[][] matrix, int expectValue, int row, int col) {
    // 遍历完了,说明这条线上元素相同
    if (row >= matrix.length || col >= matrix[0].length) {
        return true;
    }
    // 这条线上元素不同,返回false
    if (matrix[row][col] != expectValue) {
        return false;
    }

    // 递归遍历这条线
    return check(matrix, expectValue, row + 1, col + 1);
}

565. 数组嵌套(Medium)

索引从 0 开始长度为 N 的数组 A,包含 0 到 N - 1 的所有整数。找到最大的集合S并返回其大小,其中 S[i] = {A[i], A[A[i]], A[A[A[i]]], ... }且遵守以下的规则。

假设选择索引为 i 的元素 A[i] 为 S 的第一个元素,S 的下一个元素应该是 A[A[i]],之后是 A[A[A[i]]]... 以此类推,不断添加直到S出现重复的元素。

示例 1:

输入: A = [5,4,0,3,1,6,2]
输出: 4
解释: 
A[0] = 5, A[1] = 4, A[2] = 0, A[3] = 3, A[4] = 1, A[5] = 6, A[6] = 2.

其中一种最长的 S[K]:
S[0] = {A[0], A[5], A[6], A[2]} = {5, 6, 2, 0}

提示:

  • N 是 [1, 20,000] 之间的整数。
  • A 中不含有重复的元素。
  • A 中的元素大小在 [0, N-1] 之间。
遍历数组

第一个想到的办法是直接遍历数组,对于每一个遍历的值按照题目的要求进行查找,这是最笨的方法,写出来超时。

记录走过的位置

首先看题目给的数组[5,4,0,3,1,6,2],形成的最大嵌套数组为{5, 6, 2, 0},形成一个环,对应的索引是0:5, 5:6, 6:2, 2:0,在一个数组中会形成多个闭环,多个环直接没有交叉。上述遍历数组的解法有重复计算的过程,比如遍历到5, 6, 2, 0这四个元素的时候得到的环是同一个,可以使用一个 Hashset 来记录走过的数。

public int arrayNesting(int[] nums) {
	int result = 0;
    Set<Integer> set = new HashSet<>();
    for (int i = 0; i < nums.length; i++) {
    	int count = 0;
        int k = i;
        //出现的重复元素会组成一个闭环
        while (!set.contains(nums[k])) {
        	set.add(nums[k]);
            count++;
            k = nums[k];
        }
        result = Math.max(result,count);
    }
    return result;
}

769. 最多能完成排序的块(Medium)

数组arr是[0, 1, ..., arr.length - 1]的一种排列,我们将这个数组分割成几个“块”,并将这些块分别进行排序。之后再连接起来,使得连接的结果和按升序排序后的原数组相同。

我们最多能将数组分成多少块?

示例 1:

输入: arr = [4,3,2,1,0]
输出: 1
解释:
将数组分成2块或者更多块,都无法得到所需的结果。
例如,分成 [4, 3], [2, 1, 0] 的结果是 [3, 4, 0, 1, 2],这不是有序的数组。

示例 2:

输入: arr = [1,0,2,3,4]
输出: 4
解释:
我们可以把它分成两块,例如 [1, 0], [2, 3, 4]。
然而,分成 [1, 0], [2], [3], [4] 可以得到最多的块数。

注意:

arr 的长度在 [1, 10] 之间。 arr[i]是 [0, 1, ..., arr.length - 1]的一种排列。

题解

首先重复理解题目的意思,数组分成多个“块”之后原地排序,再连接起来,块之间的相对位置不变。

为了使分出来的块原地排序连接起来的数组是和按升序排序后的原数组相同,那么每个块最大的一个元素得和这个块最后那个元素的下标相等。

nums:  [1,  0,  2,  3,  4]
index: [0,  1,  2,  3,  4]
块:     [1,  0], [2],[3],[4] 
第一个块最大的元素是1 == 边界下标为1。后续的几个也符合,所以这个是最多块的解
public int maxChunksToSorted(int[] arr) {
	int max = 0;
    int tmp = 0;
    for (int i = 0; i < arr.length; i++) {
    	// 当前块的最大值
    	tmp = Math.max(tmp, arr[i]);
        // 最大值等于下标,说明i为边界
        if (tmp == i) {
        	max++;
        }
    }
    return max;
}