C++定义链表节点方式
//单链表
struct ListNode {
int val;//节点上存储的元素
ListNode *next;//指向下一个节点的指针
ListNode(int x):val(x),next(NULL){}//节点构造函数,C++默认构造函数不会初始化任何成员变量
}
一般哈希表都是用来快速判断一个元素是否出现在集合里
涉及到哈希函数映射关系:
哈希碰撞
常见三种哈希结构
当我们要使用集合来解决哈希问题的时候:
当我们遇到要快速判断一个元素是否出现集合里面的时候,就要考虑哈希表,哈希表牺牲了空间换取了时间
使用set占用的空间要比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算
四个问题:
C++中stack是容器吗?
stack不是容器,而是容器适配器
我们使用的stack是属于哪个版本的STL?
我们使用的STL中的stack是如何实现的?
底层容器完成所有的工作,对外提供统一接口
stack提供迭代器来遍历stack空间吗?
不提供
栈和队列是STL(C++标准库)里面的两个数据结构。C++标准库是有多个版本的,要知道我们使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。
三个最普遍的STL版本:
栈提供push和pop等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器,不像set和map提供迭代器来遍历所有元素。
栈是以底层容器完成其所有工作的,对外提供统一的接口,底层容器是可插拔的(也就是我们可以控制使用哪种容器来实现栈的功能)
STL中的栈不是容器,而是容器适配器
栈的底层实现可以是vector、deque、list都是可以的,主要是数组和链表。
系统输出异常Segmentation fault通常是栈溢出的错误
逆波兰表达式:是一种后缀表达式,后缀就是指运算符总是放在和它相关的操作数之后。
优先队列 priority_queue
优先队列是一种容器适配器,它的第一个元素是在是包含的元素中最大的元素。
单调队列
单调递减或者单调递增的队列。
单调队列不是对窗口里面的数进行排序。
堆是一棵完全二叉树,树中每个节点的值都不小于(或不大于)其左右孩子的值。
二叉树的种类
二叉搜索树
平衡二叉搜索树
二叉树的存储方式
二叉树的遍历方式
二叉树的定义
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x):val(x), left(NULL), right(NULL) {}
};
二叉树的递归遍历
实际项目开发过程中,我们要尽量避免递归,因为项目代码参数、调用关系都比较复杂,不容易控制递归深度,甚至会栈溢出。
递归算法三要素:(以前序遍历为例)
确定递归函数的参数和返回值
void traversal(TreeNode* cur, vector& vec)
确定终止条件
if (cur == nullptr) return;
确定单层递归的逻辑
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
前序遍历
class Solution {
public:
void traversal(TreeNode* cur, vector& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
vector preorderTraversal(TreeNode* root) {
vector result;
traversal(root, result);
return result;
}
};
二叉树的迭代遍历
迭代法中序遍历
class Solution {
public:
vector inorderTraversal(TreeNode* root) {
vector result;
stack st;
TreeNode* cur = root;
while (cur != NULL || !st.empty()) {
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
cur = cur->left; // 左
} else {
cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
st.pop();
result.push_back(cur->val); // 中
cur = cur->right; // 右
}
}
return result;
}
};
迭代法前序遍历
class Solution {
public:
vector preorderTraversal(TreeNode* root) {
stack st;
vector result;
if (root == NULL) return result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top(); // 中
st.pop();
result.push_back(node->val);
if (node->right) st.push(node->right); // 右(空节点不入栈)
if (node->left) st.push(node->left); // 左(空节点不入栈)
}
return result;
}
};
迭代法后序遍历
class Solution {
public:
vector postorderTraversal(TreeNode* root) {
stack st;
vector result;
if (root == NULL) return result;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
result.push_back(node->val);
if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
if (node->right) st.push(node->right); // 空节点不入栈
}
reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
return result;
}
};
层序遍历
一层一层的去遍历二叉树,==通过队列实现。==队列先进先出,符合一层一层遍历的逻辑。
上面前中后遍历是用栈先进后出模拟深度优先遍历。(也就是递归逻辑)
层序遍历就是广度优先遍历
二叉树层序遍历模板
class Solution {
public:
vector> levelOrder(TreeNode* root) {
queue que;
if (root != NULL) que.push(root);
vector> result;
while (!que.empty()) {
int size = que.size();
vector vec;
// 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(vec);
}
return result;
}
};
翻转二叉树
深度和高度的区别
求二叉树的高度,必然要后序遍历
递归法,后序遍历求二叉树的高度,一定要掌握
// 递归代码
class Solution {
public:
bool isBalanced(TreeNode* root) {
return getDepth(root) == -1 ? false : true;
}
int getDepth(TreeNode* node) {
if (node == nullptr) return 0;
int leftdepth = getDepth(node -> left);
if (leftdepth == -1) return -1; // 说明左子树已经不是二叉平衡树
int rightdepth = getDepth(node -> right);
if (rightdepth == -1) return -1; // 说明右子树已经不是二叉平衡树
return abs(rightdepth - leftdepth) > 1 ? -1 : 1 + max(leftdepth, rightdepth);
}
};
左叶子的定义
左节点不为空
左节点没有左右孩子
if (node->left != NULL && node->left->left == NULL && node->left->right == NULL) {
左叶子节点处理逻辑
}
二叉树递归,递归函数什么时候需要返回值,什么时候需要返回值,什么时候不需要返回值?
从中序和后序遍历序列构造二叉树
树的还原过程描述
从中序和后序遍历序列构造二叉树完整代码
// 递归法
class Solution {
public:
TreeNode* buildTree(vector& inorder, vector& postorder) {
int size = inorder.size();
// 将中序遍历的值存入哈希表,方便在中序遍历中找到根节点
for (int i = 0; i < size; i++) {
inorder_vec[inorder[i]] = i;
}
post_vec = postorder;
return buildTree(0, size - 1, 0, size - 1);
}
TreeNode* buildTree(int i_left, int i_right, int p_left, int p_right) {
if (i_left > i_right || p_left > p_right) return nullptr;
int root_val = post_vec[p_right];//后序遍历最后一个元素是根节点
int root_pos = inorder_vec[root_val];//根据根节点值在中序遍历中找到根节点的索引
TreeNode* node = new TreeNode(root_val);//使用根节点值创建根节点
node -> left = buildTree(i_left, root_pos - 1, p_left, p_left + root_pos - i_left - 1);//递归构建左子树
node -> right = buildTree(root_pos + 1, i_right, p_left + root_pos - i_left, p_right - 1);//递归构建右子树
return node;
}
private:
unordered_map inorder_vec;
vector post_vec;
};
从前序与中序遍历序列构造二叉树
// 递归法
class Solution {
public:
TreeNode* buildTree(vector& preorder, vector& inorder) {
int size = preorder.size();
pre_vector = preorder;
for (int i = 0; i < size; i++) {
in_map[inorder[i]] = i;
}
return buildTree(0, size - 1, 0, size - 1);
}
TreeNode* buildTree(int p_left, int p_right, int i_left, int i_right) {
if (p_left > p_right && i_left > i_right) return nullptr;
int root_val = pre_vector[p_left];//前序遍历的第一个元素是根节点
int root_pos = in_map[root_val];//根据根节点值在中序遍历中找到根节点的索引
TreeNode* node = new TreeNode(root_val);
node -> left = buildTree(p_left + 1, root_pos + p_left - i_left, i_left, root_pos - 1);//递归构建左子树
node -> right = buildTree(root_pos + p_left - i_left + 1, p_right, root_pos + 1, i_right);//递归构建右子树
return node;
}
private:
vector pre_vector;
unordered_map in_map;
};
一提到二叉树遍历的迭代法
验证二叉搜索树
二叉树、二叉平衡树、完全二叉树、二叉搜索树
动态规划,英文名,Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
动态规划中每一个状态一定是由上一个状态推导出来的。
举个例子:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。
对于刷题,知道动态规划是由前一个状态推导出来的,贪心是局部直接选最优的,就够了。
动态规划五部曲:
做动态规划题目,代码出问题很正常,找问题最好的方法就是把dp数组打印出来,看看究竟是不是按照自己的思路推导的
做动态规划题目,写代码之前一定要把状态转移在dp数组上的具体情况模拟一遍,心中有数,确定最后推出的是想要的结果
动态规划灵魂三问:
动态规划爬楼梯问题拓展,一步一个台阶、两个台阶、三个台阶、直到一步m个台阶,有多少种方法爬到n阶楼顶。
class Solution {
public:
int climbStairs(int n) {
vector dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) { // 把m换成2,就可以AC爬楼梯这道题
if (i - j >= 0) dp[i] += dp[i - j];
}
}
return dp[n];
}
};
背包问题
01背包,01背包问题是重中之重
基本上都是01背包应用方面的题目,也就是需要转换为01背包问题
完全背包,完全背包是对01背包的变化,完全背包的物品数量是无限的
01背包
有N件物品和一个最多能装重量为W的背包
第i件物品的重量为weight[i],得到的价值是value[i]
每件物品只能用一次
求解将哪些物品装进背包里,物品的价值总量最大
举例:
背包最大能容纳的重量是4
物品为:
物品 | 重量 | 价值 |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
背包背的物品最大价值是多少?
这是一个01背包问题:动态规划五部曲
二维数组01背包问题
代码实现:
void test_2_wei_bag_problem1() {
vector weight = {1, 3, 4};
vector value = {15, 20, 30};
int bagWeight = 4;
// 二维数组
vector> dp(weight.size() + 1, vector(bagWeight + 1, 0));
// 初始化
for (int j = weight[0]; j <= bagWeight; j++) {
dp[0][j] = value[0];
}
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
cout << dp[weight.size() - 1][bagWeight] << endl;
}
int main() {
test_2_wei_bag_problem1();
}
一维数组背包问题
把i-1这一层的数据拷贝到i这一层,就可以只用一个一维数组了,也就是一个滚动数组(上一层可以重复利用,直接拷贝到当前层)
确定dp数组的定义
dp[j]表示容量为j的背包,所背的物品价值最大为dp[j]
一维数组的递推公式
可以由两个方向推导来
一维dp数组如何初始化
遍历顺序
倒序遍历,二维数组不会用上一层来覆盖下一层的结果,所以可以从前往后遍历
举例推导dp数组
void test_1_wei_bag_problem() {
vector weight = {1, 3, 4};
vector value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_1_wei_bag_problem();
}