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 仔细观察题目我们其实可以发现,这个两个数组本身其实就是已经有序的了,这一点我们没有利用上,所以改进的办法就是利用“双指针”,每次从两个数组头部取出比较小的数字放到结果中。
这种方法的时间复杂度是多少呢?因为要把两个数组各循环一遍,所以时间复杂度是O(m+n),在处理上因为需要一个额外的空白数组来存放两个数组的值,所以空间复杂度也是O(m+n)。
方法2 方法2中还需要一个长度为m+n的临时数组作为中转,能不能不要这个数组呢?因为题目中的整数数组 nums1的后面还有空位,完全可以利用上。所以改进方法就是,依然使用双指针,但是倒序处理。
这种情况下,时间复杂度是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即可。
代码示例 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 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:
示例 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}。
然后继续扫描第二个数,是2,我们可以把它和一个不是2的数字抵消掉了,因为我们之前扫描到一个1,所以array变成了{}。
继续扫描第三个数1,无法抵消,于是array变成了{1};接下来扫描到3,可以将3和array数组里面的1抵消,于是array变成了{}。
接下来扫描到1,此时array为空,所以无法抵消这个1,array变成了{1};接下来扫描到1,此时虽然array不为空,但是array里也是1,所以还是无法抵消,把它也加入这个array,于是array变成了{1,1}(其实到这我们可以发现,array里面只可能同时存在一种数,因为只有array为空或当前扫描到的数和array里的数字相同时才把这个数字放入array)。
接下来扫描到2,把它和一个1抵消掉,至于抵消哪一个1,无所谓,array变成了{1}。
接下来扫描到1,不能抵消,array变成了{1,1},result{1,1,5}接下来扫描到5,可以将5和一个1抵消,array变成了{1}
至此扫描完成了数组里的所有数,array里剩了1,所以1就是大于一半的数字。
再回顾一下这个过程,其实就是删除(抵消)了(1,2),(1,3),(1,5)剩下了一个1。
可以看到,前面提到array里只可能同时存储一种数字,所以我们用两个变量来优化上述过程,currentNum来表示当前array里存储的数,count表示array的长度,即目前暂时无法删除的元素个数。
依然输入为:{1,2,1,3,1,1,2,1,5},用这两个变量再重复一遍上面的过程,currentNum 初始化为数组第一个元素,count 初始化为1
扫描到2,它不等于currentNum,于是可以抵消掉一个currentNum, count -= 1,此时count = 0,其实可以理解为扫到的元素都抵消完了,这里可以暂时不改变currentNum的值。
扫描到1,它等于currentNum,于是count += 1 => count = 1
扫描到3,它不等于currentNum,可以抵消一个currentNum => count -= 1 => count = 0,此时又抵消完了。
扫描到1,它等于currentNum,于是count += 1 => count = 1
扫描到1,他等于currentNum,无法抵消 => count += 1 => count = 2 (扫描完前六个数,剩两个1无法抵消)
扫描到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-11) 盛最多水的容器 题目 给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
示例 1:
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 [1, 8, 6, 2, 5, 4, 8, 3, 7] ^ ^
在初始时,左右指针分别指向数组的左右两端,它们可以容纳的水量为 \min(1, 7) * 8 = 8min(1,7)∗8=8。
此时我们需要移动一个指针。移动哪一个呢?直觉告诉我们,应该移动对应数字较小的那个指针(即此时的左指针)。这是因为,由于容纳的水量是由
决定的。如果我们移动数字较大的那个指针,那么前者「两个指针指向的数字中较小值」不会增加,后者「指针之间的距离」会减小,那么这个乘积会减小。因此,我们移动数字较大的那个指针是不合理的。因此,我们移动 数字较小的那个指针。
所以,我们将左指针向右移动:
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)使其变为升序,而无需对该区间进行排序。
注意
如果在步骤 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:
1 2 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:
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]]
分析 方法:用翻转代替旋转 我们以题目中的示例
作为例子,先将其通过水平轴翻转得到:
再根据主对角线翻转得到:
就得到了答案。这是为什么呢?对于水平轴翻转而言,我们只需要枚举矩阵上半部分的元素,和下半部分的元素进行交换,即
对于主对角线翻转而言,我们只需要枚举对角线左侧的元素,和右侧的元素进行交换,即
代码 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) **螺旋矩阵 题目 给你一个 m
行 n
列的矩阵 matrix
,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
示例 1:
1 2 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,2,3,6,9,8,7,4,5]
示例 2:
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,进入下一层继续遍历,直到遍历完所有元素为止。
代码 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。
不过仔细分析上述过程可以发现,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],则可以归入第一种情况处理。
可以看到,比较 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] 中寻找。
需要注意的是,二分的写法有很多种,所以在判断 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:
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:
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; } }