目录
例题5:leetcode 80 数组元素重复的数组最多保留两个
例题8:在一个整数序列中寻找第K大的元素(经典) leetcode215
例题9:找到数组中两个元素和为target的索引 leetcode167
例题11:给一个字符串,返回字符串的倒序类似于反转一个数组 leetcode344
例题12:给一个字符串,反转字符串中的元音字母 leetcode345
例题14:返回满足条件长度最小的子数组 leetcode209
例题15:给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 leetcode3
例题16:找到字符串中所有字母异位词。 leetcode438
1.数组问题很常见
2.二分查找法
public class BinarySearch {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
int i = binarySearch(arr, 4);
}
//找到就返回target 没找到就返回-1
static int binarySearch(int[] arr, int target) {
if (arr == null || arr.length == 0) {
return -1;
}
int left = 0;
int right = arr.length;
int mid;
while (left < right) {
mid = (left + right) >> 1;
if (arr[mid] == target) {
return target;
}
if (arr[mid] > target) {
right = mid;
} else {
left = mid + 1;
}
}
return -1;
}
}
例题二: 移除数组中的0元素 leetcode 283
解题思路:
1.首先想到了冒泡排序,冒泡排序的特点是第一轮排序过后,最大的一个元素排到了正确的位置,第二轮排序就会把第二大的元素放到正确的位置,那么我们就可以n个数我们排序n趟,每一趟我们就把为零的数据上浮就可以了。但是这样的话这个算法的时间复杂度就是O(n^2),空间复杂度是O(1)
2. 第二个我想到的是开始遍历的时候我做一个标记,将非0的元素依次排好,这个时候标记点的下标就是非0元素的个数。然后我在遍历一遍数组,但是是从标记点开始遍历,标记点以后的数据全部置为0。这个算法的时间复杂度是O(n),但是遍历了两边数组。
3.可不可以只用遍历一次就能完成呢?当然可以,采用双指针的思路,后面元素覆盖前面的
// O(n^2)
//
static void moveZero(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1; j++) {
if (arr[j] == 0) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// O(n)
static void moveZero2(int[] arr) {
int flagNonZero = 0;
for (int i = 0; i <= arr.length - 1; i++) {
if (arr[i] != 0) {
arr[flagNonZero] = arr[i];
flagNonZero++;
}
}
for (int i = arr.length - 1; i >= flagNonZero; i--) {
arr[i] = 0;
}
}
例题3 删除数组中指定的元素:leetcode 27
解题思路:
遍历数组,将不等于该value的元素都移动到数组的前面排列,然后后面的都置为0
//时间复杂度O(n)
//空间复杂度O(1)
static void removeE(int[] arr, int e) {
int flagNoNe = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] != e) {
arr[flagNoNe] = arr[i];
flagNoNe++;
}
}
for (int i = arr.length - 1; i >= flagNoNe; i--) {
arr[i] = 0;
}
}
例题4 删除数组中重复的元素:leetcode 26
解题思路:
1.因为是有序数组,所以相同的元素都在连续的位置,所以开始遍历数组,起两个下标 分别指向第一个和第二个元素,判断他们是否相等,如果相等,第二个元素的位置置为特殊数,第二个下标++,如果不等,第一个下标++,第二个下标++。遍历一次过后就可以知道还剩多少个元素。
2.还是利用双指针的思想,因为第二个重复的元素可以覆盖,所以讲后面的元素覆盖前面的元素即可。
//时间复杂度 O(n)
//空间复杂度 O(1)
static void removeE2(int[] arr) {
int temp = arr[0];
for (int i = 0; i < arr.length - 1; i++) {
int temp2 = arr[i + 1];
if (temp == temp2) {
temp = temp2;
arr[i + 1] = 100000;
} else {
temp = temp2;
}
}
int flag1 = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] != 100000) {
arr[flag1] = arr[i];
flag1++;
}
}
for (int i = arr.length - 1; i >= flag1; i--) {
arr[i] = 0;
}
}
//时间复杂度O(n)
//空间复杂度O(1)
class Solution {
public int removeDuplicates(int[] arr) {
int i = 0;
int j = 1;
while (j < arr.length) {
if (arr[i] == arr[j]) {
j++;
} else {
arr[i + 1] = arr[j];
i++;
j++;
}
}
return i+1;
}
}
例题5:leetcode 80 数组元素重复的数组最多保留两个
1.解题思路:
上一道题是只能有一个重复 这一题是孩子能有两个重复 所以比较的也不再是相邻的两个元素,所以还是采用双指针的思想,比如i==0,j==2,i和j位置的对比,如果他们相等,则i+2的位置是可以替换的位置,i不动。j++.如果位置和j位置的元素不等,那么需要替换i和i+2位置的元素,同时两个指针往后移动一位。
class Solution {
public int removeDuplicates(int[] arr) {
if (arr.length <= 2) {
return arr.length;
}
int i = 0;
int j = 2;
while (j < arr.length) {
if (arr[i] == arr[j]) {
j++;
} else {
arr[i + 2] = arr[j];
i++;
j++;
}
}
return i+2;
}
}
基础算法思路拓展成为一些面试题
例题6 对数组元素进行分组:leetcode 75
解法1:可以使用一种排序算法对数组排序即可 及暴力解法
//时间复杂度 O(n^2)
//空间复杂度O(1)
static void test(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}
而这道题的特殊条件是元素的个数是在太少了 所以我们还可以使用计数排序的算法来优化
这个解法虽然在时间复杂度上优化了,但是也扫描了数组两遍,能不能值扫描一遍数组呢?答案是可以的。
//时间复杂度O(n)
//空间复杂度 O(1)
static void countSort(int[] arr) {
int zero = 0;
int one = 0;
int two = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] == 0) {
zero++;
}
if (arr[i] == 1) {
one++;
}
if (arr[i] == 2) {
two++;
}
}
for (int i = 0; i < arr.length; i++) {
if (i < zero) {
arr[i] = 0;
}
if (i >= zero && i < (zero + one)) {
arr[i] = 1;
}
if (i >= (zero + one)) {
arr[i] = 2;
}
}
}
只扫描一遍数组
、三路快速排序
i为在数组中遍历的下标 e为在数组中遍历的元素
当e为1的时候:直接纳入中间的这个区域
当e为2的时候,纳入2的这个区域
当e为0的时候纳入第一个区域:
循环从左边开始,当从左边交换过来的元素后 一定是1,从右边交换过来的元素可能为0
只有从右边交换过来的元素需要再次判断
// 最主要的就是找到临界条件
// 时间复杂度O(n)
//空间复杂度 O(1)
static void quickSort(int[] arr) {
int zero = -1; //属于0的区域就是[0,zero]=0
int two = arr.length; //属于2的区域就是 [two,arr.length-1]=2
//属于1的区域就是 (zero,two)
for (int i = 0; i < two; ) {
if (arr[i] == 1) {
i++;
} else if (arr[i] == 0) {
zero++;
int temp = arr[i];
arr[i] = arr[zero];
arr[zero] = temp;
i++;
} else {
two--;
int temp = arr[two];
arr[two] = arr[i];
arr[i] = temp;
}
}
}
int zero = 0;
int two = arr.length - 1;
for (int i = 0; i <= two; ) {
if (arr[i] == 1) {
i++;
} else if (arr[i] == 0) {
int temp = arr[zero];
arr[zero] = arr[i];
arr[i] = temp;
i++;
zero++;
} else {
int temp = arr[two];
arr[two] = arr[i];
arr[i] = temp;
two--;
}
}
例题7:有序合并两个数组:归并排序 leetcode88
利用归并排序的方法归并两个归并段。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int s1=0; //nums1数组的起始下标
int s2=0; //nums2数组的起始下标
int [] temp =new int[m+n];
int i=0; //临时数组的起始下标
//合并的话两个数组必须都有元素才行
while(s1<m && s2<n){
if(nums1[s1]>=nums2[s2]){
temp[i]=nums2[s2];
i++;
s2++;
}else{
temp[i]=nums1[s1];
s1++;
i++;
}
}
//nums1还有元素
while(s1<m){
temp[i]=nums1[s1];
s1++;
i++;
}
while(s2<n){
temp[i]=nums2[s2];
s2++;
i++;
}
//把临时数组放到nums1
for(int j=0;j<temp.length;j++){
nums1[j]=temp[j];
}
}
}
例题8:在一个整数序列中寻找第K大的元素(经典) leetcode215
利用快速排序的思想,我们知道快速排序是先找一个基准数,然后排一趟序,基准数会找到他的正确位置,并且被基准数分成的两个段一个大于一个小于,利用这个特性我们找到数组中第k大的元素应该在数组中排在第arr.length-2的位置,我们每次确定完一个基准数据后就可以知道目标数据在哪一个段,以及基准的下标是不是我们要的位置,直到返回我们要的位置为止。
package com.wx.sort;
/**
* Created by wangxiang on 2022/2/26
*/
public class FindKthLargest {
public static void main(String[] args) {
int[] data = {3, 2, 1, 5, 6, 4};
int k = 2;
int kthLargest = findKthLargest(data, k);
}
public static int findKthLargest(int[] nums, int k) {
int low = 0;
int high = nums.length - 1;
int pos = nums.length - k;
while (true) {
int index = partition(nums, low, high);
if (index == pos) {
return nums[index];
} else if (index < pos) {
low = index + 1;
} else {
high = index - 1;
}
}
}
private static int partition(int[] nums, int low, int high) {
int i = low;
int j = high;
int base = nums[low];
while (i != j) {
while (nums[j] >= base && i < j) {
j--;
}
while (nums[i] <= base && i < j) {
i++;
}
int temp;
temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
//排出第几个元素
nums[low] = nums[i];
nums[i] = base;
return i;
}
}
例题9:找到数组中两个元素和为target的索引 leetcode167
解析思路,
1.首先我想到的是暴力解法遍历两遍数组,找到可以返回,但是提交的结果却是超出时间限制,说明这个算法的时间复杂度不满足要求,因为没有利用起来这是个有序数组的条件。
解题思路2:
当我们看到一个数组是有序的情况下,首先反应出来就是应该使用二分搜索来检索。这里就注意多加一个条件罢了,当找到目标值的时候index1不等于index2 ,时间复杂度为O(nlogn) ,空间复杂度O(1)
public static int[] twoSum2(int[] numbers, int target) {
for (int i = 0; i < numbers.length; i++) {
int index1 = i;
int left = 0;
int right = numbers.length - 1;
while (left <= right) {
int index2 = getIndex2(left, right);
if (numbers[index1] + numbers[index2] == target && index1 != index2) {
return new int[]{index1 + 1, index2 + 1};
} else if (numbers[index1] + numbers[index2] > target) {
right = index2 - 1;
} else {
left = index2 + 1;
}
}
}
return null;
}
private static int getIndex2(int left, int right) {
return (left + right) >>> 1;
}
解题思路3:
存不存在一个O(n)级别的解题算法呢?答案是存在的。这个解题思路就是利用双指针,对撞指针的方式来解决这个问题 ,从两头开始扫描数组,直到满足条件返回即可,因为题目要求返回一组解即可。
public static int[] twoSum3(int[] numbers, int target) {
int index1 = 0;
int index2 = numbers.length - 1;
while (index1 < index2) {
if (numbers[index1] + numbers[index2] == target) {
break;
} else if (numbers[index1] + numbers[index2] < target) {
index1++;
} else {
index2--;
}
}
return new int[]{index1 + 1, index2 + 1};
}
下面再介绍几个问题是使用这种对撞指针的思路来解题的。
例题10:验证回文串 leetcode125
解题思路:双指针碰撞
需要注意的是字符串转换为字符数组以后,有一个判断他是不是字母或者数字的方法
Character.isLetterOrDigit(chars[i])
public static boolean isPalindrome(String s) {
s = s.toLowerCase();
char[] chars = s.toCharArray();
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < chars.length; i++) {
if (Character.isLetterOrDigit(chars[i])) {
stringBuilder.append(Character.toLowerCase(chars[i]));
}
}
s = stringBuilder.toString();
chars = s.toCharArray();
int i = 0;
int j = chars.length - 1;
while (i < j) {
if (chars[i] != chars[j]) {
return false;
} else {
i++;
j--;
}
}
return true;
}
例题11:给一个字符串,返回字符串的倒序类似于反转一个数组 leetcode344
解题思路,由于限制空间复杂度为O(1) ,所以采用双指针碰撞的思路解题。
public static void reverseString(char[] s) {
int i = 0;
int j = s.length - 1;
while (i < j) {
char c = s[i];
s[i] = s[j];
s[j] = c;
i++;
j--;
}
}
例题12:给一个字符串,反转字符串中的元音字母 leetcode345
解题思路双指针碰撞,两边开始检索,遇到元音字母则停下来交换。
public static String reverseVowels(String s) {
char[] chars = s.toCharArray();
int i = 0;
int j = chars.length - 1;
while (i < j) {
while (i < j && !isVowel(chars[i])) {
i++;
}
while (i < j && !isVowel(chars[j])) {
j--;
}
//交换元音字母
char c = chars[i];
chars[i] = chars[j];
chars[j] = c;
i++;
j--;
}
return String.valueOf(chars);
}
private static boolean isVowel(char aChar) {
return aChar == 'a' || aChar == 'e' || aChar == 'i' || aChar == 'o' || aChar == 'u' ||
aChar == 'A' || aChar == 'E' || aChar == 'I' || aChar == 'O' || aChar == 'U' ? true : false;
}
例题13:容器可以容纳最多的水 leetcode11
解题思路1:
暴力解法,循环两遍,i-j=lang arr[i]-arr[j]=high 然后就是找到lang*high的最大值,但是在leetcode上无法获得通过,因为算法的时间复杂度太大了,这是一个O(n^2)级别的算法,leetcode上运行超出了时间限制。
class Solution {
public int maxArea(int[] height) {
int max = 0;
for (int i = 0; i < height.length; i++) {
for (int j = 0; j < height.length; j++) {
int high = height[i] > height[j] ? height[j] : height[i];
int lang = i > j ? i - j : j - i;
if (lang * high > max) {
max = (i - j) * high;
}
}
}
return max;
}
}
解题思路2:
可以通过双指针碰撞来解决这个问题,i=0;j=arr.length-1,
high = height[i] > height[j] ? height[j] : height[i]; lang = j - i;
盛水面积就为high * lang,接下来就是如何移动双指针的问题,这也是这个题的难点所在,一开始我想的是判断双指针的下一个指针,下一个指针谁小就就不动,移动另外一个,这种想法忽略了当前值与下一个值的对比,所以漏了解.而如果我每次只移动当前最小的值,那么就不会漏。
会找到最大的一个解 这个解法的时间复杂度就是O(n)空间复杂度就是O(1)
class Solution {
public int maxArea(int[] height) {
int max = 0;
int i = 0;
int j = height.length - 1;
while (i < j) {
int high = height[i] > height[j] ? height[j] : height[i];
int lang = j - i;
if (high * lang > max) {
max = high * lang;
}
if (height[i] > height[j]) {
j--;
} else {
i++;
}
}
return max;
}
}
双指针碰撞其实就是双索引的问题,双索引在数组中滑动就会形成一个滑动窗口。
例题14:返回满足条件长度最小的子数组 leetcode209
解题思路1暴力解法:
解题思路2:双指针形成的滑动窗口:首先有这样一个子数组,当他的和不到target的时候,j指针就往右一定一位,直到满足>=target的条件,记录下这个子数组的长度,然后i++,是不是还满足target,如果还满足记录值继续移动。直到不满足再往右移动。
时间复杂度O(n) 空间复杂度O(1)
public static int minSubArrayLen(int target, int[] nums) {
int i = 0;
int j = 0;
int re = nums.length + 1;
int sum = nums[0];
while (i <= j && j <= nums.length - 1) {
if (sum >= target) {
int temp = j - i + 1;
if (temp < re) {
re = temp;
}
sum = sum - nums[i];
i++;
} else {
j++;
if (j <= nums.length - 1) {
sum = sum + nums[j];
}
}
}
return re == (nums.length + 1) ? 0 : re;
}
例题15:给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。 leetcode3
解题思路:这是一个典型的可以使用滑动窗口的问题
当子串没有重复的字母时:
当子串中有了重复的字母时:
还有一个问题就是 如何判断下一个字母在子串中是存在的?肯定不能每次都去扫一遍吧,可以使用一个freq来记录重复字符的频率,这样判断是否有是否重复就是O(1)的算法。
class Solution {
public int lengthOfLongestSubstring(String s) {
char[] chars = s.toCharArray();
int[] freq = new int[256];
int i = 0;
int j = -1;
int re = 0;
//双指针形成的滑动窗口开始右移
while (j < chars.length && i<chars.length) {
//滑动窗口的左边往右移动的条件,是左边元素的下一个元素的出现频率为0
if (j + 1 < chars.length && freq[chars[j + 1]] == 0) {
freq[chars[j + 1]]++;
j++;
} else {
//当滑动窗口右边不滑动左边开始往右滑动的时候
freq[chars[i]]--;
i++;
}
re = Math.max((j - i + 1), re);
}
return re;
}
}
继续做滑动窗口相关的题目
例题16:找到字符串中所有字母异位词。 leetcode438
解题思路,我们继续考虑使用双指针加滑动窗口的思路来解题,根据题目要求,我们需要在字符串 ss 寻找字符串 pp 的异位词。因为字符串 pp 的异位词的长度一定与字符串 pp 的长度相同,所以我们可以在字符串 ss 中构造一个长度为与字符串 pp 的长度相同的滑动窗口,所以这次的滑动窗口的长度不是动态的。一开始我们就将滑动窗口赋值,滑动窗口的值就是字母出现的次数,如果滑动窗口的数组等于子串对应数组的值,那么就是解
public static List<Integer> findAnagrams(String s, String p) {
//先将p字符串中字母出现的频率放到一个数组中 同时初始化滑动窗口对应字符串的频率数组
int[] pArr = new int[256];
int[] sArr = new int[256];
char[] chars = p.toCharArray();
for (int i = 0; i < chars.length; i++) {
pArr[chars[i]]++;
sArr[s.charAt(i)]++;
}
List<Integer> re = new ArrayList<>();
//滑动窗口的大小 固定不变,滑动窗口围成的这个数组等于s的时候即是解
int i = 0;
int j = chars.length - 1;
while (j < s.length()) {
//滑动窗口围成的数组的字母频率相等
if (Arrays.equals(sArr, pArr)) {
re.add(i);
}
//右移滑动窗口
j++;
if (j < s.length()) {
sArr[s.charAt(j)]++;
}
if (sArr[s.charAt(i)] != 0) {
sArr[s.charAt(i)]--;
}
i++;
}
return re;
}
继续滑动窗口的问题
例题17 最小覆盖子串 leetcode76
解题思路 :
这个明显还是需要用滑动窗口的思想来解决,这个题目的重点是如何确定滑动窗口围成的字符串就包含了所有t中的元素,我们在 s上滑动窗口,通过移动 右 指针不断扩张窗口。当窗口包含 t 全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。此时这个最小窗口可能不是我们需要的答案即最小的子串,我们记录下这个位置后继续操作后面的元素,等遍历完成后,返回最小的子串记录
这里判断两个子串是否包含就看临时数组中字母出现的频率是不是一样,临时数组是不是相等