数组
二分法
- 注意循环不变量
- 二分法是算法面试中的常考题,需要锻炼自己手撕二分算法的能力
- O(n) 二分法时间复杂度:O(logn)
双指针法
- 通过一个快指针和一个慢指针在一个for循环下完成两个for循环的工作csdn
- O(n^2) 双指针时间复杂度:O(n)
- C++中vector和array区别一定要清楚,vector底层实现是array,所以vector展现出友好的一些都是因为已经包装过了
- 面试题也需要掌握手撕双指针法
滑动窗口
- O(n^2) 滑动窗口时间复杂度:O(n)
- 根据当前子序列和的大小,不断调节子序列的起始位置
链表
链表类型
- 单链表
- 节点只能指向节点的下一个节点
- 双链表
- 每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点
- 既可以向后查询,又可以向前查询
- 循环链表
- 链表首尾相连
链表的定义
-
C++定义链表节点方式
//单链表 struct ListNode { int val;//节点上存储的元素 ListNode *next;//指向下一个节点的指针 ListNode(int x):val(x),next(NULL){}//节点构造函数,C++默认构造函数不会初始化任何成员变量 }
删除节点
- 如上图,只要将C节点的next指针指向E节点就可以了。但在C++中最好手动释放D节点这块内存
- 两种方式
- 直接使用原来的链表进行删除操作
- 设置一个虚拟头节点再进行删除操作
- 移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点的,而头结点没有前一个节点
添加节点
虚拟头结点
- 每次对头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题
链表基本操作
- 获取链表第index个节点的数值
- 在链表最前面插入一个节点
- 在链表最后面插入一个节点
- 在链表第index个节点前插入一个节点
- 删除链表的第index个节点的值
反转链表
- 迭代法和递归法
删除倒数第n个节点
- 虚拟头结点和双指针
链表相交
- 双指针来找到两个链表的交点
环形链表
- 比较难的题,代码简洁
哈希表
-
一般哈希表都是用来快速判断一个元素是否出现在集合里
-
涉及到哈希函数映射关系:
- 比如说学校里学生的名字,可以通过哈希函数映射为哈希表上的索引,然后可以通过查询索引快速知道某同学是否在名单里面。
-
哈希碰撞
- 就是指两个学生的名字映射到同一个索引下面
- 两种解决方法:
- 拉链法:发生冲突的元素被存储在链表里面
- 线性探测法:一定要保证tablesize大于datasize,发生碰撞则向下找一个空位来放置发生冲突的元素
-
常见三种哈希结构
-
数组
- 数组的大小是受限制的,而且如果元素很少,哈希值太大会造成内存空间的浪费
-
set(集合)
- set是一个集合,里面放的元素只能是一个key
std::set和std::multiset的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改
-
map(映射)
- map是一种key、value的存储结构,可以用key保存数值,用value保存数值所在的下表
std::map和std::multimap的底层实现是红黑树,同理,key值也是有序的
-
-
当我们要使用集合来解决哈希问题的时候:
- 先使用unordered_set,它的查询和增删效率最优
- 如果需要集合是有序的,就用set
- 如果要求不仅有序还要有重复数据,就用multiset
-
当我们遇到要快速判断一个元素是否出现集合里面的时候,就要考虑哈希表,哈希表牺牲了空间换取了时间
-
使用set占用的空间要比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算
字符串
- **KMP算法:**主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容。
- 前缀表:起始位置到下表i之前(包括i)的子串中,有多大长度的相同前缀后缀。
- 前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
- 后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。
双指针法
- 双指针可以提高效率,一般是将o(n^2)的时间复杂度降为o(n)。
- 数组篇
- 字符串篇
- 链表篇
- N数之和篇
栈与队列
-
四个问题:
-
C++中stack是容器吗?
stack不是容器,而是容器适配器
-
我们使用的stack是属于哪个版本的STL?
-
我们使用的STL中的stack是如何实现的?
底层容器完成所有的工作,对外提供统一接口
-
stack提供迭代器来遍历stack空间吗?
不提供
-
栈和队列是STL(C++标准库)里面的两个数据结构。C++标准库是有多个版本的,要知道我们使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。
-
三个最普遍的STL版本:
- HP STL 其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。
- P.J.Plauger STL 由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。
- SGI STL 由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。
-
栈提供push和pop等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器,不像set和map提供迭代器来遍历所有元素。
栈是以底层容器完成其所有工作的,对外提供统一的接口,底层容器是可插拔的(也就是我们可以控制使用哪种容器来实现栈的功能)
STL中的栈不是容器,而是容器适配器
栈的底层实现可以是vector、deque、list都是可以的,主要是数组和链表。
-
系统输出异常Segmentation fault通常是栈溢出的错误
-
逆波兰表达式:是一种后缀表达式,后缀就是指运算符总是放在和它相关的操作数之后。
- 平常使用的算式则是一种中缀表达式,(1 + 2) * (3 + 4)
- 逆波兰表达式的写法((1 2 +) (3 4 +) * )
- 去掉括号后表达式无歧义
- 适合栈操作运算:遇到数字则入栈,遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
-
优先队列 priority_queue
优先队列是一种容器适配器,它的第一个元素是在是包含的元素中最大的元素。
-
单调队列
单调递减或者单调递增的队列。
单调队列不是对窗口里面的数进行排序。
-
堆是一棵完全二叉树,树中每个节点的值都不小于(或不大于)其左右孩子的值。
- 其父节点是大于等于左右孩子就是大顶堆。
- 其父节点是小于等于左右孩子就是小顶堆。
二叉树
-
二叉树的种类
- 满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子节点二叉树。
- 定义:一个二叉树,如果每一层的结点数都达到最大值,这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且节点总数是(2^k)-1,则它就是满二叉树。
- 完全二叉树:对于深度为k的,有n个结点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中编号为1至n的结点一一对应。
- 定义:若二叉树的深度为h,除第h层外,其他各层(1~h-1)的结点数都达到最大个数,第h层所有节点都连续集中在最左边,这就是完全二叉树。
- 堆就是一棵完全二叉树
- 满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子节点二叉树。
-
二叉搜索树
- 二叉搜索树是有数值的,二叉搜索树是一个有序树。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左右子树也分别为二叉排序树。
- 二叉搜索树是有数值的,二叉搜索树是一个有序树。
-
平衡二叉搜索树
- 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
- C++中map、set、multimap、multiset的底层实现都是平衡二叉搜索树,具体一点就是红黑树,所以map、set的增删操作时间时间复杂度是logn。
- unordered_map、unordered_set底层实现是哈希表。
-
二叉树的存储方式
- 链式存储:指针
- 顺序存储:数组
-
二叉树的遍历方式
- 深度优先遍历:先往深走,遇到叶子节点再往回走
- 前序遍历(递归法、迭代法)中左右
- 中序遍历(递归法、迭代法)左中右
- 后序遍历(递归法、迭代法)左右中
- 迭代和递归的区别:
- 时间复杂度上迭代和递归差不多
- 空间复杂度上递归开销大一点,因为递归需要系统堆栈存参数返回值等
- 广度优先遍历:一层一层去遍历
- 层序遍历(迭代法)
- 深度优先遍历:先往深走,遇到叶子节点再往回走
-
二叉树的定义
-
struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x):val(x), left(NULL), right(NULL) {} };
-
-
二叉树的递归遍历
实际项目开发过程中,我们要尽量避免递归,因为项目代码参数、调用关系都比较复杂,不容易控制递归深度,甚至会栈溢出。
-
递归算法三要素:(以前序遍历为例)
-
确定递归函数的参数和返回值
void traversal(TreeNode* cur, vector<int>& 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<int>& vec) { if (cur == NULL) return; vec.push_back(cur->val); // 中 traversal(cur->left, vec); // 左 traversal(cur->right, vec); // 右 } vector<int> preorderTraversal(TreeNode* root) { vector<int> result; traversal(root, result); return result; } };
-
-
二叉树的迭代遍历
-
迭代法中序遍历
class Solution { public: vector<int> inorderTraversal(TreeNode* root) { vector<int> result; stack<TreeNode*> 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<int> preorderTraversal(TreeNode* root) { stack<TreeNode*> st; vector<int> 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<int> postorderTraversal(TreeNode* root) { stack<TreeNode*> st; vector<int> 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<vector<int>> levelOrder(TreeNode* root) { queue<TreeNode*> que; if (root != NULL) que.push(root); vector<vector<int>> result; while (!que.empty()) { int size = que.size(); vector<int> 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; } };
-
-
翻转二叉树
- 针对二叉树的问题,解题之前一定要清楚究竟是前中后序遍历,还是层序遍历。
- 针对这道题目,中序遍历是不可以的。
-
深度和高度的区别
- 二叉树结点的深度:从根结点到该结点的最长简单路径边的条数,求深度是从上到下去查,前序遍历
- 二叉树结点的高度:从该结点到叶子结点的最长简单路径边的条数,求高度只能从下到上去查,后序遍历
- 根节点的高度,而根节点的高度就是这颗树的最大深度,所以才可以使用后序遍历。
-
求二叉树的高度,必然要后序遍历
- 明确递归函数的参数和返回值
- 参数:传入节点指针
- 返回值:返回传入节点为根节点树的深度
- 明确终止条件
- 递归过程中,遇到空结点为终止,返回0,表示当前节点为根节点的树高度为0
- 明确单层递归的逻辑
递归法,后序遍历求二叉树的高度,一定要掌握
// 递归代码 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<int>& inorder, vector<int>& 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<int, int> inorder_vec; vector<int> post_vec; };
-
从前序与中序遍历序列构造二叉树
-
// 递归法 class Solution { public: TreeNode* buildTree(vector<int>& preorder, vector<int>& 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<int> pre_vector; unordered_map<int, int> in_map; };
-
-
一提到二叉树遍历的迭代法
- 使用栈来模拟深度遍历
- 使用队列来模拟广度遍历
- 对于二叉搜索树,具有特殊性,可以不使用辅助栈或者队列就可以写出迭代法
-
验证二叉搜索树
- 中序遍历下,二叉搜索树节点的数值是有序序列
- 验证二叉搜索树就相当于判断一个序列是不是递增的
- 三个陷阱
- 不能单纯的比较左结点小于中间结点,右结点大于中间结点,左子树的所有结点小于中间结点,右子树的所有结点大于中间结点
- 样例中最小结点可能是int最小值
-
二叉树、二叉平衡树、完全二叉树、二叉搜索树
- 平衡二叉搜索树是不是二叉搜索树和平衡二叉树的结合?是的
- 平衡二叉树和完全二叉树的区别在于底层节点的位置?是的
- 堆是完全二叉树和排序的结合,而不是平衡二叉搜索树?堆是一个完全二叉树,同时保证父子节点的顺序关系(有序)。但完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树
贪心算法
- 贪心算法的本质是选择每一阶段的局部最优,从而达到全局最优。
- 贪心算法一般解题步骤
- 将问题分成若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
动态规划
-
动态规划,英文名,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数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
-
做动态规划题目,代码出问题很正常,找问题最好的方法就是把dp数组打印出来,看看究竟是不是按照自己的思路推导的
-
做动态规划题目,写代码之前一定要把状态转移在dp数组上的具体情况模拟一遍,心中有数,确定最后推出的是想要的结果
-
动态规划灵魂三问:
- 这道题目我举例推导状态转移公式了吗
- 我打印dp数组的日志了吗
- 打印出来的dp数组和我想的一样吗
-
动态规划爬楼梯问题拓展,一步一个台阶、两个台阶、三个台阶、直到一步m个台阶,有多少种方法爬到n阶楼顶。
class Solution { public: int climbStairs(int n) { vector<int> 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背包问题:动态规划五部曲
- 确定dp数组及其下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
代码实现:
void test_2_wei_bag_problem1() { vector<int> weight = {1, 3, 4}; vector<int> value = {15, 20, 30}; int bagWeight = 4; // 二维数组 vector<vector<int>> dp(weight.size() + 1, vector<int>(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]
-
一维数组的递推公式
可以由两个方向推导来
- 不放物品i
- 放物品i
-
一维dp数组如何初始化
-
遍历顺序
倒序遍历,二维数组不会用上一层来覆盖下一层的结果,所以可以从前往后遍历
-
举例推导dp数组
void test_1_wei_bag_problem() { vector<int> weight = {1, 3, 4}; vector<int> value = {15, 20, 30}; int bagWeight = 4; // 初始化 vector<int> 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(); }
-