常见数组与矩阵算法题总结

283. 移动零

难度:简单

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

示例:

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

说明:

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

双指针,一个fast用来找到非零元素,slow用来指向 0 元素。

public void moveZeroes(int[] nums) {
    int fast = 0;
    int slow = 0;
    while (fast < nums.length)
    {
        if (nums[fast] == 0)
        {
            fast++;
        }else{
            if(fast != slow){
                int temp = nums[fast];
                nums[fast] = nums[slow];
                nums[slow] = temp;
            }
            fast++;
            slow++;
        }
    }
}

566. 重塑矩阵

难度:简单

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

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

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

如果具有给定参数的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. 给定矩阵的宽和高范围在 [1, 100]。
  2. 给定的 r 和 c 都是正数。

要是两个矩阵行数列数相乘都不相等,就直接输出原矩阵。

用一个count记录当前元素在原矩阵中按行遍历顺序的序号,再利用count将这个序号转换成新矩阵的坐标。其中:

  • 行 = count / n
  • 列 = count % n

实际上跟原矩阵的行数无关。

public int[][] matrixReshape(int[][] nums, int r, int c) {
    int m = nums.length;
    int n = nums[0].length;
    if (m * n != r * c) {
        return nums;
    }

    int[][] reshape = new int[r][c];
    int count = 0;
    for (int i = 0; i < r; i++) {
        for (int j = 0; j < c; j++) {
            reshape[i][j] = nums[count / n][count % n];
            count++;
        }
    }
    return reshape;
}

485. 最大连续1的个数

难度:简单

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

示例 1:

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

注意:

  • 输入的数组只包含 01
  • 输入数组的长度是正整数,且不超过 10,000。

变量max表示当前最大连续 1 的个数,变量count表示当前连续 1 的个数。遍历数组,如果当前元素是 1 就count加1,如果不是 1 就重新计数,然后实时更新max

public int findMaxConsecutiveOnes (int[] nums) {
    int max = 0, count = 0;
    for (int num : nums) {
        count = num == 1 ? count + 1 : 0;
        max = Math.max(count, max);
    }
    return max;
}

240. 搜索二维矩阵 II

难度:中等

编写一个高效的算法来搜索 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和当前元素matrix[row][col]进行比较。

  • 如果target大于当前元素,则说明target的坐标肯定在当前元素的下面,而不可能在左边
  • 如果target小于当前元素,则说明target的坐标肯定在当前元素的左边,而不可能在下面
  • 遍历完整个二维数组都没有找到匹配的target,则返回false
public boolean searchMatrix(int[][] matrix, int target) {
    if(matrix == null || matrix.length < 1 || matrix[0].length < 1)
        return false;

    int r = 0;
    int c = matrix[0].length - 1;
    while (r < matrix.length && c >=0){
        if(target == matrix[r][c]){
            return true;
        }else if(target < matrix[r][c]){
            c--;
        }else{
            r++;
        }
    }
    return false;

}

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

难度:中等

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

示例:

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

返回 13。

说明:
你可以假设 k 的值永远是有效的, 1 ≤ k ≤ n^2^ 。


二分查找法:用一个计数器count来表示当前的mid在有序矩阵中排第几。

public int kthSmallest (int[][] matrix, int k) {
    int m = matrix.length;
    int n = matrix[0].length;

    int lo = matrix[0][0], hi = matrix[m - 1][n - 1];
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;
        int count = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n && matrix[i][j] <= mid; j++) {
                count++;
            }
        }
        if (count < k) {
            lo = mid + 1;
        } else {
            hi = mid - 1;
        }
    }
    return lo;
}

645. 错误的集合

难度:简单

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

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

示例 1:

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

注意:

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

注意到题目条件:数组中的元素是1到n(先不管出错的情况)。

简单点的方法是直接对数组排序,再查看哪里出错了。这种方法时间复杂度为 O(NlogN)。

时间复杂度O(N)的方法主要思想是:通过交换数组元素,使每个元素出现在正确的位置上。再查看哪里出错了。

public int[] findErrorNums (int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        while (nums[i] != i + 1 && nums[nums[i] - 1] != nums[i]) {
            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 temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}

287. 寻找重复数

难度:中等

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

示例 1:

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

示例 2:

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

说明:

  1. 不能更改原数组(假设数组是只读的)。
  2. 只能使用额外的 O(1) 的空间。
  3. 时间复杂度小于 O(n^2^) 。
  4. 数组中只有一个重复的数字,但它可能不止重复出现一次。

双指针,有点类似于环形链表找环的入口。

public int findDuplicate(int[] nums) {
    int slow = nums[0], fast = nums[nums[0]];
    while (fast != slow) {
        slow = nums[slow];
        fast = nums[nums[fast]];
    }
    fast = 0;
    while (fast != slow) {
        slow = nums[slow];
        fast = nums[fast];
    }
    return slow;
}

667. 优美的排列 II

难度:中等

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

① 如果这个数组是 [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

提示:

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

下标在[0,k]时,偶数下标填[1,2,3,…]依次递增,奇数下标填[k+1,k,k-1,…]依次递减,最后[k+1,n-1]范围内的下标就按正常的填。

public int[] constructArray (int n, int k) {
    int[] ans = new int[n];
    ans[0] = 1;
    for (int i = 1; i <= k; i++) {
        ans[i] = i % 2 == 0 ? i / 2 + 1 : k + 1 - i / 2;
    }
    for (int i = k + 1; i < n; i++) {
        ans[i] = i + 1;
    }
    return ans;

}

697. 数组的度

难度:简单

给定一个非空且只包含非负数的整数数组 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范围内的整数。

使用三个 HashMap来分别存储一个元素第一次出现的索引,最后一次出现的索引 和出现的次数。最大次数即为数组的度。
再遍历数组,得到每个元素出现的次数,找到出现次数等于数组的度的元素中,首尾最近的。

public int findShortestSubArray (int[] nums) {
    Map<Integer, Integer> firstIndex = new HashMap<>();
    Map<Integer, Integer> lastIndex = new HashMap<>();
    Map<Integer, Integer> count = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        int num = nums[i];
        count.put(num, count.getOrDefault(num, 0) + 1);
        lastIndex.put(num, i);
        if (!firstIndex.containsKey(num)) {
            firstIndex.put(num, i);
        }
    }
    int degree = 0;
    for (int num : nums) {
        degree = Math.max(degree, count.get(num));
    }
    int ans = nums.length;
    for (int i = 0; i < nums.length; i++) {
        int num = nums[i];
        int cnt = count.get(num);
        if (cnt != degree) continue;
        ans = Math.min(ans, lastIndex.get(num) - firstIndex.get(num) + 1);
    }
    return ans;
}

766. 托普利茨矩阵

难度:简单

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

给定一个 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]"上的元素不同。

说明:

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

进阶:

  1. 如果矩阵存储在磁盘上,并且磁盘内存是有限的,因此一次最多只能将一行矩阵加载到内存中,该怎么办?
  2. 如果矩阵太大以至于只能一次将部分行加载到内存中,该怎么办?

对矩阵的第一行和第一列使用check,看往右下走的矩阵的元素的值是不是跟第一个值相等。

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 = 0; 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;
    }
    if (matrix[row][col] != expectValue) {
        return false;
    }
    return check(matrix, expectValue, row + 1, col + 1);
}

565. 数组嵌套

难度:中等

索引从0开始长度为N的数组A,包含0N - 1的所有整数。找到并返回最大的集合SS[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}

注意:

  1. N[1, 20,000]之间的整数。
  2. A中不含有重复的元素。
  3. A中的元素大小在[0, N-1]之间。

对数组中的每一个元素进行遍历,也就是说,假设每个元素都可以当做S的开头,使用一个计数器count记录当前这个序列的长度。然后往下嵌套,每次计数器 + 1,标记访问过的元素。每次遍历后更新最大长度。

public int arrayNesting(int[] nums) {
    int max = 0;
    for (int i = 0; i < nums.length; i++) {
        int count = 0;
        for (int j = i; nums[j] != -1;) {
            count++;
            int t = nums[j];
            nums[j] = -1; // 标记该位置已经被访问
            j = t;
        }
        max = Math.max(max,count);
    }
    return max;
}

769. 最多能完成排序的块

难度:中等

数组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]的一种排列。

根据给定数组的特点:数组元素是 0 到 N - 1 的一种排列,所以对每一个元素,有下面两种情况:

  1. arr[0]arr[i]的区间内的最大值 = i ,那么 第 i 个元素右边的最小值一定大于 i,i 和 i+1 之间可以作为一个切分点
  2. arr[0]arr[i]的区间内的最大值 > i, 那么 第 i 个元素右边一定存在小于等于 i 的数,则 i 和 i+1 之间不能作为一个切分点
public int maxChunksToSorted (int[] arr) {
    int ans = 0;
    int leftMax = arr[0];
    for (int i = 0; i < arr.length; i++) {
        leftMax = Math.max(leftMax, arr[i]);
        if (leftMax == i) ans++;
    }
    return ans;
}

 上一篇
有关图的基础算法题 有关图的基础算法题
二分图785. 判断二分图难度:中等 给定一个无向图graph,当这个图为二分图时返回true。 如果我们能将一个图的节点集合分割成两个独立的子集A和B,并使图中的每一条边的两个节点一个来自A集合,一个来自B集合,我们就将这个图称为二分图。
2019-09-16
下一篇 
常见排序算法题总结 常见排序算法题总结
常用排序算法总结 冒泡排序比较相邻的两个元素,如果前一个元素比后一个元素大,则交换它们的位置。第一趟做完后,最后一个元素肯定是数组中最大的元素。 开始第二趟比较,第二趟比较把最后一个元素排除在外。 重复以上步骤,直到没有一对元素需要比较。
2019-09-16
  目录