第二天、明天去dating、休息一天
- 第二天 keep going
- [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/)(二分查找)(mid)
- [131. 分割回文串](https://leetcode.cn/problems/palindrome-partitioning/)(回溯)(mid)
- [322. 零钱兑换](https://leetcode.cn/problems/coin-change/)(动态规划)(mid)
- [560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/) (子串-前缀和&哈希表优化)(mid)
- [53. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/) (动态规划/分治)(dp简单、分治mid)
- [160. 相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/)(链表)(easy)
- [56. 合并区间](https://leetcode.cn/problems/merge-intervals/) (数组、排序技巧)(mid)
- [102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/)(二叉树)(mid)
- [39. 组合总和](https://leetcode.cn/problems/combination-sum/) (回溯+剪枝)(mid)
第二天 keep going
34. 在排序数组中查找元素的第一个和最后一个位置(二分查找)(mid)
思路:
一般的二分查找:
if(nums[mid] < target) left = mid + 1;
else if(nums[mid] > target) right = mid - 1;
else return mid;
寻找第一个满足target的二分查找,这里以找到第一个>=target的元素为例
在找到一个满足条件的元素后,向左缩小区间,检查左边的区间中是否还有满足条件的元素
这里的关键就是 满足条件时,要记录ret = mid
if(nums[mid] >= target) {
rigth = mid -1; //这里已经找到了一个满足条件的了,向左缩小区间
//记录当前的元素下标,因为每一次找到满足条件的都会向左继续寻找,可以保证这里的ret一定是满足条件中最小的
ret = mid;
}
else left = mid + 1;//不满足条件,就向右寻找
return ret;
代码:
为什么左边界要找第一个大于等于的呢?为什么右边界要找第一个大于的呢?
分析给定数组的情况:
- 有目标元素
- 左边界为第一个target,右边界为第一个大于target元素的位置或者为
nums.size()
(这种情况下,就是target为数组中的最大元素)
- 左边界为第一个target,右边界为第一个大于target元素的位置或者为
- 没有
- 全部小于目标元素:左边界=右边界=
nums.size()
- 全部大于目标元素:左边界=右边界=0;
- 有大于、有小于:左边界=右边界,都是找的第一个大于target元素的位置
- 全部小于目标元素:左边界=右边界=
class Solution {
public:
int findLeft(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1, ret = nums.size();
while(left <= right) {
int mid = (left + right)/2;
if(nums[mid] >= target) {
right = mid - 1;
ret = mid; //这里用ret记录每一次找到满足条件的mid,由于会缩小区间,可以保证ret的值一定是递减的
}
else left = mid + 1 ;
}
return ret;
}
int findRight(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1, ret = nums.size();
while(left <= right) {
int mid = (left + right)/2;
if(nums[mid] > target) {
right = mid - 1;
ret = mid;
}
else left = mid + 1 ;
}
return ret;
}
vector<int> searchRange(vector<int>& nums, int target) {
int left = findLeft(nums,target);
int right = findRight(nums,target) - 1;
//1.没有对应元素 left > right 2.有一个 left = right 3.有两个及以上 left < right
if(left <= right) return {left,right};
return {-1,-1};
}
};
131. 分割回文串(回溯)(mid)
回溯的一般解法
void func() {
if(终止条件)
结束要做的事
return
for(按序遍历) {
if(判断是否剪枝) {
//todo
递归去子问题
回溯。以便进行for循环的下一轮
}
}
}
代码:
class Solution {
private:
vector<vector<string>> ret;
vector<string> path;
bool isPalindrome(const string& s, int start, int end) {
for(int i = start, j = end; i < j; ++i,--j) {
if(s[i]!=s[j]) return false;
}
return true;
}
void backTracking(const string& s, int start) {
if(start >= s.size()) { //顺利的切割完
ret.push_back(path);
return;
}
for(int i = start; i < s.size(); ++i) {
if(isPalindrome(s,start,i)) { //这里的判断可以做剪枝
string str = s.substr(start, i-start+1);
path.push_back(str);
backTracking(s, i+1); //继续向后切割
path.pop_back(); // 回溯过程,弹出本次已经添加的子串
}
}
}
public:
vector<vector<string>> partition(string s) {
ret.clear();
path.clear();
backTracking(s,0);
return ret;
}
};
322. 零钱兑换(动态规划)(mid)
dp数组:dp[j]:凑足总额为j所需钱币的最少个数为dp[j]
dp[j] = min(dp[j - coins[i]] + 1, dp[j])
背包问题的外层遍历和内层遍历
针对本题,外层物品、内层背包;外层背包、内层物品都可以
以外层物品、内层背包为例:
- 外层遍历硬币,尝试将一种硬币放入背包
- 内存遍历背包,就是遍历dp数组,
- 这里为什么要从cons[i]开始遍历呢?因为当价格小于当前币值时,无法将硬币放入,所以这样可以减少不必要的计算
- 最开始时,由dp[j - coins[i]]可以找到dp[0] = 0;这样对于j恰好为币种面值局的情况时,硬币的数量当然最少为1。
- 在内存循环的判断逻辑中,如果dp[j - coins[i]]为初始值,说明目前还没有找到可以满足j - coins[i]元的硬币方案,因此就没有必要尝试将该种硬币放入了。
外层背包、内层物品思路:
对于每一种价格,尝试将一种硬币放入,如果j - coins[i]元对应的方案还是初始值,说明将该种硬币放入后,剩下的钱没有合理的分配方案,所有会被淘汰。
代码:
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount+1, INT_MAX);
dp[0] = 0;
for(int i = 0; i < coins.size(); ++i) { //遍历物品
for(int j = coins[i]; j <= amount; j++) {//遍历背包
if(dp[j - coins[i]] != INT_MAX) {
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if(dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
560. 和为 K 的子数组 (子串-前缀和&哈希表优化)(mid)
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
思路:
暴力枚举的优化方案:
【0…j-1】【j…i】
pre[j-1] + k
【0…i】
pre[i]
pre[i]−pre[j−1]==k
将前缀和的出现次数记录到哈希表中,当遍历到i时,用pre-k来在哈希表中检索,如果pre-k这个前缀和曾经出现过,则表明当前存在mp[pre-k]个以当前元素为最后一个元素的子数组满足入选的条件。
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp; // <前缀和,前缀和的出现次数>
mp[0] = 1; //说明只有当前的单个元素作为一个数组
int count = 0, pre = 0;
for(auto & x : nums) {
pre += x;
if(mp.find(pre - k) != mp.end()) {
count += mp[pre - k];
}
mp[pre]++; //表明遇到了一个新的前缀和,其和为 pre
}
return count;
}
};
53. 最大子数组和 (动态规划/分治)(dp简单、分治mid)
dp解法:
dp[i] = max(nums[i], dp[i-1] + nums[i])
这里可以使用前缀和pre去优化内存空间
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// vector<int> dp(nums.size());
// dp[0] = nums[0];
int pre = 0;
int ret = nums[0];
for(int i = 0; i < nums.size(); ++i) {
pre = max(nums[i], pre + nums[i]);
ret = max(ret, pre);
}
return ret;
}
};
分治法:
#include <iostream>
#include <vector>
using namespace std;
struct Node {
int lSum,rSum,sum,mSum;
//lsum 以左边界为开头的最大的子段和
//rsum 以右边界为开头的最小的子段和
//sum 整段的和
//mSum 最大的子段和
};
Node dfs(vector<int>& nums, int l, int r)
{
if(l==r) return {nums[l],nums[l],nums[l],nums[l]};
int mid = l+r>>1;
auto left = dfs(nums,l,mid); //左半部分
auto right = dfs(nums,mid+1,r); //右半部分
return {
max(left.lSum, left.sum+right.lSum),
max(right.rSum, left.rSum + right.sum),
left.sum + right.sum,
max(left.rSum+right.lSum, max(left.mSum,right.mSum))
};
}
int maxSubArray(vector<int>& nums) {
return dfs(nums,0,nums.size()-1).mSum;
}
int main()
{
vector<int> array1 = {-2,1,-3,4,-1,2,1,-5,4};
cout << maxSubArray(array1) << endl;
}
160. 相交链表(链表)(easy)
情况:
- 相交:
- 相交点前节点数相同:直接按序遍历走到一起,走到相交点结束循环
- 相交点前节点数不同:当走完AC时,继续走BC;走完BC时,继续走AC
- 在第二次会走进C时,会同时走到相交点
- 不相交:
- 有一方为空:返回null
- 都不为空:
- 等长:第一次为null时,对方也是null,相等,退出循环
- 不等长:第二次为null时,对方也是null,相等,退出循环
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(!headA || !headB) return nullptr;
ListNode* pA = headA, *pB = headB;
while(pA != pB) {
pA = pA == nullptr ? headB : pA->next;
pB = pB == nullptr ? headA : pB->next;
}
return pA;
}
};
56. 合并区间 (数组、排序技巧)(mid)
先将数组按照左边界进行排序,遍历数组时,如果merged数组为空,或者最后一个数组与当前遍历访问的数组不相交,就将该新数组加入merged
如果有相交的部分,就将merged中最后一个数组的边界进行扩展
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
//排序后合并
sort(intervals.begin(),intervals.end());
vector<vector<int>> merged;
for(int i = 0; i < intervals.size(); ++i) {
int L = intervals[i][0], R = intervals[i][1];//记录当前数组的左右边界
if(!merged.size() || merged.back()[1] < L) //如果列表为空,或者列表中的最后一个的右边界小于该数组的左边界(说明没有相交部分)
merged.push_back(intervals[i]);
else
merged.back()[1] = max(R,merged.back()[1]);
}
return merged;
}
};
102. 二叉树的层序遍历(二叉树)(mid)
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
//队列
queue<TreeNode*> q;
vector<vector<int>> ret;
if(root) q.push(root);
while(!q.empty()) {
int count = q.size();
vector<int> vec;
for(int i = 0 ; i < count; ++i) {
TreeNode* node = q.front();
q.pop();
vec.push_back(node->val);
if(node->left) q.push(node->left);
if(node->right) q.push(node->right);
}
ret.push_back(vec);
}
return ret;
}
};
39. 组合总和 (回溯+剪枝)(mid)
常规回溯写法
class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
int sum = 0;
void backTracking(vector<int>& candidates, int target, int start) {
//终止条件
if(sum >= target) {
if(sum == target) ret.push_back(path);
return;
}
for(int i = start; i < candidates.size(); ++i) {
sum += candidates[i];
path.push_back(candidates[i]);
backTracking(candidates, target ,i);
path.pop_back();
sum -= candidates[i];
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
ret.clear();
path.clear();
backTracking(candidates, target, 0);
return ret;
}
};
剪枝优化需要先对candidates数组进行递增排序
class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
int sum = 0;
void backTracking(vector<int>& candidates, int target, int start) {
//终止条件
if(sum == target) {
ret.push_back(path);
return;
}
for(int i = start; i < candidates.size(); ++i) {
if(sum + candidates[i] > target) return; //剪枝
sum += candidates[i];
path.push_back(candidates[i]);
backTracking(candidates, target ,i);
path.pop_back();
sum -= candidates[i];
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
ret.clear();
path.clear();
sort(candidates.begin(),candidates.end()); //排序
backTracking(candidates, target, 0);
return ret;
}
};