链表
定义
struct ListNode { int val; ListNode* next; ListNode(int x) :val(x), next(nullptr); }; ListNode* createLinkedList(const std::vector<int>& values) { if (values.empty()) { return nullptr; } ListNode* head = new ListNode(values[0]); ListNode* current = head; for (size_t i = 1; i < values.size(); ++i) { current->next = new ListNode(values[i]); current = current->next; } return head; } void printLinkedList(ListNode* head) { ListNode* current = head; while (current != nullptr) { std::cout << current->val << " "; current = current->next; } std::cout << std::endl; }
203. 移除链表元素
细节
-
while 中的中止条件及区别,若要处理最后的节点应用while (cur != nullptr),此时cur会停留在最后一个节点上
-
使用
cur != nullptr
作为终止条件,循环会在当前节点为nullptr
时终止 -
而while (cur ->next != nullptr)会停留在最后一个节点上因此不会处理最后一个节点
方法
-
很简单,注意应在cur->next->val == val跳过就行
-
卧槽,写错了 ,由于判断的是if(cur->next->val == val)的值因此while (cur ->next != nullptr)
-
会停留在最后一个,但在上一个时,其已经判断了最后一个值是否为val,所以处理了最后一个节点
-
为避免连续出现val , 应用if - else 而不是无脑 cur = cur->next;
while (cur->next != NULL) { if(cur->next->val == val) { ListNode* tmp = cur->next; cur->next = cur->next->next; delete tmp; } else { cur = cur->next; } }
707. 设计链表
细节
-
私有变量,虚拟头节点(不能是头节点!!!)以及链表的长度
-
MyLinkedList(),功能为初始化两个私有变量
-
get 函数中,index和数组一样第0个指的是 头节点!因此index的越界判定为if (index >= _size || index < 0 )
方法
-
挨着挨着写,注意节点的释放!要养成习惯
-
关注index越界判定 应该是 >= 还是>
206. 反转链表
细节
-
可以新建一个链表用前插法来反转,我一直这样写的,这样空间复杂度较高
-
直接反转 ,需要用一个变量存储 head —>next
-
反转是一前一后俩指针进行反转
-
中止条件为head != nullptr ,原因是要处理最后一个节点
-
pre指针一开始的赋值为ListNode* pre = NULL;这里不是虚拟头节点很关键,因为虚拟头节点值为0,会导致反转后的链表多出一个0
方法
while (head != nullptr) { tem = head->next; head->next = pre; pre = head; head = tem; }
24. 两两交换链表中的节点
细节
-
要求时间发复杂度O(n),空间复杂度为O(1)
-
用三个节点进行前中后的操作进行交换,同时用一个节点去记录pre 后面的节点
-
分两个步骤: 交换、前移
-
循环不断前移,注意tem 记录的是pre 下一个节点,因此其为空时只需进行交换,而不用再往前移动
-
会有两种情况:奇数 和 偶数个节点 ,奇数个节点会因为 pre == nullptr 而终止,
-
而偶数个节点则tem 先为空 此时只需进行交换,而不用再往前移动
19. 删除链表的倒数第 N 个结点
细节
-
最初想法:先计数然后在便利,时间复杂度O(n),但空间复杂度更大
-
实际做法:快慢指针,pre 从虚拟头节点移动n个,此时 last 会停在 pre 前 n + 1 个 ,便于删除
-
执行删除操作! over
面试题 02.07. 链表相交
细节
-
相交的节点不是值相等而应该判断指针是否相等
-
相同长度两个链表如何判断是否有相交节点? 从起点开始同时向前移,判断if (curA == curB)return curA;
-
那对于不同长度的链表,则需要计算链表的长度,然后移动较长的链表至较短链表的头部
-
也就是说将不同长度的链表转换为相同长度的链表后,进行判断
-
tips:通过以下代码交换,可简化代码,因为A始终为长的链表
if (lenA < lenB) { swap(lenA, lenB); swap(curA, curB); }
-
142. 环形链表 II
思路
-
快慢指针,快指针走两步,慢指针走一步,若是存在环则一定会遇见
-
不存在环的判定条件为:while (fast != nullptr &&fast -> next != nullptr ),因为走两步,所以两个都要判断
-
若停在最后一个节点,那么fast -> next ->next 就无法访问
-
相遇后,slow 的路程为 t fast 路程为 2t ,那么环长度为 t
-
slow回原点和fast同时出发,一定会在环的入口相遇!
哈希表
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
-
数组
-
set (集合)
-
map(映射)
这里数组就没啥可说的了,我们来看一下set。
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
相关注意事项
-
底层实现为红黑树的键值是有序不可修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
242. 有效的字母异位词
细节
-
可以通过数组模拟哈希表,26位刚好,对应位加一进行记录
-
第二个字符串的时候对应位减一
-
判断是否为全0,全0return true
349. 两个数组的交集
细节
-
去重用unordered_set,同时可以快速插入:unordered_set<int> mySet(nums1.begin(),nums1.end());
-
unordered_set是一个无序表,不能存储重复的值,具有去重的作用。
-
导入mySet后,与nums2进行对比,若出现相同的值就存入unordered_set<int> result中
-
最终return的值为一个数组,将其转换为数组即可return vector<int>(result.begin(), result.end());
202. 快乐数
细节
-
题目内容 : 无限循环 但始终变不到 1,也就是说会出现重复的值,因此用哈希法
-
while(1)进行循环,循环中干 3 件事情
-
计算每位数平方的和,也就是sum
-
sum == 1 直接返回
-
先 在哈希表中查找是否有这个值 ,如果有证明开始无限循环了 ,return false 否则就将其insert
-
n = sum ; sum = 0;
-
1. 两数之和
细节
-
由于需要存储下标,因此不能采用set 结构体 ,map 同时存储值和下标
-
因此使用map进行存储,存储target - nums[i]的对应值
-
先判断if (record.find(target - nums[i]) != record.end()),如果满足条件 return vector<int>{i, record.at(target - nums[i]) }
Map 相关操作
-
具体值的设定: Map[key] = val;
-
访问对饮key 的值 val = Map.at(key);
-
判断键值是否存在:Map.count(key),如果值为1则存在,值为0则不存在;
-
也可用record.find(target - nums[i]) != record.end(),注意find 返回的是一个迭代器
-
变量则使用迭代器进行遍历,auto iter = map.find(target - nums[i]);
-
获取map的大小:size_t mapSize = myMap.size();
迭代器
迭代器是一种用于遍历容器(如数组、链表、哈希表等)中元素的对象。
在C++中,迭代器提供了一种统一的方式来访问容器中的元素,无需关心容器的内部实现细节。
454. 四数相加 II
思路
-
哈希表存储前两个数组的和,记录出现的次数
-
次数的用处:遍历后两个数组和时,查找记录的哈希表中是否存在的同时! 所记录的次数就是当前和与前面数组的和为0 的总次数
-
用count 记录
class Solution { public: int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) { unordered_map<int ,int > myMap; int length = nums1.size(); int count = 0; for (int i = 0; i < length; i++) { for (int j = 0; j < length; j++) { int sum = 0; sum = nums1[i] + nums2[j]; myMap[sum]++; } } for (int i = 0; i < length; i++) { for (int j = 0; j < length; j++) { int sum = 0; sum = nums3[i] + nums4[j]; if (myMap.find(0 - sum) != myMap.end() ) { count+=myMap[0 -sum]; } } } return count; } };
383. 赎金信
二叉树
递归遍历
方法论:
-
关注前中后,指的是访问根节点的顺序,同时得知道两个顺序才能还原二叉树
-
无非是动作的排列组合,具体实现如下(中序为例子)
-
判断是否为空,很关键,if (cur == NULL) return; 无论如何都要先判断截止条件
-
左 :递归执行 traversal(cur->left, vec);
-
中: vec.push_back(cur->val);
-
右: traversal(cur->right, vec); // 右
class Solution { public: void traversal(TreeNode* cur, vector<int>& vec) { if (cur == NULL) return; traversal(cur->left, vec); // 左 vec.push_back(cur->val); // 中 traversal(cur->right, vec); // 右 } vector<int> inorderTraversal(TreeNode* root) { vector<int> result; traversal(root, result); return result; } };
-
迭代遍历(非递归)
前序细节
-
用栈模拟递归前序遍历
-
先放根节点 ,然后先放右节点 后放左节点
-
然后依次弹出
-
弹出时需要判断if (tem != nullptr)result.push_back(tem->val);若为空就 continue;
class Solution { public: vector<int> preorderTraversal(TreeNode* root) { vector<int> result; TreeNode* tem; stack<TreeNode*> st; st.push(root); while (!st.empty()) { tem = st.top(); st.pop(); if (tem != nullptr)result.push_back(tem->val); else continue; st.push(tem->right); st.push(tem->left); } return result; } };
后序细节
-
后续顺序为 左右中 ,因此可以根据前序遍历的数据,先产生中右左的数据 ,然后reverse(result );
中序细节
-
访问顺序和处理顺序不同
-
先一直向左访问,并且用栈存储访问过的节点,直到叶子节点
-
若当前节点为空就从栈中取元素,然后访问处理右节点
-
key:左为空的话,就弹出该节点,右为空则说明是叶子节点,弹出该节点的父节点
class Solution { public: vector<int> inorderTraversal(TreeNode* root) { vector<int> res; stack<TreeNode*>st; TreeNode *cur = root; while(cur != nullptr || !st.empty()){ if(cur != nullptr){ st.push(cur); cur = cur ->left; }else{ cur = st.top(); st.pop(); res.push_back(cur ->val); cur = cur ->right; } } return res; } };
迭代遍历统一写法(看不懂)
层序遍历
-
队列实现层序遍历将每层的内容存在队列中,并且记录队列长度size
-
上一次队列的长度决定弹出多少,弹出的同时将所弹出的节点的子节点压入队列中
-
用一个数组记录 每层弹出的内容
-
每层遍历完,压入二维数组中
class Solution { public: vector<vector<int>> levelOrder(TreeNode* root) { vector<vector<int>> res ; queue<TreeNode* > myque; if(root != nullptr) myque.push(root); while(!myque.empty()){ int size = myque.size(); vector <int> tem ; while(size -- ){ TreeNode *node = myque.front(); myque.pop(); tem.push_back(node ->val); if(node ->left) myque.push(node ->left); if(node ->right) myque.push(node ->right); } res.push_back(tem); } return res; } };
199. 二叉树的右视图
429. N 叉树的层序遍历
-
和二叉树的层序遍历一样,但要关注如何访问子节点的问题
-
for(Node* child : node->children) 为访问所有子节点
-
因此只需要把层序遍历的先左后右 换成for(Node* child : node->children)myque.push(child);
515. 在每个树行中找最大值
-
依旧是层序遍历
-
注意需要记录每层的最大值,因此遍历过程中需要更新最大值,而值一开始设定为INT_MIN
-
方便更新
116. 填充每个节点的下一个右侧节点指针
104. 二叉树的最大深度
111. 二叉树的最小深度
-
以上三题均是层序遍历,至于深度应该可以用深度优先遍历的方式但还是采用广度优先遍历
对称二叉树
-
运用递归判断,从root出发,对比两个子树的内外值是否相等
-
递归三部曲,先明确终止条件的书写,然后写递归步骤,然后return
104. 二叉树的最大深度
-
不用层序遍历采用递归的办法
-
终止条件:root 为空时终止,也就是传入的指针为空时停止
-
递归操作:判断左子树的最大深度,判断又子树的最大深度
-
最终返回:左子树和右子树深度的最大值 + 1;
class solution { public: int getdepth(TreeNode* node) { if (node == NULL) return 0; int leftdepth = getdepth(node->left); // 左 int rightdepth = getdepth(node->right); // 右 int depth = 1 + max(leftdepth, rightdepth); // 中 return depth; } int maxDepth(TreeNode* root) { return getdepth(root); } };
111. 二叉树的最小深度
-
同样采用递归的方式
-
刚开始会以为和最大一样:但是!!!对于以下二叉树不能是深度为1,因此终止条件需要调整
-
具体终止条件如下,思路和求取最大深度相同。
class Solution { public: int minDepth(TreeNode* root) { if(!root ) return 0; if(root ->left == nullptr && root -> right != nullptr){ return minDepth(root -> right ) + 1; } if(root ->right == nullptr && root -> left != nullptr){ return minDepth(root -> left ) + 1; } int right = minDepth(root -> right) ; int left = minDepth(root -> left) ; return min(left , right) + 1 ; } };
222. 完全二叉树的节点个数
-
先求左子树个数再求右子树个数 ,和最大深度一样
-
确定中止条件 if(root == null) return 0 ;
-
left = countNodes(root ->left)
-
right= countNodes(root ->right)
-
return right + left + 1
110. 平衡二叉树
-
和最大深度类似,重要还是要取出左右子树的高度
-
左右子树高度做差,大于1 则返回 -1 ,否则返回该子树的最大深度 方便上层继续判断
-
为什么要返回该子树的最大深度,因为先计算的左边整个子树的深度再右边
-
所以一定要记录,不然在计算右边整个子树时就无法判断了