高效制胜
day01 求和问题
T1 1. 两数之和 数组
哈希表
方1:遍历
class Solution {
public int[] twoSum(int[] nums, int target) {
for(int i=0;i<nums.length;i++){
//由于前i位已经互相匹配,j从i+1位开始不会遗漏
for(int j=i+1;j<nums.length;j++){
if(nums[i]+nums[j] == target){
return new int[]{
i,j};
}
}
}
return new int[0]; //空整型数组
}
}
方2:hashMap
方法 | 描述 |
---|---|
containsKey() | 检查 hashMap 中是否存在指定的 key 对应的映射关系。 |
containsValue() | 检查 hashMap 中是否存在指定的 value 对应的映射关系。 |
get() | 获取指定 key 对应对 value |
class Solution {
public int[] twoSum(int[] nums, int target) {
//键是 nums[i] 值是i
HashMap<Integer,Integer> map = new HashMap<>();
for(int i=0;i<nums.length;i++){
//target-nums[i] map中包含目标值
//可以避免二次循环
if(map.containsKey(target-nums[i])){
return new int[]{
i,map.get(target-nums[i])};
}
map.put(nums[i],i);
}
return new int[0];
}
}
T2 167. 两数之和 II - 输入有序数组
非递减顺序排列 就想到了二分
返回值的顺序是一定的 不能用hashMap
方1:二分 自己想的
二分模板
int BinarySearch(int array[], int n, int value)
{
int left = 0;
int right = n - 1;
// 如果这里是 int right = n 的话,那么下面有两处地方需要修改,以保证一一对应:
// 1、下面循环的条件则是 while(left < right)
// 2、循环内当 array[middle] > value 的时候,right = middle
while (left <= right) // 循环条件,适时而变
{
int middle = left + ((right - left) >> 1); // 防止溢出,移位也更高效。同时,每次循环都需要更新。
if (array[middle] > value)
right = middle - 1;
else if (array[middle] < value)
left = middle + 1;
else
return middle;
// 可能会有读者认为刚开始时就要判断相等,但毕竟数组中不相等的情况更多
// 如果每次循环都判断一下是否相等,将耗费时间
}
return -1;
}
题解
class Solution {
public int[] twoSum(int[] nums, int target) {
int temp,res;
for(int i=0;i<nums.length;i++){
temp = target-nums[i];
//一个数只能用一次,因为是非递减顺序排列所以有个相等的一定挨在一起(同时包含不相等但相加符合目标的情况)
if((i+1)<nums.length && nums[i]+nums[i+1]==target)
return new int[]{
i+1,i+2}; //因为是返回从1开始对下标处理
//非相等或邻近符合目标
//使用二分法快速查找目标值
res = BinarySearch(nums,i,nums.length-1,temp);
if(res != -1){
return new int[]{
i+1,res+1};
}
}
return new int[0];
}
public int BinarySearch(int[] nums,int left,int right,int target){
int mid;
while(left <= right){
mid = left + ((right-left) >>1); //右移1位 比除2快
if(nums[mid] > target)
right = mid - 1;
else if(nums[mid] < target)
left = mid + 1;
else
return mid;
}
return -1;
}
}
方二 : 双指针
class Solution {
public int[] twoSum(int[] nums, int target) {
int low = 0,high = nums.length - 1,sum;
//双指针
while(low<=high){
sum = nums[low]+nums[high];
if(sum == target)
return new int[]{
low+1,high+1};
//因为是非递减顺序排列
//比目标大,右指针左移 ;比目标小,左指针右移
if(sum<target) low++;
if(sum>target) high--;
}
return new int[0];
}
}
day02 求和问题
T1 15. 三数之和
排序 + 双指针 本题的难点在于如何去除重复解
暴力的话3重循环,时空开销大 官解想到双指针的过程很精妙
二重和三重可以是并列的,由此想到了双指针
- 特判,对于数组长度 n,如果数组为 null或者数组长度小于 3,返回 []。
- 对数组进行排序。
- 遍历排序后数组
-
若 nums[i]>0:因为已经排序好(小到大),所以后面不可能有三个数加和等于 0,直接返回结果。
-
对于重复元素:跳过,避免出现重复解 如[-3,0,2,2,2,3]
-
令左指针 L=i+1,右指针 R=n-1,当 L<R 时,执行循环:
-
当 nums[i]+nums[L]+nums[R]=0,执行循环,判断左界和右界是否和下一位置重复,去除重复解。并同时将 L,R 移到下一位置,寻找新的解
-
若和大于 0,说明 nums[R] 太大,R左移
-
若和小于 0,说明 nums[L]太小,L 右移
-
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
//数组为空或者长度小于3不会有结果
if(nums==null && nums.length < 3) return res;
//数组排序
Arrays.sort(nums); //小到大排序
int left,right,tmp,len = nums.length; //左右边界,三数和
//遍历排序后的数组
for(int i=0; i<len; i++){
//对于小到大的数组当前指大于0,后面不可能有解了
if(nums[i] >0) return res;
//跳过重复的数字
if(i>0 && nums[i] == nums[i-1]) continue;
left = i+1;
right = len-1; //边界初始化
while(left < right){
tmp = nums[i]+nums[left]+nums[right]; //三数和
if(tmp == 0){
List<Integer> list = new ArrayList<>();
list.add(nums[i]);
list.add(nums[left]);
list.add(nums[right]);
res.add(list);
//跳过重复元素
while(left<right && nums[left+1]==nums[left]) left++;
while(left<right && nums[right-1]==nums[right]) right--;
//边界改变继续求值
left++;
right--;
}else if(tmp <0){
//比0小左边界右移
left++;
}else{
//比0大,右边界左移
right--;
}
}
}
return res;
}
}
T2 18. 四数之和
暴力4重循环,和三数之和一样最后两重简化成双指针 即n-2重循环+双指针
官解 排序+双指针
具体实现时,还可以进行一些剪枝操作:
在确定第一个数之后,如果 nums[i]+nums[i+1]+nums[i+2]+nums[i+3] > target
说明此时剩下的三个数无论取什么,四数之和一定大于target,因此退出第一重循环;
在确定第一个数之后,如果
nums[i]+nums[n-3]+nums[n-2]+nums[n-1] < target
说明此时剩下的三个数无论取什么,四数之和一定小于 target因此第一重循环直接进入下一轮,枚举 nums[i+1]
在确定前两个数之后,如果
nums[i]+nums[j]+nums[j+1]+nums[j+2] > target
说明此时剩下的两个数无论取什么,四数之和一定大于 target,因此退出第二重循环;
在确定前两个数之后,如果
nums[i]+nums[j]+nums[n-2]+nums[n-1] < target
说明此时剩下的两个数无论取什么值,四数之和一定小target,因此第二重循环直接进入下一轮,枚举 nums[j+1]。
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> res = new ArrayList<>();
//特例处理
if(nums == null || nums.length<4) return res; //空数组或者长度构不成答案返回空
Arrays.sort(nums); //数组排序
int left,right,len = nums.length,sum;
for(int i=0;i<len-3;i++){
//去掉重复值
if(i>0 && nums[i]==nums[i-1]) continue;
//确定一个数的剪枝
if(nums[i]+nums[i+1]+nums[i+2]+nums[i+3] > target) break;
if(nums[i]+nums[len-3]+nums[len-2]+nums[len-1] < target) continue;
for(int j=i+1;j<len-2;j++){
//去掉重复
if(j>i+1 && nums[j]==nums[j-1]) continue;
//确定二个数的剪枝
if(nums[i]+nums[j]+nums[j+1]+nums[j+2] > target) break;
if(nums[i]+nums[j]+nums[len-2]+nums[len-1] < target) continue;
//初始化边界值
left = j+1;
right = len-1;
//双指针
while(left < right){
sum = nums[i]+nums[j]+nums[left]+nums[right];
if(sum == target){
res.add(Arrays.asList(nums[i],nums[j],nums[left],nums[right])); //合法解加入结果集
//边界重复值处理
while(left<right && nums[left]==nums[left+1]) left++;
while(left<right && nums[right]==nums[right-1]) right--;
//边界同时改变
left++;
right--;
}else if(sum <target){
//比目标小,右移左边界
left++;
}else{
//比目标大,左移右边界
right--;
}
}
}
}
return res;
}
}
day03 斐波拉契数列
T1 509. 斐波那契数
递归 暴力
终止条件 | f(0)=0,f(1)=1 |
---|---|
递归方程 | f(n) = f(n-1)+f(n-2) |
class Solution {
public int fib(int n) {
if(n==0) return 0; //终止条件
if(n==1) return 1;
return fib(n-1)+fib(n-2);
}
}
时间复杂度大
带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图
动态规划叫做「自底向上」
方一:动态规划
状态转移方程
1 n=1,2
f(n) = f(n-1)+f(n-2) n>2
由于 F(n)只和 F(n-1) 与 F(n-2) 有关,因此可以使用「滚动数组思想」把空间复杂度优化成 O(1)
class Solution {
public int fib(int n) {
if(n<2) return n; //终止条件
int p=0,q=0,r=1;
for(int i=2 ; i<=n ; i++){
//滚动数组
p = q;
q = r;
r = p+q;
}
return r;
}
}
方2 矩阵快速幂,方3 通项公式 数学方法
T2 70. 爬楼梯
斐波拉契数组 用动态规划
初始条件 | dp[1]=1,dp[2]=2 |
---|---|
动态转移方程 | dp[n] = dp[n-1]+dp[n-2] |
class Solution {
public int climbStairs(int n) {
if(n<2) return n;
int a=0,b=1,r=1;
for(int i=2;i<=n;i++){
//滚动数组
a = b;
b = r;
r = a+b;
}
return r;
}
}
day04 动态规划
T1 53. 最大子序和
max{dp[n-1]+nums[i],nums[i]} 自己想到的
方一:动态规划
复杂度
时间复杂度:O(n)O(n),其中 nn 为 \textit{nums}nums 数组的长度。我们只需要遍历一遍数组即可求得答案。
空间复杂度:O(1)O(1)。我们只需要常数空间存放若干变量。
要求的答案是 max{dp(0) , dp(1) , dp(2) , …dp(i) }
动态转移 方程 dp[i] = max{dp[n-1]+nums[i],nums[i]}
class Solution {
public int maxSubArray(int[] nums) {
int res=nums[0],tmp=0;
for(int i=0;i<nums.length;i++){
tmp = tmp+nums[i] > nums[i] ? tmp+nums[i]:nums[i];
res = Math.max(res,tmp); //dp[0]...dp[i-1]的最大值是res
}
return res;
}
}
方二 :分治
官解 线段树
对于一个区间 [l,r],我们可以维护四个量:
lSum 表示 [l,r] 内以 ll 为左端点的最大子段和
rSum 表示 [l,r]内以 rr 为右端点的最大子段和
mSum 表示 [l,r]内的最大子段和
iSum 表示 [l,r]的区间和
-
首先最好维护的是iSum,区间 [l,r]的 iSum 就等于「左子区间」的 iSum 加上「右子区间」的 iSum。
-
对于 [l,r]的 lSum,存在两种可能,它要么等于「左子区间」的 lSum,要么等于「左子区间」的 iSum 加上「右子区间」的 lSum,二者取大。
-
对于 [l,r]的 rSum,同理,它要么等于「右子区间」的 rSum,要么等于「右子区间」的 iSum 加上「左子区间」的 rSum,二者取大。
-
当计算好上面的三个量之后,就很好计算 [l,r]的 mSum 了。我们可以考虑 [l,r]的 mSum 对应的区间是否跨越 mm——它可能不跨越 mm,也就是说 [l,r]的 mSum 可能是「左子区间」的 mSum 和 「右子区间」的 mSum 中的一个;它也可能跨越 mm,可能是「左子区间」的 rSum 和 「右子区间」的 lSum 求和。三者取大。
class Solution {
public int maxSubArray(int[] nums