Day 1:
两个栈可以实现队列,一个队列就可实现一个栈。
两个栈实现队列的时候,只有在出队列时 ,如果 “出栈”栈为空,需要有 入栈栈到出栈栈的转移操作。
一个队列实现栈时,每次入栈都需要将队列进行“翻转”(重新全部入队列,最终将队尾移动到对头)。
遇到对时间复杂度有要求的就空间换时间。
INT_MAX 可用来表示正无穷,INT_MIN表示负无穷。
倒序一个链表,优先想到栈(先入后出!)
判断链表有无环,快慢双指针。 画图能够迅速帮你找到解决思路!!!!杨老师!谢谢你 画图列方程
由于链表经常要用前一个值操作后一个值,所以经常要考虑边界值。
对于深度克隆,比如随机链表复制,无向图复制,要想到哈希表。
Day 2:
- 链表一定要考虑边界值啊!!第2次了。链表操作A->next的时候一定要考虑A是否为空;
- 非递减数列两数求和,别想那么多,先给大窗口,然后缩小,
- 双指针,快慢双指针,左右双指针(中心扩张,两边收缩)
- 如果有正负的情况,千万不要自作从聪明,将某个值设为0,不然比大小时候会出现bug。
- 如果说写出的代码分为好多种情况,那么思路一定是错了。
- 树的层序遍历需要用到队列。当需要记录层数时,需要用到额外的变量,并用到for循环
- 动态规划问题的第一个性质:重叠子问题 。 设置备忘录。一般是哈希表。数组也可以。如果可以看出数组长度不会超过某个最大值,就可以用数组。但这是个稀疏数组。
Day 3:
回溯代码框架,设最后返回的集合是res,res里面的元素是方案(路径),路径中为节点,第一步我们要搞清,方案是什么,路径是什么,节点又是什么。(比如n皇后问题,方案是整个棋盘,选择是在第row行选择第col个点)所以backtrace的参数就是(棋盘,row)
回溯算法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void backtrace(状态参数)
{
if(设置结束条件)
{
res.push_back(路径);
return;
}
//路径
for(int )
{
//排除不合法选择
if(!is_valid())
continue;
选择();
进入下一层
退选择
}
}其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」?
组合问题,给定数组自带顺序,且答案中[1,2] [2,1]算是相同结果的,甚至可以不用is_valid;因为仔细想一下,比如选择到2,那么1 一定选过了 ,3一定没选过。(自行理解其中奥义很简单)。
求 有重复数据的子集 要先排序。然后
1
2
3
4
5
6
7
8
9
10for(int i = start;i<nums.size();i++)
{
//因为和上一个相同的话, 下一个back_trace(nums,k,i+1)会得到相同的结果
if (i > start && nums[i] == nums[i - 1])
continue;
subresult.push_back(nums[i]);
back_trace(nums,k,i+1);
subresult.pop_back();
}BFS: 将问题抽象为一幅图 , 找到节点 ,以及节点的邻居。那么算法的流程就是从初始节点开始。比如开密码锁,每个节点就是 密码锁当前的数字,那么邻居就是,转动一位得到的数字。
Day 4 :
- 搞不定边界就尽量不要使用双重二分。
- 检查是不是break和continue 用混了!!!!
- 动态寻找最大值最小值的时候要初始化一个结果!不能只声明。
- 集合之间对比,要想到unordered_map
Day 5:
- 股票问题因为给的数组不是有序的,所以不能使用双指针法。
- 动态规划超出时间限制就要反思是否有重叠子问题,设置备忘录解决。
- 自顶向下的动归是递归,有可能会遇到重复子问题,自底向上的动归是迭代,但是空间复杂度高一点。
- 如果动态规划的开始和末尾相互影响。就要分出情况。
- vector.end() 并不是最后一个元素的指针 而是最后一个元素的下一个指针
- 回溯法毕竟是穷举,拥有极高的时间复杂度。如果有其他解法尽量不要用穷举。
- 不该超出时间限制的时候超出时间限制,检查起始条件 ,终止条件,以及变化条件。
Day 6
- 使用双指针时要确保指针移动啊。
- 差分数组要多一位(其实也没必要,得看情况)。
Day 7
- 二分搜索时候左右边界问题别搞错了啊。 左边界查看left==num.size() 右边界查看left-1<0;
- 二分搜索如果没找到不返回特殊值的话,左右边界还是很好写的。
- 转换思路才能二分搜索。比如给定天数,让你求运输船最小承载。这个不好算,那就转换成给出承载,返回最小天数。这就可以用二分了。这个转变思路非常巧妙。
- 无论是升序降序 左边界返回left 右边界返回left-1;
Day 8
- vector erase和遍历数组没有区别,时间复杂度很高。易超出时间限制
- 排序的 比较函数 大于为真是降序 小于为真是升序
- unordered_set 是不支持随机存取的。所以要用unordered_map 与 vector实现随机读取;
- 要分清出子序列与子串的区别,字串必须连续,子序列不必连续;
- 都tm什么时候了还在 犯”=” 和 “==”的错误。现在还非常容易犯小错误
Day 9
- 树的遍历中,如果遍历结果包含着空指针,那么不需要有别的遍历辅助,也能够确定一棵树。
- stoi()可以把字符串转化成为数字。 to_string 可以将数字转化为字符串
- 可以用 map用于关联字符串与节点, set用于检测字符串重复。
Day 10
- 涉及到区间和 那就一定会有前缀和数组!! 而对区间进行统一操作,那就一定会有差分数组?
- 归并排序的一个重要思想,凡是已经在同一部分里的,就一定发生过关系了。只有和另一部分的没有发生过关系。(非常重要的思想)
- 而且如果无序很难,但是发现有序很好做,那么可以去想想归并排序, 或者看到题目中有 数组的性质是, 当i<j
- 前缀和 要思考 有没有
- BST 的中序遍历结果是有序的(升序)。 可以说遇到二叉搜索树,大概率是中序遍历
Day 11
快速排序是 树的前序遍历 归并排序是树的后序遍历
快速排序为了达到稳定的快速,数组要是乱序。函数shuffle() 可以随机打乱
快速排序中
1
2
3
4
5
6while(cur<=right && nums[cur]>=nums[p]) cur++;//自己体会
//而且需要特殊的交换操作
swap(nums[p],nums[cur]);
swap(nums[p+1],nums[cur]);// 交换后这
p+=1;
cur = p+1;srand(time(0)); 生成随机树种子
堆是完全二叉树 ,然后再满足父节点大于等于 或小于等于子节点。所以插入的时候,先插入满足完全二叉树的位置,在满足第二个要求。删除的时候 先交换 再下降。
完全二叉树用数组实现非常方便。 节点 i 的左子节点 2i 右子节点2i+1
优先级队列定义比较函数必须是静态成员函数的?
判断完全二叉树是否是满二叉树,左节点一直找下去,右节点一直找下去,看看高度是否相同。
if 后面如果不是一句话 一定不要忘了加花括号啊
Day 12
图结构表示方法,如果目的不是要迅速判断两节点之间是否有边,那还是使用邻接表方便一点。
拓扑排序 需要额外的数组记录节点的入度;
int 类型的0 1 可以用逻辑运算符操作。
连通性使用 并查集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57class UF {
private:
// 记录连通分量
int count;
// 节点 x 的父节点是 parent[x]
vector<int> parent;
int find_root(int x)
{
// 根节点的 parent[x] == x
if (parent[x] != x) {
parent[x] = find_root(parent[x]);
}
return parent[x];
}
public:
/* 构造函数,n 为图的节点总数 */
UF(int n)
{
// 一开始互不连通 联通分量等于节点数
count = n;
// 父节点指针初始指向自己
//尺寸全部初始化为1
parent.resize(n);
for (int i = 0; i < n; i++)
{
parent[i] = i;
}
}
/* 其他函数 */
void union_(int p, int q)
{
int rootP = find_root(p);
int rootQ = find_root(q);
if (rootP == rootQ)
return;
// 将两棵树合并为一棵 以下二者选一即可
{
parent[rootQ] = rootP;
parent[rootP] = rootQ;
}
count--; // 两个分量合二为一
}
int getcount() {
return count;
}
bool connected(int p, int q)
{
int rootP = find_root(p);
int rootQ = find_root(q);
return rootP == rootQ;
}
};并查集可以建立虚拟头部,从而排除异己。
在二位坐标中控制方向常用方法。
1
vector<vector<int>> dir = {{1,0},{0,1},{-1,0},{0,-1}} //对应下 右 上 左
类的后面要加;
只有判断无向图是否带环时才能用并查集。
克鲁斯卡尔算法 Kruskal 算法 就是无向有权图 按权重大小一次加边;
并查集的时间复杂度是0(1);
无向有权图的最小生成树算法 prim算法需要转换成图的邻接表格式。 Kruskal 不用 ,给的如果是边的集合 就krusal 如果是 graph prim。
Day 13
- dijkstra 算法: 优先级队列里存放边 因为这样可以通过边的权重来知晓
Day 14
前缀树
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52//力扣667
class TrieNode {
public:
int val;
vector<TrieNode *> next;
TrieNode():val(0),next(26) {
for (int i = 0; i < 26; ++i) {
next[i] = nullptr;
}
}
};
class MapSum {
private:
TrieNode * root;
unordered_map<string, int> cnt;
public:
MapSum() {
root = new TrieNode();
}
void insert(string key, int val) {
int delta = val;
//如果之前存在 算出两数值差好做修改
if (cnt.count(key)) {
delta -= cnt[key];
}
cnt[key] = val;
TrieNode * node = root;
for (auto c : key) {
if (node->next[c - 'a'] == nullptr) {
node->next[c - 'a'] = new TrieNode();
}
node = node->next[c - 'a'];
node->val += delta;
}
}
int sum(string prefix) {
TrieNode * node = root;
for (auto c : prefix) {
if (node->next[c - 'a'] == nullptr) {
return 0;
} else {
node = node->next[c - 'a'];
}
}
return node->val;
}
};真不能上来直接写,一定要先想好各种情况。
Day 15
优先级队列设置升序降序,less代表降序 ,greater代表升序
单调栈,要把它想象成,个子高挡住个子矮的。从一个方向上望过去,是单调递增做递减的。
单调栈不一定存放数值,也可以存放索引,反正可以通过索引来找值。
for循环一定要检查是i++ 还是 i—
单调栈 找比自己大的 那栈顶就是栈里最小的。好好理解一下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//力扣503
class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums) {
stack<int> st;
int n = nums.size();
vector<int> result(nums.size());
for(int i =2*n-1;i>=0;i--)
{
//找到第一个大于自己的元素
while(!st.empty() && st.top()<=nums[i%n]) st.pop();
//没找到 那自己就是最大的
if(st.empty()) result[i%n] = -1;
//找到了保存结果
else result[i%n] = st.top();
//把自己放进去
st.push( nums[i%n]);
}
return result;
}
};