1 数组相关
目录
1 二分查找法
2 典型例题
LeetCode-283-移动零
LeetCode-26-删除排序数组中的重复项
LeetCode-27-移除元素
LeetCode-80--删除排序数组中的重复项||
3 基础算法思路的应用
LeetCode-75-颜色分类
LeetCode-88-合并两个有序数组
LeetCode-215-数组中的第K个最大元素
4 双索引技术-对撞指针
LeetCode-167-两数之和 II - 输入有序数组
LeetCode-125-验证回文串
LeetCode-344-反转字符串
LeetCode-345-反转字符串中的元音字母
LeetCode-11-盛最多水的容器
5 双索引技术-滑动窗口
LeetCode-209-长度最小的子数组
LeetCode-3-无重复字符的最长子串
LeetCode-438-找到字符串中所有字母异位词
LeetCode-76-最小覆盖子串
1 二分查找法
这里可以参考博客:https://www.cnblogs.com/youngao/p/12418599.html中的二分查找分析
2 典型例题
以LeetCode 283为例,题目中要求不适用额外空间,就有了一个提示即原地修改,常用的方法就是双指针,利用双指针根据不同的情况进行移动。
class Solution { public void moveZeroes(int[] nums) { if (nums.length < 2){ return; } int i=0,j=1; while (j != nums.length){ if (nums[i] == 0 && nums[j] ==0){ j++; } if (j == nums.length) break; if (nums[i] == 0 && nums[j] !=0){ swap(nums,i,j); i++; j++; } if (j == nums.length) break; /* if (nums[i] != 0 && nums[j] !=0){ i++; j++; } if (j == nums.length) break; if (nums[i] != 0 && nums[j] ==0){ i++; j++; } */ //利用这段代码来代替上面的一段代码 if (nums[i] != 0){ i++; j++; } } } private void swap(int[] arr,int m,int n){ int temp = arr[m]; arr[m] = arr[n]; arr[n] = temp; } }
上面的是自己写的代码,虽然运行成功但是整个代码显得特臃肿,复杂,下面是视频中给出的代码:
class Solution { public void moveZeroes(int[] nums) { int k =0; for (int i =0; i<nums.length; i++){ if (nums[i] !=0){ if (i != k){ swap(nums,i,k); } k++; } } } private void swap(int[] arr ,int i,int j){ int tmep = arr[i]; arr[i] = arr[j]; arr[j] = tmep; } }
一个算法实现的功能越多就越复杂,在这里是把0移动,每次都交换的确是正确的移动,但是完全可以最后填充而不是交换着移动,这样整个算法实现的功能就又少了一点。图示如下:
class Solution { public void moveZeroes(int[] nums) { // nums[0...k)中均未非0元素 int k =0; //遍历到第i个元素后,保证[0...i]中所有非0元素都按顺序排列在[0...k]中 for (int i =0; i<nums.length; i++){ if (nums[i] !=0){ nums[k] = nums[i]; k++; } } for (int i = k; i < nums.length; i++) { nums[i] =0; } } }
类似题目有26、27、80。题目26是用了双指针交换;题目27其实就是用了上面的不交换直接覆盖的方法;题目80则相对要难一点,但有一点要注意题目中已经给出了有序数组则是降低了难度,差别在于要求是可以连续有两个因此判断的条件也要进行修改。
3 基础算法思路的应用
以LeetCode75题为例,先不考虑题目中不适用库函数的要求,那么解决这个问题最直观的方法就是利用Java中的排序算法解决,其复杂度为O(nlogn)级别的。考虑到题目中的特殊性只有三种数字,那么完全可以第一次遍历数组记录0、1、2出现的次数,然后直接填写就行了,这也是计数排序的思路,其时间复杂度为O(n),计数排序就适合于数字种类非常少的排序。
//时间复杂度为O(n),空间复杂度为O(k),k为数字的种类,这里因为k是已知的,所以空间复杂度为O(1) public void sortColors(int[] nums) { //三种数字 int[] count = new int[]{0,0,0}; for (int i = 0; i < nums.length; i++) { count[nums[i]]++; } //三个for循环相当于遍历一遍数组 for (int i = 0; i < count[0]; i++) { nums[i] =0; } for (int i = 0; i < count[1]; i++) { nums[i] =1; } for (int i = 0; i < count[2]; i++) { nums[i] =2; } }
下面是自己写的代码,在体现空间复杂度上没有那么直观了
public void sortColors(int[] nums) { int m,n, for (int i = 0; i < nums.length; i++) { if (nums[i] == 0) { m++; }else if (nums[i]==1){ n++; } } for (int i = 0; i < nums.length; i++) { if (i < m){ nums[i] = 0; }else if (i<m+n){ nums[i] = 1; }else { nums[i] = 2; } } }
虽然上面代码的时间复杂度为O(n)但是还没有达到最优解,因为要遍历数组两遍,那么有没有可能只遍历一遍就解决问题,那么就是用快速排序中的三路排序原理。
i指针作为移动指针,根据遍历的数据来选择具体分配到哪一类中,最简单是i位置的元素为1,此时只需要i++即可,如下图所示:
当遍历到的元素为2时,则把2类区域前面的元素与遍历到的2互换位置,同时让two指针减减,这样相当于2的区域增加了
移动完2后再来看元素e,若元素为0,那么就要找到zero+1位置的元素即1,那么便交换位置,同时把zero++即可
交换完后移动i,便进行下一轮
最终便会得到如下结果:
代码如下所示:
class Solution { public void sortColors(int[] nums) { //因为设定的闭区间,所以一开始把初始值设置为无效状态 int zero = -1; //nums[0...zero] == 0 int two = nums.length; //nums[two...n-1] == 2 for (int i = 0; i < two; ) { if (nums[i] == 1){ i++; }else if (nums[i] == 2){ two--; swap(nums,i,two); }else { //nums[i] == 0 zero++; swap(nums,zero,i); i++; } } } private static void swap(int[] nums,int i,int j){ int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } }
上述算法借用三路快排思路,其他的有LeetCode 88做归并排序中归并的那一步,215普通思路是利用堆排序,时间复杂度为O(nlogn)也可以借鉴快排的中将标定点放在正确位置上的性质,使时间复杂度降为O(n),如下图所示:
若寻找第2大的元素,只需要在大于4的区域中寻找即可。
4 双索引技术---对撞指针
以LeetCode 167为例,题目中已经给出了明确的提示,有序数组,那么对于有序数组而言最常用的就是二分查找了。对于这道题最直观的思路就是两层遍历一个个试那么时间复杂度为O(n^2)级别的,这种暴力解决在没有思路的时候可以使用;那么根据有序数组便可以利用二分查找求解了,其图示如下:
代码如下:
class Solution { public int[] twoSum(int[] numbers, int target) { for (int i = 0; i < numbers.length; i++) { int temp = target - numbers[i]; int index = binarySearch(numbers,i,temp); if (index != -1){ return new int[]{i+1,index+1}; } } return null; } private int binarySearch(int[] arr,int start,int target){ int left = start, r = arr.length-1; while (left <= r){ int mid = left + (r -left)/2; if (arr[mid] == target){ return mid; } if (target > arr[mid]){ left = mid; }else { r = mid; } } return -1; } }
上面的代码应该是正确的但是提交后显示超时,因此需要进行优化。如何想要只遍历一遍,再利用数组本身是有序的可以考虑利用双指针,分别从两端进行遍历,不过问题的关键在于当nuns[i]+nums[j]与target有不同的关系时如何移动i与j,因为数组是递增的,当大于target时应该让j--,当小于target时应该让i++,根据这些思路可以写出代码如下:
class Solution { public int[] twoSum(int[] numbers, int target) { int i=0,j=numbers.length - 1; while (i<j){ int m = numbers[i] + numbers[j]; if (m == target){ return new int[] {i+1,j+1}; }else if (m < target){ i++; }else { //target > m j--; } } return null; } }
上面的思路根据两个指针对向遍历就是常说的对撞指针。三路快排的算法从某种程度上也可以理解为对撞指针。与此类似的题目有LeetCode 125、344、345、11
5 双索引技术---滑动窗口
以LeetCode 209为例,在这道题目中首先要了解什么是子数组。在一般的计算机定义中,子数组一般是不要求连续的,但在这道题目中已经要求了连续子数组。在这道题目中同样也是采用双指针来解决,不过不再是对撞指针了,而是进行滑动窗口,其图示如下:
在一开始有一个连续子数组,当这个数组和小于s时便考察下一个元素j++
如果继续小于s便一直进行上述的操作,直至某一个时刻小于连续数组和大于s
那么此时便从i这一端开始进行缩小连续子数组,那么一直缩小的某一个时刻连续数组之和小于s,此时继续增加j,直至找到符合条件的连续数组。
//时间复杂度为O(n),空间复杂度为O(1)级别的 class Solution { public int minSubArrayLen(int s, int[] nums) { // nums[left...r]为滑动窗口 //一开始定义区间是无效的状态 int left = 0,r = -1; // 连续数组之和 int sum =0; // 连续数组的长度 int res = nums.length + 1; while (left < nums.length){ //要保证右侧不会越界,所以要对r的值进行限定 if (r+1 < nums.length &&sum < s){ r++; sum += nums[r]; }else { sum -= nums[left]; left --; } //因为要求长度最小,所以要对长度进行更新并取最小值 if (sum >= s){ res = Math.min(res,r-left+1); } } // 当遍历完也找不到满足要求的数组时返回0 if (res == nums.length + 1){ return 0; } return res; } }
另一道类似的题目为LeetCode 3,在这道题目中同样是利用滑动窗口对数组进行遍历,只不过比较的是字符吗,有一点在题目中没有说明,该问题对大小写是敏感的,这一点要注意。在这里主要考察当我们的字符在某一刻遍历时遇到了重复字符的情况,如下图所示:
[i..j]内是没有重复字符的,但遍历到j的下一个时发下该字符与没有重复字符区域内的某一个字符重复了,那么此时就应该记录此时的字符长度,并进行下一次移动。
在这次移动中要先让i移动到该重复字符的下一个位置,然后让j++移动到重复字符处
在理清了上述思路后还有一个问题待解决,如何记录重复字符?每次都去扫描i到j位置的元素这样的遍历肯定是不合适的,因此选择用一个数组来记录字符出现的频率freq[256],那么k处便记录的是ASCII码为k的字符对应的频率。对应代码如下:
class Solution { public int lengthOfLongestSubstring(String s) { // 初始频率为0 int[] freq = new int[256]; Arrays.fill(freq,0); // 滑动窗口为s[left...r] int left = 0,r = -1; int res = 0; while (left < s.length()){ // 保证右边不越界 if (r+1<s.length() && freq[s.charAt(r+1)] == 0){ r++; freq[s.charAt(r)]++; }else { freq[s.charAt(left)] --; left++; } res = Math.max(res,r-left+1); } return res; } }
与此类似的题目有:LeetCode 438、76
0