算法-数列查找

算法-数列查找

1. 从两个正序数组中找出中位数

说明:给定两个正序(从小到大)整数数组:nums1 = int[n]nums2 = int[m],找出这两个数组全部元素的中位数,假定两个数组均非空(n, m >= 0),且不同时为空数组(n + m >= 1)。

例 1:

nums1 = [1,3]nums2 = [2];则合并后数组为 [1, 2, 3],中位数为 2。

例 2:

nums1 = [1,2]nums2 = [3,4];则合并后数组为 [1, 2, 3, 4],中位数为 (2 + 3) / 2 = 2.5

1.1 数组归并法

首先「归并」两个有序数组,假设数组长度分别为 length1length2,归并后中位数下标为 midIndex

  • 由于归并后元素个数 sum 可能为奇数或偶数,所以 midIndex 需要取更大的,确保包括中位数,
    • 奇数个的中位数位于 (sum / 2) + 1,下标为 sum / 2
    • 偶数个的中位数位于 sum / 2(sum / 2) + 1,下标为 (sum / 2) - 1sum / 2
    • 取最大的,也就是 midIndex = sum / 2
  • 所以偶数个 isEven == true 时,可以记录 value = merge[midIndex - 1],再累加 value += merge[midIndex],然后遍历结束后,如果 isEven == true,则 value /= 2
  • 但实际上,由于题目只需要查找中位数,所以只需要保证中位数及之前的元素有序即可,因此只需要排序 midIndex + 1 个元素,可以通过循环 for (i = 0; i <= midIndex; i++) 来控制。由于题目给出两个数组都是正序排列(从小到大),可以每次都从两个数组中取一位最小的,直到一共取了 midIndex + 1 个元素为止,因此需要两个变量 index1index2 分别记录两个数组下一次从哪个元素开始取。
  • 由于 midIndex 是两个数组归并后的中位数下标,所以从逻辑上可以确定一个特征:
    • 如果循环到第 i 次时,一个数组已被取完,即 index1 >= length1index2 >= length2 下标越界,但归并后数组元素个数仍然小于 midIndex + 1 个,则另一个数组的下标 i 一定还未越界。换句话说,从 i = 0 循环到 i = midIndex 的过程中,两个数组对下标 i 的取值不可能同时越界。
  • 归并时从哪个数组取了元素,就将对应数组的 index 更新到该元素的下标,表示数组剩余元素最小值的下标。

例如 [1, 2][0, 3, 4, 5, 6],用 > 表示两个数组归并过程中的的 index,一共有 7 个元素,则归并后的中位数下标 midIndex == 3;也即一共只需要归并 4 个元素即可:

归并下标 i merge nums1 nums2
初始化 [] [>1, 2] index1 = 0 [>0, 3, 4, 5, 6] index2 = 0
0 [0] [>1, 2] index1 = 0 [0, >3, 4, 5, 6] index2 = 1
1 [0, 1] [1, >2] index1 = 1 [0, >3, 4, 5, 6] index2 = 1
2 [0, 1, 2] [1, 2 >] index1 = 2 越界 [0, >3, 4, 5, 6] index2 = 1
3 [0, 1, 2, 3] [1, 2 >] index1 = 2 越界 [0, 3, >4, 5, 6] index2 = 2

归并完成,中位数即为 merge[midIndex] == merge[3] == 3,如果元素总数为偶数,思路也是一样的,只是最终计算中位数为 (merge[midIndex - 1] + merge[midIndex]) / 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
class Solution_Merge {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
if ((nums1 == null || nums1.length == 0) && (nums2 == null || nums2.length == 0)) {
return 0;
}
// 元素总个数:
int sumLength = nums1.length + nums2.length;
// 是否总共有偶数个元素,如果是,则需要累加中间的两个元素并除以二:
boolean isEven = (sumLength % 2 == 0);
// 中位数下标,如果共有奇数个则代表中位数本身,如果是偶数个则代表中间两个元素中下标更大的:
int midIndex = sumLength / 2;
// 用于记录两个数组分别归并到了第几位元素,也即接下来需要归并的第一位元素的下标:
int index1 = 0, index2 = 0;
// 用于记录实际中位数的值,注意因为精确到小数,所以要用 double:
double midValue = 0;
// 用于记录最新一个被归并的元素的值
int value;
// 只需要归并 midIndex + 1 次
for (int i = 0; i <= midIndex; i++) {
if (index1 >= nums1.length) {
// 数组 1 越界说明已经被归并完了,则下一位被归并的就是从数组 2 取接下来的第 1 位元素
value = nums2[index2++];
} else if (index2 >= nums2.length) {
// 数组 2 越界说明已经被归并完了,则下一位被归并的就是从数组 1 取接下来的第 1 位元素
value = nums1[index1++];
} else if (nums1[index1] > nums2[index2]) {
// 否则如果数组 1 接下来一位比数组 2 接下来一位更小,则取数组 1 的接下来一位
value = nums2[index2++];
} else {
// 否则如果数组 2 接下来一位比数组 1 接下来一位更小,则取数组 2 的接下来一位
value = nums1[index1++];
}
// 如果共有偶数个元素,则中位数需要取中间两位,因此在 i == midIndex - 1 时先赋值,
// 否则不处理相当于先赋了默认值 0。
if (isEven && i == midIndex - 1) {
midValue = value;
}
// 不论共有奇数个还是偶数个,都需要加上 midIndex 元素的值,为了兼容两种情况所以使用累加
if (i == midIndex) {
midValue += value;
}
}
// 此时如果共有奇数个,则 midValue 就是最终中位数,否则 midValue 等于中间两个数的和,
// 所以如果是偶数个,还需要除以二:
if (isEven) {
midValue /= 2.0d;
}
return midValue;
}
}

2. 从数组中找出和为给定值的两个元素

说明:给定一个整型数组 nums,找出「和为给定整数 target」的「两个」元素下标(忽略顺序)。数组中的每个元素不能被重复选择,假定每个输入有且仅有一个解。

例 1:

nums = [2, 7, 11, 15]target = 9,则输出 [0, 1][1, 0]

例 2:

nums = [3, 3]target = 6,则输出 [0, 1][1, 0]

2.1 双重循环暴力法

用双重循环比较任意两个元素的和是否与给定的 target 相等,如果相等则直接返回两个元素对应的下标组成的新数组,否则返回一个默认值 [0, 0],由于题目假定每个输入都会有且仅有一个解,因此可以假定一定会返回有效解。

注意:由于双重循环,判断两个元素之和在内层循环中,找到有效解时应当直接跳出外层循环,所以应该使用「定义循环名」的方式,避免多用一个 Flag 变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution_DoubleLoop {
public int[] twoSum(int[] nums, int target) {
int i = 0, j = 0;
// 定义外层循环名
outer:
for (i = 0; i < nums.length - 1; i++) {
// 题目给出了 2 <= nums.length <= 10^4,所以不用判断 j = i + 1 是否越界
for (j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) {
// 找到了,直接跳出外层循环
break outer;
}
}
}
return new int[] {i, j};
}
}

2.2 Map哈希法

因为主要目标是找到和等于 target 的两个元素,所以在确定其中一个元素 nums[i] 时,另一个元素「应该的取值」就已经确定为 nums[j] = target - nums[i],假如存在则结果就为 [i, j]。所以问题可以转换为:每遍历到一个元素 nums[i] 时,都查找是否存在 nums[j] = target - nums[i]

  • 遍历时将每个遍历过的元素及其对应下标都存入一个 Map;
  • 每遍历一个新的元素 nums[i] 时,计算 nums[i]「应该」匹配的数为 diff = target - nums[i]
  • 判断是否存在 Map.contains(diff)
    • 如果存在,则获取这个数的下标 j = Map.get(diff),然后返回 [i, j]
    • 否则继续遍历,直到最终仍找不到匹配的两个元素,返回默认值(依照题意则不会遇到这种情况)。
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution_MapHash {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int index = 0; index < nums.length; index++) {
int diff = target - nums[index];
if (map.containsKey(diff)) {
return new int[] {index, map.get(diff)};
}
map.put(nums[index], index);
}
return new int[0];
}
}