int i = 0, j = 1;
),将不重复的元素向左移动当问题要讨论的情况较小时,可以使用if或case条件分支
。
情况较多时,用状态机模型,每个时刻都有一个状态s1
,然后根据输入值in_Val
转移到下一状态s2
,找出所有状态转换关系
① 建立状态表,可以通过hash表,如unordered_map
或用一个二维数组也行
② 根据当前不同的状态,确定下一状态的函数get_s()
③ 对不同的状态进行处理 函数 get()
max(root->left, root->right) + 1;
// priority_queue 默认 为 大顶堆
priority_queue <int,vector<int>,greater<int> > q; // 升序队列,小顶堆
priority_queue <int,vector<int>,less<int> >q; // 降序队列,大顶堆
//greater和less是std实现的两个仿函数
// 需要注意的是,如果使用less<int>和greater<int>,需要头文件:#include
来源微信公众号labuladong
回溯定义:相当于遍历整个决策树,穷举出所有可能的结果,深度优先算法(DFS,Depth First Search)每个分支深入到底
def backtrack(路径, 选择列表):
if 满足结束条件:
result.insert(路径)
return;
// 回溯的核心模块
for 选择 in 选择列表
#做选择
将选择从列表中移除
路径.insert(选择);
backtrack(路径, 选择列表)
#撤销选择
路径.remove(选择);
将该选择再加入选择列表
常见题型:全排列,N皇后,以及组合和子集问题,注意要添加判断避免访问同一节点
// 寻找不重复数组中 所有的子集
vector<vector<int>> res;
vector<vector<int>> subset(vector<int>& nums) {
vector<int> path;
backtrack(nums, 0, path);
return res;
}
void backtrack(vector<int>& nums, int start, vector<int>& path) {
res.push_back(path); // 每次将path中的结果添加到res中
for(int i = start; i < nums.size(); i++) {
// start开始,排除已选择过的数字
path.push_back(nums[i]); // 做选择
backtrack(nums, i+1, path); // 开始回溯
path.pop_back(); // 取消选择
}
}
广度优先算法(Breadth-First Search,BFS)要找从Start到target的最短路径,空间复杂度比DFS高;
一般利用队列,每次将一个节点周围的所有节点加入队列。
// BFS框架
int BFS(TreeNode* start, TreeNode* target) {
queue<TreeNode* > q;
set<TreeNode* > visited; // 避免走回头路
q.push(start); // 将起点加入队列
visited.insert(start);
int step; // 记录扩散步数
while(!q.empty()) {
for(int i = 0; i < q.size(); i++) {
TreeNode* tmp = q.front(); q.pop();
if(tmp == target) return step; // 判断是否到达终点
for(TreeNode* x : tmp.adj()) {
// 将当前节点tmp的 相邻节点加入队列
if(visited.count(x) == 0) {
q.push(x); visited.insert(x);}
}
}
step++; // 更新步数
}
return step;
}
算法大致逻辑如下
int left = 0, right = 0;
while (right < s.size()) {
// 增大窗口
window.add(s[right]);
right++;
while (window needs shrink) {
// 缩小窗口
window.remove(s[left]);
left++;
}
}
滑动窗口的时间复杂度为O(n),比较高效
最小覆盖子串解法:needs和window相当于计数器,分别记录T中字符出现次数和「窗口」中的相应字符的出现次数。
思路:如果一个字符进入窗口,应该增加window计数器;如果一个字符将移出窗口的时候,应该减少window计数器;当valid满足need时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
string minWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
// 记录最小覆盖子串的起始索引及长度
int start = 0, len = INT_MAX;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
if (need.count(c)) {
window[c]++;
if (window[c] == need[c])
valid++;
}
// 判断左侧窗口是否要收缩
while (valid == need.size()) {
// 在这里更新最小覆盖子串
if (right - left < len) {
start = left;
len = right - left;
}
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
if (need.count(d)) {
if (window[d] == need[d])
valid--;
window[d]--;
}
}
}
// 返回最小覆盖子串
return len == INT_MAX ? "" : s.substr(start, len);
}
动态规划般用于求最值,如“最小增长子序列”;关键是穷举,但是存在着重叠子序列,利用【DP table】避免重复的部分
框架:「状态」,有两个,也就是说我们需要一个二维dp数组,一维表示可选择的物品,一维表示背包的容量。
dp[i][w]
的定义如下:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值是dp[i][w]
。
// 0-1背包问题:物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。
int dp[N+1][W+1]
dp[0][..] = 0
dp[..][0] = 0
for i in [1..N]:
for w in [1..W]:
dp[i][w] = max(
把物品 i 装进背包,
不把物品 i 装进背包
)
return dp[N][W]
// C++代码
int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
// vector 全填入 0,base case 已初始化
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; i++) {
for (int w = 1; w <= W; w++) {
if (w - wt[i-1] < 0) {
// 当前背包容量装不下,只能选择不装入背包
dp[i][w] = dp[i - 1][w];
} else {
// 装入或者不装入背包,择优
dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1],
dp[i - 1][w]);
}
}
}
return dp[N][W];
}
递归需要明确递归函数的定义/功能
// 递归方法的基本代码
ListNode* reverse(ListNode* head) {
// 反转整个链表
if(head->next == NULL) return head; // 只有一个节点时,反转之后还是自己
ListNode* last = reverse(head->next);
head->next->next = head;
head->nxet = NULL;
return last; // 返回反转之后的头结点last,head已经变成尾结点了
}
// 反转链表的一部分 区间 [m, n]之间的链表
ListNode* sucessor = NULL;
ListNode* reverseBetween(ListNode* head, int m, int n) {
if( m == 1) {
return reverseN(head, n); // 到达第m个节点后,相当于反转前n-m个节点
}
head->next = reverseBetween(head->next, m-1, n-1); // 将头结点移动到第m个节点
return head;
}
ListNode* reverseN(ListNode* head, int n) {
// 反转链表的前N个节点
if( n == 1) {
sucessor = head->next; // 后继节点记录第n+1个节点
return head;
}
ListNode* last = reverseN(head, n-1);
head->next->next = head;
head->next = sucessor; // 反转之后的head节点 与 第n+1个节点相连
return last;
}
判断回文链表的思路:从两端向中间收缩
方法:① 构造一条新的反转链表; ②利用链表的后序遍历,用栈结构;③ 利用快慢指针,找到链表的中点,反转后半部分,可以将空间复杂度降为O(1)
动态规划:一般用于求最值,如“最小增长子序列”;关键是穷举,但是存在着重叠子序列,利用【DP table】避免重复的部分
三要素:重叠子问题、最优子结构、状态转移方程
最优子结构:只有当子问题相互独立时,才有最优子结构
状态转移方程:当前状态dp[i]与其他状态的关系