链表
- 部分操作需要设置虚拟头指针来方便循环
- 快慢指针
反转链表
- 通过双指针来控制链表的逆置,关键在于指针的初始化和循环中逆置的方式。需要用两个指针分别指向当前节点及前驱。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre=NULL;
ListNode* cur=head;
while(cur){ //当前指针指向不为空时即,还存在需要逆置的结点时
ListNode* temp=cur->next; //存储原本的后继结点,防止因改变指针丢失
cur->next=pre;
pre=cur;
cur=temp;
}
return pre;
}
};
- 根据双指针法写递归时,递归入口参数即为while循环中更改的两个指针,主函数中传入的参数即为双指针初始化形式,而递归退出条件即为链表遍历结束。
class Solution {
public:
ListNode* reverse(ListNode *pre,ListNode* cur){
if(cur==NULL) return pre;
ListNode* temp=cur->next;
cur->next=pre;
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head) {
return reverse(NULL,head);
}
};
环形链表思路非常重要
哈希表
基础知识
- 三种哈希方法:数组,set,map
- 当我们要使用集合时,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要有序,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset
- 当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要想到哈希法。
字母异位词
- 思路基本相同,只需找到对应的映射,使用数组或者unordered_map均可
求两个数组的交集
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
multiset<int> flag(nums1.begin(),nums1.end()); //允许Key重复出现因此使用Multiset
vector<int> res;
for(auto p:nums2){ //遍历数组Nums2
if(flag.find(p)!=flag.end()){ //set关联容器的查找操作
res.push_back(p);
flag.erase(flag.find(p)); //关联容器的删除操作
}
}
return res;
}
};
- set的查找操作
set.find()
未找到则返回end,找到返回迭代器 - set的删除操作
set.erase(x)
。有两种重载函数,第一种删除set中所有x的元素。第二种传入迭代器x,仅删除指定位置的元素
滑动窗口
- 适合用于求连续数组的符合规则的子数组,外层for循环右指针添加元素,内层while循环左指针删除元素。
求最小连续窗口
模板
for (右指针在给定数组范围内循环) {
将元素加入滑动窗口;
窗口内计数++;
while (滑动窗口内符合最小规则) {
min最小值和滑动窗口长度;
收缩滑动窗口;
}
}
长度最小的子数组
最小覆盖子串
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> dirs, dirt; //记录
for (int i = 0; i < t.size(); i++) {
dirt[t[i]]++;
}
int i = 0;
string res;
int cnt = 0;
for (int j = 0; j < s.size(); j++) {
dirs[s[j]]++;
if (dirs[s[j]] <= dirt[s[j]]) cnt++; //是一个新的元素
while (dirs[s[i]] > dirt[s[i]]) { //收缩窗口
dirs[s[i++]]--;
}
if (cnt==t.size()) { //窗口内满足条件
//再取最小子串
if (res.empty() || res.size() > j - i + 1) {
res = s.substr(i, j - i + 1);
}
}
}
return res;
}
};
- 使用哈希表统计字母出现情况
- 且在加入元素后需要统计是否满足模式串的内容
求最大连续窗口
模板
for (右指针在给定数组范围内循环) {
if (是否算作新元素) {
规定++;
将元素加入滑动窗口;
}
while (滑动窗口内不符合规则) {
收缩滑动窗口
}
max取最大值和滑动窗口长度
}
水果成篮
class Solution {
public:
int totalFruit(vector<int>& fruits) {
unordered_map<int ,int> box; //哈希表 无序 不能重复 不可修改
int cnt=0; //记录篮子中的种类数目
int i=0;
int bodary=0;
for(int j=0;j<fruits.size();j++){
if(box[fruits[j]]++==0){ //该元素是一个新元素
cnt++;
}
while(cnt>2){ //篮子内不止两种元素时 收缩窗口
box[fruits[i]]--;
if(box[fruits[i]]==0){ //窗口收缩完成
cnt--;
}
i++;
}
bodary=max(bodary,j-i+1); //滑动窗口内取最大值
}
return bodary;
}
};
双指针法
- 思路与滑动窗口中有很多重复,两者可以结合使用
通过前后两个指针向中间逼近,在一个for循环下完成两个for循环的工作。
N数之和问题
- 将指针与滑动窗口放缩思想结合更容易理解
- 基础为三数之和,3+数添加一层for循环即可
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(),nums.end());
for(int i=0;i<nums.size();i++){
if(nums[i]>0){ //剪枝优化可略去
return res;
}
if(i>0&&nums[i]==nums[i-1]){ //去重
continue;
}
int left=i+1;
int right=nums.size()-1;
while(right>left){ //滑动窗口,在该窗口内判断条件
if(nums[i]+nums[left]+nums[right]>0){
right--;
}else if(nums[i]+nums[left]+nums[right]<0){
left++;
}else {
res.push_back(vector<int>{nums[i],nums[left],nums[right]});
//两个while循环均用来去重
while(right>left&&nums[right]==nums[right-1]){
right--;
}
while(right>left&&nums[left]==nums[left+1]){
left++;
}
right--;
left++;
}
}
}
return res;
}
};
//多添加一层循环加一个指针,之后继续按照三数之和来讨论,以此类推
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
//还可以剪枝优化
if (i > 0 && nums[i] == nums[i - 1]) { //a去重
continue;
}
//转换成三数之和的形式
auto temp = target - nums[i];
for (int j = i + 1; j < nums.size(); j++) {
if (j!=i+1&&nums[j] == nums[j - 1]) { //a去重
continue;
}
int left = j + 1;
int right = nums.size() - 1;
while (right > left) {
if ((long)nums[j] + nums[left] + nums[right] > temp) {
right--;
}
else if ((long)nums[j] + nums[left] + nums[right] < temp) {
left++;
}
else {
res.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right] });
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left+1]) left++;
left++;
right--;
}
}
}
}
return res;
}
};
模拟
- 控制每次循环的量不变,如区间的选择,每次应保持一致找出规律
字符串处理
- 使用
getline(cin,s)
函数来获取字符串时,仅当输入换行符才会停止 - 当在处理时,规定条件可逆向思考。如:要求删除指定条件可转化为保留满足条件的字符串;
字符串反转
KMP算法
- 通过计算部分匹配值构造Next数组
大小写转换
- 使用
#include <cctype>
函数库包含着isupper(c) islower (c)
函数判断字符是否为大小写,然后通过c=toupper(c) 或 c= tolower(c)
来转换大小写 - 判断语句分类讨论
子串输出
- 使用函数
string& insert(int pos, int n, char c); //在指定位置插入n个字符c
string substr(int pos = 0, int n = npos) const; //返回由pos开始的n个字符组成的字符串
进制转换
- 从后向前遍历,根据字符串的值和权值相乘来累加,需要用到
#include <cctype>
库的大小写判断,以及#include <cmath>
库的pow函数 - 以及一种新的标准输出函数
while(cin >> hex >> res) //hex表示读入十六进制数
cout << dec << res << endl; //dec表示输出十进制数
字符串分类问题
错误记录
- 使用了string查找函数
rfind(c)
从后向前查找c的位置 map<string,int> p
来方便管理关键字与键值的关系。其中的直接使用字符串下标来访问以及存储语法非常好用,需要学会deq<string> q
的双端数组来限定所存储元素个数
字符串转换
- 前两步合并与提取很简单
- 第三步的转换采用了map容器,列出字典来转化相应字符。其中使用了关联容器的
p.find(x)
函数来在map中查找x元素,若不存在则返回p.end()
该函数主要用在查找map中是否含有某元素的关系且不需要插入新的元素的情况。若使用下标来查找,会自动插入新元素
字符串排序
按指定规则来排序字母
- 本想重载sort函数中的排序方法函数,但能力不足想不出来,唉
- 双层循环,外层基于字母排序遍历26个字母,内层遍历字符串。此时可以保证相同英文字母的稳定排序,之后将排好序的字母插入新的字符串。最后重新遍历原字符串,遇到字母输出新字符串内容,否则输出原字符串。这是一种用空间换时间的方法。
回溯算法
- 通过递归,将大问题划分为小问题后,再通过回溯(类似出栈操作)来尝试同一前置类下的其他序列。解决组合、切割、子集、排列、棋盘问题
- 注意可以剪枝优化
模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点; //判断该种组合是否符合题目要求
backtracking(路径,选择列表); // 将该种组合向下递归
回溯,撤销处理结果 //同时撤
}
}
组合问题
数字组合
class Solution {
public:
vector<vector<int>> res;
vector<int> path; //已经遍历的路径
void backtracking(int n, int k,int Index) { //k为需要遍历的层数
if (k == path.size()) {
res.push_back(path);
return;
}
for (int i = Index; i <= n; i++) {
path.push_back(i);
backtracking(n, k , i + 1); //向下层遍历
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
res.clear();
backtracking(n, k, 1);
return res;
}
};
- 同样看作树
电话号码的组合
class Solution {
public:
vector<string> result;
string temp;
void backtracking(vector<string> p, int high) {
if (high == p.size()) {
result.push_back(temp);
return;
}
for (int i = 0; i < p[high].size(); i++) {
temp.push_back(p[high][i]); //在组合结果中插入新的字符
backtracking(p, high + 1); //递归下一层
temp.pop_back(); //回溯
}
}
vector<string> letterCombinations(string digits) {
vector<string> p;
vector<string> dir = { "abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
for (int i = 0; i < digits.size(); i++) { //将数字转化成字符串集合
p.push_back(dir[digits[i]-'2']);
}
result.clear();
if (digits.size() == 0) { //空串处理
return result;
}
backtracking(p, 0);
return result;
}
};
组合总和
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& candidates,int index,int target,int sum){
if(sum==target){
res.push_back(path);
return;
}
for(int i=index;i<candidates.size()&&sum+candidates[i]<=target;i++){ //剪枝优化
sum+=candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,i,target,sum);
sum-=candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
res.clear();
sort(candidates.begin(), candidates.end()); // for循环中加入提前判断语句,则需要排序
backtracking(candidates,0,target,0);
return res;
}
};
- 在本题中元素可以重复则传入下标不用+1,运用排序将所给元素序列升序,在for循环中加入target和sum的判断语句,就可以减少下一层的无用递归
组合加上重复性问题
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& candidates,int index,int target,int sum){
if(sum==target){
res.push_back(path);
return;
}
for(int i=index;i<candidates.size()&&sum+candidates[i]<=target;i++){ //剪枝优化
if(i>index&&candidates[i]==candidates[i-1]) continue; //同一层不能重复取相同元素
sum+=candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,i+1,target,sum);
sum-=candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
res.clear();
sort(candidates.begin(), candidates.end()); // for循环中加入提前判断语句,则需要排序
backtracking(candidates,0,target,0);
return res;
}
};
if(i>index&&candidates[i]==candidates[i-1]) continue;
该行语句来控制组合重复性问题,同一层中相同元素不可以重复选取
切割问题
- 组合是判断for遍历的下标之前的字符串,而切割是判断下标之后的字符串
分割回文串
class Solution {
public:
vector<vector<string>> res;
vector<string> path;
void backtracking(string s,int index){
if(index>=s.size()){
res.push_back(path);
return;
}
for(int i=index;i<s.size();i++){
if(isValid(s,index,i)) { //是回文串则在此切割,向下一个字符遍历
path.push_back(s.substr(index,i-index+1));
backtracking(s,i+1);
path.pop_back();
}
}
}
bool isValid(string str,int index,int i){ //判断区间index~i之间的字符是不是回文串
for(;index<i;i--,index++){
if(str[index]!=str[i]){
return false;
}
}
return true;
}
vector<vector<string>> partition(string s) {
string temp;
backtracking(s,0);
return res;
}
};
IP地址分割
class Solution {
public:
vector<string> res;
string path;
void backtracking(string s,int index,int k) {
if(k==4){ //当前已经分割出了四段
if (index==s.size()) { //整个字符串都已经遍历完,则满足所有条件可入结果
res.push_back(path);
}
return;
}
string temp;
for (int i = index; i < s.size(); i++) {
if (isValid(s, index, i)) { //判断待分割合法性
temp = s.substr(index, i - index + 1);
path+=temp; //插入分割字符
if (k != 3) { //还没分成四份,则加.
path += '.';
}
backtracking(s, i + 1, k + 1); //继续下层分割
if (k != 3) { //回溯,
path.pop_back();
}
path = path.erase(path.size() - (i - index + 1), i - index + 1);
}else{
break; //当前已经不合法,直接退出
}
}
}
bool isValid(string s, int index, int i) { //判断从index——i的字符串是否符合条件
if (s[index] == '0'&&index!=i) return false; //前导0的情况
int sum = 0;
int mi = 0;
for (; i >= index; i--,mi++) {
sum += (s[i] - '0') * pow(10, mi);
}
if (sum <= 255) {
return true;
}
else {
return false;
}
}
vector<string> restoreIpAddresses(string s) {
res.clear();
backtracking(s, 0, 0);
return res;
}
};
- 加强版的分割串,注意边界条件以及退出遍历的条件
子集问题
子集
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums, int index) {
res.push_back(path);
if (index == nums.size()) { //可省略,for循环中已经保证index<nums.size()
return;
}
for (int i = index; i < nums.size(); i++) {
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
res.clear();
backtracking(nums, 0);
return res;
}
};
- 相比于前两种较简单,完全套模板,注意每一层都保存结果即可
子集 II
- 在以上模板的基础上,添加上面组合中解决重复性问题的判断语句即可
递增子序列
- 在不对原有序列排序的情况下,解决每一层的重复性问题
if(used[nums[i]+100]==1) continue;
该题采用标记数组来哈希,还可以使用unorder_set,但占用空间较大
排列问题
全排列
- 不用控制每一层重复性,而需要控制每一棵子树中不能重复,在参数中传入一个标记数组,标记已经访问过的下标即可
排列中的重复性
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int> nums, vector<bool>& flag) {
if (nums.size() == path.size()) {
res.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++) {
if (flag[i] == true) continue;
if(i>0&&nums[i]==nums[i-1]&&flag[i-1]==false) continue;
flag[i] = true;
path.push_back(nums[i]);
backtracking(nums, flag);
path.pop_back();
flag[i] = false;
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> flag(nums.size(), false);
sort(nums.begin(), nums.end());
backtracking(nums, flag);
return res;
}
};
flag[i-1]==false
语句代表同一层上,前一位已经使用过了- 使用
i>0&&nums[i]==nums[i-1]
来判断重复性,需要排序
棋盘问题
- 把棋盘看作一个二维数组,用树来理解,对于一个x*y的棋盘,树的深度就是棋盘的行数,数的宽度就是棋盘的列数,每个结点代表棋盘中选择的每一处
N皇后问题
vector<vector<string>> result; //p中的每个元素都记录不同的棋盘
void backtracking(vector<string>& board, int n, int row) { //传入一种棋盘的情况,给定的棋盘行数n,该层递归遍历的棋盘行row
if (row == n) { //终止条件:遍历到最后一行,即所有类型的组合都已经尝试完
result.push_back(board); //在结果中加入该种情况
return;
}
for (int col = 0; col < n; col++) { //遍历该行中的每一个元素,来尝试不同组合情况
if (isValid(board, n, row, col)) {
board[row][col] = 'Q';
backtracking(board, n, row + 1); //该处满足情况,可以继续向下层尝试
board[row][col] = '.'; //回溯该处,并继续for循环防止干扰组合结果
}
}
}
bool isValid(vector<string>& board, int n, int row, int col) { //判断若在棋盘的[row][col]处放上皇后,会出错吗
//不需要判断同一行是否有,因为在递归中的每一层for循环中,都会回溯使得同一行一定都是.
//判断同一列
for (int i = 0; i < n; i++) {
if (board[i][col] == 'Q') { //只要该列不存在其他Q
return false;
}
}
//判断左上角方向
for (int i = row, j = col; i >= 0 && j >= 0; i--, j--) { //向棋盘左上方遍历即可
if (board[i][j] == 'Q') {
return false;
}
}
//判断右上角方向
for (int i = row, j = col; i >= 0 && j < n; i--, j++) { //向棋盘右上方遍历即可
if (board[i][j] == 'Q') {
return false;
}
}
return true;
}
哈希排序
- 可以采用数组,set容器,map容器来解决查找对应的问题
动态规划
对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
- 确定dp数组以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组