代码随想录刷题
数组
二分查找
- 思路: 有序数组,数组中无重复元素
移除元素
-
思路: 数组在内存地址中是连续的,不能单独删除某个数组中的某个元素,只能覆盖
-
快慢指针法,用于数组、链表、字符串等的操作
-
双向指针法,更优,移动更少的元素
-
-
注意:
-
补充快慢指针法的代码
-
交换时候注意只有在交换的时候,左右数组的下标才额外在交换之后移动一位,用left--和right++
-
有序数组的平方
-
思路: 复数有可能成为最大值,左右比较,放到一个新的数组里面
- 双向指针法:时间复杂度为o(n)
- sort的时间复杂度为o(nlogn)
-
注意: 这个里面先求出大的数,因此想得到递增序列之只能从后往前存储。
长度最小的子数组
-
思路: 暴力算法时间复杂度十分高,会超时
考虑用更优的算法-
暴力解法:如果用暴力解法,两个for循环,时间复杂度为o(n^2)
-
滑动窗口解法:设置一个窗口,不断的移动头和尾,很类似于双指针法
每一个元素都被操作了两次,是o(n)级别的算法
-
-
注意:
- 设置最开始的窗口值要设置成最大的数,防止出现第一次的窗口值很大,小于返回值的初始值,导致无法更新返回值的情况
- INT32_MAX是c++中定义的宏常量,代表int类型32位整数的最大值
-
代码:
class Solution { public: int minSubArrayLen(int target, vector<int>& nums) { //初始化滑动窗口的大小尾0 //INT32_MAX是c++标准库中定义的一个宏函数,代表32位有符号整数的最大值 //这里把窗口值的初始值设置为最大 //防止第一次出现的窗口过于大,导致第一次窗口的值无法赋给返回值的情况 int min_sub_array_len = INT32_MAX; int left = 0; //初始化sum的值 int sum = 0; //循环遍历找大于target的窗口 for (int i = 0; i < nums.size(); i++) { sum += nums[i]; while (sum >= target) { //更新最小窗口值 min_sub_array_len = (min_sub_array_len < (i - left + 1)) ? min_sub_array_len : (i - left + 1); //将窗口左边右移,并且将左值减去 sum -= nums[left++]; } } return min_sub_array_len == INT32_MAX ? 0 : min_sub_array_len; } };
螺旋矩阵
-
思路: 坚持循环不变量原则
-
注意: 考察对代码的控制
-
代码:
class Solution { public: vector<vector<int>> generateMatrix(int n) { //二维数组中螺旋矩阵,遵循循环不变量原则 //都左闭右开,1-n^2,总共有n行n列 vector<vector<int>> res(n, vector<int>(n, 0)); //总共要转n/2圈 int loop = n / 2; //定义每次打印的初始边界 int starx = 0, stary = 0; //填充的元素 int count = 1; //右边界,每次循环有边界收缩一位 int offset = n; //总共转loop圈 while (loop--) { int i = starx; int j = stary; //填充从左到右 //左闭右开,右边最后一个元素不填充 for (j = stary; j < offset - 1; j++) { res[i][j] = count; count++; } //填充从上到下 for (i = starx; i < offset - 1; i++) { res[i][j] = count; count++; } //填充从右到左 for (; j > starx; j--) { res[i][j] = count; count++; } //填充从下到上 for (; i > stary; i--) { res[i][j] = count; count++; } //第二圈的时候,起始位置++ starx++; stary++; //收缩右边的值 offset--; } //当n为奇数的时候,单独填充中间的元素 if (n % 2 != 0) { res[n / 2][n / 2] = count; } return res; } };
-
总结:
-
数组中的元素是不能删除的,只能覆盖
-
vector 和 array的区别:
vector的底层实现是array,vector严格上说只是容器,不是数组
-
数组的经典算法
-
二分法
注意界限的控制
注意循环不变量原则
看left==right的时候有没有定义成不成立
时间复杂度是O(logn)
暴力解决的时间复杂度是O(n)
-
双指针法
快慢指针法
时间复杂度是O(n)
暴力解决的时间复杂度是O(n^2)
-
滑动窗口
一种特殊的双指针法
时间复杂度为O(n)
暴力解决的时间复杂度是O(n^2)
-
模拟行为
注意循环不变量原则
主要考察对代码的控制力
链表
移除链表元素
-
代码:
struct ListNode { int val; ListNode* next; //三个构造函数 ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} ListNode(int x, ListNode* next) : val(x), next(next) {} }; class Solution { public: ListNode* removeElements(ListNode* head, int val) { //如果头节点的值等于val //删除头节点 while (head != NULL && head->val == val) { ListNode* temp = head; head = head->next; //delete用于释放内存 delete temp; } //删除非头节点 ListNode* curr = head; //检查的是curr的下一个是不是val while (curr != NULL && curr->next != NULL) { if (curr->next->val == val) { ListNode* temp = curr->next; curr->next = curr->next->next; delete temp; } else { curr = curr->next; } } return head; } };
设计链表
-
思路: 两种方法
- 带头节点
- 不带头节点:实际上应用中都没有头节点
翻转链表
- 思路: 不需要额外空间,直接定义指针
class Solution {
public:
ListNode* reverseList(ListNode* head) {
//翻转链表
//保存curr下一个结点
ListNode* temp;
ListNode* curr = head;
ListNode* pre = NULL;
//当temp不等于NULL的时候
while (curr){
temp = curr->next;
curr->next = pre;
pre = curr;
curr = temp;
}
return pre;
}
};
两两交换链表中的节点
- 思路: 如果没有虚拟头结点,第一次对于头节点的处理会比较困难
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
//设置一个虚拟头节点
//设置虚拟头节点和头指针有什么区别?
//如果设置头节点指针,仍然第一个要对头节点进行操作
//操作不统一
ListNode* dummyhead = new ListNode(0);
dummyhead->next = head;
//操作要从虚拟节点开始
ListNode* curr = dummyhead;
ListNode* temp1 = NULL;
ListNode* temp2 = NULL;
while (curr->next != NULL && curr->next->next != NULL) {
temp1 = curr->next;
temp2 = curr->next->next->next;
curr->next = curr->next->next;
curr->next->next = temp1;
temp1->next = temp2;
curr = temp1;
}
return dummyhead->next;
}
};
删除链表的倒数第N个结点
- 思路: 利用快慢指针,让fast指针先走n步,等fast指针指向末尾的时候,slow就指向了倒数第n个结点,要存储slow的pre结点
- 注意: 这个题目在Leecode中free结点编译不通过
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummynode = new ListNode(0);
dummynode->next = head;
ListNode* fast = dummynode, * slow = dummynode;
//让fast先走n+1步
for (int i = 0; i < n+1; i++) {
if (fast == NULL) {
return dummynode->next;
}
fast = fast->next;
}
//当fast到末尾的时候。slow指针指向了倒数第n-1个结点
while (fast) {
fast = fast->next;
slow = slow->next;
}
//slow结点现在指向了删除节点的上一个结点
//ListNode* temp;
//temp = slow->next;
slow->next = slow->next->next;
//free(temp);
return dummynode->next;
}
};
链表相交
-
思路: 遍历求出两个链表的长度,然后从后往前比较结点的地址是不是相同的
-
注意:
- 注意应该是地址相同,而不是内容相同。
- 链表只能单向移动,无法从后往前移动
- 写代码要细心,不要总是丢失条件
class Solution {
public:
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
int lenA = 0;
int lenB = 0;
ListNode* currA = headA;
ListNode* currB = headB;
while (currA != NULL)
{
lenA++;
currA = currA->next;
}
while (currB != NULL)
{
lenB++;
currB = currB->next;
}
//此时currA和currB链表指针都在末尾
//现在知道了链表的长度
//让长链表先走差值步
//直接设置,lenA指向长指针
currA = headA;
currB = headB;
if (lenA < lenB) {
swap(lenA,lenB);
swap(currA,currB);
}
//长指针先走n步
int gap = lenA - lenB;
while (gap--) {
currA = currA->next;
}
//此时让两个指针一起行动,直到相等
while (currA!=NULL)
{
if (currA == currB) {
return currA;
}
currA = currA->next;
currB = currB->next;
}
return NULL;
}
};
环形链表II
- 思路:
- 快慢指针,先判断是不是有环
- 如果有环的话,计算环的大小
- 快慢指针回到原点,快指针先走环的大小
- 快慢指针同时走
- 相遇的地方就是环的入口
class Solution {
public:
ListNode* detectCycle(ListNode* head) {
ListNode* fast = head;
ListNode* slow = head;
int circle_size = 0;
//判断是不是有环
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
//证明有环
//计算环的大小
slow = slow->next;
circle_size++;
while (fast != slow) {
slow = slow->next;
circle_size++;
}
//再次跳进时候已经计算得到了circle_size
//将fast向前走circle_size距离
fast = head;
slow = head;
while (circle_size--)
{
fast = fast->next;
}
while (fast != slow)
{
fast = fast->next;
slow = slow->next;
}
//再次跳出是相遇在结点
return fast;
}
}
return NULL;
}
};
链表经典题目
-
虚拟头节点
一些头节点需要单独判断是不是为空的操作
-
反转链表
递归和迭代,设置头节点会简单很多
-
删除倒数第N个节点
-
链表相交
-
环形链表
哈希表
哈希表理论基础
-
哈希函数就是一种数组的映射
-
可以用来快速的判断一个元素是否出现集合里
数组查询的时间复杂度为O(n)
哈希表查询的时间复杂读为O(1),以空间换时间
-
哈希碰撞
解决哈希碰撞的两种方法
拉链法:链表
线性探测法:talesize大于datesize,不然就没有位置去存放冲突的数据了
-
常见的三种哈希结构
数组
set(集合)
map(映射)
有效的字母异位词
-
思路:
暴力解法:两层for循环哈希表:空间为O(n),时间复杂度为O(1)
-
注意: 利用ASCII值进行计算
-
代码:
class Solution { public: bool isAnagram(string s, string t) { //申请一个大小为26的数组 int arr[26] = { 0 }; //这个大小位26的数组初始值为0 //遍历s,将其映射到数组中 for (int i = 0; i < s.size(); i++) { arr[s[i] - 'a']++; } for (int i = 0; i < t.size(); i++) { arr[t[i] - 'a']--; } for (int i = 0; i < 26; i++) { if (arr[i] != 0) { return false; } } return true; } };
两个数组的交集
-
思路:利用unorder_set,底层实现是哈希表,无序,key无序,key值不可重复,查询效率为O(1)
-
注意:利用数组做哈希表的,都是有大小的数,无大小的数用来做哈希表,容易造成空间的浪费。
-
代码:
class Solution { public: vector<int> intersection(vector<int>& nums1, vector<int>& nums2) { //用unorder_set数据结构 //存放结果,用哈希set主要是为了给结果去重 unordered_set<int> result_set; //括号里加入的是插入nums_set数组的范围 unordered_set<int> nums_set(nums1.begin(), nums1.end()); //循环遍历nums2 for (int num : nums2) { //可以理解为尾部的指针,如果找不到num就返回一个尾部的指针 if (nums_set.find(num) != nums_set.end()) { result_set.insert(num); } } //是一个函数,相当于把result_set的值复制到一个新的数组中 //这里复制到一个新的数组中是应为函数返回值类型为vector<int> return vector<int>(result_set.begin(), result_set.end()); } };
快乐数
-
思路:
如果sum值重复出现,那就进入无限循环了
否则就一计算,计算到sum=1为止
如果不适用哈希表,每次都进行查找,那么时间复杂度会为n^2
-
注意:
-
代码:
class Solution { public: int getSum(int n) { int sum = 0; //当n不等于0的时候 while (n) { sum += (n % 10) * (n % 10); n = n / 10; } return sum; } bool isHappy(int n) { //定义了一个sum_set数组 unordered_set<int> sum_set; //循环运行 while (1) { int sum = getSum(n); //是快乐数的返回值 if (sum == 1) { return true; } //跳出循环的条件 if (sum_set.find(sum) != sum_set.end()) { return false; } else { //插入sum值 sum_set.insert(sum); } n = sum; } } };
两数之和
-
思路:
数组、set、map这三种哈希表的结构中,只有map是key,value对应的
数组、set、map这三种哈希表的结构都有三种底部实现
底层实现是红黑树的都是要求key有序
底层实现是哈希表的查询效率和增删效率都最高,但是是无序实现
-
注意:
-
代码:
class Solution { public: vector<int> twoSum(vector<int>& nums, int target) { //定义一个map数组 unordered_map<int,int> map; //遍历数组,在已经访问过的map中寻找是不是有target-nums[i]的数组 for (int i = 0; i < nums.size(); i++) { auto iter = map.find(target - nums[i]); if (iter != map.end()) { return { iter->second,i }; } //没找到就把数值插入进map中 //其中pair<int,int>代表插入一个有序的键值对,模板参数 map.insert(pair<int,int>(nums[i], i)); } //都没找到返回一个空数组 return {}; } };
四数相加
-
思路:
四个独立的数组,不用考虑有重复的四个元素相加的情况
和计算两数之和一样,先将一个和存放到数组中,然后对比target-和
-
注意:
此题不用考虑不许重复出现
这个题中的unorder_map的用法同上一问不同
-
代码:
class Solution { public: int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) { //定义一个哈希map unordered_map<int, int> map; int size = nums1.size(); //循环遍历插入map //map中的key为和,value为出现的次数 for (int a : nums1) { for (int b : nums2) { map[a + b]++; } } int count = 0; //这个时候map里面就存着两数之和了 for (int c : nums3) { for (int d : nums4) { if (map.find(0 - (c + d)) != map.end()) { //找到了累加 //这个时候map里面就代表着key,数组的值就代表着value count += map[0 - (c + d)]; } } } return count; } };
赎金信
-
思路:
暴力枚举:两层for循环
哈希解法:如果用map的话,底层实现是红黑树和哈希表,map消耗的空间比较大
因此用数组更加直接
-
注意:
-
代码:
class Solution { public: bool canConstruct(string ransomNote, string magazine) { //ransomNote中的字母要在magzine中出现 int map[26] = {0}; for (int i = 0; i < magazine.size(); i++) { //将字符减a作为下标 int idex = magazine[i] - 'a'; map[idex]++; } //遍历递减,如果出现负数,跳出 for (int i = 0; i < ransomNote.size(); i++) { int idex = ransomNote[i] - 'a'; map[idex]--; if (map[idex] < 0) { return false; } } return true; } };
三数之和
-
思路:
-
哈希法:(思路)
不好去重,用哈希法来确定a-b的值
时间复杂度是O(n^2)
哈希表的额外空间复杂度是O(n)
-
双指针法
时间复杂度O(n^2)
空间复杂度O(1)
-
-
注意:
-
去重的逻辑思考,剪枝操作
-
总共有一轮剪枝
两轮去重
第一轮去重是控制第一个元素不要重复
第二轮去重是控制剩下两个元素不要重复
-
-
代码:
class Solution { public: vector<vector<int>> threeSum(vector<int>& nums) { //定义一个返回的数组值 vector<vector<int>> result; //首先对数组进行排序 sort(nums.begin(), nums.end()); //进行循环 for (int i = 0; i < nums.size(); i++) { //第一轮剪枝,如果第一个数值大于零,则不存在三数之和为0的情况 if (nums[i] > 0) { return result; } //第一轮去重,结果的一元数组不可以相同,但是一元数组内的元素可以相同 //如果当前位置和下一个位置进行比对,会出现,-1,-1,2 //因此去重操作应该是当前位置和前面的元素进行比较 if (i > 0 && nums[i] == nums[i - 1]) { continue; } //每一轮开始遍历 int left = i + 1; int right = nums.size() - 1; //只要遍历基点n的值确定不重复,那就可以确定不重复 //基点右边是一个二分查找 while(left<right) { //如果大于0要将数值减小 if (nums[i] + nums[left] + nums[right] > 0) { right--; } //小于零要将数值增大 else if (nums[i] + nums[left] + nums[right] < 0) { left++; } //等于零要将值存进三元组,同时收缩left和right else { result.push_back(vector<int>{nums[i], nums[left], nums[right]}); //第二次去重,对三元组中的两个元素进行去重 //这里面把指针移动到唯一的right和left处 //再进行加一,进行下一个循环 while (right > left && nums[right] == nums[right - 1]) { right--; } while (right > left && nums[left] == nums[left + 1]) { left++; } right--; left++; } } } return result; } };
四数之和
-
思路:
四数之和的思路和三数之和的思路一样
这两个之间就是通过双指针法把原本的时间复杂度降低一个等级
-
注意:
-
注意剪枝和去重
剪枝得时候注意,由于target是未知的,不可以直接用首元素大于target来进行剪枝
不然会出现-4>-10,但是数组接下来的几个数都是负数
但是还是可以进行剪枝
剪枝的本质就是正数的大于是无法再合成targrt的,不用target>0是因为剪枝不彻底,会漏掉target<0,但是第一个元素已经大于0的情况,用首元素大于0,且大于target是合理的。
-
注意四数之和相加得时候会出现数组越界,这个时候要用long,不然比较溢出
-
-
代码:
class Solution { public: vector<vector<int>> fourSum(vector<int>& nums, int target) { //定义一个返回的数组值 vector<vector<int>> result; //首先对数组进行排序 sort(nums.begin(), nums.end()); //第一层循环 for (int i = 0; i < nums.size(); i++) { //第一层剪枝 //如果第一个数就大于0,那就一定不会有四数之和等于0 //这里不可以直接用这个剪枝,因为target是一个未确定的值 if (nums[i] > target && nums[i] >= 0) { //这里用break,最后再统一返回 break; } //第一次去重 if (i > 0 && nums[i] == nums[i - 1]) { continue; } //第二层循环 for (int j = i + 1; j < nums.size(); j++) { //第二次剪枝,如果最开始的两个数加在一起都大于target if (nums[i] + nums[j] > target && nums[i] >= 0) { //跳出当前所有循环 break; } //第二次去重 if (j > i + 1 && nums[j] == nums[j - 1]) { //继续本次循环的下一次循环 continue; } //寻找两个数的和 int left = j + 1; int right = nums.size() - 1; while (left < right) { if ((long)nums[i] + nums[j] + nums[left] + nums[right] > target) { right--; } else if ((long)nums[i] + nums[j] + nums[left] + nums[right] < target) { left++; } else { //将此数组存进result中 result.push_back(vector<int>{nums[i], nums[j], nums[right], nums[left]}); //第三次去重 while (left < right && nums[left] == nums[left + 1]) { left++; } while (left < right && nums[right] == nums[right - 1]) { right--; } left++; right--; } } } } return result; } };
双指针总结
- 数组
- 数组不可以凭空消失,只能进行覆盖,移除查找的时候用双指针法
- 字符串
- 双指针法
- 链表
- 快慢指针法
- 双指针法
- N数之和法
- 双指针法
- 哈希法(其实用双指针法可以解决,但是用双指针法求下标不太ok,双指针会将表先进行排序,打乱下标,如果进行映射的话还会占用额外空间)
栈与队列
栈与队列理论基础
-
栈和队列是STL(c++标准库)里面的两个数据结构
-
STL是有多个版本的,只有知道使用的STL是哪个版本的才能知道对应的栈和队列的实现原理
三个最为普遍的STL版本:
- HP STL其他版本的C++ STL,一般是以HP STL为蓝本实现的,HP STL是C++ STL的第一个实现版本,且开放源代码
- P.J.Planger STL是由P.J.Plaunger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的
- SGL STL是由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGL STL是开源软件,源码可读性甚高。
-
关于SGI STL里面的数据结构
-
栈
栈先进后出,提供push和pop等接口,不提供走访功能,不提供迭代器。
栈是以底层容器来完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
STL一般不被归为容器,而是容器适配器(container adapter)
栈的内部结构可以是vector, deque, list,主要就是数组和链表的底层实现
-
SGI STL如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。
deque是一个双向队列,只要封住一段,开通另一端就可以实现栈的逻辑了
-
-
-
用栈实现队列
- 思路:用栈,一个模仿进队列,一个模仿出队列
- 注意:
- 代码:
用队列实现栈
-
思路:用一个队列,将最后一个元素前面的元素重新插入队列,
-
注意:
-
代码:
class MyStack { public: queue<int> que; MyStack() { } void push(int x) { que.push(x); } int pop() { //将队首元素重新插回到队尾 int size = que.size(); size--; for (int i = 0; i < size; i++) { int temp = que.front(); que.push(temp); que.pop(); } int num = que.front(); que.pop(); return num; } int top() { int size = que.size(); size--; for (int i = 0; i < size; i++) { int temp = que.front(); que.push(temp); que.pop(); } int num = que.front(); que.pop(); que.push(num); return num; } bool empty() { return que.empty(); } };
有效的括号
-
思路:利用栈的特性实现括号匹配
-
注意:通过一些巧妙的方式利用栈,多练,无他
-
代码:
class Solution { public: bool isValid(string s) { //如果括号是偶数,一定不符合条件 if (s.size() % 2 != 0) { return false; } stack<char> string; for (int i = 0; i < s.size(); i++) { if (s[i] == '(') { string.push(')'); } else if (s[i] == '[') { string.push(']'); } else if (s[i] == '{') { string.push('}'); } else { if (string.empty()||string.top() != s[i]) { return false; } string.pop(); } } return string.empty(); } };
删除字符串中的所有相邻重复项
-
思路:同括号匹配原理相似
-
注意:用字符串也可以实现栈的功能
-
代码:
class Solution { public: string removeDuplicates(string S) { string result; for (char s : S) { //当字符串为空或者当前字符不相等的时候 if (result.empty() || result.back() != s) { result.push_back(s); } else { result.pop_back(); } } return result; } };
逆波兰表达式求值
-
思路:后缀表达式求和思想
-
注意:注意弹栈时候进行加减乘除的时候num1和num2是有顺序的
stoll()函数是把字符串转化成longlong类型
-
代码:
class Solution { public: int evalRPN(vector<string>& tokens) { stack<long long> st; long long num1; long long num2; int size = tokens.size(); for (int i = 0; i < size; i++) { if(tokens[i] == "+"||tokens[i] == "-"|| tokens[i] == "*"|| tokens[i] == "/"){ num1 = st.top(); st.pop(); num2 = st.top(); st.pop(); if (tokens[i] == "+") st.push(num2 + num1); if (tokens[i] == "-") st.push(num2 - num1); if (tokens[i] == "*") st.push(num2 * num1); if (tokens[i] == "/") st.push(num2 / num1); } else { //stoll()将字符串转成longlong的整数 st.push(stoll(tokens[i])); } } return st.top(); } };
滑动窗口最大值(困难)
-
思路:
单调队列:保证队列里的元素单调递增或单调递减的原则
-
注意:
- if主要用于判断,while用于迭代
- 实际上时间复杂度是O(n)
-
代码:
class Solution { private: class MyQueue { public: deque<int> que; //每次弹出的时候,比较当前要弹出的数值是否等于队列出口的元素的数值,如果相等则弹出 //pop之前要判断队列是否为空 void pop(int value) { //相等的时候可以弹出,如果不相等,证明这个数现在不是最大值,没有在队列里面或者在后面 //可以不用弹出 //如果相等说明最大值弹出了 if (!que.empty() && value == que.front()) { que.pop_front(); } } //push是从后往前插入 void push(int value) { while (!que.empty() && que.back() < value) { que.pop_back(); } que.push_back(value); } //查询当前的最大值直接返回前端就可以了 int front() { return que.front(); } }; public: //实现一个单调队列 vector<int> maxSlidingWindow(vector<int>& nums, int k) { MyQueue que; vector<int> result; //首先将前n个放入数组中 for (int i = 0; i < k; i++) { que.push(nums[i]); } //开始将第一轮的最大元素输入到result result.push_back(que.front()); //迭代 for (int i = k; i < nums.size(); i++) { //移动滑动窗口 que.pop(nums[i-k]); que.push(nums[i]); //记录当前的最大值 result.push_back(que.front()); } return result; } };
前K个高频元素(思路)
-
思路:
- 首先构建map,map用来统计数值出现的次数
- 再将map中的元素进行排序寻找到前K个高频元素
- 用小顶堆,小顶堆弹出最小的n个值,最终得到了前K个高频元素
- 为什么不能用有序map呢?
-
注意:
- map和小顶堆的实现
- 有很多新的语法
-
代码:
class Solution { public: //定义了一个比较规则 class mycomparion { public: //pair是c++标准模板库中定义的一个模板类,用于表示一对值 //这个函数其实是定义了一种比较规则,按照两个pair数组的secound值的降序进行比较 bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) { //如果lhs的secound值大于rhs的secound值,就返回true return lhs.second > rhs.second; } }; vector<int> topKFrequent(vector<int>& nums, int k) { //用map统计元素出现的频率 unordered_map<int, int> map; for (int i = 0; i < nums.size(); i++) { //这个里面key值是nums[i] //value是key出现的次数 //对key进行排序并没有意义 //因此用无序map map[nums[i]]++; } //对value值进行排序 //定义一个小顶堆,大小为k //将所有频率的数值存进去,小的数值自然就被弹出 //priority_queue是一个容器适配器,用于实现基于某种比较的排序 //其底层实现逻辑是堆,保证头部永远是优先级最高的元素 //三个参数分别为,比较对象,存储比较对象的容器,比较逻辑 priority_queue < pair<int, int>, vector<pair<int, int>>,mycomparion> pri_que; //用固定大小为k的小顶堆,扫描所有频率的数值 //::是作用域解析符用来指定特定的命名空间或类的成员 //iterator是unordered_map<int,int>数据结构中定义的迭代器,用于遍历map for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) { //把其中的it指向的内容放进去 pri_que.push(*it); if (pri_que.size() > k) { pri_que.pop(); } } //小顶堆先输出的是最小的,采用倒叙来输出数组 vector<int> result(k); for (int i = k - 1; i >= 0; i--) { result[i] = pri_que.top().first; pri_que.pop(); } return result; } };
栈与队列的总结
-
栈与队列的理论基础
- 容器
- deque(双端队列)
- list(列表)
- vector(数组)
- 容器适配器
- stack(栈)
- queue(队列)
- priority_queue{元素,元素容器,优先级规则},(优先级队列)
- 栈与队列的问题
- 容器
- 总结
- 单调队列
- 优先级队列
二叉树
二叉树理论基础篇
-
题目分类
- 二叉树的遍历方式
- 二叉树的属性
- 二叉树的修改与构造
- 求二叉树的搜索属性
- 二叉树的公共祖先问题
- 二叉搜索树的修改与构造
-
二叉树的类型
-
满二叉树
-
完全二叉树
-
二叉搜索树
-
平衡二叉搜索树
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。
-
-
二叉树的存储方式
- 链式存储
- 顺序存储
-
二叉树的遍历方式
栈其实就是递归的一种实现结构
- 深度优先遍历
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历
- 层次遍历(迭代法)
- 深度优先遍历
-
二叉树的定义方式
-
struct TreeNode { int val; TreeNode *left; TreeNode *right; //初始化的时候左右指针为空 TreeNode(int x) : val(x), left(NULL), right(NULL) {} };
-
-
总结
二叉树是一种基础数据结构
二叉树的递归遍历
-
思路:
递归的思路:
1.确定递归函数的参数和返回值
2.确定中止条件
3.确定单层递归逻辑
-
注意:
-
代码:
二叉树的迭代遍历
- 思路:
- 注意:
- 代码:
二叉树的统一迭代法
- 思路:
- 注意:
- 代码:
二叉树层序遍历
二叉树的层序遍历
-
思路:层序遍历的底层逻辑是队列,先进先出
-
注意:
-
代码:
模板代码(打十个)
vector<vector<int>> levelOrder(TreeNode* root) { //定义一个存放Treenode指针的队列 queue<TreeNode*> que; //如果root不为空的时候将,root入队 if (root != NULL) { que.push(root); } //返回的是每一层 vector<vector<int>> result; //遍历直到队列为空 while (!que.empty()) { int size = que.size(); vector<int> layer; for (int i = 0; i < size; i++) { TreeNode* temp = que.front(); que.pop(); layer.push_back(temp->val); if (temp->left != NULL) { que.push(temp->left); } if (temp->right != NULL) { que.push(temp->right); } } result.push_back(layer); } return result; }
二叉树的层序遍历II
-
思路:
这种方式会超出内存限制
用一个栈去存储结果队列出队顺序层,右,左入栈之后再出栈顺序,左,右,层如果用栈的话如何进行分层呢
直接用队列,然后最后将result数组进行反转
-
注意:
反转数组的函数
二叉树的右视图
-
思路:
层次遍历,
但是只把最右的节点入栈,把每一层的最后一个节点入栈 -
注意:代码细节注意
二叉树的层平均值
- 思路:每一层的数相加,相除
- 注意:
N叉树的层序遍历
-
思路:
Node的数据结构是,存储节点的值,存储一个children数组
-
注意:
在每个树行中找最大值
-
思路:
层次遍历,找每一层的最大值
-
注意:
//INT_MIN是c++标准库中定义的宏常量,通常是一个最小的负数值,用来求最大值的时候定义初始量 int max = INT_MIN;
填充每个节点的下一个右侧节点指针
-
思路:
将next指针指向同层的下一个节点
层次遍历,每一层队列不为空的时候指向下一个节点,队列为空的时候指向NULL
-
注意:
填充每个节点的下一个右侧节点指针II
- 思路:完全二叉树和非完全二叉树没有什么区别
- 注意:
二叉树的最大深度
- 思路:
- 层次遍历,计算总共有多少层
- 递归,求左子树和右子树的最大深度,取最大值+1返回depth
- 注意:
二叉树的最小深度
- 思路:
- 层次遍历,如果有一个节点是叶节点就返回深度
- 递归,注意与求最大深度的单层递归逻辑不同,如果左子树为空,右子树不为空,返回的值应该是右子树的深度+1,而不是深度为1
- 注意:
翻转二叉树(拓展部分,中序遍历不可以)
-
思路:
递归实现
-
注意:
注意递归出口和递归条件
-
代码:
TreeNode* invertTree(TreeNode* root) { //递归出口 if (root == NULL) { return root; } //递归体 swap(root->left, root->right); invertTree(root->left); invertTree(root->right); }
对称二叉树
-
思路:
-
递归
-
确定递归函数的参数和返回值
比较左右两棵子树是不是对称的
比较的不是左右孩子,而是左右子树的对称节点
-
确定终止条件
首先处理空结点,非空结点比较的是数值
左节点为空,右节点不为空,false
左节点不为空,右节点为空,false
左右节点都为空,对称,返回true
-
确定单层递归逻辑
处理节点不为空的时候,比较val的值
并且将左节点的左孩子和右节点的右孩子进行比较
左节点的右孩子和右节点的左孩子进行比较
-
-
队列
-
栈
-
-
注意:注意代码细节
-
代码:
- 递归法
bool compare(TreeNode* left, TreeNode* right) { //判出条件 if (left == NULL && right == NULL) { return true; } else if (left == NULL && right != NULL) { return false; } else if (left != NULL && right == NULL) { return false; } else if (left->val != right->val) { return false; } //此时是左右节点都不为空,且数值相同的情况 //此时做递归判断 //只有左右递归都返回true的时候才返回true; bool L = compare(left->left, right->right); bool R = compare(left->right, right->left); return L && R; } bool isSymmetric(TreeNode* root) { if (root == NULL) { return true; } return compare(root->left, root->right); }
完全二叉树的节点个数
-
思路:
- 递归:分别计算左子树和右子树有多少个节点,再相加
- 层序遍历:统计总共遍历了多少层
-
注意:
//这样的代码是错误的 //忽略掉了一些情况 int countRecursion(TreeNode* left, TreeNode* right) { //判出条件 if (left == NULL && right != NULL) { return 1 + countRecursion(right->left, right->right); } else if (left != NULL && right == NULL) { return 1 + countRecursion(left->left, left->right); } else if (left == NULL && right == NULL) { return 1; } //这个时候就是左右节点都非空结点 int num = countRecursion(left->left, right->right) + 1; return num; } int countNodes(TreeNode* root) { if (root == NULL) { return 0; } return countRecursion(root->left, root->right); }
-
代码:
int countNodes(TreeNode* root) { //判出条件 if (root == NULL) { return 0; } int left = countNodes(root->left); int right = countNodes(root->right); int num = left + right + 1; return num; }
平衡二叉树
-
思路:
-
平衡二叉树:左右两棵子树的高度之差小于1
-
递归:一棵树是平衡二叉树,它的左右子树也是平衡二叉树
如果直接返回true和false逻辑很复杂
应该直接把函数分成两步,判断树是不是平衡二叉树,将左右两个树的判断结果进行与运算
//存在逻辑错误的代码 bool isBalanced(TreeNode* root) { //递归出口 if (root == NULL) { return true; } else { int lefthigh = treeHigh(root->left); int righthigh = treeHigh(root->right); if (lefthigh >= righthigh) { //在这一行会直接返回,永远不会达到right && left return (lefthigh - righthigh) <= 1; } else { return (righthigh - lefthigh) <= 1; } } bool right = isBalanced(root->right); bool left = isBalanced(root->left); return right && left; }
-
迭代:手动实现一个栈,用迭代的方法再写一遍
-
-
注意:
-
代码:
int treeHigh(TreeNode* root) { //用层次遍历求树的高度 queue<TreeNode*> que; int num = 0; if (root != NULL) { que.push(root); } TreeNode* temp; while (!que.empty()) { int size = que.size(); for (int i = 0; i < size; i++) { temp = que.front(); que.pop(); if (temp->left != NULL) { que.push(temp->left); } if (temp->right != NULL) { que.push(temp->right); } } //执行一次for循环就证明遍历了一个层 num++; } return num; } bool isBalanced(TreeNode* root) { //递归出口 if (root == NULL) { return true; } else { int lefthigh = treeHigh(root->left); int righthigh = treeHigh(root->right); if (lefthigh >= righthigh) { if (lefthigh - righthigh > 1) { return false; } } else { if (righthigh - lefthigh > 1) { return false; } } } bool right = isBalanced(root->right); bool left = isBalanced(root->left); return right && left; }
二叉树的所有路径
-
思路:
回溯
回溯和递归是一一对应的,有一个递归就要有一个回溯,递归和回溯要永远在一起
这道题目要求从根节点到叶子的路径,用前序遍历
递归:
1.要传入根节点,记录每一条路径的path和存放结果集的string
2.确定递归的终止条件
本题是要找到叶子节点,不让cur指向NULL
中止的时候,把vector
中的数据转化称string存入vector 数组中,函数进行回溯调用,继续寻找下一条路径。 3.先存根节点
如果根节点的左右节点为控,递归的时候就不进行下一层递归了
否则将左右节点加入,进行下一层递归,当达成判出条件的时候,要进行回溯
一个递归一个回溯,因此这里递归要加在花括号之前
//错误写法 //只有一条路径,最后pop_back一个数据,并没有起到回溯的作用 if (cur->left) { traversal(cur->left, path, result); } if (cur->right) { traversal(cur->right, path, result); } path.pop_back(); //正确写法 //回溯的时候,先往右走,然后右节点如果存在,再递归加入右节点的路径 if (cur->left) { traversal(cur->left, path, result); path.pop_back(); // 回溯 } if (cur->right) { traversal(cur->right, path, result); path.pop_back(); // 回溯 }
-
注意:
精简代码,二刷时候看一下
-
代码:
void traversal(TreeNode* cur, vector<int>& path, vector<string>& result) { //将cur压入栈中 path.push_back(cur->val); //判出条件 if (cur->left == NULL && cur->right == NULL) { string spath; for (int i = 0; i < path.size() - 1; ++i) { spath += to_string(path[i]); spath += "->"; } spath += to_string(path[path.size() - 1]); result.push_back(spath); } //递归体 //左 if (cur->left != NULL) { traversal(cur->left, path, result); path.pop_back(); } //右 if (cur->right != NULL) { traversal(cur->right, path, result); path.pop_back(); } } vector<string> binaryTreePaths(TreeNode* root) { vector<int> path; vector<string> result; if (root == NULL) { return result; } traversal(root, path, result); return result; }
左叶子之和
-
思路:
首先判断什么是左叶子,左叶子首先是左孩子,其次左右节点为空
因此判断是不是左叶子要看它的父节点
-
注意:
-
代码:
int sumOfLeftLeaves(TreeNode* root) { //判出条件 if (root == NULL) { return 0; } if (root->left == NULL && root->right == NULL) { return 0; } //递归体 int leftValue = sumOfLeftLeaves(root->left); //当左子树是左叶子的时候,修改左子树的值 if (root->left != NULL && root->left->left == NULL && root->left->right == NULL) { leftValue = root->left->val; } int rightValue = sumOfLeftLeaves(root->right); return leftValue + rightValue; }
找树左下角的值(二刷用递归写)
-
思路:
层序遍历:最后一层中的第一个节点
递归:
递归到最左边要判断是不是最后一行
1.确定递归函数的参数和返回值
2.确定终止条件
3.确定单层递归的逻辑
-
注意:
-
代码:
层序遍历
int findBottomLeftValue(TreeNode* root) { queue<TreeNode*> que; int result; que.push(root); while (!que.empty()) { int size = que.size(); TreeNode* left = que.front(); result = left->val; for (int i = 0; i < size; i++) { TreeNode* temp = que.front(); que.pop(); if (temp->left != NULL) { que.push(temp->left); } if (temp->right != NULL) { que.push(temp->right); } } } return result; }
递归
路径总和
-
思路:
递归+回溯
如果父节点不拎出来判断,root = NULL并不是返回的条件,叶子节点才是返回的条件
栈模拟:有空写一下
-
注意:
-
代码:
bool travel(TreeNode* root, int count) { //递归出口 if (root->left == NULL && root->right == NULL) { return count == 0; } //左节点 if (root->left != NULL) { count -= root->left->val; if (travel(root->left, count)) { return true; } //如果到这一步证明该节点下不存在路径,回溯 count += root->left->val; } //右节点 if (root->right != NULL) { count -= root->right->val; if (travel(root->right, count)) { return true; } count += root->right->val; } //遍历到这证明左右子树都没有路径 return false; } bool hasPathSum(TreeNode* root, int targetSum) { if (root == NULL) { return false; } //这个时候已经把根节点加入路径了 return travel(root, targetSum - root->val); }
路经总和II
-
思路:
同路径总和不同的是,这个需要找出所有的路径,递归不要返回值
-
注意:
注意根节点的处理,先把根节点放入path中,如果path递归到最后count不为0,这个path不会放到result中。
-
代码:
class Solution { private: vector<vector<int>> result; vector<int> path; void travel(TreeNode* root, int count) { //当遇到叶子节点的时候 if (!root->left && !root->right && count == 0) { //路径结束,且是正确路径 result.push_back(path); } //左节点 if (root->left) { //先将左节点入栈 path.push_back(root->left->val); count -= root->left->val; travel(root->left, count); count += root->left->val; path.pop_back(); } //右节点 if (root->right) { path.push_back(root->right->val); count -= root->right->val; travel(root->right, count); count += root->right->val; path.pop_back(); } return; } public: vector<vector<int>> pathSum(TreeNode* root, int targetSum) { if (root == NULL) { return result; } path.push_back(root->val); travel(root, targetSum - root->val); return result; } };
从中序与后序遍历序列构造二叉树
-
思路:
后序遍历:左右根
中序遍历:左根右
递归出口:
后续遍历队列为空就是
递归体:
根据后续遍历的最后一个元素,把中序割成小的数组
把最后一个节点入队
左中序遍历+左后序遍历确定左节点
右中序遍历+右后序遍历确定右节点
-
注意:
找边界值:保持边界值的计算是不变的,左闭右开或者左开右闭,或者左闭右闭
这里一般用左闭右开
-
代码: