Hexo

点滴积累 豁达处之

0%

leetcode算法-数组

leetcode算法

(LeetCode-1)两数之和

题目

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

1
2
3
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

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

分析

方法1

暴力穷举,复杂度O(n2)。

对数组中每个元素,都去计算它和数组中其他元素的和sum是否等于目标值 target,如果是则返回结果,不是则继续循环,直到将所有元素检查一遍。

方法2

这道题最优的做法时间复杂度是O(n)。用一个哈希表,存储每个数对应的下标。具体做法是:顺序扫描数组,对每一个元素,在map中找能组合给定值的另一半数字,如果找到了,直接返回2个数字的下标即可。如果找不到,就把这个数字存入map 中,等待扫到“另一半”数字的时候,再取出来返回结果。

代码示例:

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
51
52
public class doubleSum {
public static void main(String[] args) {
int nums[] = {11,2,7,15};
int target = 9;
System.out.println(Arrays.toString(twoSum(nums, target)));
}
// 暴力破解
// public static int[] twoSum(int nums[], int target){
// for (int i = 0; i < nums.length; i++) {
// int start = nums[i];
// for (int j = i + 1; j < nums.length; j++) {
// if (nums[j] + start == target){
// return new int[]{i,j};
// }
// }
// }
// return new int[]{};
// }

// 借助map
// public static int[] twoSum(int nums[], int target){
// Map<Integer, Integer> map = new HashMap<Integer, Integer>();
// for (int i = 0; i < nums.length; i++) {
// map.put(nums[i], i);
// }
//
// for (int i = 0; i < nums.length; i++) {
// Integer res = map.get(target - nums[i]);
// if (res != null){
// return new int[]{i,res};
// }
// }
// return new int[]{};
// }

// 借助map(优化)
public static int[] twoSum(int nums[], int target){
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
int [] result = new int[2];
for (int i = 0; i < nums.length; i++) {
int anothier = target - nums[i];
Integer index = map.get(anothier);
if(null != index){
return new int[]{i,index};
}else {
map.put(nums[i], i);
}
}

return new int[]{};
}
}

(LeetCode-88) 合并两个有序数组

题目

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

示例 1:

1
2
3
4
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。

示例 2:

1
2
3
4
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。

示例 3:

1
2
3
4
5
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。

进阶:你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?

分析

方法1

仔细观察题目我们其实可以发现,这个两个数组本身其实就是已经有序的了,这一点我们没有利用上,所以改进的办法就是利用“双指针”,每次从两个数组头部取出比较小的数字放到结果中。

Leetcode_35

这种方法的时间复杂度是多少呢?因为要把两个数组各循环一遍,所以时间复杂度是O(m+n),在处理上因为需要一个额外的空白数组来存放两个数组的值,所以空间复杂度也是O(m+n)。

方法2

方法2中还需要一个长度为m+n的临时数组作为中转,能不能不要这个数组呢?因为题目中的整数数组 nums1的后面还有空位,完全可以利用上。所以改进方法就是,依然使用双指针,但是倒序处理。

Leetcode_36

这种情况下,时间复杂度是O(m+n),空间复杂度则变为O(1)。

代码示例

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
public class DoubleArr {
public static void main(String[] args) {
int nums1[] = {4,5,6,0,0,0};
int nums2[] = {1,2,3};
merge(nums1,3, nums2,3);
System.out.println(Arrays.toString(nums1));
}

// 利用jdk的api
public static void merge(int[] nums1, int m, int[] nums2, int n) {
for(int i = 0; i < n; i++){
nums1[m + i] = nums1[i];
}
Arrays.sort(nums1);
}

// 双指针,从后往前遍历
// public static void merge(int[] nums1, int m, int[] nums2, int n) {
// int index1 = m - 1; // 数组一的最大下标(不包含末尾非有效数字)
// int index2 = n - 1; // 数组二的最大下标
// int resultIndex = m + n - 1; // 数组一的最大下标(包含末尾非有效数字)
//
// while (index1 >= 0 && index2 >= 0){
// int num1 = nums1[index1];
// int num2 = nums2[index2];
// if (num1 > num2){
// index1--;
// nums1[resultIndex] = num1;
// }else {
// nums1[resultIndex] = num2;
// index2--;
// }
// resultIndex--;
// }
//
// if (index2 >= 0){ // 数组2 还有剩余 同步移动
// for (int i = 0; i <= index2; i++) {
// nums1[i] = nums2[i];
// }
// }
// }
}

(LeetCode-283)移动零

题目

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

示例:

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

说明:

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

分析

方法

依然可以用双指针的办法,两个指针i和j。i负责遍历整个数组,在遍历数组的时候,j用来记录当前所有非0元素的个数。遍历的时候每遇到一个非0元素就将其往数组左边挪,挪动到J所在的位置,注意是挪动,不是交换位置,j同时也移动一个位置。当第一次遍历完后,j指针的下标就指向了已经排完了位置的最后一个非0元素下标。

进行第二次遍历的时候,起始位置就从j开始到结束,将剩下的这段区域内的元素全部置为0即可。

Leetcode_37

Leetcode_37

代码示例

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
public class MoveZero {
public static void main(String[] args) {
// int nums[] = {0,1,0,3,12};
// int nums[] = {0,0,0,3,12};
int nums[] = {0,12,0,3,0};
moveZeroes(nums);
System.out.println(Arrays.toString(nums));
}

// 交换(一次遍历)
// public static void moveZeroes(int[] nums) {
// int iz = 0; // 需移动的起始位置
// int moveNum = 0; // 0的个数
// for(int i = 0; i < nums.length; i++){
// if(nums[i] == 0){
// moveNum+=1;
// }
// if( nums[i] != 0){
// if (moveNum > 0){ // 移动
// nums[iz] = nums[i];
// nums[i] = 0;
// }
// iz+=1;
// }
// }
// }

// 整体清除(两次遍历)
public static void moveZeroes(int[] nums) {
int j = 0; // 需移动的起始位置
for(int i = 0; i < nums.length; i++){
if(nums[i] != 0){
nums[j++] = nums[i];
}
}

for(int i = j; i < nums.length; i++){
nums[j++] = 0;
}
}
}

(LeetCode-448)找到所有数组中消失的数字

题目

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。

示例 1:

1
2
输入:nums = [4,3,2,7,8,2,3,1]
输出:[5,6]

示例 2:

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

提示:

1
2
3
n == nums.length
1 <= n <= 105
1 <= nums[i] <= n

进阶:你能在不使用额外空间且时间复杂度为 O(n) 的情况下解决这个问题吗? 你可以假定返回的数组不算在额外空间内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DailNum {
public static void main(String[] args) {
int nums[] = {4,3,2,7,8,2,3,1};
for(Integer aa : findDisappearedNumbers(nums)){
System.out.println(aa);
}
}
public static List<Integer> findDisappearedNumbers(int[] nums) {
int n = nums.length;
for(int num : nums){
int x = (num - 1) % n; // 对n 取模来还原它本来的值
nums[x] += n;
}
List<Integer> result = new ArrayList<Integer>();
for(int i = 0; i < n; i++){
if(nums[i] <= n){
result.add(i + 1);
}
}
return result;

}
}

(LeetCode-169) 多数元素

题目

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

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

示例 2:

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

分析

方法1

很明显,这个题可以使用HashMap来解决这个问题,key表示一个元素,value表示该元素出现的次数。

我们用一个循环遍历数组 nums 并将数组中的每个元素加入哈希映射中。在这之后,我们遍历哈希映射中的所有键值对,返回值最大的键。

很明显这种方法的时间复杂度就是O(n),空间复杂度也是O(n),但是不符合题目进阶的要求,所以我们不考虑具体的实现。

方法2

这个问题还可以使用Boyer-Moore 投票算法来解决。

这个算法是个什么意思呢?它的核心思想就是对拼消耗。

比如玩一个诸侯争霸的游戏,游戏的胜利条件是:最后还有人活下来的国家就是赢家。

假设你的人口超过总人口一半以上,而且你微操也不错,能保证每个人口出去干仗都能一对一同归于尽。那就完全可以大混战,即使最差的情况下所有人都联合起来对付你,最后肯定你赢。把其他的国家的所有人全部一对一兑子,最后能剩下的必定是自己人。

用个实例来看看,我们引入一个array数组。

为了方便理解举一个示例输入:{1,2,1,3,1,1,2,1,5}

从第一个数字1开始,我们想要把它和一个不是1的数字一起从数组里抵消掉,但是目前我们只扫描了一个1,所以暂时无法抵消它,把它加入到array,array变成了{1}。

Leetcode_123

然后继续扫描第二个数,是2,我们可以把它和一个不是2的数字抵消掉了,因为我们之前扫描到一个1,所以array变成了{}。

Leetcode_124

继续扫描第三个数1,无法抵消,于是array变成了{1};接下来扫描到3,可以将3和array数组里面的1抵消,于是array变成了{}。

Leetcode_125

接下来扫描到1,此时array为空,所以无法抵消这个1,array变成了{1};接下来扫描到1,此时虽然array不为空,但是array里也是1,所以还是无法抵消,把它也加入这个array,于是array变成了{1,1}(其实到这我们可以发现,array里面只可能同时存在一种数,因为只有array为空或当前扫描到的数和array里的数字相同时才把这个数字放入array)。

Leetcode_126

接下来扫描到2,把它和一个1抵消掉,至于抵消哪一个1,无所谓,array变成了{1}。

接下来扫描到1,不能抵消,array变成了{1,1},result{1,1,5}接下来扫描到5,可以将5和一个1抵消,array变成了{1}

至此扫描完成了数组里的所有数,array里剩了1,所以1就是大于一半的数字。

Leetcode_127

再回顾一下这个过程,其实就是删除(抵消)了(1,2),(1,3),(1,5)剩下了一个1。

可以看到,前面提到array里只可能同时存储一种数字,所以我们用两个变量来优化上述过程,currentNum来表示当前array里存储的数,count表示array的长度,即目前暂时无法删除的元素个数。

依然输入为:{1,2,1,3,1,1,2,1,5},用这两个变量再重复一遍上面的过程,currentNum 初始化为数组第一个元素,count 初始化为1

Leetcode_128

扫描到2,它不等于currentNum,于是可以抵消掉一个currentNum, count -= 1,此时count = 0,其实可以理解为扫到的元素都抵消完了,这里可以暂时不改变currentNum的值。

Leetcode_129

扫描到1,它等于currentNum,于是count += 1 => count = 1

Leetcode_130

扫描到3,它不等于currentNum,可以抵消一个currentNum => count -= 1 => count = 0,此时又抵消完了。

Leetcode_131

扫描到1,它等于currentNum,于是count += 1 => count = 1

扫描到1,他等于currentNum,无法抵消 => count += 1 => count = 2 (扫描完前六个数,剩两个1无法抵消)

Leetcode_132

扫描到2,它不等于currentNum,可以抵消一个currentNum => count -= 1 => count = 1,此时还剩1个1没有被抵消

扫描到1,它等于currentNum,无法抵消 => count += 1 => count = 2

扫描到5,它不等于currentNum,可以抵消一个currentNum => count -= 1 => count = 1至此扫描完成,还剩1个1没有被抵消掉,它就是我们要找的数。

Leetcode_133

(LeetCode-11) 盛最多水的容器

题目

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明:你不能倾斜容器。

示例 1:

Leetcode_134

1
2
3
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

示例 2:

1
2
输入:height = [1,1]
输出:1

分析

方法一:双指针

我们先从题目中的示例开始,一步一步地解释双指针算法的过程。稍后再给出算法正确性的证明。

题目中的示例为:

1
2
[1, 8, 6, 2, 5, 4, 8, 3, 7]
^ ^

在初始时,左右指针分别指向数组的左右两端,它们可以容纳的水量为 \min(1, 7) * 8 = 8min(1,7)∗8=8。

此时我们需要移动一个指针。移动哪一个呢?直觉告诉我们,应该移动对应数字较小的那个指针(即此时的左指针)。这是因为,由于容纳的水量是由

1
两个指针指向的数字中较小值∗指针之间的距离

决定的。如果我们移动数字较大的那个指针,那么前者「两个指针指向的数字中较小值」不会增加,后者「指针之间的距离」会减小,那么这个乘积会减小。因此,我们移动数字较大的那个指针是不合理的。因此,我们移动 数字较小的那个指针。

所以,我们将左指针向右移动:

1
2
[1, 8, 6, 2, 5, 4, 8, 3, 7]
^ ^

此时可以容纳的水量为 \min(8, 7) * 7 = 49min(8,7)∗7=49。由于右指针对应的数字较小,我们移动右指针:

1
2
[1, 8, 6, 2, 5, 4, 8, 3, 7]
^ ^

此时可以容纳的水量为 min(8, 3) * 6 = 18min(8,3)∗6=18。由于右指针对应的数字较小,我们移动右指针:

1
2
[1, 8, 6, 2, 5, 4, 8, 3, 7]
^ ^

此时可以容纳的水量为 min(8, 8) * 5 = 40min(8,8)∗5=40。两指针对应的数字相同,我们可以任意移动一个,例如左指针:

1
2
[1, 8, 6, 2, 5, 4, 8, 3, 7]
^ ^

此时可以容纳的水量为 min(6, 8) * 4 = 24min(6,8)∗4=24。由于左指针对应的数字较小,我们移动左指针,并且可以发现,在这之后左指针对应的数字总是较小,因此我们会一直移动左指针,直到两个指针重合。在这期间,对应的可以容纳的水量为:min(2, 8) * 3 = 6min(2,8)∗3=6,min(5, 8) * 2 = 10min(5,8)∗2=10,min(4, 8) * 1 = 4min(4,8)∗1=4。

在我们移动指针的过程中,计算到的最多可以容纳的数量为 4949,即为最终的答案。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MaxArea {

public static void main(String[] args) {
int[] nums = {1,8,6,2,5,4,8,3,7};
System.out.println(maxArea(nums));
}

public static int maxArea(int[] height) {
int i = 0, j = height.length - 1;
int masWater = 0;
while(i < j){
if(height[i] < height[j]){
masWater = Math.max(masWater, (j - i ) * height[i++]);
}else {
masWater = Math.max(masWater, (j - i ) * height[j--]);
}

}
return masWater;
}
}

(LeetCode-15) 三数之和

题目

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:

1
2
3
4
5
6
7
8
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

1
2
3
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

1
2
3
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

分析

解题思路:

  • 暴力法搜索为 O(N^3)时间复杂度,可通过双指针动态消去无效解来优化效率。
  • 双指针法铺垫: 先将给定 nums 排序,复杂度为 O(NlogN)。
    双指针法思路: 固定 3 个指针中最左(最小)数字的指针 k,双指针 i,j 分设在数组索引 (k, len(nums))两端,通过双指针交替向中间移动,记录对于每个固定指针 k 的所有满足 nums[k] + nums[i] + nums[j] == 0 的 i,j 组合:
    • 当 nums[k] > 0 时直接break跳出:因为 nums[j] >= nums[i] >= nums[k] > 0,即 33 个数字都大于 00 ,在此固定指针 k 之后不可能再找到结果了。
    • 当 k > 0且nums[k] == nums[k - 1]时即跳过此元素nums[k]:因为已经将 nums[k - 1] 的所有组合加入到结果中,本次双指针搜索只会得到重复组合。
    • i,j 分设在数组索引 (k, len(nums))两端,当i < j时循环计算s = nums[k] + nums[i] + nums[j],并按照以下规则执行双指针移动:
      当s < 0时,i += 1并跳过所有重复的nums[i];
      当s > 0时,j -= 1并跳过所有重复的nums[j];
      当s == 0时,记录组合[k, i, j]至res,执行i += 1和j -= 1并跳过所有重复的nums[i]和nums[j],防止记录到重复组合。

代码

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
public class ThreeSum {

public static void main(String[] args) {
int[] nums = {-1,0,1,2,-1,-4};
List<List<Integer>> list = threeSum(nums);
System.out.println("");
}

public static List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
for(int k = 0; k < nums.length - 2; k++){
if(nums[k] > 0) break;
if(k > 0 && nums[k] == nums[k - 1]) continue;
int i = k + 1, j = nums.length - 1;
while(i < j){
int sum = nums[k] + nums[i] + nums[j];
if(sum < 0){
while(i < j && nums[i] == nums[++i]); // 数字重复,则结果还是小于0,继续查找下一个
} else if (sum > 0) {
while(i < j && nums[j] == nums[--j]);
} else {
res.add(new ArrayList<Integer>(Arrays.asList(nums[k], nums[i], nums[j])));
while(i < j && nums[i] == nums[++i]);
while(i < j && nums[j] == nums[--j]);
}
}
}
return res;
}
}

(LeetCode-31)下一个排列

题目

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1]

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
  • 而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

示例 1:

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

示例 2:

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

示例 3:

1
2
输入:nums = [1,1,5]
输出:[1,5,1]

分析

方法一:两遍扫描

思路及解法

注意到下一个排列总是比当前排列要大,除非该排列已经是最大的排列。我们希望找到一种方法,能够找到一个大于当前序列的新序列,且变大的幅度尽可能小。具体地:

  • 我们需要将一个左边的「较小数」与一个右边的「较大数」交换,以能够让当前排列变大,从而得到下一个排列。
  • 同时我们要让这个「较小数」尽量靠右,而「较大数」尽可能小。当交换完成后,「较大数」右边的数需要按照升序重新排列。这样可以在保证新排列大于原来排列的情况下,使变大的幅度尽可能小。

以排列 [4,5,2,6,3,1][4,5,2,6,3,1] 为例:

  • 我们能找到的符合条件的一对「较小数」与「较大数」的组合为 2 与 3,满足「较小数」尽量靠右,而「较大数」尽可能小。
  • 当我们完成交换后排列变为 [4,5,3,6,2,1][4,5,3,6,2,1],此时我们可以重排「较小数」右边的序列,序列变为 [4,5,3,1,2,6][4,5,3,1,2,6]。

具体地,我们这样描述该算法,对于长度为 n 的排列 a

  • 首先从后向前查找第一个顺序对 (i,i+1),满足 a[i] < a[i+1]。这样「较小数」即为 a[i]。此时 [i+1,n) 必然是下降序列。

如果找到了顺序对,那么在区间[i+1,n) 中从后向前查找第一个元素 jj 满足 a[i] < a[j]。这样「较大数」即为 a[j]。

交换 a[i] 与 a[j,此时可以证明区间 [i+1,n) 必为降序。我们可以直接使用双指针反转区间 [i+1,n)使其变为升序,而无需对该区间进行排序。

Leetcode_135

注意

如果在步骤 1 找不到顺序对,说明当前序列已经是一个降序序列,即最大的序列,我们直接跳过步骤 2 执行步骤 3,即可得到最小的升序序列。

代码

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
public class NextPermutation {
public static void main(String[] args) {
int[] nums = {1,2};
nextPermutation(nums);
System.out.println(Arrays.toString(nums));
}

public static void nextPermutation(int[] nums) {
int numCount = nums.length;
int start = numCount - 2;
while (start >= 0 && nums[start] >= nums[start + 1]) { // 寻找 较小数
start = start -1;
}

if(start >= 0){
int end = nums.length - 1;
while (end >= 0 && nums[start] >= nums[end]){ // 寻找较大值
end = end - 1;
}
swap( nums, start, end);
}
reverse( nums, start + 1);
}
public static void reverse(int[] nums, int start) {
int left = start, right = nums.length - 1;
while(left < right){
swap(nums, left, right);
left++;
right--;
}
}
public static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}

**(LeetCode-48) **旋转图像

题目

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

示例 1:

Leetcode_136

1
2
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]

示例 2:

Leetcode_137

1
2
输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

分析

方法:用翻转代替旋转

我们以题目中的示例

Leetcode_138

作为例子,先将其通过水平轴翻转得到:

Leetcode_139

再根据主对角线翻转得到:

Leetcode_140

就得到了答案。这是为什么呢?对于水平轴翻转而言,我们只需要枚举矩阵上半部分的元素,和下半部分的元素进行交换,即

Leetcode_141

对于主对角线翻转而言,我们只需要枚举对角线左侧的元素,和右侧的元素进行交换,即

Leetcode_142

代码

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
public class Rotate {
public static void main(String[] args) {
int[][] matrix = {{5,1,9,11},{2,4,8,10},{13,3,6,7},{15,14,12,16}};
rotate( matrix);
System.out.println("");
}
public static void rotate(int[][] matrix) {
int n = matrix.length;
// 水平翻转
for(int i = 0; i < n / 2; i++){
for(int j = 0; j < n; j++){
int temp = matrix[i][j];
matrix[i][j] = matrix[n - i - 1][j];
matrix[n - i - 1][j] = temp;
}
}
// 主对角线翻转
for(int i = 0; i < n ; i++){
for(int j = 0; j <= i; j++){
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
}
}

**(LeetCode-54) **螺旋矩阵

题目

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

示例 1:

Leetcode_143

1
2
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

示例 2:

Leetcode_144

1
2
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]

分析

方法二:按层模拟

可以将矩阵看成若干层,首先输出最外层的元素,其次输出次外层的元素,直到输出最内层的元素。

定义矩阵的第 kk 层是到最近边界距离为 kk 的所有顶点。例如,下图矩阵最外层元素都是第 1 层,次外层元素都是第 2 层,剩下的元素都是第 3 层。

1
2
3
4
5
[[1, 1, 1, 1, 1, 1, 1],
[1, 2, 2, 2, 2, 2, 1],
[1, 2, 3, 3, 3, 2, 1],
[1, 2, 2, 2, 2, 2, 1],
[1, 1, 1, 1, 1, 1, 1]]

对于每层,从左上方开始以顺时针的顺序遍历所有元素。假设当前层的左上角位于(top,left),右下角位于 (bottom,right),按照如下顺序遍历当前层的元素。

  • 从左到右遍历上侧元素,依次为(top,left) 到(top,right)。
  • 从上到下遍历右侧元素,依次为 (top+1,right) 到 (bottom,right)。
  • 如果left<right 且 top<bottom,则从右到左遍历下侧元素,依次为(bottom,right−1) 到(bottom,left+1),以及从下到上遍历左侧元素,依次为 (bottom,left) 到(top+1,left)。

遍历完当前层的元素之后,将 left 和 top 分别增加 11,将 right 和bottom 分别减少 11,进入下一层继续遍历,直到遍历完所有元素为止。

Leetcode_145

代码

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
public class SpiralOrder {

public static void main(String[] args) {
// int[][] matrix = {{1, 2,3},{4,5,6},{7,8,9}};
int[][] matrix = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
//[[1,2,3,4],[5,6,7,8],[9,10,11,12]]
List<Integer> list = spiralOrder( matrix);
System.out.println("");
}

public static List<Integer> spiralOrder(int[][] matrix) {
List<Integer> order = new ArrayList<Integer>();
// 边界判断
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return order;
}
// 行 列
int rows = matrix.length, columns = matrix[0].length;
// 边界值
int left = 0, right = columns - 1, top = 0, bottom = rows - 1;
// 遍历 边界值为截止条件
while (left <= right && top <= bottom) {
// 左 -> 右
for (int column = left; column <= right; column++) {
order.add(matrix[top][column]);
}
// 上 -- > 下
for (int row = top + 1; row <= bottom; row++) {
order.add(matrix[row][right]);
}
// 如果左等于又,或上等于下 则说明为单列或单行,则无需反向操作
if (left < right && top < bottom) {
// 右 -> 左
for (int column = right - 1; column > left; column--) {
order.add(matrix[bottom][column]);
}
// 下 -- > 上
for (int row = bottom; row > top; row--) {
order.add(matrix[row][left]);
}
}
// 当前层遍历完, 缩小边界值
left++;
right--;
top++;
bottom--;
}
return order;
}
}

**(LeetCode-55) **跳跃游戏

题目

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

示例 1:

1
2
3
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。

示例 2:

1
2
3
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

分析

方法1

这个问题,一定要看清楚题目中的“最大长度”,也就是说,如果某个元素的值为3,能够跳跃的长度有1,2,3这三种选择,并不是只能跳跃3步。

由此我们可以推出,如果数组中的元素没有为0的,比如全部都是1或者是大于1的数,那就一定可以到达数组的末尾。如果数组中的元素存在0,则必须在0值之前找到一个值,能够避开这个0,否则的话是没法到达数组的末尾。

于是,这个题目就可以这样处理了:从后往前遍历,如果遇到nums[i] = 0,就找i前面的元素j,使得nums[j] > i - j。如果找不到,则不可能跳跃到num[i+1],只会落在nums[i] = 0上,返回false。

虽然在具体的代码实现上会有两个循环,但是实际值遍历了数组一次,所以时间复杂度就是O(n)。

方法2

这个问题,还可以用动态规划来求解,这是个非常明显的“多阶段决策最优解”类型的问题,每达到一个数组的元素,都是一个决策,这个决策的最优解就是我们当前阶段我们能够跳跃的最大值是多少。

很明显,按照我们求解动态规划问题的步骤和思想,首先设定一个dp数组,dp[i]表示在下标i处能够跳跃的最大值。

对于dp[i],它等于dp[i-1]跳一格到达i处后剩余的步数和nums[i]的两者中的最大值。因此状态转移方程为:dp[i]=max(dp[i-1]-1,nums[i])

初始化条件就是dp[0]=nums[0]。

在每次循环开始,我们判断dp[i-1]是否等于0,若是,则不可能到达下标i处,因此直接返回false。循环结束后返回true。

Leetcode_146

不过仔细分析上述过程可以发现,dp[i]只和dp[i-1]有关,因此可以用一个变量来取代数组。

代码

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
public class CanJump {
public static void main(String[] args) {
int[] nums = {3,2,1,0,4};
// int[] nums = {2,3,1,1,4};
System.out.println(canJump( nums));
}

// 动态规划
public static boolean canJump(int[] nums) {
int n = nums.length;
int maxStep = nums[0];
for(int i = 1; i < n; i++){
if(maxStep == 0) return false;
maxStep = Math.max(nums[i], maxStep - 1);
}
return true;
}

// 遍历查找为0的是否可以跳过
// public static boolean canJump(int[] nums) {
// if(nums == null || nums.length < 2){
// return true;
// }
// for(int i = nums.length - 2; i >= 0; i--){
// if(nums[i] == 0){
// if(!checkJump(nums, i)){
// return false;
// }
// }
// }
// return true;
// }
//
// // 是否可以调过
// private static boolean checkJump(int[] nums, int i) {
// int index = i - 1 ;
// while(index >= 0 ){
// if(nums[index] > i - index ){
// return true;
// }
// index--;
// }
// return false;
// }
}

**(LeetCode-215)**数组中的第K个最大元素

题目

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

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

示例 2:

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

分析

方法一:基于快速排序的选择方法

思路和算法

我们可以用快速排序来解决这个问题,先对原数组排序,再返回倒数第 kk 个位置,这样平均时间复杂度是O(nlogn),但其实我们可以做的更快。

首先我们来回顾一下快速排序,这是一个典型的分治算法。我们对数组a[l⋯r] 做快速排序的过程是(参考《算法导论》):

  • 分解: 将数组 a[l⋯r] 「划分」成两个子数组 a[l⋯q−1]、a[q+1⋯r],使得 a[l⋯q−1] 中的每个元素小于等于 a[q],且 a[q] 小于等于 a[q+1⋯r] 中的每个元素。其中,计算下标 q 也是「划分」过程的一部分。
  • 解决: 通过递归调用快速排序,对子数组 a[l⋯q−1] 和 a[q+1⋯r] 进行排序。
  • 合并: 因为子数组都是原址排序的,所以不需要进行合并操作,a[l⋯r] 已经有序。
  • 上文中提到的 「划分」 过程是:从子数组 a[l⋯r] 中选择任意一个元素 x 作为主元,调整子数组的元素使得左边的元素都小于等于它,右边的元素都大于等于它, xx 的最终位置就是 q。

由此可以发现每次经过「划分」操作后,我们一定可以确定一个元素的最终位置,即 x 的最终位置为 q,并且保证 a[l⋯q−1] 中的每个元素小于等于a[q],且 a[q] 小于等于 a[q+1⋯r] 中的每个元素。所以只要某次划分的 q 为倒数第 k 个下标的时候,我们就已经找到了答案。 我们只关心这一点,至于 a[l⋯q−1] 和 a[q+1⋯r] 是否是有序的,我们不关心。

代码

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class QuickSort {

public static void main(String[] args) {
int[] nums = {3,2,1,5,6,4};
int temp = findKthLargest( nums, 2);
System.out.println(temp);
}

public static int findKthLargest(int[] nums, int k){
int baseNum = nums[0];
int start = 0;
int end = nums.length - 1;
// 转化一下, 第k个元素的下标是nums.length - k
int target = nums.length - k;
while(true){
int temp = partition( nums, start, end);
if(temp == target){
return nums[temp];
}else if(temp < target){
start = temp + 1;
}else{
end = temp - 1;
}
}
}

// public static int partition(int[] nums, int start, int end){
// int i = start;
// int j = end;
// if(i == j) return i;
// int baseNum = nums[i];
// if(i < j){
// //
// while (i < j){
// while (i < j && baseNum < nums[j]) {
// j--;
// }
// if(i < j) {
// nums[i] = nums[j];
// i++;
// }
// while (i < j && baseNum > nums[i]){
// i++;
// }
// if(i < j) {
// nums[j] = nums[i];
// j--;
// }
// }
// nums[i] = baseNum;
// }
// int result = i;
// return result ;
// }

/**
* 快速排序分区方法
*/
public static int partition(int[] array, int start, int end) {
/*只有一个元素时,无需再分区*/
if(start == end) return start;
/*随机选取一个基准数*/
int pivot = start;
/*zoneIndex是分区指示器,初始值为分区头元素下标减一*/
int zoneIndex = start - 1;
/*将基准数和分区尾元素交换位置*/
swap(array, pivot, end);
for (int i = start; i <= end; i++){
/*当前元素小于等于基准数*/
if (array[i] <= array[end]) {
/*首先分区指示器累加*/
zoneIndex++;
/*当前元素在分区指示器的右边时,交换当前元素和分区指示器元素*/
if (i > zoneIndex)
swap(array, i, zoneIndex);
}
}
return zoneIndex;
}

/**
* 交换数组内两个元素
*/
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}

(LeetCode-347) 前 K 个高频元素

题目

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

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

示例 2:

1
2
输入: nums = [1], k = 1
输出: [1]

分析

方法一:堆

思路与算法

首先遍历整个数组,并使用哈希表记录每个数字出现的次数,并形成一个「出现次数数组」。找出原数组的前 kk 个高频元素,就相当于找出「出现次数数组」的前 kk 大的值。

最简单的做法是给「出现次数数组」排序。但由于可能有O(N) 个不同的出现次数(其中 N 为原数组长度),故总的算法复杂度会达到 O(NlogN),不满足题目的要求。

在这里,我们可以利用堆的思想:建立一个小顶堆,然后遍历「出现次数数组」:

  • 如果堆的元素个数小于 k,就可以直接插入堆中。
  • 如果堆的元素个数等于 k,则检查堆顶与当前出现次数的大小。如果堆顶更大,说明至少有 k 个数字的出现次数比当前值大,故舍弃当前值;否则,就弹出堆顶,并将当前值插入堆中。

遍历完成后,堆中的元素就代表了「出现次数数组」中前 k 大的值。

代码

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
public class TopKFrequent {

public static void main(String[] args) {
int[] nums = {1,1,1,2,2,2,2,4,4,4,4,4,5,5,5,5,5,5,6,6,6,6,6,6,6,3};
int[] result = topKFrequent( nums, 2);
System.out.println(Arrays.toString(result));
}

public static int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
for (int num : nums) {
occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
}

// int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] m, int[] n) {
return m[1] - n[1];
}
});
for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
int num = entry.getKey(), count = entry.getValue();
if (queue.size() == k) {
if (queue.peek()[1] < count) {
queue.poll();
queue.offer(new int[]{num, count});
}
} else {
queue.offer(new int[]{num, count});
}
}
int[] ret = new int[k];
for (int i = 0; i < k; ++i) {
ret[i] = queue.poll()[0];
}
return ret;
}
}

**(LeetCode-4) **寻找两个正序数组的中位数

题目

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。

算法的时间复杂度应该为 O(log (m+n)) 。

示例 1:

1
2
3
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2

示例 2:

1
2
3
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

分析

方法一:二分查找

给定两个有序数组,要求找到两个有序数组的中位数,最直观的思路有以下两种:

  • 使用归并的方式,合并两个有序数组,得到一个大的有序数组。大的有序数组的中间位置的元素,即为中位数。
  • 不需要合并两个有序数组,只要找到中位数的位置即可。由于两个数组的长度已知,因此中位数对应的两个数组的下标之和也是已知的。维护两个指针,初始时分别指向两个数组的下标 0 的位置,每次将指向较小值的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针),直到到达中位数的位置。

假设两个有序数组的长度分别为 m 和 n,上述两种思路的复杂度如何?

第一种思路的时间复杂度是 O(m+n),空间复杂度是 O(m+n)。第二种思路虽然可以将空间复杂度降到 O(1),但是时间复杂度仍是 O(m+n)。

如何把时间复杂度降低到 O(log(m+n)) 呢?如果对时间复杂度的要求有 log,通常都需要用到二分查找,这道题也可以通过二分查找实现。

根据中位数的定义,当 m+n 是奇数时,中位数是两个有序数组中的第(m+n)/2 个元素,当m+n 是偶数时,中位数是两个有序数组中的第(m+n)/2 个元素和第(m+n)/2+1 个元素的平均值。因此,这道题可以转化成寻找两个有序数组中的第 k 小的数,其中 k 为(m+n)/2 或(m+n)/2+1。

假设两个有序数组分别是 A 和 B。要找到第 k 个元素,我们可以比较 A[k/2−1] 和 B[k/2−1],其中 / 表示整数除法。由于 A[k/2−1] 和 B[k/2−1] 的前面分别有 A[0..k/2−2] 和 B[0..k/2−2],即 k/2−1 个元素,对于A[k/2−1] 和 B[k/2−1] 中的较小值,最多只会有 (k/2−1)+(k/2−1)≤k−2 个元素比它小,那么它就不能是第 kk 小的数了。

因此我们可以归纳出三种情况:

  • 如果 [k/2-1] < B[k/2−1],则比 A[k/2−1] 小的数最多只有 A 的前 k/2−1 个数和 B 的前 k/2−1 个数,即比 A[k/2−1] 小的数最多只有k−2 个,因此 A[k/2−1] 不可能是第 kk 个数,A[0] 到 A[k/2−1] 也都不可能是第 k 个数,可以全部排除。
  • 如果 A[k/2−1] > B[k/2−1],则可以排除 B[0] 到 B[k/2−1]。
  • 如果 A[k/2−1] = B[k/2−1],则可以归入第一种情况处理。

Leetcode_147

可以看到,比较 A[k/2−1] 和 B[k/2−1] 之后,可以排除 k/2 个不可能是第 k 小的数,查找范围缩小了一半。同时,我们将在排除后的新数组上继续进行二分查找,并且根据我们排除数的个数,减少 k 的值,这是因为我们排除的数都不大于第 k 小的数。

有以下三种情况需要特殊处理:

  • 如果 A[k/2−1] 或者 B[k/2−1] 越界,那么我们可以选取对应数组中的最后一个元素。在这种情况下,我们必须根据排除数的个数减少 k 的值,而不能直接将 k 减去k/2。
  • 如果一个数组为空,说明该数组中的所有元素都被排除,我们可以直接返回另一个数组中第 k 小的元素。
  • 如果 k=1,我们只要返回两个数组首元素的最小值即可。

用一个例子说明上述算法。假设两个有序数组如下:

1
2
A: 1 3 4 9
B: 1 2 3 4 5 6 7 8 9

两个有序数组的长度分别是 4 和 9,长度之和是 13,中位数是两个有序数组中的第 7 个元素,因此需要找到第 k=7 个元素。

比较两个有序数组中下标为 k/2−1=2 的数,即A[2] 和B[2],如下面所示:

1
2
3
4
A: 1 3 4 9

B: 1 2 3 4 5 6 7 8 9

由于A[2]>B[2],因此排除 B[0] 到 B[2],即数组 B 的下标偏移(offset)变为 3,同时更新 k 的值:k=k−k/2=4。

下一步寻找,比较两个有序数组中下标为 k/2−1=1 的数,即 A[1] 和 B[4],如下面所示,其中方括号部分表示已经被排除的数。

1
2
3
4
A: 1 3 4 9

B: [1 2 3] 4 5 6 7 8 9

由于 A[1]<B[4],因此排除A[0] 到 A[1],即数组 A 的下标偏移变为 2,同时更新 k 的值:k=k−k/2=2。

下一步寻找,比较两个有序数组中下标为k/2−1=0 的数,即比较A[2] 和 B[3],如下面所示,其中方括号部分表示已经被排除的数。

1
2
3
4
A: [1 3] 4 9

B: [1 2 3] 4 5 6 7 8 9

由于A[2]=B[3],根据之前的规则,排除A 中的元素,因此排除 A[2],即数组 A 的下标偏移变为 3,同时更新 k 的值: k=k−k/2=1。

由于 k 的值变成 1,因此比较两个有序数组中的未排除下标范围内的第一个数,其中较小的数即为第 k 个数,由于 A[3]>B[3],因此第 kk 个数是 B[3]=4。

1
2
3
4
A: [1 3 4] 9

B: [1 2 3] 4 5 6 7 8 9

代码

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
51
52
53
54
55
56
57
58
59
60
61
public class FindMedianSortedArrays {
public static void main(String[] args) {
int[] nums1 = {1,2,3};
int[] nums2 = {4,5,6,7,8,9,10,11,12,13,14,17};
System.out.println(findMedianSortedArrays( nums1, nums2));
}
public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
int length1 = nums1.length, length2 = nums2.length;
int totalLength = length1 + length2;
if (totalLength % 2 == 1) {
int midIndex = totalLength / 2;
double median = getKthElement(nums1, nums2, midIndex + 1);
return median;
} else {
int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
double median = (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
return median;
}
}
private static double getKthElement(int[] nums1, int[] nums2, int k) {
/* 主要思路:要找到第 k (k>1) 小的元素,那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
* 这里的 "/" 表示整除
* nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个
* nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个
* 取 pivot = min(pivot1, pivot2),两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个
* 这样 pivot 本身最大也只能是第 k-1 小的元素
* 如果 pivot = pivot1,那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums1 数组
* 如果 pivot = pivot2,那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums2 数组
* 由于我们 "删除" 了一些元素(这些元素都比第 k 小的元素要小),因此需要修改 k 的值,减去删除的数的个数
*/
int length1 = nums1.length, length2 = nums2.length;
int index1 = 0, index2 = 0;
int kthElement = 0;

while (true) {
// 边界情况
if (index1 == length1) {
return nums2[index2 + k - 1];
}
if (index2 == length2) {
return nums1[index1 + k - 1];
}
if (k == 1) {
return Math.min(nums1[index1], nums2[index2]);
}

// 正常情况
int half = k / 2;
int newIndex1 = Math.min(index1 + half, length1) - 1;
int newIndex2 = Math.min(index2 + half, length2) - 1;
int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
if (pivot1 <= pivot2) {
k -= (newIndex1 - index1 + 1);
index1 = newIndex1 + 1;
} else {
k -= (newIndex2 - index2 + 1);
index2 = newIndex2 + 1;
}
}
}
}

(LeetCode-33)搜索旋转排序数组

题目

整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

1
2
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

示例 2:

1
2
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1

示例 3:

1
2
输入:nums = [1], target = 0
输出:-1

分析

方法一:二分查找

思路和算法

对于有序数组,可以使用二分查找的方法查找元素。

但是这道题中,数组本身不是有序的,进行旋转后只保证了数组的局部是有序的,这还能进行二分查找吗?答案是可以的。

可以发现的是,我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的。拿示例来看,我们从 6 这个位置分开以后数组变成了 [4, 5, 6] 和 [7, 0, 1, 2] 两个部分,其中左边 [4, 5, 6] 这个部分的数组是有序的,其他也是如此。

这启示我们可以在常规二分查找的时候查看当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:

  • 如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [nums[l],nums[mid]),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
  • 如果 [mid, r] 是有序数组,且 target 的大小满足 (nums[mid+1],nums[r]],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。

Leetcode2_14

需要注意的是,二分的写法有很多种,所以在判断 target 大小与有序部分的关系的时候可能会出现细节上的差别。

代码

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
public class MidSearch {
public static void main(String[] args) {
//int[] nums = {4,5,6,7,0,1,2};
int[] nums = {3, 1};
// int[] nums = {4,5,6,7,0,1,2};
//int[] nums = {1};
int a = new MidSearch().search(nums, 1);
System.out.println();
}

public int search(int[] nums, int target) {
int n = nums.length;
if (n == 0) {
return -1;
}
if (n == 1) {
return nums[0] == target ? 0 : -1;
}
return search1( nums, 0, nums.length - 1, target);
}

public int search1(int[] nums, int start, int end, int target) {
while (start <= end){
int mid = (end + start) / 2;
if(nums[mid] == target){
return mid;
}
if(nums[start] <= nums[mid]){
// 判断是否在范围内
if(nums[start] <= target && target < nums[mid]){
end = mid -1;
}else {
start = mid + 1;
}
}else {
if(nums[mid] < target && target <= nums[end]){
start = mid + 1;
}else {
end = mid -1;
}
}
}
return -1;
}
}

(LeetCode- 34) 在排序数组中查找元素的第一个和最后一个位置

题目

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

1
2
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

1
2
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

1
2
输入:nums = [], target = 0
输出:[-1,-1]

分析

方法

由于数组已经排序,我们可以利用二分法来加速查找的过程。

代码

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
public class SearchRange {
public static void main(String[] args) {
int[] nums = {5,7,7,8,8,10};
int[] result = new SearchRange().searchRange( nums, 8);
System.out.println("");
}

public int[] searchRange(int[] nums, int target) {
int left = searchLeft( nums, 0, nums.length - 1, target);
int right = searchRight( nums, 0, nums.length - 1, target);
int[] result = {left, right};
return result;
}

public int searchLeft(int[] nums, int start, int end, int target) {
int idx = -1;
while (start <= end){
int mid = ( start + end ) / 2 ;
if(nums[mid] >= target){
end = mid - 1;
}else {
start = mid + 1;
}
if( nums[mid] == target){
idx = mid;
}
}
return idx;
}

public int searchRight(int[] nums, int start, int end, int target) {
int idx = -1;
while (start <= end){
int mid = ( start + end ) / 2 ;
if(nums[mid] > target){
end = mid - 1;
}else {
start = mid + 1;
}
if( nums[mid] == target){
idx = mid;
}
}
return idx;
}
}

(LeetCode- 153) 寻找旋转排序数组中的最小值

题目

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

分析

代码

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
public class FindMin {
public static void main(String[] args) {
}
public int findMin(int[] nums) {
if(nums.length == 1){
return nums[0];
}
int n = nums.length;
int start = 0;
int end = n - 1;
while (start <= end){
if(start + 1 == end){
return Math.min(nums[start], nums[end]);
}
int mid = ( start + end ) / 2;
if(nums[start] <= nums[mid] && nums[mid] <= nums[end]){
return nums[start];
//在左边
}else if( nums[start] >= nums[mid] && nums[mid] < nums[end] ){
end = mid ;
}else{
start = mid;
}
}
return -1;
}
}

(LeetCode- 240) 搜索二维矩阵 II

题目

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

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

示例 1:

Leetcode2_15

1
2
输入: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

示例 2:

Leetcode2_16

1
2
输入: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 = 20
输出:false

提示:

1
2
3
4
5
6
7
m == matrix.length
n == matrix[i].length
1 <= n, m <= 300
-109 <= matrix[i][j] <= 109
每行的所有元素从左到右升序排列
每列的所有元素从上到下升序排列
-109 <= target <= 109

分析

方法三:Z 字形查找

思路与算法

我们可以从矩阵matrix 的右上角 (0,n−1) 进行搜索。在每一步的搜索过程中,如果我们位于位置(x,y),那么我们希望在以 matrix 的左下角为左下角、以(x,y) 为右上角的矩阵中进行搜索,即行的范围为 [x, m - 1][x,m−1],列的范围为 [0, y][0,y]:

  • 如果 [x,y]=target,说明搜索完成;
  • 如果 [x,y]>target,由于每一列的元素都是升序排列的,那么在当前的搜索矩阵中,所有位于第 y 列的元素都是严格大于 target 的,因此我们可以将它们全部忽略,即将 y 减少 1;
  • 如果 [x,y]<target,由于每一行的元素都是升序排列的,那么在当前的搜索矩阵中,所有位于第 x 行的元素都是严格小于 target 的,因此我们可以将它们全部忽略,即将 x 增加 1。

在搜索的过程中,如果我们超出了矩阵的边界,那么说明矩阵中不存在 target。

代码

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
public class SearchMatrix {
public static void main(String[] args) {
int[][] 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} };
boolean falew = new SearchMatrix().searchMatrix( matrix, 29);
System.out.println(matrix.length);
}
public boolean searchMatrix(int[][] matrix, int target) {
if(matrix == null){
return false;
}
// 矩阵行和列下标的最大值
int cols = matrix[0].length-1;
int rows = matrix.length-1;
if(target > matrix[rows][cols]) {
return false;
}
int currentCol = cols;
int currentRow = 0;
while (currentCol >= 0 && currentRow <= rows){
if(target == matrix[currentRow][currentCol]){
return true;
}else if(target < matrix[currentRow][currentCol]){
currentCol--;
}else if(target > matrix[currentRow][currentCol]){
currentRow++;
}
}
return false;
}
}