在算法面试和日常编程中,数组和矩阵相关的问题占据了相当大的比重。这类问题往往考察程序员对基础数据结构的理解、边界条件的处理能力以及优化思维。本文将深入解析《剑指Offer》中5个经典的数组与矩阵算法题,从核心思路到代码实现,再到优化技巧,带你彻底掌握这些高频考点。
这5道题目涵盖了数组和矩阵处理的典型场景:
它们共同的特点是:看似简单但暗藏陷阱,暴力解法容易想到但往往存在更优解。理解这些题目的优化思路,能帮助我们建立解决类似问题的通用思维框架。
给定一个长度为n的数组,所有数字都在0~n-1范围内。数组中某些数字是重复的,找出任意一个重复的数字。
示例:
输入:[2, 3, 1, 0, 2, 5, 3]
输出:2或3
最直观的解法是使用哈希表记录已出现的数字,时间复杂度O(n),空间复杂度O(n)。但题目给出了关键条件:数字范围在0~n-1,这提示我们可以找到空间复杂度O(1)的解法。
核心思想:利用数组本身作为哈希表,通过交换操作将数字放到其值对应的下标位置。
算法步骤:
java复制public int duplicate(int[] numbers) {
if (numbers == null || numbers.length == 0) return -1;
for (int i = 0; i < numbers.length; i++) {
while (i != numbers[i]) {
if (numbers[i] == numbers[numbers[i]]) {
return numbers[i];
}
swap(i, numbers[i], numbers);
}
}
return -1;
}
private void swap(int i, int j, int[] arr) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
时间复杂度:虽然有两层循环,但每个数字最多被交换两次就能找到正确位置,整体是O(n)
空间复杂度:O(1),只使用了常数额外空间
提示:这个解法修改了原数组,如果要求不能修改原数组,需要使用二分查找的变种,时间复杂度O(nlogn)
在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
示例:
矩阵:
[
[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]
]
查找数字5,返回true;查找数字20,返回false
暴力解法是遍历整个矩阵,时间复杂度O(mn)。利用矩阵有序的特性可以优化:
java复制public boolean Find(int target, int[][] array) {
if (array == null || array.length == 0 || array[0].length == 0) {
return false;
}
int rows = array.length;
int cols = array[0].length;
int r = 0, c = cols - 1; // 从右上角开始
while (r < rows && c >= 0) {
if (target == array[r][c]) {
return true;
} else if (target > array[r][c]) {
r++; // 向下移动
} else {
c--; // 向左移动
}
}
return false;
}
这种解法之所以有效,是因为:
时间复杂度:O(m+n),最坏情况下需要遍历一行加一列
空间复杂度:O(1)
变种思考:
请实现一个函数,把字符串中的每个空格替换成"%20"。
示例:
输入:"We Are Happy"
输出:"We%20Are%20Happy"
在Java中,字符串是不可变的,直接拼接效率较低。推荐使用StringBuilder:
java复制public String replaceSpace(String s) {
if (s == null || s.isEmpty()) return s;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == ' ') {
sb.append("%20");
} else {
sb.append(c);
}
}
return sb.toString();
}
在C++中,可以原地修改字符串(如果空间足够):
cpp复制void replaceSpace(char *str, int length) {
if (str == nullptr || length <= 0) return;
int originalLength = 0;
int numberOfBlank = 0;
int i = 0;
while (str[i] != '\0') {
originalLength++;
if (str[i] == ' ') numberOfBlank++;
i++;
}
int newLength = originalLength + numberOfBlank * 2;
if (newLength > length) return;
int indexOfOriginal = originalLength;
int indexOfNew = newLength;
while (indexOfOriginal >= 0 && indexOfNew > indexOfOriginal) {
if (str[indexOfOriginal] == ' ') {
str[indexOfNew--] = '0';
str[indexOfNew--] = '2';
str[indexOfNew--] = '%';
} else {
str[indexOfNew--] = str[indexOfOriginal];
}
indexOfOriginal--;
}
}
时间复杂度:O(n),需要遍历整个字符串
空间复杂度:Java解法O(n),C++原地修改O(1)
这种字符串替换操作在:
注意:在实际工程中,应该使用语言内置的URL编码函数(如Java的URLEncoder)而非手动实现,以处理更多特殊字符情况
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
示例:
输入:
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]
]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
将矩阵看作由若干层组成,从外层到内层依次打印:
java复制public ArrayList<Integer> printMatrix(int[][] matrix) {
ArrayList<Integer> res = new ArrayList<>();
if (matrix == null || matrix.length == 0) return res;
int r1 = 0, r2 = matrix.length - 1;
int c1 = 0, c2 = matrix[0].length - 1;
while (r1 <= r2 && c1 <= c2) {
// 从左到右
for (int j = c1; j <= c2; j++) {
res.add(matrix[r1][j]);
}
// 从上到下
for (int i = r1 + 1; i <= r2; i++) {
res.add(matrix[i][c2]);
}
// 从右到左(防止单行重复)
if (r1 != r2) {
for (int j = c2 - 1; j >= c1; j--) {
res.add(matrix[r2][j]);
}
}
// 从下到上(防止单列重复)
if (c1 != c2) {
for (int i = r2 - 1; i > r1; i--) {
res.add(matrix[i][c1]);
}
}
// 缩小边界
r1++; r2--; c1++; c2--;
}
return res;
}
需要特别注意两种特殊情况:
时间复杂度:O(mn),每个元素只访问一次
空间复杂度:O(1)(不考虑输出结果的空间)
在一个字符串中找到第一个只出现一次的字符,并返回它的位置。如果没有则返回-1。
示例:
输入:"abaccdeff"
输出:'b'的位置1
使用哈希表统计每个字符出现的次数,然后再次遍历字符串找到第一个计数为1的字符:
java复制public int FirstNotRepeatingChar(String str) {
if (str == null || str.isEmpty()) return -1;
int[] count = new int[128]; // ASCII码范围
for (int i = 0; i < str.length(); i++) {
count[str.charAt(i)]++;
}
for (int i = 0; i < str.length(); i++) {
if (count[str.charAt(i)] == 1) {
return i;
}
}
return -1;
}
时间复杂度:O(n),两次线性遍历
空间复杂度:O(1),使用固定大小的计数数组
这种统计思想广泛应用于:
通过这5道题目,我们掌握了数组和矩阵处理的几种核心技巧:
进阶思考:
这些问题的思考将帮助你更深入地理解数组和矩阵算法的精髓。在实际面试中,面试官常常会在你给出解法后提出类似的变种问题,考察你的思维灵活性和对算法本质的理解。