Day1 Python to CPP
vector、循环、条件判断和函数;首先,和python不同,cpp在定义变量时需要先声明类型,如:
int age = 20;
long long total = 10000000000LL;
double score = 95.5;
bool passed = true;
char letter = 'A';
string name = "Kirawii";注意true和false都是小写,每句话通常以 ; 结束;
其次,是条件判断:
if (x > 0) {
cout << "positive" << endl;
} else if (x == 0) {
cout << "zero" << endl;
} else {
cout << "negative" << endl;
}用花括号代表代码块,不依赖缩进。
然后是循环,分为按下标遍历:
for (int i = 0; i < nums.size(); i++) {
cout << i << " " << nums[i] << endl;
}初始化,循环条件,以及每轮结束后做什么
直接遍历元素:
for (int x : nums) {
cout << x << endl;
}while循环:
while (left < right) {
left++;
}最后是cpp的列表/数组:vector
vector<int> nums = {1, 2, 3};
nums.push_back(4); // 添加元素
nums.pop_back(); // 删除最后一个元素
int n = nums.size();
cout << nums[0] << endl;
cout << nums.back() << endl;
nums[0] = 100; //修改元素
swap(nums[0], nums[1]);//交换两个元素练习题
数组求和:没什么好说的
class Solution {
public:
int arraySum(vector<int>& nums) {
int sum = 0;
// 在这里遍历 nums
for(int x : nums){
sum+=x;
}
return sum;
}
};leetcode283:移动零:
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int slow = 0;
for(int fast = 0;fast < nums.size();fast++){
if (nums[fast]!=0){
swap(nums[slow],nums[fast]);
slow++;
}
}
}
};思路是双指针,因为我们需要把所有0移动到末尾,且要保持非0元素的相对顺序,我们让slow指向下一个非0元素应该在的地方,一开始也就是0,然后fast遍历整个数组,遇到非0元素,就交换fast下标的数值和slow下标的数值,然后让slow++,移动到下一个非0元素应该在的地方。至于初始状态是无所谓的,如果slow指向的是0,会把非0元素交换过来,如果slow指向的是非0元素,fast也指向的是slow,此时会自己和自己发生交换swap,无所谓。
leetcode1:两数之和
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
for(int i =0;i<nums.size();i++){
for(int j =i+1;j<nums.size();j++){
if(nums[i]+nums[j] == target){
return {i,j};
}
}
}
return {};
}
};先用暴力熟悉一下cpp,新的知识是return {i,j},表示返回一个vector<int>,这个算法的时间复杂度是O(n^2).
练习题:返回数组中的最大值:返回数组中偶数的数量:原地交换数组的第一个和最后一个元素
int findMax(vector<int>& nums) {
int max = nums[0];
for (int i=0;i<nums.size();i++) {//这里可以从1开始,因为0用来初始化了
if (nums[i]>max) {
max = nums[i];
}
}
return max;
}
//命名尽量不用max,有库函数也叫max,建议叫maxValue/maximum
int countEven(vector<int>& nums) {
int sum = 0;
for (int i = 0;i<nums.size();i++) {
if (nums[i]%2==0) {
sum+=1;
}
}
return sum;
}
//可以用只需要元素的循环方式
void swapEnds(vector<int>& nums) {
int temp = nums[0];
nums[0]=nums[nums.size()-1];
nums[nums.size()-1]=temp;
}
//可以加一个判断,nums.empty(),防止空数组越界几个新操作:
nums.empty() // 判断是否为空
nums.front() // 第一个元素
nums.back() // 最后一个元素最后一道题:给定一个整数数组,将数组原地反转。要求使用左右双指针,不创建新的数组。
void reverseArray(vector<int>& nums) {
if (nums.empty()) {
return;
}
int right = nums.size()-1;
for (int left = 0;left <nums.size();left++) {
if (left == right) {
return;
}
swap(nums[left],nums[right]);
right--;
}
}
//此乃错误答案,只能在奇数数组的情况下运作。偶数不会出现left=right的情况,直接让left在小于right的情况下运作就行。void reverseArray(vector<int>& nums) {
if (nums.empty()) {
return;
}
int right = nums.size()-1;
for (int left = 0;left <right;left++) {
swap(nums[left],nums[right]);
right--;
}
}AI评价:循环终止条件还需要更敏感,变量命名可以更准确,解释代码时要区分“执行”和“效果”
Day2 Hash Map
- 理解
unordered_map; - 理解
unordered_set; - 把两数之和从
O(n²)优化到O(n); - 完成一道集合练习。
哈希表的作用是:
快速判断某个值是否已经出现,以及它出现在哪个下标。
平均查询时间可以看作:O(1) 核心操作:先查找,后插入。
unordered_map<int, int> mapping;
mapping[2] = 0;
mapping[7] = 1;
//判断键是否存在
mp.count(key)
mp.find(key)
if (mp.find(10)!=mp.end()){
cout<<"exist!"<<endl;
}这段类型unordered_map<int, int>,表示key类型int,value类型int,在两数之和中,key = 数组中的数字,value = 这个数字的下标。因为我们想要的是下标,而已知的是数值。所以反过来了。
mp.find(key)返回的是一个迭代器,而start和end就不用多说了。
Python中的集合是set(),在cpp中是:
unordered_set<int> seen;
seen.insert(5);
if (seen.count(5)) {
cout << "存在" << endl;
}
seen.insert(10); // 添加
seen.erase(10); // 删除
seen.count(10); // 判断是否存在
seen.size(); // 元素数量
seen.empty(); // 是否为空如果只需要判断“某个数字是否出现过”,使用 unordered_set。
如果还需要保存它的下标、频率或其他信息,使用 unordered_map。
练习题
练习1:判断是否存在重复数字
bool containsDuplicate(vector<int>& nums) {
unordered_set<int> seen;
for (int i:nums) {
if (seen.count(i)==1) {
return true;
}
seen.insert(i);
}
return false;
}练习 2:统计元素频率
unordered_map<int, int> countFrequency(vector<int>& nums) {
unorder_map<int,int>frequency;
for(intx:nums) {
frequency[x]++;
}
return frequecy;
}练习3:使用unordered_map优化两数之和
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int>indexMap;
for (int i =0;i<nums.size();i++) {
int need = target - nums[i];
if (indexMap.find(need)!=indexMap.end()) {
return {indexMap[need],i};
}
indexMap[nums[i]]=i; //最重要的,先查询再插入,插入的时候,key是数值,value是坐标
}
return {};
}练习4:字母异位词
bool isAnagram(string s, string t) {
unordered_map<char,int>times;
unordered_map<char,int>times2;
for (char x:s) {
times[x]++;
}
for (char x:t) {
times2[x]++;
}
return times==times2;
}leetcode49. 字母异位词分组
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string,vector<string>> groups;
for (string s:strs){
string key = s;
sort(key.begin(),key.end());
groups[key].push_back(s);
}
vector<vector<string>> result;
for (auto& pair:groups){
result.push_back(pair.second);
}
return result;
}
};遍历哈希表时:for (auto& pair : groups)
每个pair都包含
pair.first // key
pair.second // value哈希表最核心的三种用途:
- 判断元素是否出现;
- 保存元素对应的下标;
- 统计元素出现次数。
Day3 栈、队列与链表指针
今日目标
- 会使用
stack和queue; - 看懂
ListNode*、nullptr、>; - 完成有效括号、反转链表、环形链表;
- 理解链表题为什么必须先保存
next。
第一部分是栈,栈的特点就是后进先出
stack<int> st;
st.push(10);
st.push(20);
st.push(30);
int x = st.top();//获取栈顶数值
st.pop();//弹出
st.empty(); // 是否为空
st.size(); // 元素数量注意和Python不同的是,cpp的pop只会直接弹出元素,不会返回,因此,需要先读取再删除。
另外,栈为空时不能调用top和pop。
练习题1 leetcode 20. 有效的括号
class Solution {
public:
bool isValid(string s) {
stack<char> st;
for(char c:s){
if(c=='(' || c == '{' || c == '['){
st.push(c);
}else{
if(st.empty()){
return false;
}
if((c == ')' && st.top()=='(')||(c == ']' && st.top()=='[')||(c == '}' && st.top()=='{')){
st.pop();
continue;
}else{
return false;
}
}
}
if (st.empty()){
return true;
}else{
return false;
}
}
};主要是要考虑几个边界条件,时刻注意栈是不是为空。另外读取栈顶后要记得pop。
第二部分队列,FIFO,先进先出。
queue<int> q;
q.push(10);
q.push(20);
q.push(30);
q.front(); //访问队首
q.back(); //访问队尾
q.pop(); //删除队首
q.empty();
q.size();
和栈一样,q.pop() 不返回元素:
第三部分,链表。
数组中的元素通常连续存储,可以通过下标访问:
而链表由一个个节点组成,每个节点保存:
- 当前值;
- 下一个节点的位置。
1 → 2 → 3 → nullptr
leetcode中通常已经定义好了。
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};所以先不研究构造函数,主要看 int val和ListNode* next
ListNode* head; //可以理解为head保存某个链表节点的位置他不是节点本身,而是指向节点的指针。
访问当前节点 head→val
访问下一个节点 head→next
nullptr表示不指向任何节点,类似Python的None。
链表最后一个节点满足 node→next == nullptr;
空链表满足 head == nullptr
简写成
if (!head) {
return;
}如何遍历列表?
ListNode* cur = head;
while (cur != nullptr) {
cout << cur->val << endl;
cur = cur->next;
}小练习:计算链表长度。
int getLength(ListNode* head) {
ListNode* cur = head;
int sum = 0;
while(cur!= nullptr){
sum++;
cur=cur->next;
}
return sum;
}leetcode.206 反转列表
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* cur = head;
while(cur!=nullptr){
ListNode* nextNode = cur->next;
cur->next = prev;
prev = cur;
cur = nextNode;
}
return prev;
}
};注意最后返回的不是cur,而是prev,prev在循环结束后是指向原链表最后一个节点的。所以新的头节点是prev。
三指针是比较常见的O(1)做法,还有复制链表/维护一个栈,空间复杂度高一点。
leetcode.141 环形链表
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode* slow = head;
ListNode* fast = head;
while(fast!=nullptr && fast->next != nullptr){
slow=slow->next;
fast=fast->next->next;
if(slow==fast){
return true;
}
}
return false;
}
};快慢双指针。进阶问题:如果存在环,如何判断环的长度呢?方法是,快慢指针相遇后继续移动,直到第二次相遇。两次相遇间的移动次数即为环的长度。
DAY4 双指针与滑动窗口
双指针是一种遍历方式,常见形式有三种
左右相向:数组反转,有序数组查找,最多水的容器,三数之和
int left = 0;
int right = nums.size() - 1;
while (left < right) {
// 根据条件移动 left 或 right
}
快慢指针:移动0,删除重复元素,原地修改数组
int slow = 0;
for (int fast = 0; fast < nums.size(); fast++) {
// 根据 fast 的内容更新 slow
}
滑动窗口:连续子数组,连续子字符串,最长最短满足某种条件的区间。
int left = 0;
for (int right = 0; right < nums.size(); right++) {
// 加入 nums[right]
while (窗口不合法) {
// 移除 nums[left]
left++;
}
}练习题
leetcode.11 盛最多水的容器
class Solution {
public:
int maxArea(vector<int>& height) {
int left = 0;
int right = height.size()-1;
int answer = 0;
int area = min(height[left],height[right])*(right-left);
int temp = 0;
while(left<right){
if (height[left]<height[right]){
left++;
}else{
right--;
}
temp = min(height[left],height[right])*(right-left);
if(temp>area){
area = temp;
}
}
return area;
}
};一次过,主要是要考虑面积的计算,因为接水的时候主要受短边影响。所以我们只需要一直移动短边,然后遍历结束后保留一个面积的最大值即可。
leetcode.15 三数之和
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<vector<int>> result;
for(int i = 0;i <nums.size();i++){
if(i>0 && nums[i]==nums[i-1]){
continue;
}
int left = i+1;
int right = nums.size()-1;
while(left<right){
if (nums[left]+nums[right]>-nums[i]){
right--;
}else if(nums[left]+nums[right]<-nums[i]){
left++;
}else{
result.push_back({nums[i],nums[left],nums[right]});
while(left < right && nums[left]==nums[left+1]){
left++;
}
while(left < right && nums[right]==nums[right-1]){
right--;
}
left++;
right--;
}
}
}
return result;
}
};最重要的是注意三个去重,
第一处是对固定下标 i 去重:若 nums[i] == nums[i - 1],说明以这个数作为第一个数的组合已经搜索过,应直接跳过。
第二处和第三处是在找到答案后,对 left 和 right 去重:相同的第二个数和第三个数会生成相同的三元组,因此应先跳过重复值,再分别执行 left++ 和 right--,继续寻找新的组合。这里先去重再移动,是因为当前的 nums[left] 和 nums[right] 正是刚刚加入答案的值。
剩下的其实三数之和就是多了一个循环的两数之和。
leetcode.3 无重复字符的最长子串
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_set<char> window;
int left = 0;
int answer = 0;
for (int right =0;right<s.size();right++){
while(window.count(s[right])){
window.erase(s[left]);
left++;
}
window.insert(s[right]);
int length = right - left +1;
answer = max(answer,length);
}
return answer;
}
};三个判断信号:
- 数组两端/两个元素构成答案
- 有序数组中找两个数的和
- 最长或最短连续子串
DAY5 前缀和、最大子数组、前后缀面积
什么是前缀和,就是数组中的prefix[i] 表示前 i 个元素的总和。
本质是先保存从开头到各位置的累计结果,以后可以快速计算任意连续区间的和。
练习题
leetcode.560 和为K的子数组
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int,int> prefixCount;
prefixCount[0]=1;
int prefix = 0;
int answer = 0;
for (int x:nums){
prefix += x;
int need = prefix -k;
if(prefixCount.count(need)){
answer+=prefixCount[need];
}
prefixCount[prefix]++;
}
return answer;
}
};假设当前累计和为:
prefix我们想找到一段子数组的和等于 k。
如果某个更早的前缀和是:
previousPrefix那么:
prefix - previousPrefix = k移项:
previousPrefix = prefix - k所以遍历到当前元素时,只需要问:
之前出现过多少次 prefix - k?出现几次,就说明存在几个以当前位置结尾、和为 k 的子数组。
哈希表存什么?
unordered_map<int,int>prefixCount;这里:
key:某个前缀和;value:这个前缀和出现的次数。
例如:
0 → 1
1 → 2
3 → 1表示:
- 前缀和
0出现过一次; - 前缀和
1出现过两次; - 前缀和
3出现过一次。
注意要初始化状态,prefixCount[0]=1;否则会漏掉从下标 0 开始的合法子数组。
不用滑动窗口的原因是滑动窗口依赖某种单调性,但是负数会破坏单调性,因此使用前缀和。
leetcode.53 最大子数组和
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int current = nums[0];
int answer = nums[0];
for(int i = 0;i<nums.size();i++){
current = max(current+nums[i],nums[i]);
answer = max(answer,current);
}
return answer;
}
};如果前面的累计和有帮助,就继续连接;如果前面的累计和是负担,就从当前位置重新开始。因为我们只需要记录最大值,而不是记录最大值对应的数组/区间。
leetcode.283 除自身以外的数组的乘积
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
vector<int> answer(nums.size(),1);
int leftProduct = 1;
for(int i = 0;i<nums.size();i++){
answer[i] = leftProduct;
leftProduct *= nums[i];
}
int rightProduct = 1;
for(int i = nums.size()-1;i>=0;i--){
answer[i]*=rightProduct;
rightProduct *= nums[i];
}
return answer;
}
};DAY6 矩阵遍历与原地修改
二维矩阵在cpp中通常表示为
vector<vector<int>> matrix;
访问第i行第j列就是matrix[i][j];
行数和列数 matrix.size(),matrix[0].size(),注意矩阵不为空.
二维数组的遍历:
for (int i = 0; i < matrix.size(); i++) {
for (int j = 0; j < matrix[0].size(); j++) {
cout << matrix[i][j] << endl;
}
}leetcode.73 矩阵置0
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
unordered_set<int> zeroRows;
unordered_set<int> zeroCols;
for(int i = 0;i<matrix.size();i++){
for(int j = 0;j<matrix[0].size();j++){
if(matrix[i][j]==0){
zeroCols.insert(j);
zeroRows.insert(i);
}
}
}
for(int i = 0;i<matrix.size();i++){
for(int j = 0;j<matrix[0].size();j++){
if(zeroCols.count(j) || zeroRows.count(i)){
matrix[i][j]=0;
}
}
}
}
};直接狠狠暴力,用哈希表记录一下存在0的行和列。特别注意不能读取到0就全置0,会影响判断,不知道到底是原来的0还是被修改后的0,导致扩大范围。
leetcode.54 螺旋矩阵
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int top = 0;
int bottom = matrix.size()-1;
int left = 0;
int right = matrix[0].size()-1;
vector<int> result;
while(top<=bottom && left<=right){
for(int i = left;i<=right;i++){
result.push_back(matrix[top][i]);
}
top++;
for(int j = top;j<=bottom;j++){
result.push_back(matrix[j][right]);
}
right--;
if(top<=bottom){
for(int k = right;k>=left;k--){
result.push_back(matrix[bottom][k]);
}
bottom--;
}
if(left<=right){
for(int l = bottom;l>=top;l--){
result.push_back(matrix[l][left]);
}
left++;
}
}
return result;
}
};其实本质就是把一个大矩阵拆成不同的边界,关键的是控制边界然后触发不同的行为,就四种。另外边界本身也需要访问。所以是≤,还要检查边界和收缩对应边。一边想一边写就行。
| 遍历部分 | 为什么通常不用/要检查 |
|---|---|
| 从左向右 | 外层 while 已保证当前上边界合法 |
| 从上向下 | 即使行边界失效,正序 for 会自然不执行 |
| 从右向左 | 可能重复访问已经走过的单行,必须检查 top <= bottom |
| 从下向上 | 可能重复访问已经走过的单列,必须检查 left <= right |
leetcode.48 旋转图像
顺时针 90° = 转置 + 每行反转
逆时针 90° = 转置 + 每列反转
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
for(int i = 0;i<matrix.size();i++){
for(int j = i+1;j<matrix[0].size();j++){
swap(matrix[i][j],matrix[j][i]);
}
}
for(int i = 0;i<matrix.size();i++){
reverse(matrix[i].begin(),matrix[i].end());
}
}
};第一次遍历给矩阵转置,第二次遍历给每行反转,正好是顺时针旋转90度。
DAY7 链表进阶
为什么链表题经常需要虚拟头结点?因为如果不设置虚拟头结点,我们往往要单独处理head,而设置虚拟头以后,就可以直接把head当做普通节点处理了。
ListNode dummy(0);
dummy.next = head;注意这里用 . 是访问节点对象的成员,创建节点指针访问其成员用箭头。
练习题
leetcode.21 合并两个有序链表
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode dummy(0);
ListNode* tail = &dummy;
while(list1 != nullptr && list2 != nullptr){
if(list1->val < list2->val){
tail->next = list1;
list1 = list1->next;
}else{
tail->next = list2;
list2 = list2->next;
}
tail = tail->next;
}
if(list1){
tail->next = list1;
}else{
tail->next = list2;
}
return dummy.next;
}
};其实就是新创建一个链表,用虚拟头结点。比较大小然后连接。主要是要知道最后返回的是dummy.next。另一个就是一条链表比完以后,另一条剩下的直接连上去就行。
leetcode.19 删除链表倒数第N个节点
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode dummy(0);
dummy.next = head;
ListNode* fast = &dummy;
ListNode* slow = &dummy;
for(int i = 0;i<=n;i++){
fast = fast->next;
}
while(fast!=nullptr){
fast= fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
return dummy.next;
}
};用快慢双指针来找倒数第几个节点。
leetcode.160 相交链表
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* pA = headA;
ListNode* pB = headB;
while(pA != pB){
pA = (pA == nullptr) ? headB:pA->next;
pB = (pB == nullptr)? headA:pB->next;
}
return pA;
}
};指针换路法,这个明天再看看。
DAY8 二叉树基础-递归DFS与层序BFS
今天开始二叉树。目标是掌握两套模板:
- 递归 DFS:向左右子树继续处理;
- 队列 BFS:一层一层遍历。
一、二叉树节点怎么表示
LeetCode 通常已经定义:
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x)
: val(x), left(nullptr), right(nullptr) {}
};每个节点有三个成员:root→val root→left root→right
空树和空子树用nullptr表示
常见的递归出口:
if (root == nullptr) {
return ...;
}二、理解树的递归
链表只有一条后路:
cur =cur->next;二叉树有两条后路:
root->left;root->right;递归处理二叉树通常写成:
返回类型dfs(TreeNode*root) {
if (root==nullptr) {
return 空节点对应的结果;
}
左子树结果 =dfs(root->left);
右子树结果 =dfs(root->right);
return 根据当前节点和左右结果计算答案;
}你可以把它理解为:
我只负责当前节点;左子树交给递归处理,右子树也交给递归处理。
练习题:
leetcode.104 二叉树的最大深度
class Solution {
public:
int maxDepth(TreeNode* root) {
if(root == nullptr){
return 0;
}
int leftD = maxDepth(root->left);
int rightD = maxDepth(root->right);
return max(leftD,rightD)+1;
}
};leetcode.226 翻转二叉树
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(root == nullptr){
return nullptr;
}
swap(root->left,root->right);
invertTree(root->left);
invertTree(root->right);
return root;
}
};三、DFS 的三种遍历顺序
对于当前节点、左子树、右子树,有三种常见顺序。
前序遍历
当前节点 → 左子树 → 右子树void preorder(TreeNode* root) {
if (root == nullptr) {
return;
}
cout << root->val;
preorder(root->left);
preorder(root->right);
}中序遍历
左子树 → 当前节点 → 右子树void inorder(TreeNode* root) {
if (root == nullptr) {
return;
}
inorder(root->left);
cout << root->val;
inorder(root->right);
}后序遍历
左子树 → 右子树 → 当前节点void postorder(TreeNode* root) {
if (root == nullptr) {
return;
}
postorder(root->left);
postorder(root->right);
cout << root->val;
}leetcode.102 二叉树的层序遍历
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> q;
q.push(root);
vector<vector<int>> result;
if (root == nullptr){
return result;
}
while(!q.empty()){
int levelSize = q.size();
vector<int> level;
for(int i =0;i<levelSize;i++){
TreeNode* node = q.front();
q.pop();
level.push_back(node -> val);
if(node->left != nullptr){
q.push(node->left);
}
if(node->right != nullptr){
q.push(node->right);
}
}
result.push_back(level);
}
return result;
}
};看到递归关系
例如:
- 树的高度;
- 是否对称;
- 路径和;
- 翻转树;
- 子树信息影响父节点。
优先考虑 DFS:
看到“每一层”
例如:
- 层序遍历;
- 每层最大值;
- 右视图;
- 最小深度。
优先考虑 BFS: