c++ map,set封装

map 是一个 kv 结构, set 是 k结构。
我们前面模拟实现了 红黑树,但是我们实现的红黑树把 kv 结构写死了,怎么样才能用泛型编程的思想来实现map和set呢
我们先简单看一下原码中是怎么实现的

1.原码实现逻辑

我们打开这里的 stl_set.h
在这里插入图片描述
通过我们前面学习红黑树,我们看到这里的 rb_tree 的名字 就是一棵纯正的红黑树
我们打开 tree.h 的头文件去找这个 rb_tree
在这里插入图片描述
在这里插入图片描述

这里用 true 和 false 来表示 red 和 black,和我们的枚举有区别,但是没影响,只要能判断红黑即可。
这里map 和 set 都使用了红黑树的代码
接下来开始我们的重头戏
在这里插入图片描述
我们实现节点是一个 struct 就实现了,但是这里还用到了继承,并且我们的红黑树传入了两个模版参数 K, V ,但是这里的节点却只有一个 Value 的模版参数,我们接着往下看
在这里插入图片描述
我们看到,源代码实现的红黑树传入参数有点多,而且下这个整体的封装过程挺麻烦的。
在这里插入图片描述

首先,他的 __rb_tree_node 只传入了一个 Value 的模版参数。但是 map 是 kv 模型,只传入了一个 Value ,怎么能让 map 满足两个类型呢?
我们打开 stl_set.h ,虽然这里重命名了,但是 key_type 和 value_type 的原本类型都是 Key 类型
在这里插入图片描述
我们把这里联系起来
在这里插入图片描述
那这样我们猜测,map 中应该也有一个 key_type 和 value_type ,且这两个类型不同
在这里插入图片描述
这里我们能看到,key_type 和 value_type 的原本类型不同,且 value_type 还是用 pair<const Key, T> 来实现的
在这里插入图片描述
我们先简单理解一下,这里通过一个模版参数 value_type 来确定是哪种类型,如果传入的只是一个 value_type,就是 k 模型,如果传入的参数是键值对 pair<const Key, T>,那这里就是 kv 模型
这里就通过传入参数,使 map 和 set 可以使用同一段代码,实现了泛型编程的思想

2. 模拟实现

set

namespace xsz1
{template<class K>class set{private:RBTree<K, K> _t;}
}

map

namespace xsz2
{template<class K, class T>class map{private:RBTree<K, pair<K, T>> _t;}
}

这里我们先把 K 设置为可修改的。
在这里插入图片描述
我们这里的写法比源代码简化了很多,因为只是模拟实现,不是完全实现,所以只是实现基本功能。
上面的 map 和 set 写完后,我们还需要修改 红黑树的基本代码

template<class T>
struct RBTreeNode
{RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;//pair<K, V> _kv;Colour _col;T _data;RBTreeNode(const T& data):_left(nullptr),_right(nullptr),_parent(nullptr),_data(data),_col(RED){}
};
template<class K, class T>
class RBTree
{
public:typedef RBTreeNode<T> Node;//...
};

2.1. insert

但是还有一个问题需要处理,第二参数怎么比较大小?
如果是 set 比较大小很简单,都是 key,直接比较,那么 map 的键值对呢
在这里插入图片描述
我们看到,键值对是可以比较的,但是这个比较大小和我们想要的不一样,这里的比较大小,会先去比较 first,在 first 相同的情况下,会去比较 second,但是我们只需要比较 first,所以这里我们还需要单独处理一下
但是要怎么处理才能让 set 和 map 都适合这种用法呢

while(cur)
{if(cur->_data <data){parent = cur;cur = cur->-right;}else if(cur->_data >data){parent = cur;cur = cur->_left;}else{return false}
}

我们能不能用类型对比的方式?

if(cur->_data.type == K)
{//...
}

如果 data 的类型和传入的第一模版参数类型相同,我们就直接比,如果传入的类型是 pair 类型,我们再对比 data.first
但是这样写肯定不行,其实 c 语言中有一种写法能支持这种思路
typeid.name()

if(typeif(cur->_data).name == K)
{//...
}

typeif.name 可以把传入的内容转化成字符串,可以按照这个思路写下去,但是这样写不太符合我们的泛型编程的思想。
还有一个方法

template<class K, class T>
class map
{
public:struct MapKeyofT{const K& operator()(const K& key){return key.first;}};
private:RBTree<K, pair<K, T>, MapKeyofT> _t;
};
template<class K>
class set
{
public:struct SetKeyofT{const K& operator()(const K& key){return key;}};
private:RBTree<K, K, SetKeyofT> _t;
template<class K, class T, class KeyofT>
class RBTree
{//....KeyofT kot;kot(cur->_data <kot < kot(data));
}

我们在 set 和 map 中实现两个仿函数,这两个仿函数在调用时会返回该对象的 value 。
如果是 set 调用,会返回 key,如果是 map 调用,会返回 键值对中的 first。
我们把这两个类作为模版参数传入 RBTree 中,在 RBTree 中,我们只需要去调用这个 kot 即可,kot 会把set , map 中需要比较的部分返回供我们比较,也就不需要我们自己去考虑怎么处理了。

2.2. 迭代器

2.2.1. 红黑树迭代器

红黑树,树形结构,和之前学的链表的迭代器很像。

template<class T, class KeyofT>
struct __TreeIterator
{typedef RBTreeNode<T> Node;typedef __TreeIterator<T, KeyofT> Self;Node* _node;__TreeIterator(Node* node):_node(node){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}
}

我们先把最基础的部分写出来。
上面这些都没什么问题,迭代器支持 ++,那么我们的迭代器怎么写才能支持++呢?
拿这棵树研究研究
在这里插入图片描述

一般情况下,会从 begin() 到 end()
begin() 的位置很好确认,就是 _root 的最左节点
在这里插入图片描述
观察后,我们发现,如果当前节点不存在孩子节点时,会去找节点是父亲左节点的父亲节点节点。
在这里插入图片描述
比如当 it== 5 或者 it == 10 时,应该会去找 节点是父亲节点左孩子节点的那个父亲节点
在这里插入图片描述

比如这里如果 it 是 9 ,先去找 10 ,然后走到10 的右孩子节点,此时要向上走,10这棵树已经走完了,那么就去找 10 的父节点。it 在 10 的右孩子节点时,不满足 节点是父亲左节点的条件,接着往上找,10 是 11 的父亲节点的左节点,所以 _node = 11,开始下一步。
如果节点的右子树不为空,那就去右子树中找最左节点。
这句比较好理解,和 begin() 位置一样,直接找最左节点。
我们在实现 ++ 前,先把 begin() 和 end() 实现了

iterator begin()
{Node* cur = _root;while(cur && cur->_left){cur = cur->_left;}return iterator(cur);
}
iterator end()
{return iterator(nullptr);
}

这里的 begin 和 end 函数都比较好理解
begin 最开始要指向最小的节点,所以最开始就是 cur 找最左节点即可
但是 end 要注意,并不是指向最右的节点,按照我们之前对vector 那些迭代器的理解,end 返回的是最后一个节点后面位置的位置,但是当遍历到最大节点时,树中是没有下一个节点的,所以我们默认直接返回 nullptr(按照上面++ 的逻辑,最后会指向 _root 的父节点,但是 _root 本身就是空节点,所以直接用 nullptr 返回即可)。
下面开始实现 ++ 的操作

Self& operator++()
{if(_node->_right){Node* cur = _node->_right;while(cur && cur->_left){cur = cur->_left;}_node = cur;}else{}return *this;
}

右不为空的情况下,去找右子树的最左节点
右为空的情况下,去找节点是父节左的那个父亲节点

Self& operator++()
{if(_node->_right){Node* cur = _node->_right;while(cur && cur->_left){cur = cur->_left;}_node = cur;}else{Node* cur = _node;Node* parent = cur->_parent;while(parent && cur == parent->_right){cur = parent;parent = cur->_parent;}_node = parent;}return *this;
}

这样,我们的 ++ 逻辑就写完了
接下来是 != 操作

bool operator!=(const Self& s)
{return _node != s._node;
}

这里注意是节点指针进行比较,不是对比节点的值,或者是传入的 s 对象。
减减操作和加加操作相反。
++:
右树存在找右树最左节点
右树不存在,找节点是父亲左的那个父亲节点
–:
可以把情况想的极端一点,++是从最左节点遍历到最右节点,–是从最右节点遍历到最左节点,也就是说这俩操作是个完全对称的。
左树存在找最右节点
左树不存在,找节点是父亲右的那个父亲节点

        Self& operator--(){if (_node->_left){Node* cur = _node->_left;while (cur->_right){cur = cur->_right;}_node = cur;}else{Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_left){cur = parent;parent = cur->_parent;}_node = parent;}return *this;}

在这里插入图片描述

2.2.2. set,map封装迭代器

红黑树的迭代器写完了,接下来要在 set 中也实现迭代器,因为我们要通过 set 的接口去使用红黑树。

template<class K>
class set
{
public://..typedef RBTree<K, K, SetKeyofT>::iterataor iterator;iterator begin(){return _t.begin();}iterator end(){return _t.end()}
private:RBTree<K, K, SetKeyofT> _t;
}

既然迭代器写好了,那么我们来试试吧

void test_set()
{xsz1::set<int> s;s.insert(4);s.insert(1);s.insert(2);s.insert(3);s.insert(2);xsz1::set<int>::iterator it = s.begin();while (it != s.end()){cout << *it << "  ";++it;}cout << endl;
}

当我们运行这段代码后,我们会惊奇的发现
在这里插入图片描述
他崩了
注意这里显示出来的问题,iterator 不是类型,我们明明 typedef 了 红黑树的iterator, 为什么这里说 iterator 不是类型呢?
这里要注意,域作用访问限定符

typedef RBTree<K, K, SetKeyofT>::iterataor iterator;

一般除了这里会用到 这个操作符,我们在声明 其类中的静态成员变量也会用到这个操作符,因为我们的红黑树代码是用模版来实现的 ,所以这段代码在预编译的时候并不存在红黑树的代码(实例化后才会出现,想要实例化也是在运行期间),而预编译时访问到了这段代码,编译器就不知道你是想给 iterator 这个静态成员变量重命名,还是给这个类型命重命名,所以会报错。
因此如果想通过这里,我们必须强制说明一下

typedef typename  RBTree<K, K, SetKeyofT>::iterataor iterator;

现在可以开测我们的代码了
在这里插入图片描述
我们看到,通过迭代器,随机插入的数据正向输出了。
迭代器实现了,范围 for 也就随便用。
然后是 map 的迭代器,与 set 同理

template<class K>
class set
{
public://..typedef RBTree<K, pair<K, T>, MapKeyofT>::iterataor iterator;iterator begin(){return _t.begin();}iterator end(){return _t.end()}
private:RBTree<K, pair<K, T>, MapKeyofT> _t;
}

我们运行之后
在这里插入图片描述
完全没什么问题。
虽然 set,map 大题功能已经实现,但不是还存在一个很严重的问题。
如果我们去修改 迭代器位置的值

(*it) = 100;

在这里插入图片描述
我们看到,修改后的数据并不会自动去调整,而且最小的值被修改成了 100,我们的红黑树结构也遭到了破坏。
所以接下来我们要想办法,如何修改才能使key 值不可修改。

2.2.3. key 值不可修改

想要解决这个问题,我们先去库里看看大佬的思路。
set.h
在这里插入图片描述
set 只含有一个 key,在这里 iterator 被处理为 const_iterator,我们去看看 map 是怎么处理的
在这里插入图片描述
map并的处理方式和 set 完全不一样。
set 只含有一个值,这个key值本身就不能被修改,所以这里的 const_iterator 就能保证其中的 key 值不会被修改。
但是 map 中含有 key 和 value,map 中的 key 值不能被修改,但是 value 值是可以被修改的,所以 map 就不能那么暴力的直接全部修改为 const_iterator。
这里 set 和 map 的实现逻辑就不太一样了。
首先,我们先把insert 的返回值修改一下,库里面的返回值是键值对,这里我们也返回键值对

pair<iterator, bool> insert(const T& data)
{//...
}

红黑树的 insert 修改后,要注意 map.h 和 set.h 中的 insert 的返回值也要修改。

pair<iterator, bool> insert(const T& data)
{return _t.insert(data);
}

因为 set 需要用到const_iterator 所以我们先在红黑树中把 const_iterator 实现了
这里和我们前面实现 list 的const_iterator 的做法一样,我们在迭代器的模版参数中传入 Ref(reference参考值) 和 Ptr(指针), 当我们调用 const_iterator 时,就给 迭代器传入 const T& 和 const T*,如果我们要用 iterator, 我们就传入 T& 和 T*

template<class T, class Ref, class Ptr, class KeyofT>
struct __TreeIterator
{//...
}
template<class K, class T, class KeyofT>
class RBTree
{
public:typedef __TreeIterator<T, T&, T*, KeyofT>  iterator;typedef __TreeIterator<T, const T&, const T*, KeyofT> const_iterator;//...
}

const_iteraor 写好后,我们就开始操作 set

typedef typename RBTree<K, K, SetKeyofT>::const_iterator iterator ;
typedef typename RBTree<K, K, SetKeyofT>::const_iterator iterator;
pair<iterator, bool> insert(const K& key)
{return _t.insert(key);
}
iterator begin()const
{return _t.begin();
}
iterator end()const
{return _t.end()
}

把这里写好之后,我们尝试修改 set 中的值
在这里插入图片描述
这个时候,这里就会报错,我们不拿这里处理之后,运行代码,我们发现
在这里插入图片描述
还是报错了。
这里的问题就比较复杂了,
先看我们 红黑树 和 set 中 insert 中的返回值

pair<iterator, bool> insert(const T& data)
{return make_pair(iterator(cur), true);
}
//set.h
pair<iterator , bool> insert(const K& key)
{return _t.insert(key);
}

这里和 pair 的特性相关
在这里插入图片描述
我们知道,pair 的牛逼之处在于它的拷贝,他不需要我们一定要传入相同类型的值才能拷贝构造,而是只要 传入参数 pair 中的 第一第二参数能构造 被传入 的 pair 中的第一第二参数,就能完成 pair 的拷贝构造

 void func(pair<string, const int> p1){//..}int main(){pair<const char*, int> p;func(p);return 0;}

在这里插入图片描述这里是没有问题的,因为 const char* 能构造 string, int 能构造 const int,所以 p 就能去构造 p1。
现在回到上面的问题,为什么这里会报错。
我们先清楚一件事,set 的 insert 返回值pair 中的类型iterator,对红黑树来说是 const_iterator 类型。
红黑树返回的类型是 pair<iterator, bool> 类型,
这里的 iterator 构造出来的类型 是 __TreeIterator<T, Ref, Ptr, KeyofT> 的类型
但是我们set 接收到这个类型后·,再返回 pair<const_iterator, bool> 类,型这里的 返回类型 是
RBTree<K, K, SetKeyofT>::const_iterator 类型,也就是
__TreeIterator<K, T, const T&, const T*, KeyofT>。
红黑树的 insert 返回的 pair 中 的迭代器没有 const T 的类型,但是 从 set 的 insert 返回的 pair 的迭代器中含有 const T 类型,pair 能做到 模版参数只要参数类型能构造就行,但是 我们模拟实现的迭代器是不支持自我完善的,也就是说 __TreeIterator<T, Ref, Ptr, KeyofT> 不能去构造 __TreeIterator<K, T, const T&, const T*, KeyofT> 类型 。
解决方法
所以我们可以考虑,不要使用它的默认拷贝构造,我们自己去写一个拷贝构造函数
除了这个方法,还有个更简单的方法
既然 pair 不需要传入和目标相同的模版参数,只要类型能构造就行,那么我们管目标是什么迭代器,我们直接把节点给set,set拿到节点爱怎么构造不关我红黑树的事

pair<Node*, bool> insert(const T& datd)
{//...return make_pair(cur, true);//...
}

在这里插入图片描述
此时我们的代码就能正常运行了,set 拿到节点,就要去自己用节点去构造 const_iterator。
这里要注意 const_iterator 和 const iterator 不是一个东西,const iterator只能保证这个迭代器不能修改,也就不能进行++,–等操作, 但是是可以修改 iterator 内的值。而 const_iterator 是我们自己实现的可以进行 ++,–等操作的类,只是这个类中的成员都用 const 进行修饰,成员不能修改。

然后就是 map 了,map 因为迭代器不那样暴力处理,我们想既然 key 不能修改,但是 value 可以修改,那么把 所有返回和 K 相关的地方,都返回 const K 应该就行了吧。

        template<class K, class T>class map{public:struct MapKeyofT{const K& operator()(const pair<K, T>& key){return key.first;}};typedef        typename RBTree<K, pair<const K, T>, MapKeyofT>::iterator iterator;typedef        typename RBTree<const K, const pair<const K, T>, MapKeyofT>::iterator const_iterator;pair<iterator, bool> insert(const pair<const K, T>& key){return _t.insert(key);}iterator begin(){return _t.begin();}iterator end(){return _t.end();}T& operator[](const K& key){pair<iterator, bool> ret = insert(make_pair(key, T()));return (ret.first)->second;}private:RBTree<K, pair<const K, T>, MapKeyofT> _t;};

在这里插入图片描述
大概逻辑写完后,我们看到 map 中的 第一参数不能修改,第二参数可以修改。
在这里插入图片描述

2.2.4. operator[]

既然我们上面把迭代器整体都玩完了,那么再写一个 [] 操作助助兴。
在这里插入图片描述
这里的解析在 map_set 学习的那节,想要了解这个怎么操作的可以去那里了解,这里主要进行实现

T& operator[](const K& key)
{pair<iterator, bool> ret = insert(make_pair(key, T());return (ret.first)->second;
}

注意这个操作要完成的事,如果 [] 位置不存在值则是插入,如果存在值则返回其中的 value

void test_map2()
{string arr[] = { "香蕉", "甜瓜", "苹果", "西瓜","香蕉", "甜瓜", "苹果", "西瓜", "香蕉", "甜瓜", "苹果", "西瓜", "香蕉", "甜瓜", "苹果", "西瓜", "香蕉", "甜瓜", "苹果", "西瓜" };xsz2::map<string, int> m;for (auto& e : arr){m[e]++;;}for (auto& e : m){cout << e.first << "  " << e.second << "    ";}cout << endl;
}

在这里插入图片描述
这样我们的 [] 操作节完成了。
最后一个问题,我们在 RBTree 中传入了 模版参数 K,但是我们这里全程没用到过这个类型,我们需要 key 时直接使用了 KeyofT,这个 K 模版参数真的有必要吗?
有,因为我们这里主要实现的是 insert 操作,set,map中含有其他操作,比如 find,如果要实现find 操作

iterator find(const k& key)
{//...
}

可以说我们在简单的模拟实现这里没用上,但是绝对不能说他没有用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/11238.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Dubbo基本使用

Dubbo基本使用 1.项目介绍2.开发步骤2.1 启动注册中心2.2 初始化项目2.3 添加 Maven 依赖2.3.1 父pom.xml2.3.1 consumer模块和provider模块pom.xml 2.4 定义服务接口2.5 定义服务端的实现2.6 配置服务端 Yaml 配置文件2.7 配置消费端 Yaml 配置文件2.8 基于 Spring 配置服务端…

芯片原厂工程师带你学 Linux 驱动

芯片原厂工程师&#xff0c;手把手带你学Linux驱动&#xff0c;感兴趣的点个关注私聊呀。 介绍&#xff1a; https://b2qtatgfkp.feishu.cn/docx/HoBKdezVFo6HlVx0hXPc8R7QnWc

JINGWHALE 数字认证体系 · 进阶知识库

JINGWHALE 数字认证体系 是 JINGWHALE 数字科学艺术创新中心 的数字认证服务。 ◢◤ 宗旨 致力于数字化知行合一的知识赋能&#xff01; ◥ 数字化人才培养 培养数字化思维&#xff0c;传播数字化知识&#xff0c;赋能各行业数字化。 ◥ 职业人才发展 无缝衔接学校高等…

LeetCode题目104: 二叉树的最大深度(递归\迭代\层序遍历\尾递归优化\分治法实现 )

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

EPICS database练习

给定一个以下的数据库&#xff1a; # 指定Limit的上限&#xff0c;初始为10&#xff0c;可以通过通道访问进行设置&#xff0c;上限为100 record(ao, "$(P)Limit") { field(DRVH, "100") field(DOL, "10") field(PINI, "YES") }# 一个…

el-menu 保持展开点击不收缩 默认选择第一个菜单

<el-menu:default-openeds"[/system]" 数组 默认展开第一个:collapse"isCollapse"close"handleClose" 点击关闭的时候 让菜单打开 就可以实现保持展开效果ref"menus":unique-opened"true":active-text-color"se…

2024中国(重庆)人工智能展览会8月举办

2024中国(重庆)人工智能展览会8月举办 邀请函 主办单位&#xff1a; 中国航空学会 重庆市南岸区人民政府 招商执行单位&#xff1a; 重庆港华展览有限公司 【报名I59交易会 2351交易会 9466】 展会背景&#xff1a; 2024中国航空科普大会暨第八届全国青少年无人机大赛在…

QT 项目打包(为了后期远程实验用)

一、环境准备 1、一个项目工程 二、步骤 1、将编译器设置调整为Release模式 二、对项目重新编译构建 三、可以看到工程目录这个文件夹 打开工程目录文件夹的Release文件夹&#xff0c;我的路径如下 四、新建一个文件夹&#xff0c;将上述路径文件夹下的exe文件复制到新的文…

Windows的消息过程调用与窗口位于同一个线程

消息过程函数和窗口通常在同一个线程中运行。 在Windows中&#xff0c;每个窗口都有一个与之相关联的线程&#xff0c;这个线程负责处理窗口的消息。当窗口接收到消息时&#xff0c;系统会将消息发送给创建窗口的线程&#xff0c;并在该线程上调用窗口过程函数来处理消息。 这…

领导跳槽邀请,测试员该如何抉择?

在职场中&#xff0c;领导跳槽并邀请下属一同前往新公司&#xff0c;是一个既常见又令人纠结的选择。对于测试员来说&#xff0c;这个决定更是充满了未知与风险。那么&#xff0c;面对这样的机会&#xff0c;我们该如何权衡利弊&#xff0c;做出明智的选择呢&#xff1f; 首先&…

你眼中的IT行业现状与未来趋势

一&#xff1a;阐述 现在IT、科技行业从业人员开始求稳&#xff0c;部分从业人员开始转向DBA、运维&#xff08;企业相当稳定&#xff09;、硬件工程师等&#xff08;技术过硬&#xff0c;不是随便可以转的&#xff09;&#xff0c;但是这些行业职位少&#xff0c;薪水相对不是…

LLM大语言模型(十四):LangChain中Tool的不同定义方式,对prompt的影响

背景 ChatGLM3-6B的函数调用功能&#xff0c;和LangChain的Tool调用&#xff0c;在prompt上并没有对齐。 参考&#xff1a;LLM大语言模型&#xff08;十二&#xff09;&#xff1a;关于ChatGLM3-6B不兼容Langchain 的Function Call_error: valueerror: caught exception: unk…

神卓互联内网穿透之快速创建https类型通道【最新】

神卓互联最近上线了V9.0内网穿透通信传输模式&#xff0c;相比与之前的V8.0在速度和延迟方面确实提升了很多&#xff0c;控制台也进行了改版升级&#xff0c;这里是对升级后的控制台创建https通道方法进行记录&#xff1a; 登录神卓互联控制台 选择【内网穿透】-【映射管理】…

如何利用AI提高内容生产效率与AIGC典型案例分析

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

有趣的css - 打字机动画效果

大家好&#xff0c;我是 Just&#xff0c;这里是「设计师工作日常」&#xff0c;今天分享的是使用 css 实现好玩的单行打字机效果&#xff0c;和我一起看看吧。 《有趣的css》系列最新实例通过公众号「设计师工作日常」发布。 目录 整体效果核心代码html 代码css 部分代码 完整…

centos7同步银河麒麟服务器SP系列外网yum源包同步不完整问题

centos7同步银河麒麟服务器SP系列外网yum源包同步不完整问题 一 问题描述二 解决方法三 外网源配置步骤 一 问题描述 Red 7.7 x86架构同步银河麒麟服务器SP arm架构外网源的yum包不完整问题&#xff0c;yum repolist查看源里面有15000左右的包&#xff0c;使用reposync命令同步…

疾病防范:拯救微笑,关于儿童抑郁的注意事项

引言&#xff1a; 儿童抑郁是一种常见但常被忽视的心理健康问题&#xff0c;对孩子的身心健康和成长都会造成严重影响。本文将探讨儿童抑郁的注意事项&#xff0c;以帮助家长和教育者更好地识别、理解和应对儿童抑郁问题。 1. 深入了解抑郁症&#xff1a; 抑郁症并非一种偶发的…

B端设计与C端设计,用户模型区别!

B端设计和C端设计到底有哪些不同&#xff1f;这篇文章里&#xff0c;作者就做了相对详细的阐述和分析&#xff0c;不妨来看一下。 C 全称是 Customer 即消费者&#xff08;泛指用户&#xff09;的产品&#xff0c;个人用户或终端用户&#xff0c;使用的是客户端。例如&#xff…

羊大师解析,如何遵守《节约用水条例》

羊大师解析&#xff0c;如何遵守《节约用水条例》 《节约用水条例》的实施旨在倡导和推动全社会形成节约用水的良好风尚&#xff0c;以应对日益严峻的水资源短缺问题。羊大师发现随着这一条例的深入实施&#xff0c;越来越多的人开始意识到节约用水的重要性&#xff0c;并积极…

Redis—图文详解高可用原因

本文不会讲解Redis的用途&#xff0c;关于用途会发另一片文章讲解&#xff0c;本文主要讲的是高可用的原理。 Redis高可用主要有以下三个原因&#xff1a;主从模式(上一篇讲Kafka的文章里有涉及到)&#xff0c;哨兵模式&#xff0c;Redis-Cluster(Redis集群)。 什么是主从模式…