1 二分查找
关于二分法的详细原理,可以参见文章[双指针技巧]第3.1小节。
T704-二分查找
见LeetCode第704题[二分查找]
题目描述
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
题解
public int search(int[] nums, int target) {
if (nums == null || nums.length <= 0) return -1;
int l = 0;
int r = nums.length;
while (l < r) {
int mid = l + (r - l) / 2;
if (target > nums[mid]) {
l = mid + 1;
} else if (target == nums[mid]) {
return mid;
} else {
r = mid;
}
}
return -1;
}
注意搜索区间是个左闭右开的区间,即,因此
r往左边收缩的时候,赋值语句为r = mid循环结束条件为
l == r
T35-搜索插入位置
见LeetCode第35题[搜索插入位置]
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例
输入: nums = [1,3,5,6], target = 5
输出: 2
我的思路
- 二分法的变种,如果没找到,就返回大于
target的最小的那个值的索引 - 需要在原始二分法中,锁定右指针
r
实现代码
public int searchInsert(int[] nums, int target) {
if (nums == null || nums.length == 0) return 0;
int l = 0;
int r = nums.length;
while (l < r) {
int mid = l + (r - l) / 2;
if (target == nums[mid]) {
r = mid; // 锁定右指针
} else if (target > nums[mid]) {
l = mid + 1;
} else if (target < nums[mid]) {
r = mid;
}
}
return r;
}
T34-排序数组中寻找目标元素的第一个和最后一个位置
见LeetCode第34题[寻找目标元素]
题目描述
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
我的思路
- 二分法的改进算法,返回目标元素最左侧的坐标,或者是小于目标元素中最大元素的索引
l - 判断
nums[l] == target,如果相等,往后遍历,找到结束位置r - 如果不相等,直接返回
[-1, -1]
实现代码
/**
* 你找出给定目标值在数组中的开始位置和结束位置。
*
* @param nums
* @param target
* @return 如果数组中不存在目标值 target,返回 [-1, -1]
*/
public int[] searchRange(int[] nums, int target) {
int[] range = {-1, -1};
if (nums == null || nums.length == 0) return range;
int l = 0;
int r = nums.length;
while (l < r) {
int mid = l + (r - l) / 2;
if (nums[mid] > target) {
r = mid;
} else if (nums[mid] < target) {
l = mid + 1;
} else if (nums[mid] == target) {
r = mid; // 相等的时候,往左收缩,寻找到左边界
}
}
// 如果存在 target, l 应该在第一次出现的位置,否则应该是小于目标元素最大的元素的索引
if (l >= nums.length || target != nums[l]) {
return range;
}
range[0] = l;
while (l < nums.length && nums[l] == target) {
l++;
}
range[1] = --l;
return range;
}
T69-x的平方根
见LeetCode第69题[x的完全平方根]
题目描述
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
**注意:**不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
我的思路
- 从开始进行二分查找
- 判断
mid ^ 2 == target - 返回最左边界值
public int mySqrt(int x) {
if (x <= 1) return x;
int l = 1;
int r = x / 2 + 1;
while (l < r) {
int mid = l + (r - l) / 2;
long powMid = mid * mid;
if (mid * mid > x) { // 溢出风险
r = mid;
} else if (mid * mid == x) {
return mid;
} else if (mid * mid < x) {
l = mid + 1;
}
}
return --l;
}
- 这段代码中,对
mid平方会有溢出的风险 - 解决方案:开
long - 时间复杂度为:
解法二:袖珍计算器
[袖珍计算器]是一种利用指数函数和对数函数代替平方根函数的方法,如下面公式:
由于计算器无法存储浮点数的精确值,指数函数和对数函数的参数和返回值均为浮点数,因此运算的过程会出现误差,在对据结果进行取整的时候,可能会得出错误的结果。
在得到结果的整数部分ans之后,需要判断ans和ans + 1那个是正确答案。
/**
* 袖珍计算器
* @param x
* @return
*/
public int mySqrtI(int x) {
if (x <= 1) return x;
int ans = (int) Math.exp(0.5 * Math.log(x));
return (long) (ans + 1) * (ans + 1) <= x ? ans + 1 : ans;
}
时间复杂度为:
T367-有效的完全平方根
见LeetCode第367题[有效的完全平方根]
题目描述
给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。
不能使用任何内置的库函数,如 sqrt 。
示例 1:
输入:num = 16
输出:true
解释:返回 true ,因为 4 * 4 = 16 且 4 是一个整数。
我的思路
- 和上一题的解法同样
2 双指针
T27-移除元素
见LeetCode第27题[移除元素]
题目描述
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。
假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:
- 更改
nums数组,使nums的前k个元素包含不等于val的元素。nums的其余元素和nums的大小并不重要。 - 返回
k。
我的思路
- 快慢指针,慢指针指向下一个有效的位置,即有效数组末尾
- 快指针用于判断元素是否为目标元素
public int removeDuplicates(int[] nums) {
if (nums == null || nums.length <= 1) {
return nums == null ? 0 : nums.length;
}
int slow = 0;
int fast = 0;
while (fast < nums.length) {
if (nums[fast] != nums[slow]) {
nums[++slow] = nums[fast];
} else {
fast++;
}
}
return ++slow;
}
- 解题技巧:快慢双指针
- 时间复杂度:,快指针过一遍数组。
T26-删除有序数组中的重复项
题目描述
给你一个 非严格递增排列 的数组 nums ,请你** 原地** 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
- 更改数组
nums,使nums的前k个元素包含唯一元素,并按照它们最初在nums中出现的顺序排列。nums的其余元素与nums的大小不重要。 - 返回
k。
示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
我的思路
- 使用快慢指针技巧
- 慢指针指向重复元素的第一个位置
- 快指针元素和慢指针做判断,
- 如果相同,快指针往后走,
- 否则,慢指针往前移动,并获得快指针所指元素
时间复杂度为:,快指针遍历了一遍数组
T283-移动零
题目描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
我的思路
- 快慢指针
- 慢指针指向数组的有效位置
- 快指针用来寻找非零元素
- 快指针结束循环之后,需要将慢指针之后的元素都赋值为0
时间复杂度分析:快指针遍历了一遍数组,慢指针也遍历了一遍数组,因此时间复杂度为
T844-比较含退格的字符串
题目描述
给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。
**注意:**如果对空文本输入退格字符,文本继续为空。
示例 1:
输入:s = "ab#c", t = "ad#c"
输出:true
解释:s 和 t 都会变成 "ac"。
我的思路
- 使用两个栈来模拟退格
- 非
#字符就入栈,否则的话就弹出 - 最后比较两个栈的元素是否相同
思路二:快慢指针
- 快指针用来判断是否为有效字符
- 慢指针用来表示有效的索引范围
- 然后比较两个原地修改后的字符数组是否相同
/**
* 使用 StringBuilder 构建操作之后的字符串
* @param s
* @param t
* @return
*/
public boolean backspaceCompareII(String s, String t) {
return operation(s).equals(operation(t));
}
private String operation(String s) {
StringBuilder sb = new StringBuilder();
int p = 0; // 表示有效位数
for (char c : s.toCharArray()) {
if (c == '#') {
if (p > 0) {
sb.deleteCharAt(--p);
}
} else {
sb.append(c);
p++;
}
}
return sb.toString();
}
使用
StringBuilder要比操作char[]数组更快时间复杂度:分别遍历了字符串
s和t,因此时间复杂度为级别,m | n分别表示两个字符串的长度
T977-有序数组的平方
题目描述
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
我的思路
- 找到距离0最近的元素坐标
- 双指针,从中间向两边扩散,,合并两个升序数组, 加入到新的数组
- 最后对新的数组做平方
/**
* 有序数组的平方
* @param nums
* @return
*/
public int[] sortedSquares(int[] nums) {
int minAbs = Integer.MAX_VALUE;
int start = -1;
for (int i = 0; i < nums.length; i++) {
if (Math.abs(nums[i]) < minAbs) {
minAbs = Math.abs(nums[i]);
start = i;
}
}
int l = start;
int r = start + 1;
int[] res = new int[nums.length];
int count = 0;
while (l >= 0 && r < nums.length) {
if (Math.abs(nums[l]) > Math.abs(nums[r])) {
res[count++] = (int) Math.pow(nums[r++], 2);
} else {
res[count++] = (int) Math.pow(nums[l--], 2);
}
}
if (l == -1) {
while (r < nums.length) {
res[count++] = (int) Math.pow(nums[r++], 2);
}
} else if (r == nums.length) {
while (l >= 0) {
res[count++] = (int) Math.pow(nums[l--], 2);
}
}
return res;
}
优化思路
- 为什么从中间往两端扩散呢?直接左右指针,往中间走
- 省去了寻找离0最近元素坐标的复杂度
/**
* 从两端开始,左右指针
* @param nums
* @return
*/
public int[] sortedSquaresI(int[] nums) {
int[] res = new int[nums.length];
int tail = nums.length - 1;
int l = 0;
int r = tail;
while (l <= r) {
if (nums[l] * nums[l] > nums[r] * nums[r]) {
res[tail--] = nums[l] * nums[l];
l++;
} else {
res[tail--] = nums[r] * nums[r];
r--;
}
}
return res;
}
时间复杂度:
l指针和r指针共同遍历了nums数组,时间复杂度为
3 滑动窗口
T209-长度最小的子数组
题目描述
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的
子数组[numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
我的思路
- 使用滑动窗口
- 窗口小于目标则右指针往前移动,添加元素
- 窗口大于目标则左指针往前移动,移出元素
- 满足目标的,更新最小长度
/**
* 优化之后的代码
* @param target
* @param nums
* @return
*/
public int minSubArrayLenI(int target, int[] nums) {
int l = 0;
int r = 0;
int minLen = nums.length + 1;
int winSum = 0;
while (r < nums.length) {
winSum += nums[r++];
while (winSum >= target) {
minLen = Math.min(r - l, minLen);
winSum -= nums[l++];
}
}
return minLen == nums.length + 1 ? 0 : minLen;
}
T904-水果成篮
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。
你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:
- 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
- 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
- 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。
翻译为人话,给你一个数组,求出最多有两种元素的子数组的最大长度
示例 3:
输入:fruits = [1,2,3,2,2]
输出:4
解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。
我的思路
- 滑动窗口法,需要有一个
HashMap<Integer, Integer> notes记录当前果篮里头水果的种类和个数 - 当水果种类少于
2就右滑,然后多于2,收缩窗口,同时改变notes
/**
* 寻找最多包含 2 个元素的子数组的最大长度
* @param fruits
* @return
*/
public int totalFruit(int[] fruits) {
int maxLen = 0;
HashMap<Integer, Integer> notes = new HashMap<>();
int l = 0;
int r = 0;
while (r < fruits.length) {
notes.put(fruits[r], notes.getOrDefault(fruits[r], 0) + 1);
r++;
if (notes.size() <= 2) {
maxLen = Math.max(maxLen, r - l);
} else {
while (notes.size() > 2) {
// 将左边的水果扔掉
if (notes.get(fruits[l]) == 1) {
notes.remove(fruits[l]);
} else {
notes.put(fruits[l], notes.get(fruits[l]) - 1);
}
l++;
}
}
}
return maxLen;
}
- 时间复杂度:快指针遍历一遍数组
- 空间复杂度:额外空间哈希表,空间复杂度为
T76-最小覆盖字串
题目描述
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:
- 对于
t中重复字符,我们寻找的子字符串中该字符数量必须不少于t中该字符数量。 - 如果
s中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
- 什么是涵盖?
- 就是
s的词频表完全大于t
- 就是
- 使用
new int[128] dict表示字符串的词频 - 遍历母串,添加元素,然后判断是否涵盖,如果涵盖,返回窗口中所有的字符串,否则返回
""
/**
* 判断 s 中涵盖 t 的最小子串
* @param s
* @param t
* @return
*/
public String minWindow(String s, String t) {
if (s.length() < t.length()) return "";
int[] subDict = new int[128];
// 初始化模式串的词频表
int[] tDict = new int[128];
for (char c : t.toCharArray()) {
tDict[c]++;
}
int l = 0;
int r = 0;
int[] candidates = {0, s.length() + 1};
while (r < s.length()) {
// 将当前字符加入窗口
subDict[s.charAt(r++)]++;
while (isCovered(subDict, tDict)) {
// 如果当前窗口小于已经记录的最大窗口
if (r - l < candidates[1] - candidates[0]) {
candidates[0] = l;
candidates[1] = r;
}
// 从里面移出元素
subDict[s.charAt(l++)]--;
}
}
StringBuilder sb = new StringBuilder();
if (candidates[1] == s.length() + 1) return sb.toString();
for (int i = candidates[0]; i < candidates[1]; i++) {
sb.append(s.charAt(i));
}
return sb.toString();
}
/**
* 判断 subDict 是否涵盖 tDict
* @param subDict
* @param tDict
* @return
*/
private boolean isCovered(int[] subDict, int[] tDict) {
for (int i = 0; i < subDict.length; i++) {
if (subDict[i] < tDict[i]) {
return false;
}
}
return true;
}
- 时间复杂度:需要遍历字符串
s | t以及词频数组,但是词频数组是固定长度的,因此时间复杂度为- 空间复杂度:额外空间为两个词频数组,但是固定长度,所以空间复杂度为
4 模拟
T54-螺旋矩阵
题目描述
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
示例
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
我的思路
- 何时该转向,往哪个方向转向?
- 何时转向?需要有一个组
boundary记录矩阵的有效边界,到达边界的时候需要转向 - 往哪个方向转向?
0:从左往右遍历1:从上往下2:从右往左4:从下往上
List<Integer> resList = new ArrayList<>();
int[] boundary;
/**
* 螺旋矩阵
* @param matrix
* @return
*/
public List<Integer> spiralOrder(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
boundary = new int[]{0, n, 0, m};
spiral(matrix, 0); // 0 代表从左往右遍历
return resList;
}
/**
* 螺旋遍历矩阵
* @param matrix
* @param direction
*/
private void spiral(int[][] matrix, int direction) {
if (boundary[0] == boundary[1] || boundary[2] == boundary[3]) return;
switch (direction) {
case 0: {
// 从左往右遍历
int start = boundary[0];
int end = boundary[1];
for (int i = start; i < end; i++) {
resList.add(matrix[boundary[2]][i]);
}
// 上边界加1
boundary[2]++;
// 调用从上往下遍历
spiral(matrix, 1);
break;
}
case 1: {
// 从上往下遍历
int start = boundary[2];
int end = boundary[3];
for (int i = start; i < end; i++) {
resList.add(matrix[i][boundary[1] - 1]);
}
// 右边界减1
boundary[1]--;
// 调用从右往左
spiral(matrix, 2);
break;
}
case 2: {
// 从右往左
int start = boundary[0];
int end = boundary[1] - 1;
for (int i = end; i >= start; i--) {
resList.add(matrix[boundary[3] - 1][i]);
}
// 下边界减1
boundary[3]--;
// 调用从右往左
spiral(matrix, 3);
break;
}
case 3: {
// 从下往上遍历
int start = boundary[2];
int end = boundary[3] - 1;
for (int i = end; i >= start; i--) {
resList.add(matrix[i][boundary[0]]);
}
// 左边界加1
boundary[0]++;
// 调用从右往左
spiral(matrix, 0);
break;
}
default: {
return;
}
}
}
- 时间复杂度:
O(N)- 空间复杂度:
O(1)
T59-螺旋矩阵II
题目描述
给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
我的思路
- 同样使用模拟的方式,一个一个元素填进去
代码略
- 时间复杂度:将
n个数字填入到矩阵中,所以时间复杂度为O(N)- 空间复杂度:用了
startNum记录当前数字,direction记录当前遍历方向,空间复杂度为O(1)
5 前缀和
见[前缀和技巧]
6 差分数组
见[差分数组]