文章目录
数组的常用操作是 双指针和 前缀和
2.1 双指针
面试题6:排序数组中的两个数字之和
题目:输入一个递增排序的数组和一个值k,请问如何在数组中找出两个和为k的数字并返回他们的下标?假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。例如输入数组[1, 2, 4, 6, 10],k的值为8,数组中的数字2与6的和为8,他们的下标分别为1与3。
思路:头尾双指针;或者hash表
public int[] twoSum(int[] numbers, int target){
/**
* 双指针,P1,P2;初始状态P1位于数组头部,P2位于数组尾部
* 然后判断两数之和与目标值的大小,若大于目标值,则P1右移,若小于目标值,则P2左移,直至找到目标值
* 时间复杂度O(n)
*/
int i = 0;
int j = numbers.length - 1;
while(i < j && numbers[i] + numbers[j] != target){
if(numbers[i] + numbers[j] < target){
i++;
}else {
j--;
}
}
return new int[]{i, j};
}
public int[] twoSumPro(int[] numbers, int target){
/**
* 双指针的方式只适用于有序数组
* 若是无序数组,则可使用hash表将数组的所有数字存储
* 然后遍历数组,每遍历一个数判断hash表中是否有数与之和为目标值
* 时间复杂度O(n),空间复杂度O(n)
*/
// 使用HashMap作为hash表,存储数据和下标
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < numbers.length; i++) {
map.put(numbers[i], i);
}
int i = 0;
while (i < numbers.length && !map.containsKey(target - numbers[i])) {
i++;
}
return new int[]{i, map.get(target - numbers[i])};
/**
* 题后思考:
* 1) HashMap使用数组+链表+红黑树实现的,检索的时间复杂度理想情况下为O(1),最坏情况下呢?
* 2) 如果面对可重复的数组,Hash方法还可行吗?
*/
}
面试题7:数组中和为0的三个数
题目:输入一个数组,如何找出数组中所有和为0的三个数字的三元组?需要注意的是,返回值中不得包含重复的三元组。例如,在数组[-1, 0, 1, 2, -1, -4]中有两个三元组的和为0,他们分别是[-1, 0, 1] 和 [-1, -1, 2]。
思路:固定一个数后 化为两数和问题;重点是如何去重
public List<List<Integer>> threeSum(int[] numbers){
List<List<Integer>> result = new LinkedList<>();
if(numbers.length >= 3){
Arrays.sort(numbers);
int i = 0;
while (i < numbers.length - 2){
twoNum(numbers, i, result);
// 记录当前数字的值
int temp = numbers[i];
while(i < numbers.length && numbers[i] == temp){
// 至少会执行一次,目的是跳到与当前数字不相同的下一个数字,去重
i++;
}
}
}
return result;
}
public void twoNum(int[] numbers, int i, List<List<Integer>> result){
// 从当前数字的右边开始查询,因为左边的数字的所有符合条件的组合都已经被查询出来了,这里也达到了去重的目的
int j = i + 1;
int k = numbers.length - 1;
// 固定i,双指针为j和k
int target = 0 - numbers[i];
while(j < k){
if(numbers[j] + numbers[k] == target){
result.add(Arrays.asList(numbers[i], numbers[j], numbers[k]));
int temp = numbers[j];
while(j < k && numbers[j] == temp){
// 至少会执行一次,目的是跳到与当前数字不相同的下一个数字,去重
j++;
}
} else if(numbers[j] + numbers[k] < target) {
j++;
} else {
k--;
}
}
}
可否使用hash来实现?
面试题8:和大于或等于k的最短子数组
题目:输入一个正数组成的数组和一个正整数k,请问数组中和大于或者等于k的连续子数组的最短长度是多少?如果不存在所有数字之和大于或者等于k的子数组,则返回0。例如,输入数组[5, 1, 4, 3],k的值为7,和大于或者等于7的最短连续子数组是[4, 3],因此输出他的长度2。
思路:头部双指针
public int minSubArrayLen(int k, int[] nums){
/**
* 头部双指针
* 时间复杂度为O(n)。为什么双循环还是O(n)?
* 因为内外层变量都是只增不减,外层循环变量取值范围是0~n-1,内层循环变量可看做每次只执行固定次数。
*/
int left = 0;
int sum = 0;
int minLength = Integer.MAX_VALUE;
for(int right = 0;right < nums.length; right++){
sum += nums[right];
while(left < right && sum >= k){
minLength = Math.min(minLength, right - left + 1);
sum -= nums[left++];
}
}
return minLength == Integer.MAX_VALUE? 0 : minLength;
}
}
面试题9 :乘积小于k的子数组
题目:输入一个有正整数组成的数组和一个正整数k,请问数组中有多少个数字乘积小于k的连续子数组?例如,输入数组[10, 5, 2, 6],k的值为100,有8个子数组的所有数字乘积小于100,它们分别是[10],[5],[2],[6],[10, 5],[5, 2],[2, 6],[5, 2, 6]。
思路:头部双指针
public int numSubarrayProductLessThanK(int[] nums, int k){
/**
* 使用头部双指针法,用双指针(P1、P2,P1是头指针,P2是尾指针)之间的间隔作为子数组,并记录数组元素的乘积
* 当乘积小于k时,P2右移,此时乘积变大,直至乘积大于k,同时记录新的子数组;
* 当乘积大于k时,P1右移,此时乘积变小,直至乘积小于k;
* 直至P2不能再右移
*/
int product = 1;
int left = 0;
int count = 0;
for(int right = 0; right < nums.length; right++){
product *= nums[right];
while (product > k){
product /= nums[left++];
}
count += right >= left ? right - left + 1 : 0;
}
return count;
}
思考:
count += right >= left ? right - left + 1 : 0; right - left + 1是怎么得来的?
对于有n个元素的集合:
- 包含 1 个元素的子集有 n 个
- 包含 2 个连续元素的子集有 n - 1 个
- …
- 包含 n 个连续元素的子集有 1 个
- 总共有 1 + 2 + 3 +… + n = (n+1)*n/2个连续元素的子集
那么对于n-1个元素的集合:共有(n) * (n-1)/2个连续元素的子集。
(n+1)n/2 - n(n-1)/2 = n/2 * (2)=n 对于n-1个元素的集合,增加第n个元素,则增加的连续元素子集数为n个。
对于a[l] … a[r - 1] 的集合,增加第r个元素a[r],则增加的连续元素子集数为 a[l] … a[r] 范围内的元素总数即 r - l +1个。
2.2 前缀和
面试题10:和为k的子数组
题目:输入一个整数数组和一个整数k,请问数组中有多少个数字之和等于k的连续子数组?例如,输入数组[1, 1, 1],k的值为2,有两个连续子数组之和等于2.
思路:前缀和+hash
public int subarraySum(int[] nums, int k){
/**
* 利用hash表记录前缀和出现的次数
* 遍历数组的过程中只要找到(当前前缀和-k)这个前缀和出现的次数n,说明对于当前位置,之前有n个位置满足条件
* 直至遍历结束
*/
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1);
int preSum = 0;
int count = 0;
for (int i = 0; i < nums.length; i++) {
preSum += nums[i];
count += map.getOrDefault(preSum - k, 0);
map.put(preSum, map.getOrDefault(preSum, 0) + 1);
}
return count;
}
面试题11:0和1个数相同的子数组
题目:输入一个只包含0和1的数组,请问如何求出0和1的个数相同的最长连续子数组的长度?例如,在数组0[0, 1, 0]中有两个子数组包含相同个数的0和1,分别是[0, 1]和[1, 0],他们的长度都是2,因此输出2。
思路:转化为求和问题,然后使用前缀和+hash
public int findMaxLength(int[] nums){
/**
* 求n与m个数相同的问题,考虑将m看成-n,然后化为求和为0的问题
* 同样是利用前缀和,hash表中的键为前缀和,值为该前缀和最早出现的位置
* 使用前缀和+hash表时,hash表的值可以具有很多形式
* 时间复杂度为O(n),空间复杂度为O(n)
*/
Map<Integer, Integer> map = new HashMap<>();
map.put(0, -1);
int sum = 0;
int maxLength = 0;
for (int i = 0; i < nums.length; i++) {
// 将数组中的0转化为-1
sum += nums[i] == 0 ? -1 : 1;
// 查询map中键为sum的值
if(map.containsKey(sum)){
int currentLength = i - map.get(sum);
maxLength = Math.max(maxLength, currentLength);
} else {
map.put(sum, i);
}
}
return maxLength;
}
面试题12:左右两边数组的和相等
题目:输入一个整数数组,如果一个数字左边的子数组的数字之和等于右边的子数组的数字之和,那么返回该数字的下标。如果存在多个这样的数字,则返回最左边一个数字的下标。如果不存在这样的数字,则返回-1。例如在数组[1, 7, 3, 6, 2, 9]中,下标为3的数字(值为6)的左边3个数字1、7、3的和与右边两个数2、9的和相等,都是11,因此正确的输出值是3。
思路:前缀和
public int pivotIndex(int[] nums){
/**
* 先计算出数组所有元素之和
* 再使用前缀和计算后缀和
* 时间复杂度O(n)
*/
int sum = 0;
int preSum = 0;
for (int num : nums) {
sum += num;
}
for (int i = 0; i < nums.length;i++) {
preSum += nums[i];
int sufSum = sum - preSum - nums[i];
if(preSum == sufSum){
return i;
}
}
return -1;
}
面试题13:二维子矩阵的数字之和
题目:输入一个二维矩阵,如何计算给定左上角坐标和右下角坐标的子矩阵的数字之和?对于同一个二维矩阵,计算子矩阵的数字之和的函数可能由于输入不同的坐标而被反复调用多次。
思路:一维前缀和→二维前缀和
// 二维数组前缀和:从坐标(0,0)到当前坐标(i,j)的所有元素之和
private int[][] sums;
public SumOf2DSubarray_13(int[][] matrix){
if(matrix.length == 0 || matrix[0].length == 0){
return;
}
// 前缀和数组行和列都比原数组大1,为了防止数组下标越界
sums = new int[matrix.length + 1][matrix[0].length + 1];
for(int i = 0; i < matrix.length; i++){
int rowSum = 0;
for (int j = 0; j < matrix[i].length; j++){
rowSum += matrix[i][j];
// 用上一行的结果来计算当前行
sums[i + 1][j + 1] = sums[i] [j + 1] + rowSum;
}
}
}
public int regionSum(int row1, int col1, int row2, int col2){
return sums[row2 + 1][col2 + 1] - sums[row2 + 1][col1] - sums[row1][col2 + 1] + sums[row1][col1];
}