一、二分查找
1、 二分查找模板
建议mid = left + (right - left) >> 1
其中「二分」模板其实有三套,常用的是2,3两种
l < r
1.当check(mid) == true
调整的是 left = mid
时:计算 mid 的方式应该为:mid = left + right + 1>> 1
,当最终剩余两个数字时,mid=right,调整left= mid,可以缩小范围
例如:
long l = 0, r = 1000009;
while (l < r) {
long mid = l + r + 1 >> 1;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
2.当check(mid) == true
调整的是right = mid
时:计算 mid 的方式应该为:mid = left + right >> 1
,最终只剩两个数字时,mid = left,调整right = mid,可以缩小范围
对应carl左闭右开情况 程序员carl
例如:
long l = 0, r = 1000009;
while (l < r) {
long mid = l + r >> 1;
if (check(mid)) {
r = mid;
} else {
l = mid + 1;
}
}
l <= r
3.一般情况,对应carl左闭右闭情况
while(left <= right){
int mid = (left + right) >> 1;
if(nums[mid] == target){
return mid;
}else if(nums[mid] < target){
left = mid + 1;
}else if(nums[mid] > target){
right = mid - 1;
}
}
经验总结
1.根据循环退出条件(left<right或left<=right),判断最后一次比较时left和right指针的指向,从而决定返回值(返回left,right,left-1,left+1,right-1,right+1)
以left<= right 为例,分析最后一次比较情况。如果数组中存在target值,则最后一次left和right指针同时指向target值所在的位置;如果数组中没有target值,则最后一次left和right指针同时指向小于或大于target的第一个位置
2.搜索哪个范围,则使左右指针分别是此范围的左右边界。
例如:搜索范围是[l,h],则使left = l,right = h,保证搜索不会漏掉
3.第一是尝试熟练使用一种写法,比如左闭右开或左闭右闭(便于处理边界条件),尽量只保持这一种写法;第二是在刷题时思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。实例看4
2、二分查找递归和非递归
非递归
public static int binarySearch(int[] arr,int target){
int left = 0,right = arr.length - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if (arr[mid] == target){
return mid;
}else if (arr[mid] < target){
left = mid + 1;
}else{
right = mid - 1;
}
}
return -1;
}
递归
//递归
public static int binarySearch(int left,int right,int[] arr,int target){
if (left> right){
return -1;
}
int mid = left + (right - left) / 2;
if (arr[mid] == target){
return mid;
}else if (arr[mid] < target){
return binarySearch(mid + 1,right,arr,target);
}else{
return binarySearch(left,mid - 1,arr,target);
}
}
3、二分查找(力扣704)
最简单的二分查找
public static int searchInsert(int[] nums, int target) {
int len = nums.length;
int left = 0,right = len - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] == target){
return mid;
}else if (nums[mid] < target){
left = mid + 1;
}else if (nums[mid] > target){
right = mid - 1;
}
}
return -1;
}
4、搜索插入位置(力扣35)
//1.左闭右闭
public static int searchInsert(int[] nums, int target) {
int left = 0,right = nums.length - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] == target){
return mid;
}else if (nums[mid] < target){
left = mid + 1;
}else if (nums[mid] > target){
right = mid - 1;
}
}
//return right + 1 也是对的
return left;
}
//2.左闭右开
public static int searchInsert(int[] nums, int target) {
int left = 0,right = nums.length;
while (left < right){
int mid = left + (right - left) / 2;
if (nums[mid] == target){
return mid;
}else if (nums[mid] < target){
left = mid + 1;
}else if (nums[mid] > target){
right = mid;
}
}
//return left 也可以
return right;
}
解析:以[1,3,5,6],target = 5 和 [1,3,5,6],target = 2 为例
1.左闭右闭
最终left==right,范围内只剩一个元素。如果数组中存在target,直接返回target的位置;如果数组中没有target:如果left=right=第一个小于target的位置,left = mid + 1=插入的位置,right=第一个小于target的位置,所以最终返回left或是right+1都可以;如果left=right=第一个大于target的位置,right = mid - 1,实际插入元素的位置为right + 1或是left
2.左闭右开
最终left<right,范围内剩下两个元素,mid=left,如果数组中存在target,直接返回target的位置;如果数组中没有target:则left位置<target,left = mid + 1,此时left=rght,因此返回left和right都可以
5、在排序数组中查找元素的第一个和最后一个位置(力扣34)
- 两次二分
class Solution {
public int[] searchRange(int[] nums, int target) {
if(nums == null || nums.length == 0){
return new int[]{-1, -1};
}
int leftIndex = searchLeft(nums, target);
int rightIndex = searchRight(nums, target);
if (rightIndex >= leftIndex){
return new int[]{leftIndex, rightIndex};
}
return new int[]{-1, -1};
}
public int searchLeft(int[] nums, int target){
int left = 0, right = nums.length - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] < target){
left = mid + 1;
/* 找左边界,等于的时候向左移动 */
} else if (nums[mid] >= target){
right = mid - 1;
}
}
return left;
}
public int searchRight(int[] nums, int target){
int left = 0, right = nums.length - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] > target){
right = mid - 1;
/* 找右边界,等于的时候向右移动 */
} else if (nums[mid] <= target){
left = mid + 1;
}
}
return right;
}
}
- 一次二分
class Solution {
public int[] searchRange(int[] nums, int target) {
if(nums == null || nums.length == 0){
return new int[]{-1, -1};
}
int leftIndex = searchBorder(nums, target, true);
int rightIndex = searchBorder(nums, target, false) - 1;
return leftIndex <= rightIndex ? new int[]{leftIndex, rightIndex} : new int[]{-1, -1};
}
/*
* 1.寻找左边界,就是寻找第一个大于等于目标值的下标
* 2.寻找右边界,就是寻找第一个大于目标值的下标 - 1
* */
public int searchBorder(int[] nums, int target, boolean findLeft){
int left = 0, right = nums.length - 1, ans = nums.length;
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] > target || (findLeft && nums[mid] >= target)){
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
return ans;
}
}
6、x 的平方根(力扣69)
class Solution {
public int mySqrt(int x) {
int left = 0, right = x;
while (left <= right){
int mid = left + (right - left) / 2;
if ((long)mid * mid <= x){
left = mid + 1;
} else {
right = mid - 1;
}
}
return left - 1;
}
}
7、有效的完全平方数(力扣367)
class Solution {
public boolean isPerfectSquare(int num) {
int left = 1, right = num;
while (left <= right){
int mid = left + (right - left) / 2;
if ((long)mid * mid <= num){
left = mid + 1;
} else {
right = mid - 1;
}
}
return (left - 1) * (left - 1) == num;
}
}
二、移除元素
1、移除元素(力扣27)
class Solution {
// 快慢指针
public int removeElement(int[] nums, int val) {
int leftIndex = 0, rightIndex = 0;
while (rightIndex < nums.length){
if (nums[rightIndex] != val){
nums[leftIndex ++] = nums[rightIndex];
}
rightIndex ++;
}
return leftIndex;
}
}
2、删除有序数组中的重复项(力扣26)
class Solution {
// 快慢指针
public int removeDuplicates(int[] nums) {
int slowIndex = 0, fastIndex = 0;
while (fastIndex < nums.length){
if (nums[slowIndex] != nums[fastIndex]){
nums[++ slowIndex] = nums[fastIndex];
}
fastIndex ++;
}
return slowIndex + 1;
}
}
3、移动零(力扣283)
class Solution {
public void moveZeroes(int[] nums) {
int startIndex = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0){
nums[startIndex ++] = nums[i];
}
}
for (int i = startIndex; i < nums.length; i++) {
nums[i] = 0;
}
}
}
4、比较含退格的字符串(力扣844)
class Solution {
public boolean backspaceCompare(String s, String t) {
return delBackSpace(s).equals(delBackSpace(t));
}
/* 利用栈的思想,不是#就入栈,是#就出栈 */
public String delBackSpace(String s){
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) != '#'){
sb.append(s.charAt(i));
} else {
if (sb.length() != 0){
sb.deleteCharAt(sb.length() - 1);
}
}
}
return sb.toString();
}
}
class Solution {
/* 两个字符串从后往前遍历,比较字符是否相等或记录#的数量
* 返回false有两种情况:
* 1. 两个字符不相等
* 2. 一个字符串已经遍历完,另一个还没遍历完 */
public boolean backspaceCompare(String s, String t) {
int sIndex = s.length() - 1, tIndex = t.length() - 1;
int sBackSpaceNum = 0, tBackSpaceNum = 0;
while (sIndex >= 0 || tIndex >= 0){
while (sIndex >= 0){
/* 记录回退的个数 */
if (s.charAt(sIndex) == '#'){
sBackSpaceNum ++;
sIndex --;
/* 执行回退 */
} else if (sBackSpaceNum > 0){
sBackSpaceNum --;
sIndex --;
} else {
break;
}
}
while (tIndex >= 0){
if (t.charAt(tIndex) == '#'){
tBackSpaceNum ++;
tIndex --;
} else if (tBackSpaceNum > 0){
tBackSpaceNum --;
tIndex --;
} else {
break;
}
}
if (sIndex >=0 && tIndex >= 0){
/* 两个字符不相等 */
if (s.charAt(sIndex) != t.charAt(tIndex)){
return false;
}
/* 一个遍历完,另一个还未遍历完 */
} else if (sIndex >= 0 || tIndex >= 0){
return false;
}
sIndex --;
tIndex --;
}
return true;
}
}
5、有序数组的平方(力扣977)
class Solution {
/* 平方最大值一定在nums的两端,从后向前遍历依次找最大值 */
public int[] sortedSquares(int[] nums) {
int length = nums.length;
int[] resArray = new int[length];
int left = 0, right = length - 1, index = length - 1;
while (left <= right){
if (nums[left] * nums[left] <= nums[right] * nums[right]){
resArray[index --] = nums[right] * nums[right];
right --;
} else {
resArray[index --] = nums[left] * nums[left];
left ++;
}
}
return resArray;
}
}
三、长度最小的子数组
这里主要涉及滑动窗口的思想
1、长度最小的子数组(力扣209)
- 滑动窗口
class Solution {
public static int minSubArrayLen(int target, int[] nums) {
int len = nums.length;
int curSum = 0, resLen = Integer.MAX_VALUE, leftIndex = 0, rightIndex = 0;
while (rightIndex < len){
curSum += nums[rightIndex];
while (curSum >= target){
resLen = Math.min(resLen, rightIndex - leftIndex + 1);
curSum -= nums[leftIndex ++];
}
rightIndex ++;
}
return resLen == Integer.MAX_VALUE ? 0 : resLen;
}
}
不要以为for里放一个while就是O(n^2), 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
- 前缀和+二分法
class Solution {
public int minSubArrayLen(int target, int[] nums) {
/* 1.构造前缀和数组 */
int len = nums.length;
int[] prefixSumArray = new int[len + 1];
for (int i = 1; i < len + 1; i++) {
prefixSumArray[i] = prefixSumArray[i - 1] + nums[i - 1];
}
int resLen = Integer.MAX_VALUE;
/* 2.二分查找 */
for (int i = 1; i <= len; i++) {
int targetNum = prefixSumArray[i - 1] + target;
int index = binarySearch(prefixSumArray, targetNum);
if (index <= len){
resLen = Math.min(resLen, index - i + 1);
}
}
return resLen == Integer.MAX_VALUE ? 0 : resLen;
}
/* 找到数组中大于或等于目标值的第一个位置 */
public int binarySearch(int[] nums, int target){
int left = 0, right = nums.length - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if (nums[mid] < target){
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
}
2、水果成篮(力扣904)
class Solution {
public int totalFruit(int[] fruits) {
int len = fruits.length;
int start = 0, end = 0, maxLength = 0, startNum = fruits[start], endNum = fruits[end];
while (end < len) {
//如果新加入的元素已经包含,末位置向后移动,更新长度
if (fruits[end] == startNum || fruits[end] == endNum){
maxLength = Math.max(maxLength,end - start + 1);
end ++;
//如果新加入的元素没有包含,更新始末位置和长度
}else{
//更新开始位置
//当前end位置的值是一个新值,直接更新开始位置是end的前一个值,此时包含两个值
start = end - 1;
//更新开始值
startNum = fruits[start];
//这一步很重要,看开始位置的前面有没有和开始位置相同的值
while (start >= 1 && fruits[start - 1] == startNum){
start --;
}
//更新结束值
endNum = fruits[end];
maxLength = Math.max(maxLength,end - start + 1);
}
}
return maxLength;
}
}
3、最小覆盖子串(力扣76)
/1.滑动窗口
// 完全看的解释(https://leetcode-cn.com/problems/minimum-window-substring/solution/tong-su-qie-xiang-xi-de-miao-shu-hua-dong-chuang-k/)
public static String minWindow(String s, String t) {
if (s.length() < t.length()) {
return "";
}
//维护两个数组,记录已有字符串指定字符的出现次数,和目标字符串指定字符的出现次数
//ASCII表总长128
int[] need = new int[128];
int[] have = new int[128];
//将目标字符串指定字符的出现次数记录
for (int i = 0; i < t.length(); i++) {
need[t.charAt(i)]++;
}
//分别为左指针,右指针,最小长度(初始值为一定不可达到的长度)
//已有字符串中目标字符串指定字符的出现总频次以及最小覆盖子串在原字符串中的起始位置
int left = 0, right = 0, min = s.length() + 1, count = 0, start = 0;
while (right < s.length()) {
char r = s.charAt(right);
//说明该字符不被目标字符串需要,此时有两种情况
// 1.循环刚开始,那么直接移动右指针即可,不需要做多余判断
// 2.循环已经开始一段时间,此处又有两种情况
// 2.1 上一次条件不满足,已有字符串指定字符出现次数不满足目标字符串指定字符出现次数,那么此时
// 如果该字符还不被目标字符串需要,就不需要进行多余判断,右指针移动即可
// 2.2 左指针已经移动完毕,那么此时就相当于循环刚开始,同理直接移动右指针
if (need[r] == 0) {
right++;
continue;
}
//当且仅当已有字符串目标字符出现的次数小于目标字符串字符的出现次数时,count才会+1
//是为了后续能直接判断已有字符串是否已经包含了目标字符串的所有字符,不需要挨个比对字符出现的次数
if (have[r] < need[r]) {
count++;
}
//已有字符串中目标字符出现的次数+1
have[r]++;
//移动右指针
right++;
//当且仅当已有字符串已经包含了所有目标字符串的字符,且出现频次一定大于或等于指定频次
while (count == t.length()) {
//挡窗口的长度比已有的最短值小时,更改最小值,并记录起始位置
if (right - left < min) {
min = right - left;
start = left;
}
char l = s.charAt(left);
//如果左边即将要去掉的字符不被目标字符串需要,那么不需要多余判断,直接可以移动左指针
if (need[l] == 0) {
left++;
continue;
}
//如果左边即将要去掉的字符被目标字符串需要,且出现的频次正好等于指定频次,那么如果去掉了这个字符,
//就不满足覆盖子串的条件,此时要破坏循环条件跳出循环,即控制目标字符串指定字符的出现总频次(count)-1
if (have[l] == need[l]) {
count--;
}
//已有字符串中目标字符出现的次数-1
have[l]--;
//移动左指针
left++;
}
}
//如果最小长度还为初始值,说明没有符合条件的子串
if (min == s.length() + 1) {
return "";
}
//返回的为以记录的起始位置为起点,记录的最短长度为距离的指定字符串中截取的子串
return s.substring(start, start + min);
}
四、螺旋矩阵I
在这里要注意区分所给矩阵是n * n 还是 m * n (m != n),后者需要加额外的条件判断
1、螺旋矩阵(力扣54)
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> resList = new ArrayList<>();
if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
return resList;
}
// 遍历一圈的四个边界值
int left = 0, up = 0, right = matrix[0].length - 1, down = matrix.length - 1;
// 每次进入while循环遍历一圈,所以左右和上下边界的关系都需要判断
while (left <= right && up <= down) {
for (int j = left; j <= right && up <= down; j++) {
resList.add(matrix[up][j]);
}
up++;
for (int i = up; i <= down && left <= right; i++) {
resList.add(matrix[i][right]);
}
right--;
for (int j = right; j >= left && up <= down; j--) {
resList.add(matrix[down][j]);
}
down --;
for (int i = down; i >= up && left <= right; i--) {
resList.add(matrix[i][left]);
}
left ++;
}
return resList;
}
}
2、螺旋矩阵 II(力扣59)
class Solution {
/* 分别定义四个边界 */
public int[][] generateMatrix(int n) {
int[][] resArray = new int[n][n];
int left = 0, right = n - 1, up = 0, down = n - 1, index = 1;
while (left <= right && up <= down){
for (int j = left; j <= right; j++) {
resArray[up][j] = index ++;
}
up ++;
for (int i = up; i <= down; i++) {
resArray[i][right] = index ++;
}
right --;
for (int j = right; j >= left; j--) {
resArray[down][j] = index ++;
}
down --;
for (int i = down; i >= up; i--) {
resArray[i][left] = index ++;
}
left ++;
}
return resArray;
}
}
五、ACM模式
5.1、区间和
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
/* 输入数组 */
int[] nums = new int[n];
/* 前缀和数组 */
int[] predixNums = new int[n];
int preSum = 0;
/* 构建前缀和数组 */
for (int i = 0; i < n; i++) {
nums[i] = scanner.nextInt();
preSum += nums[i];
predixNums[i] = preSum;
}
while (scanner.hasNextInt()) {
int a = scanner.nextInt();
int b = scanner.nextInt();
int sum;
if (a == 0) {
sum = predixNums[b];
} else {
sum = predixNums[b] - predixNums[a - 1];
}
System.out.println(sum);
}
/* 最后不要忘记释放资源 */
scanner.close();
}
}
六、数组题型总结
二分法、双指针法、滑动窗口、前缀和。