算法笔记C++版本(持续更新)
当前背景:准备研究生复试,重头学习C++用于机考
笔记内容:算法题目思路和答案
学习参考书籍《Hello算法》C\C++版本,《LeetCode101:A LeetCode Grinding Guide》
代码题目均来自leetcode hot-100
目录
数组
16. 最接近的三数之和
双指针实现,唯一注意的点就是aim值写法和minus值的写法
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
int ans;
int closeDis = INT_MAX;
for (int i = 0; i < nums.size() - 2; i++) {
int left = i + 1;
int right = nums.size() - 1;
int aim = target - nums[i];
while (left < right) {
int temp = nums[left] + nums[right];
int minus = abs(aim - temp);
if (minus < closeDis) {
ans = temp + nums[i];
closeDis = minus;
}
if (minus == 0) {
break;
}
if (temp < aim) {
left++;
} else {
right--;
}
}
if (closeDis == 0) {
break;
}
}
return ans;
}
};
双指针
前后遍历双指针
快慢指针
142.环形链表 2
思想:fast、slow指针,步长分别为2、1,相遇后slow不变,fast回到起点,按照步长1、1遍历,相遇即为链表环形起点。
如何理解??
答:第一次看到这个解决方案十分不理解,为什么第二次fast要回到起点,并且下次相遇就是起点。 这里不要去纠结fast的变化,而是理解他们第一次相遇的位置具有什么特别之处(都是在有环的情况下讨论)
首先,fast走的路程一定比slow多n个环,可以写出等式: fast=slow+np。fast的步长是slow的两倍,得出等式:fast=2slow,所以可以推到出slow=np。
其次,因为slow的总路程是np,这一段路程包含直线路程,所以slow走的n个环,有直线路程k在里面,不是完整的n个环路程。
最后,根据上面两个就可以得到:slow再往后走k次就能到环起点。k怎么确定?只需要再用一个步长为1的指针和slow同步移动,相遇时即为环起点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* detectCycle(ListNode* head) {
//用一个空头结点作为起点,第二次循环时才能保证正确。
ListNode* tmpHead= new ListNode(0);
tmpHead->next = head;
ListNode* fast = tmpHead;
ListNode* slow = tmpHead;
while (fast != NULL) {
//跳跃两步一定要避免访问空指针
if (fast->next != NULL) {
fast = fast->next->next;
} else {
fast = fast->next;
}
slow = slow->next;
if(fast==slow){
break;
}
}
if (fast == NULL) {
return NULL;
}
//从空头结点开始往后,次数保证正确。
fast = tmpHead;
while (fast != slow) {
fast = fast->next;
slow = slow->next;
}
delete(tmpHead);
return fast;
}
};
234.回文链表
这是一道简单题,但是考察链表两种操作,需要注意较多细节
思路:
1.使用快慢指针遍历到中间节点,遍历条件为fast不为nullptr并且fast->next不为nullptr, 中间节点分为两种情况:1)节点为偶数,slow停在前半部分的最后一个节点。2)节点为奇数,slow停在正中间节点。
2.反转链表:以slow为第一个前驱pre节点,依次把slow->next后续的所有节点next指向前一个。
3.从链表头尾开始用fast、slow往中间比较,比较到fast为空时结束。
注意:当对后半部分反转时,需要把slow的next置为空,如果不置空会导致slow和slow->next形成一个环,程序结束释放节点时会导致错误:heap-use-after-free on address。这种错误要么是使用已经释放空间的指针,要么对某个指针进行重复释放,本题属于第二种情况。
性能方面:采用step记录节点数量,在后续比较时使用,这种效率较低,改成指针遍历到空会更优
class Solution {
public:
bool isPalindrome(ListNode* head) {
ListNode* slow = head;
ListNode* fast = slow->next;
//int step = 1;
//快慢指针找到中间节点
while(fast != nullptr && fast->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
//step++;
}
ListNode* cur = slow->next;
//断开链表,避免出现循环
slow->next =nullptr;
ListNode* temp = nullptr;
//反转后半部分, 1->2->2->1 变成 1->2->null 和1->2<-2<-1
while(cur) {
temp = cur->next;
cur->next = slow;
slow = cur;
cur = temp;
}
fast = head;
while(fast) {
if(fast->val == slow->val) {
fast = fast->next;
slow = slow->next;
//step--;
} else {
return false;
}
}
return true;
}
};
DFS深度遍历(重点)
深度和广度是上机考试的重点!一般结合图的形式考察(岛屿问题)
100.相同的树(简单)
思路:考察树的遍历,使用前序遍历。只需要注意null的处理和bool返回即可
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
return dfs(p,q);
}
bool dfs(TreeNode* q, TreeNode* p) {
//先把空指针处理完
if(q == nullptr && p == nullptr) {
return true;
}
if(q == nullptr || p == nullptr) {
return false;
}
if(q->val != p->val) {
return false;
}
//可以美化下面注释的代码
// bool isSame = dfs(q->left,p->left);
// if(isSame) {
// return dfs(q->right,p->right);
// }
// return false;
return dfs(q->left,p->left) && dfs(q->right,p->right);
}
};
129. 求根节点到叶节点数字之和
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
int sumValue = 0;
public:
int sumNumbers(TreeNode* root) {
dfs(root, 0);
return sumValue;
}
void dfs(TreeNode* node, int traceSum){
if(node == nullptr) {
return;
}
if(node->left == nullptr && node->right == nullptr) {
node->val += traceSum*10;
sumValue += node->val;
return;
}
node->val += traceSum*10;
traceSum = node->val;
dfs(node->left, traceSum);
dfs(node->right, traceSum);
return;
}
};
BFS广度遍历(重点)
贪心算法
每次操作选取当前的局部最优解,最终达到全局最优
445.分发饼干
思路:对胃口和饼干数组进行升序排序,顺序查找第一个大于等于胃口的饼干(这一步就是贪心的方式),然后找下个胃口满足条件的饼干。
注意:
1.找满足下个胃口的饼干时,饼干遍历的位置应该是位于上一个被吃掉的饼干位置+1,能有效提升效率。
2.为什么每次只找一个满足大于等于胃口数量的饼干? 答:因为题目限制每个孩子只能投喂一块饼干,不存在用两个1的饼干满足胃口2的孩子。
//孩子胃口值和饼干尺寸是否有序? sort排序
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(s.begin(),s.end());
sort(g.begin(),g.end());
int ans = 0;
int cookId = 0;
for(int i = 0; i < g.size(); i++) {
for(int j = cookId; j < s.size(); j++) {
cookId = j+1;
//诶 找到满足孩子胃口的饼干了,ans记录饼干数量,并且下次从j+1的位置找下一个饼干。
if(s[j] >= g[i]) {
ans++;
s[j] = 0;
break;
}
}
}
return ans;
}
};
435 无重叠区间
思路:区间末端是重点,按照末端进行排序,从最小的开始往后找,能保证剩余的区间被覆盖的面积最小,比如[1,3] [1,2] [1,4] [2,3] ,如果从末端排序查找,[1,2]能覆盖1~4区间的最小区域,剩余区域有其他区间覆盖的可能性最大,要去除的区间数量就会最少(贪心,贪小的区间,能够保留的区间数量最大。
注意二维数组排序方法,sort的第三个比较参数用lambda,lambda内是比较元素即子向量,所以是一维数组为参数,比较第二个元素。
完全没想到怎么做。。。背一下记录一下该题,做人还是不够贪心
//选择尾区更小的进行union,保证剩余空间最大(贪心)
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), [](vector<int>& a, vector<int>& b){return a[1] < b[1];});
int ans = 0;
int left = 0;
for(int i = 0; i < intervals.size()-1;i++) {
if(intervals[left][1] > intervals[i+1][0]) {
ans++;
continue;
}
left = i+1;
}
return ans;
}
};
135.分发糖果
思路:从左往右遍历一遍,评分比左边高的,糖果数量等于左边数量+1;
再从右往左遍历一遍,评分比右边高,并且糖果数量小于等于右边的,糖果数量改为右边数量+1
不会,不懂,只觉得这个方法牛逼,下次遇到相邻对象比较的可以尝试这个方法。
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> numsCandy(ratings.size(),1);
for(int i =1; i < ratings.size(); i++) {
if(ratings[i] > ratings[i-1]) {
numsCandy[i] = numsCandy[i-1] +1;
}
}
for(int i = ratings.size()-2; i >=0; i--) {
if(ratings[i] > ratings[i+1] && numsCandy[i] <= numsCandy[i+1]) {
numsCandy[i] = numsCandy[i+1] + 1;
}
}
int ans = 0;
for(int& num : numsCandy) {
ans = ans + num;
}
return ans;
}
};
动态规划
动态规划问题的特性在于无后效性,即未来发展只与当前状态有关,和所有历史经历无关。
动态规划、回溯、分治问题的特性:
入门:爬楼梯问题
介绍三种方法,逐步引出动态规划的思想。
方法一,暴力搜索
采用递归的方式,分解原问题成为两个子问题dp[n] = dp[n-1]+dp[n-2]
int dfs(int i) {
// 已知 dp[1] 和 dp[2] ,返回之
if (i == 1 || i == 2)
return i;
// dp[i] = dp[i-1] + dp[i-2]
int count = dfs(i - 1) + dfs(i - 2);
return count;
}
/* 爬楼梯:搜索 */
int climbingStairsDFS(int n) {
return dfs(n);
}
从实际执行的二叉树中可以看出,存在大量重复计算,时间复杂度很高,这里引出一个优化方案:记忆搜素。把已经计算过的dp[n]保存下来,下次重复执行时,直接取出结果而不用重复计算(这种方法也可以成为剪枝)
方法二记忆搜索
/* 记忆化搜索 */
int dfs(int i, vector<int> &mem) {
// 已知 dp[1] 和 dp[2] ,返回之
if (i == 1 || i == 2)
return i;
// 若存在记录 dp[i] ,则直接返回之
if (mem[i] != -1)
return mem[i];
// dp[i] = dp[i-1] + dp[i-2]
int count = dfs(i - 1, mem) + dfs(i - 2, mem);
// 记录 dp[i]
mem[i] = count;
return count;
}
/* 爬楼梯:记忆化搜索 */
int climbingStairsDFSMem(int n) {
// mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录
vector<int> mem(n + 1, -1);
return dfs(n, mem);
}
方法三动态规划
自底向顶,迭代解决问题,从最小的子问题开始到最终解
dp[3] = dp[2]+dp[1] ; dp[4] = dp[3]+dp[2] ; …
/* 爬楼梯:动态规划 */
int climbingStairsDP(int n) {
if (n == 1 || n == 2)
return n;
// 初始化 dp 表,用于存储子问题的解
vector<int> dp(n + 1);
// 初始状态:预设最小子问题的解
dp[1] = 1;
dp[2] = 2;
// 状态转移:从较小子问题逐步求解较大子问题
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
322.零钱兑换
类似背包问题,都是容量、物品价值二维问题。
背包问题求解思路如下:
1.需要从每个物品开始,遍历一遍容量由1到最大。
2.判断当前物品装入和不装入哪个总价值更大,不装入时的价值等于上一个物品在当前容量的最大价值。
3.更新到容量最大,物品最后一个,就得到当前背包所能装的最大价值。零钱兑换思路如下:[coin, money]
1.从每个硬币遍历一次总金额从1到目标值的情况。
2.当前硬币大于总金额则把上个硬币的情况[coin-1,money]赋值给当前的[coin,money]。
3.当前硬币小于等于总金额则计算min([coin-1,money],[coin,money-[coin-1]]+1)
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<vector<int>> dp(coins.size()+1,vector<int>(amount+1,0));
int maxFlag = amount+1;
for(int i = 0; i <= coins.size(); i++) {
dp[i][0] = 0;
}
for(int i = 1; i <= amount; i++) {
dp[0][i] = maxFlag;
}
for(int i = 1; i <= coins.size(); i++) {
for(int money = 1; money <= amount; money++) {
if(coins[i-1] > money) {
dp[i][money] = dp[i-1][money];
} else {
dp[i][money] = min(dp[i-1][money],1 + dp[i][money-coins[i-1]]);
}
}
}
if(dp[coins.size()][amount] == maxFlag) {
return -1;
}
return dp[coins.size()][amount];
}
};
空间优化
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, 0);
int maxFlag = amount + 1;
for (int i = 1; i <= amount; i++) {
dp[i] = maxFlag;
}
for (int i = 1; i <= coins.size(); i++) {
for (int money = 1; money <= amount; money++) {
if (coins[i - 1] > money) {
dp[money] = dp[money];
} else {
dp[money] = min(dp[money], 1 + dp[money - coins[i - 1]]);
}
}
}
if (dp[amount] == maxFlag) {
return -1;
}
return dp[amount];
}
};
总结
动态规划:从最小子问题的解开始,迭代地构建更大子问题的解,直 至得到原问题的解;无后效性是动态规划能够有效解决问题的重要特性之一,其定义为:给定一个确定的状态,它的未来发展只 与当前状态有关,而与过去经历的所有状态无关。
1. 如何判断问题是否为动态规划:
总的来说,如果一个问题包含重叠子问题、最优子结构,并满足无后效性,那么它通常适合用动态规划求解。 然而,我们很难从问题描述中直接提取出这些特性。因此我们通常会放宽条件,先观察问题是否适合使用回 溯(穷举)解决。适合用回溯解决的问题通常满足“决策树模型”,这种问题可以使用树形结构来描述,其中每一个节点代表 一个决策,每一条路径代表一个决策序列。换句话说,如果问题包含明确的决策概念,并且解是通过一系列决策产生的,那么它就满足决策树模型,通常可以使用回溯来解决。 在此基础上,动态规划问题还有一些判断的“加分项”。
‧ 问题包含最大(小)或最多(少)等最优化描述。
‧ 问题的状态能够使用一个列表、多维矩阵或树来表示,并且一个状态与其周围的状态存在递推关系。
相应地,也存在一些“减分项”。
‧ 问题的目标是找出所有可能的解决方案,而不是找出最优解。
‧ 问题描述中有明显的排列组合的特征,需要返回具体的多个方案。
如果一个问题满足决策树模型,并具有较为明显的“加分项”,我们就可以假设它是一个动态规划问题,并在 求解过程中验证它。
2. 问题求解步骤:描述决策,定义状态,建立 dp 表,推导状态转移方程,确定边界条件等。
回溯
1. 思路:类似于DFS,每次遍历需要把路径上的值记录到path,每次退出当前节点后需要把该节点存在path的值弹出。
2. 框架:(1)模拟画出树结构,写出每轮保存值后path值对应多少(2)确定递归终止条件:一般是path的长度等于预定长度,或者遍历完总对象(3)写backtracking函数
3. 分类:遍历对象是否可以重复访问,可用visit记录,或者从上次访问对象顺序往后访问,则用index做参数。
46. 全排列
题目:给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
//终止条件:ans长度等于n
//遍历对象:每次从除自身外的遍历,需要用到visit做记录,退出时重置false
class Solution {
vector<vector<int>> res;
vector<int> path;
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> visit(nums.size(),false);
backtracking(nums, visit);
return res;
}
void backtracking(vector<int>& nums, vector<bool>& visit) {
if(path.size()==nums.size()){
res.emplace_back(path);
return;
}
for(int i = 0; i < nums.size(); i++) {
if(visit[i]) {
continue;
}
path.emplace_back(nums[i]);
visit[i] = true;
backtracking(nums,visit);
visit[i] = false;
path.pop_back();
}
}
};
78. 子集
题目:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
//终止条件:for循环天然实现终止
//剪枝:传递参数index,每次只能遍历i+1之后的元素
class Solution {
vector<vector<int>> res;
vector<int> path;
public:
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums, 0);
return res;
}
void backtracking(vector<int>& nums,int index) {
res.emplace_back(path);
for(int i = index; i < nums.size(); i++) {
path.emplace_back(nums[i]);
backtracking(nums, i+1);
path.pop_back();
}
}
};
17. 电话号码的字母组合
题目:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
class Solution {
public:
vector<string> ans;
string path;
unordered_map<char, string> map;
vector<string> letterCombinations(string digits) {
map.insert({'2', "abc"});
map.insert({'3', "def"});
map.insert({'4', "ghi"});
map.insert({'5', "jkl"});
map.insert({'6', "mno"});
map.insert({'7', "pqrs"});
map.insert({'8', "tuv"});
map.insert({'9', "wxyz"});
if(digits.size()==0) {
return {};
}
backtracking(digits, 0);
return ans;
}
void backtracking(string digits, int index) {
if (index == digits.size()) {
ans.push_back(path);
return;
}
string letter = map[digits[index]];
for (int i = 0 ; i < letter.size();i++) {
path.push_back(letter[i]);
backtracking(digits, index+1);
path.pop_back();
}
}
};
39. 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
class Solution {
public:
//边界条件:ans等于target则加入res,大于则不加入,但终止回溯,小于则继续回溯
//回溯前ans加上当前值, 回溯后ans减去当前值,循环下一个。
int ans;
vector<int> tempRes;
vector<vector<int>> res;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates, target,0);
return res;
}
void backtracking(vector<int>& candidates,int target, int index) {
if(ans > target) {
return;
}
if(ans == target) {
res.push_back(tempRes);
return;
}
for(int i = index; i < candidates.size(); i++) {
ans = ans+candidates[i];
tempRes.push_back(candidates[i]);
backtracking(candidates, target, i);
tempRes.pop_back();
ans = ans-candidates[i];
}
}
};
22. 括号生成
题目:数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
// 限制条件,(1)左括号小于n个(2)右括号小于n个且小于等于左括号个数
// 回溯终止条件:ans.size() == 2n
// 遍历条件与限制条件相同。
class Solution {
public:
string ans;
vector<string> res;
vector<string> generateParenthesis(int n) {
backtracking(n, 0, 0);
return res;
}
void backtracking(int n, int lNum, int rNum) {
if (ans.size() == 2 * n) {
res.push_back(ans);
return;
}
//下面的for循环和两个并列if等价
// if (lNum < n) {
// ans.append("(");
// backtracking(n, lNum + 1, rNum);
// ans.erase(ans.end() - 1, ans.end());
// }
// if (rNum < n && rNum < lNum) {
// ans.append(")");
// backtracking(n, lNum, rNum + 1);
// ans.erase(ans.end() - 1, ans.end());
// }
for (int i = 0; i < 2; i++) {
if (i==0 && lNum < n) {
ans.append("(");
backtracking(n, lNum + 1, rNum);
ans.erase(ans.end()-1,ans.end());
}
if(i == 1 && rNum < n && rNum < lNum) {
ans.append(")");
backtracking(n, lNum, rNum+1);
ans.erase(ans.end()-1,ans.end());
}
}
}
};