2021-7-19
- 全手工实现LRU(Least Recently Used)算法
#include <iostream>
#include <map>
using namespace std;
struct Node {
int key, val;
Node *pre, *next;
Node() : pre(NULL), next(NULL) {}
Node(int k, int v) : key(k), val(v) {}
};
class DoubleList {
public:
//构造
DoubleList() {
head = new Node(0, 0);
tail = new Node(0, 0);
head->next = tail;
tail->pre = head;
size = 0;
}
//在链表尾部添加元素,时间复杂度O(1)
void addLast(Node *x) {
tail->pre->next = x;
x->pre = tail->pre;
x->next = tail;
tail->pre = x;
size++;
}
//删除链表中的x节点(该节点一定存在)节点指针(地址)由哈希表给出
void remove(Node *x) {
x->pre->next = x->next;
x->next->pre = x->pre;
//free(x); 此处不能free,因为挪动节点到开头需要用到此函数
size--;
}
//删除链表中的第一个节点,并返回该节点,时间O(1)
Node *removeFirst() {
if (head->next == tail)
return NULL;
Node *first = head->next;
head->next = first->next;
head->next->pre = head;
size--;
return first;
}
int getSize() {
return size;
}
//按照LRU顺序,展示所有键值,用于测试
void show(){
Node*p=tail;
while(p->pre!=head){
cout<<p->pre->key<<":"<<p->pre->val<<" ";
p=p->pre;
}
}
private:
//头尾虚节点
Node *head, *tail;
//链表元素个数
int size;
};
class LRUCache {
public:
//构造:提供最大容量
LRUCache(int capacity) {
this->capacity = capacity;
}
//获取某键的缓存值
int get(int key) {
//没有该键的缓存值
if (Map.find(key) == Map.end()) {
return -1;
}
//将该键提升为最近使用的键
makeRencent(key);
return Map[key]->val;
}
//存放某键的缓存值
void put(int key, int val) {
//缓存键已存在,则更新键值
if (Map.find(key) != Map.end()) {
deleteKey(key);
addRencent(key, val);
return;
}
//如果已满,则淘汰最近最少使用的缓存
if (cache.getSize() == capacity) {
removeLeastRecently();
}
//添加为最近使用的元素
addRencent(key, val);
}
//测试用,按LRU顺序展示缓存中所有内容
void show(){
cache.show();
cout<<endl;
}
//将某个key提升为最近使用的
void makeRencent(int key) {
Node *x = Map[key];
cache.remove(x);
cache.addLast(x);
}
//添加最近使用的元素
void addRencent(int key, int val) {
Node *x = new Node(key, val);
Map[key] = x;
cache.addLast(x);
}
//删除某个key
void deleteKey(int key) {
Node *x = Map[key];
cache.remove(x);
Map.erase(key);
free(x);
}
/*删除最久未使用元素*/
void removeLeastRecently() {
Node *target = cache.removeFirst();
int key = target->val; //这就是为什么双向链表中节点要存储key,是为了删除的时候,反向查找在map中的位置并删除
Map.erase(key);
free(target);
}
private:
//哈希表
map<int, Node *> Map;
DoubleList cache;
int capacity;
};
int main() {
LRUCache lru(4);
while(1){
string op;
cin>>op;
if(op=="get"){
int key;
cin>>key;
int res=lru.get(key);
if(res==-1){
cout<<"Not exist!"<<endl;
continue;
}
cout<<key<<":"<<res<<endl;
lru.show();
}else if(op=="put"){
int key,val;
cin>>key>>val;
lru.put(key,val);
lru.show();
}else{
cout<<"wrong command!"<<endl;
continue;
}
}
}
2021-7-18
- 并查集Union-find算法,主要解决的是图论中的
动态连通性
- 并查集数组实现(路径压缩)
#include <iostream>
using namespace std;
#define MAXN 10
int fa[MAXN+1];
void init(){
for(int i=1;i<=MAXN;i++)
fa[i]=i;
}
//返回当前节点的根节点,并进行路径压缩
int find(int x){
if(fa[x]==x)
return x;
fa[x]=find(fa[x]);
return fa[x];
}
//合并两个节点,注意在同一棵树上的问题
void merge(int a,int b){
fa[find(a)]=find(b);
}
按秩合并
优化版:
路径压缩版本只能在find的时候实现,因此合并过程中,还是会出现很复杂的树。为了避免树过深,可以选择按秩合并(即高度低的树,合并到高度高的树上,来避免树高度的增长,当然两棵树高度相同的情况下,合并后必然有一棵树高度会+1)
#include <iostream>
using namespace std;
const int MAXN = 7;
int fa[MAXN+1];
int rank_[MAXN + 1];
void init(){
for(int i=1;i<=MAXN;i++){
fa[i]=i;
rank_[i]=1;
}
}
int find(int x){
if(x==fa[x]){
rank_[x]=1;
return x;
}
fa[x]=find(fa[x]);
rank_[x]=2;
return fa[x];
}
void merge(int a,int b){
int ap=find(a);
int bp=find(b);
//秩(高度)低的,合并到秩高的树上
if(rank_[ap] > rank_[bp]){
fa[bp]=ap;
}else{
fa[ap]=bp;
}
//如果两棵树高度相同,那么合并后,合并的目标树高度要加一(排除同一棵树的情形)
if(rank_[ap] == rank_[bp] && ap != bp){
rank_[bp]++;
}
}
2021-7-17
- 今天做一道leetcode题目1373二叉搜索子树的最大键值和
- 我花了两个小时的时间,研究怎么在一个非BST的子树中,找出最大BST键值和,费了老劲。最后发现题解中,只需要用一个flagMAX记录一下最大值即可啊!!!气。
- 还吸取了一个教训,找BST的时候,空节点直接视为一棵空BST,有时更为简单。
- 关于判断BST合法性这个问题,有三种思路
- 利用BST中序遍历有序这个性质,先对该树进行中序遍历,再判断是否是升序即可。
- 对于树的操作应该着眼于当前节点,根据BST的定义,当前节点的值应该大于所有左子树节点的值(大于左子树最大值),且小于所有右子树节点的值( 小于右子树最小值)。据此可以写出递归函数,不过要在空节点的返回值上要加以设计。
- 本题还可以为为每个节点设置阈值(上限和下限),通过递归传递和更新阈值。(此方法把节点之间缠绕在一起,单独做此题目容易,如果BST和其他问题结合,则不好处理)
class Solution {
public:
bool isValidBST(TreeNode* root) {
return checkBST(root,LONG_LONG_MIN,LONG_LONG_MAX);
}
bool checkBST(TreeNode*root,long long low,long long high){
if(!root)
return true;
if(root->val<=low || root->val>=high)
return false;
return checkBST(root->left,low,root->val)
&& checkBST(root->right,root->val,high);
}
};
class Solution {
public:
//树的递归中,以当前节点为思考对象,最佳
bool isValidBST(TreeNode* root) {
return checkBST(root)[0];
}
//三个返回值:1.是否是BST 2.最小值 3. 最大值
vector<long long>checkBST(TreeNode*root){
if(!root)
return {1,LONG_LONG_MAX,LONG_LONG_MIN};
vector<long long>l_res=checkBST(root->left);
vector<long long>r_res=checkBST(root->right);
if(l_res[0] && r_res[0] && root->val>l_res[2] && root->val<r_res[1]){
long long l_min=l_res[1];
long long r_max=r_res[2];
return {1,l_min!=LONG_LONG_MAX?l_min:root->val,r_max!=LONG_LONG_MIN?r_max:root->val};
}
return {0,0,0};
}
};
2021-7-16
- 昨天会写的二叉树的序列化,今天再去想这个问题,竟然不会写。发现自己还是没有完全领悟二叉树和递归的思维。二叉树中递归的思维,应该着眼于根节点,处理好一个节点的行为,递归下去便完成了对整棵树的操作。
- 考虑二叉树序列化这个问题,对于以root为根节点的二叉树来说,先得直到左子树和右子树的序列化内容,才能返回
左子树序列化+根节点val字符串+右子树序列化
,因此要使用后序遍历 - 需要先直到子树内容,才能知道当前树内容的,通通用后续遍历。
- 考虑二叉树序列化这个问题,对于以root为根节点的二叉树来说,先得直到左子树和右子树的序列化内容,才能返回
//用**后序遍历**的方式,实现二叉树**前序的序列化**
string serializeTree(TreeNode*root){
//终止条件
if(!root)
return "";
//逻辑
string ans=intToStr(root->val);
string lStr=serializeTree(root->left);
string rStr=serializeTree(root->right);
if(lStr!=""){
ans+=','+lStr;
}
if(rStr!=""){
ans+=','+rStr;
}
return ans;
}
string intToStr(int n){
string s;
stringstream ss;
ss<<n;
ss>>s;
ss.clear();
return s;
}
-
今天对补码有了全新的认识
其中1000 0000
是规定的最小值,然后简单+1就是-127,可以一直简单+1到127
补码的好处就是让如1-2的运算,变成硬件最简单操作的相加运算
1-2=0000 0001
+1111 1110
=1111 1111
=-1
-
C++中如何获取int型的最大值和最小值
- 方法一:头文件
climits
中有INT_MAX
,INT_MIN
- 方法二:
- 最小值:1<<31,即
1000 0000...0000
,补码规定的负数的最小值, − 2 31 − 1 -2^{31}-1 −231−1 - 最大值:unsigned int a=0;
~a>>1
即为0111 1111...1111
, 2 31 2^{31} 231
- 最小值:1<<31,即
- 方法一:头文件
2021-7-15
- 二叉搜索树BST
- 判断一棵树是否是BST:需要借助两个变量记录每个节点应该遵循的上限和下限,然后递归即可
- 增加节点:递归找到位置(NULL),然后接在上面即可。
- 删除节点:
第五种情况,操作如下:
第一步
第二步
第三步
第四步
2021-7-14
- 二叉搜索树(BST,Binary Search Tree)是递归定义的
- BST为空,或者
- 对于二叉搜索树的每一个节点root,左子树所有节点得值小于root值,右子树所有节点的值大于root值
- 对于BST的每一个节点,它的左子树和右子树都是BST
- 没有键值相等的结点
- 二叉搜索树的重要性质:中序遍历是有序的
- 二叉树的各种递归,无非是:1.前序遍历 2.中序遍历 3.后序遍历
- 二叉树的序列化,使用后序遍历(注意要以#处理叶节点的子节点)
- 对于二叉树的递归构造,应该想方设法构造根节点,只要根节点构造出来了,其他所有节点根据递归就全部都构造出来了。
2021-7-13
- 快慢指针、双指针、多指针,在链式结构中是十分重要的思想。
- ”链表是一种兼具迭代性质和递归性质的数据结构”,很有道理。
- 今天做
反转链表这道题目
,迭代和递归方法都非常精妙,其中博主labuladong对递归的思考方式令我印象深刻,受益匪浅。
Leetcode206 反转链表- “不要跳进递归(你的脑袋能压几个栈呀?)”,博主如是说
下面这一步十分精妙
- 我认为,实现递归的代码,不应该跳进递归的轮回中,应该假装递归方法已经实现,直接调用,然后补其具体递归逻辑。
- 此外,递归最为重要的就是,递归函数的含义。
- “不要跳进递归(你的脑袋能压几个栈呀?)”,博主如是说
2021/7/11
- 对于递归函数来说,最重要的就是递归函数的含义。
- 写递归算法的关键是要明确函数的「定义」是什么,然后相信这个定义,利用这个定义推导最终结果,绝不要跳入递归的细节。
2021/6/3
- 做dp的题目,首先要搞清楚状态转移方程,然后应该举个小例子草稿实现一下dp,以使对边界条件有个清晰的认识。
- 关于字符串的dp,经常把dp[i]定义成以i位置字符结尾的串的个数
2021/6/2
- 遇到问题类似最少多步、最少多少次这类问题,一定要想到用bfs或者最短路径算法
- 蓝桥杯国赛比赛之前默念:搜索、位运算、全排列、数学问题
- 当设计递归函数遇到困难,搞不清楚时,举个例子模拟一下过程就好了。
- 当算法中出现大量重复问题的时候,可以采用记录的方法,可以大大降低时间复杂度
2021/6/1
- 感悟:今天重做蓝桥杯题目对局匹配的时候,更加深刻地明白了两个道理
- 懂和会写,是完全两件事,明白怎么写不等于能写出通过oj的代码
- 编程不能闭门造车,要吸收别人优秀的代码和思想。
- 遇到统计方案数的题目,一定要注意重复问题
- dfs有爆栈的风险,可以用bfs来替代
- 对于图的相关问题如发现环,或者排序,首先要想到拓扑排序