STL源码刨析:红黑树(RB-tree)

news2024/9/23 13:17:40

目录

        1.前言

        2.RB-tree的简单介绍

        3.RB-tree的插入节点操作

        4.RB-tree的删除节点操作

        5.RB-tree的节点设计

        6.RB-tree的迭代器设计

        7.RB-tree的数据结构

        8.RB-tree的构造与内存管理

        9.RB-treed的元素操作


前言

        在文章《STL源码刨析:树的导览》中,曾简单的对树的结构,二叉搜索树和平衡二叉搜索树进行简单的讲解。而本章将重点对红黑树进行讲解,并为后续的set和map容器打下基础


RB-tree的简单介绍

        红黑树也是一种平衡二叉搜索树,而在满足平衡二叉搜索树的基础上,红黑树还需要符合以下规则:

                1.RB-tree的每一个节点不是黑色就是红色

                2.RB-tree的根节点为黑色

                3.如果在上一层节点中,其节点的颜色为红色,那该节点的子节点颜色必须为黑色(如果上一层节点颜色为黑色,则该节点的子节点颜色可以为黑色或者红色)

                4.RB-tree满足从任意一个节点到叶子节点1的路径中,所含的黑色节点的数量相同

                5.新插入的节点的父节点必须为黑色,而插入节点的颜色必须为红色

        为了方便理解RB-tree的特点,可以参考下图:

图1.红黑树示例

        为了方便辨别一棵平衡二叉搜索树是否是红黑树,基于图1还是不够准确的,应此我们还需要在图1的基础上把空节点画出来并标记为黑色,以确保符合红黑树的第四条规则,具体如下:

图2.红黑树完整示例        

        在针对图2的表示中,我们还可以发现以下规律:

                1.RB-tree中最长路径不超过最短路径的两倍

                2.RB-tree的最短路径中的节点必为全黑,最长路径的节点必为一黑一红

                3.RB-tree的中的任意节点的左右子树高度相差不超过2,而平衡二叉搜索树为1(因为红黑树的平衡性比平衡二叉搜索树的平衡性弱,当搜寻节点的平均效率几乎相等)

PS:网传一个关于红黑树的口诀:左根右(代表红黑树为平衡二叉搜索树(左<根<右)),根叶黑(根节点和叶子节点为黑色),不红红(两个子节点的颜色不连续为红色),黑路同(从任意一个节点到叶子节点1的路径中,所含的黑色节点的数量相同)


RB-tree的插入节点操作

        在了解了红黑树的规则以后,我们需要对红黑树的插入节点操作进行分析,即红黑树插入节点的情况为以下三种:

        1.插入节点为根节点:

                由于在对红黑树进行节点插入操作时,每一个插入节点的颜色视为红色,而对于红黑树来说其根节点必须为黑色,故在插入的节点为根节点时,将节点的颜色调整为黑色

图3.插入节点为根节点

        2.插入节点的叔叔节点为红色:

                若插入节点的叔叔节点(父亲节点的兄弟节点)为红色,则将父亲节点,叔叔节点和爷爷节点的颜色反转(红变黑,黑变红

图4.插入节点时,叔叔节点为红色

        3.插入节点时,叔叔节点为黑色

                在插入节点时,叔叔节点为黑色则分为四种情况,分别是RR,RL,LL,LR(右旋,右左双旋,左旋,左右双旋),以下将对这四种情况进行讲解:

                        1.LL(左旋)情况:

图5.插入节点时,叔叔节点为黑色(LL)

                        2.RR(右旋)情况:

图6.插入节点时,叔叔节点为黑色(RR)

                        3.LR(左右双旋)情况:

图6.插入节点时,叔叔节点为黑色(LR)

                         4.RL(右左双旋)情况:

图7.插入节点时,叔叔节点为黑色(RL)

        PS:更详细的操作可以参考下方视频链接

红黑树的插入操作icon-default.png?t=N7T8https://www.bilibili.com/video/BV1Xm421x7Lg/?spm_id_from=333.337.search-card.all.click&vd_source=b9666f32fe1ff418cd951c60cd1abc9d


RB-tree的删除节点操作

        在了解了红黑树的插入节点操作,接下来将对红黑树的删除节点操作进行讲解。而针对红黑树删除节点的情况主要分为以下两种

        1.删除的节点只存在左孩子或者右孩子的情况:

图8.删除节点时,只存在左孩子或右孩子

        2.删除的节点,不存在孩子节点:

                1.删除的节点为红节点:

图9.删除节点时,节点无孩子且为红节点

                2.删除的节点为黑节点:

                        1.删除的节点的兄弟节点为黑色,其该兄弟节点存在一个孩子节点为红色:

图10.删除节点时,兄弟节点为黑色且其孩子节点为红节点

                        2.删除的节点的兄弟节点为黑色,其该兄弟节点存在孩子节点都为黑色:

图10.删除节点时,兄弟节点为黑色且其孩子节点为黑节点

                        3.删除的节点的兄弟节点为红色:

图11.删除节点的兄弟节点为红色

红黑树的删除操作icon-default.png?t=N7T8https://www.bilibili.com/video/BV16m421u7Tb/?spm_id_from=333.788&vd_source=b9666f32fe1ff418cd951c60cd1abc9d


RB-tree的节点设计

        在前几小节中,我们知道RB-tree中存在黑红两个颜色的节点定义,所以在设计红黑树的节点是,我们先需要对其两种颜色进行定义,源代码如下:

typedef bool _rb_tree_color_type;
const _rb_tree_color_type _rb_tree_red = false;    //红色为0
const _rb_tree_color_type _rb_tree_black = true;   //黑色为1

        在了解其节点颜色的定义后,我们需要对节点进行设计,如下:

struct _rb_tree_node_base{
    typedef _rb_tree_color_type color_type;
    typedef _rb_tree_node_base* base_ptr;
    
    color_type color;    //节点颜色
    base_ptr parent;     //红黑树的根节点指针
    base_ptr left;       //节点的左子树
    base_ptr right;      //节点的右子树
}

template<class Value>
struct _rb_tree_node : public _rb_tree_node_base{
    typedef _rb_tree_node<Value>* link_type;
    Value value_field;    //节点值
}

        红黑树的节点具体结构可参考下图:

图12.红黑树的节点结构


RB-tree的迭代器设计

        针对RB-tree的迭代器设计,我们知道RB-tree是一种树的数据结构类型,故对元素不支持随机访问,而为了访问RB-tree中的各个元素,其设计的迭代器应支持遍历树的各个节点的操作,故把RB-tree的迭代器类型定为双向迭代器(可以访问子节点(后向),也可以访问父节点(前向))。在设计RB-tree迭代器之前,我们需要先为该迭代器设计一个基类迭代器,方便后续继承扩展其接口,且该基类迭代器应该支持所谓的前向访问和后向访问,具体源码如下:

//RB-tree的迭代器基类
struct _rb_tree_base_iterator{
    typedef _rb_tree_node_base::base_ptr base_ptr;            //节点基类
    typedef _bidirectional_iterator_tag iterator_category;    //迭代器类别
    typedef ptrdiff_t differrnce_type;                        //迭代器之间的距离
    base_ptr node;    //指向当前节点的指针

    void increment();    //自增(访问右子树)
    void decrement();    //自减(访问左子树)
}

void increment(){    //自增
    if(node->right != 0){        //存在右节点
        node = node->right;       
        while(node->left != 0){  //遍历该右子节点的左子树
            node = node->left;
        }
    }else{    //不存在右节点
        base_ptr y = node->parent;    //找到父节点
        while(node == y->right){      //寻找父节点的右子树
            node = y;                 //父节点不存在右子树则继续访问祖宗节点,以此类推
            y = y->parent;
        }
    }
    if(node->right != y){
        node =y;
    }
}

void decrement(){    //自减
    //该节点为红色,且祖宗节点的值与节点相同
    if(node->color == _rb_tree_red && node->parent->parent == node){
        node = node->right;
    }else if(node->left != 0){    //该节点存在左子节点
        base_ptr y = node->left;  //访问左子节点
        while(y->right != 0){     //该左子节点存在右子树,则遍历到叶子节点
            y = y->right;
        }
        node = y;
    }else{    //该节点无左子节点
        base_ptr y = node->parent;    //访问其父节点
        while(node == y->left){       //访问父节点的左子节点
            node = y;                 //其组织节点不存在左子节点则继续向上访问,以此类推
            y = y->parent;
        }
        node = y;    //最后访问到的节点
    }
}

        在RB-tree迭代器基类的基础上进行扩展,我们得到了RB-tree的完整的迭代器的数据结构,具体代码如下:

//RB-tree迭代器
template<class Value, class Ref, class Ptr>
struct _rb_tree_iterator : public _rb_tree_base_iterator{
    typedef Value value_type;    //迭代器值的类型
    typedef Ref reference;       //迭代器引用类型
    //非const修饰的迭代器类型
    typedef _rb_tree_iterator<Value, Value&, Value*> iterator; 
    //const迭代器类型   
    typedef _rb_tree_iterator<Value, const Value&, const Value> const_iterator;
    //迭代器自身的类型
    typedef _rb_tree_iterator<Value, Ref, Ptr> self;
    //节点的指针类型
    typedef _rb_tree_node<Value>* link_type;
    
    _rb_tree_iterator(){}
    _rb_tree_iterator(link_type x){ node = x; }
    _rb_tree_iterator(const iterator& it){ node = it.node; }

    reference operator*() const { return link_type(node)->value_field; }
#ifndef _SGI_STL_NO_ARROW_OPERATOR
    pointer operator->() const { return $$(operator*()); }
#endif /* _SGI_STL_NO_ARROW_OPERATOR */

    self& operator++(){
        incerement();
        return *this;
    }
    self& operator++(int){
        self tmp = *this;
        increment();
        return tmp;
    }
    self& operator--(){
        decrement();
        return *this;
    }
    self operator--(int){
        self tmp = *this;
        decrement();
        return tmp;
    }
}

RB-tree的数据结构

        在了解了RB-tree的迭代器结构后,我们需要着手实现RB-tree的数据结构,以下是关于RB-tree的数据结构中相关的成员以及宏定义,源码如下:

//RB-tree中的成员以及宏定义
template<class Key, class Value, class KeyOfValue, class Compare, class Alloc = alloc>
class rb_tree{
protected:
    typedef void* void _pointer;
    typedef _rb_tree_node_base* base_ptr;
    typedef _rb_tree_node<Value> rb_tree_node;
    typedef simple_alloc<rb_tree_node, Alloc> rb_tree_node_allocator;
    typedef _rb_tree_color_type color_type;
public:
    typedef Key key_type;
    typedef Value value_type;
    typedef value_type* pointer;
    typedef const value_type* const_pointer;
    typedef value_type& reference;
    typedef const value_type& const_reference;
    typedef rb_tree_node* link_type;
    typedef size_t size_type;
    typedef ptrdiff_t difference_type;
    typedef _rb_tree_iterator<value_type, reference, pointer> iterator;
protected:
    size_type node_count;    //追踪记录树的大小
    link_type header;        //根节点
    Compare key_compare;     //节点间的键值大小比较准则
}

        在了解了RB-tree的成员以及宏定义后,要对RB-tree能做的具体操作进行实现,具体代码如下:

template<class Key, class Value, class KeyOfValue, class Compare, class Alloc = alloc>
class rb_tree{
protected:
    //节点配置函数
    link_type get_node(){    //分配节点
        return rb_tree_node_allocator::allocate();
    }
    link_type create_node(const value_type& x){    //初始化节点
        link_type tnp = get_node();    //配置空间
        _STK_TRY{    //异常处理
            construct(&tmp->value_field, x);    //构造内容
        }
        _STL_UNWIND(put_node(tmp));
        return tmp;
    }
    void destroy_node(link_type p){    //释放节点
        destroy(&p->value_field);    //析构内容
        put_node(p);    //释放内存
    }
    
    //获取根节点相关的数据函数
    link_type& root() const {    //获取根节点
        return (link_type&) header->parent;
    }
    link_type& leftmost() const {    //获取根节点的左子节点
        return (link_type&) header->left;
    }
    link_type& rightmost() const {    //获取根节点的右子节点
        return (link_type&) header->right;
    }

    //获取指定节点的数据函数
    static link_type& left(link_type x){ return (link_type&)(x->left); }
    static link_type& right(link_type x){ return (link_type&)(x->right); }
    static link_type& parent(link_type x){ return (link_type&)(x->parent); }
    static reference value(link_type x){ return x->value_field; }
    static const Key& key(link_type x){ return KeyOfValue()(value(x)); }
    staicc color_type& color(link_type x){ return (color_type&)(x->color); }

    //求RB-tree中的最大值和最小值
    static (link_type) minimum(link_type x){
        return (link_type) _rb_tree_node_base::minimum(x);
    }
    static link_type maximum(link_type x){
        return (link_type) _rb_tree_node_base::maximmum(x);
    }
private:
    iterator _insert(base_ptr x, base_ptr y, const value_type& v);
    link_type _copy(link_type x, link_type p);
    void _erase(link_type x);
    void init(){
        header = get_node();    //分配节点
        color(header) = _rb_tree_red;    
        root() = 0;
        leftmost() = header;
        rightmost() = header;
    }
public:
    rb_tree(const Compare& comp = Compare()) : node_count(0), key_compare(comp){
        init();
    } 
    ~rb_tree(){
        clear();
        put_node(header);
    }
    rb_tree<Key, Value, KeyOfValue, Compare, Alloc>& operator=(const  rb_tree<Key, Value, KeyOfValue, Compare, Alloc>& x);
public:
    Compare key_comp() const { return key_compare; }
    iterator begin() { return leftmost(); }
    iterator end() { return header; }
    bool empty() const { return node_count == 0; }
    size_type size() const { return node_count; }
    size_type max_size() const { return size_type(-1); }
public:
    //将x插入到RB-tree中(保持节点值不重复)
    pair<iterator.bool> insert_uniue(const value_type& x);
    //将x插入到RB-tree中(允许节点值重复)
    iterator insert_equal(const value_type& x);
}

RB-tree的构造与内存管理

        关于RB-tree的构造方式有两种,一种是以现有的RB-tree复制一个新的RB-tree,另一种是产生一个空空如也的树,代码如下:

//空树
rb_tree<int, int, identity<int>, less<int>> itree;

//复制
rb_tree(const Compare& comp = Compare()) : node_count(0), key_compare(comp){
    init();
}

RB-treed的元素操作

        关于RB-tree的元素操作,主要在乎于节点的插入和搜寻操作。其中关于插入操作,RB-tree提供了两种插入操作:insert_unique() 和insert_equal(),前者表示插入的节点在红黑树中独一无二,后者表示插入的节点在整棵树中可以重复。具体可参考以下代码:

        1.元素的插入操作insert_equal()

//插入节点:节点值允许重复
template<class Key, class Value, class  KeyOfValue, class Compare, class Alloc>
typedef rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::iterator
rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::insert_equal(const Value& v){
    link_type y = header;
    link_type x = root();
    while(x != 0){    //遍历节点,找到插入的位置
        y = x;
        x = key_compare(KeyOfValue()(v), key(x)) ? left(x) : right(x);
        //遇大则往左,遇小于或等于则往右
    }
    return _insert(x,y,z);
}

        2.元素的插入操作insert_unique()

//插入节点:节点值不允许重复
template<class Key, class Value, class KeyOfValue, class Compare, class Alloc>
pari<typename rb_tree<Key, Value, KeyOfValue, Compare, Alloc>>::iterator, bool>
rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::insert_unique(const Value& v){
    link_type y = header;
    link_type x = root;    //从根节点开始遍历
    bool comp = true;
    while(x != 0){
        y = x;
        comp = key_compare(KeyOfValue()(v), key(x));
        x = comp ? left(x) : right(x);
    }

    iterator j = iterator(y);
    if(comp){
        if(j == begin()){
            eturn pair<iterator, bool>(_insert(x, y, v), true);
        }else{
            --j;
        }
    }
    if(key_compare(key(j.node), KeyOfValue()(v))){
        return pair<iterator, bool>(_insert(x, y, v), true);
    }
    return pair<iterator, bool>(j, false);    //新节点和树中的节点值重复,不插入
}

        3.元素的搜寻操作find()

//搜寻节点
template<class Key, class Value, class KeyOfValue, class Compare, class Alloc>
typename rb_tree<Key, Value, KeyOfValue, Compare, Alloc>>::iterator
rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::find(const Key& k){
    link_type y = header;
    link_type = root();

    while(x != 0){
        if(!key_compare(key(x), k)){
            y = x, x = left(x);
        }else{
            x= right(x);
        }
    }
    iterator j= iterator(y);
    return (j ++ ned() || key_compare(k, key(j.node))) ? end() : j;
}

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

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

相关文章

使用 MongoDB 构建 AI:Flagler Health 的 AI 旅程如何彻底改变患者护理

Flagler Health 致力于为慢性病患者提供支持&#xff0c;为其匹配合适的医生以提供合适的护理。 通常&#xff0c;身患严重病痛的患者面临的选择有限&#xff0c;他们往往需要长期服用阿片类药物&#xff0c;或寻求成本高昂的侵入性外科手术干预。遗憾的是&#xff0c;后一种方…

linux小组件:git

git是什么&#xff1f; git是版本控制器&#xff08;去中心化的分布式系统&#xff09;可以快速高效地处理从小型到大型的各种项目。易于学习&#xff0c;占地面积小&#xff0c;性能极快。它具有廉价的本地库&#xff0c;方便的暂存区域和多个工作流分支等特性。 什么叫版本…

【数据结构七夕专属版】单链表及单链表的实现【附源码和源码讲解】

本篇是博主在学习数据结构时的心得&#xff0c;希望能够帮助到大家&#xff0c;也许有些许遗漏&#xff0c;但博主已经尽了最大努力打破信息差&#xff0c;如果有遗漏还请见谅&#xff0c;嘻嘻&#xff0c;前路漫漫&#xff0c;我们一起前进&#xff01;&#xff01;&#xff0…

微信小程序--19(.wxml 模板文件简单归纳)

类似HTML用来描述当前页面的结构 一、普通样式 1.<view> 内容 </view> 二、滚波样式 1.<swiper> 内容 </swiper> 2.<swiper-item>滚波内容 </swiper-item> 3.常用属性 纵向&#xff1a;scroll-y横向&#xff1a;scroll-x圆点颜色&am…

LinuxC高级day03(Shell脚本)

【1】Shell脚本 1》Shell脚本基础概念 1> 概念 Shell使用方式&#xff1a;手动在命令行下命令或用Shell脚本 Shell脚本本质&#xff1a;Shell命令的有序集合 扩展名最好以 .sh 结尾&#xff0c;见名知义 也可以没有 Shell既是应用程序&#xff0c;又是一种脚本语言 解…

迁移学习之基本概念

迁移学习 1、通俗定义 迁移学习是一种学习的思想和模式 迁移学习作为机器学习的一个重要分支&#xff0c;侧重于将已经学习过的知识迁移应用于新的问题中 迁移学习的核心问题是&#xff0c;找到新问题和原问题之间的相似性&#xff0c;才可以顺利地实现知识地迁移 定义&…

运行pytorch报异常处理

一、问题现象及初步定位&#xff1a; 找不到指定的模块。 Error loading "D:\software\python3\Lib\site-packages\torch\lib\fbgemm.dll 此处缺少.dll文件&#xff0c;首先下载文件依赖分析工具 Dependencies https://github.com/lucasg/Dependencies/tree/v1.11.1 之后下…

leetcode169. 多数元素,摩尔投票法附证明

leetcode169. 多数元素 给定一个大小为 n 的数组 nums &#xff0c;返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的&#xff0c;并且给定的数组总是存在多数元素。 示例 1&#xff1a; 输入&#xff1a;nums [3,2,3] 输…

C# winform 三层架构 增删改查 修改数据(修改篇)

ss一.留言 本专栏三层架构已经更新了 添加 登录 显示&#xff0c;还差修改以及删除&#xff0c;本篇更新修改&#xff0c;主要操作为点击修改某一条数据&#xff0c;然后跳转页面进行修改。 二.展示 我们先看DAL代码 /// <summary>/// 修改/// </summary>/// &l…

【RTOS面试题】什么是抢占?抢占的原理、抢占的好处、抢占有什么局限性?

&#x1f48c; 所属专栏&#xff1a;【RTOS-操作系统-面试题】 &#x1f600; 作  者&#xff1a; 于晓超 &#x1f680; 个人简介&#xff1a;嵌入式工程师&#xff0c;专注嵌入式领域基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f496; 欢迎大家&#xf…

大语言模型的模型量化(INT8/INT4)技术

目录 一、LLM.in8 的量化方案 1.1 模型量化的动机和原理1.2 LLM.int8 量化的精度和性能1.3 LLM.int8 量化的实践 二、SmoothQuant 量化方案 2.1 SmoothQuant 的基本原理2.2 SmoothQuant 的实践 三、GPTQ 量化训练方案 3.1 GPTQ 的基本原理3.2 GPTQ 的实践 参考资料 一、LLM.i…

让对话AI帮助你做程序架构设计,以及解决你的疑问

我想问下对话AI,本文采取的是chatgpt免费版 我问&#xff1a; 你说程序的设计&#xff0c;前后端分离的BS架构。比如工人基础档案1000条记录&#xff0c;工程项目基础档案10条记录&#xff0c;其他相关这两个基础档案的具体功能&#xff0c;比如打卡记录&#xff0c;宿舍记录&…

SD-WAN解决方案功能概述

SD-WAN&#xff08;软件定义广域网&#xff09;是一种前沿的网络技术&#xff0c;旨在为企业提供灵活、智能且高效的广域网连接。SD-WAN的主要功能可以分为四大类&#xff1a;路由、安全性、性能优化和管理控制。 路由功能 路由功能是SD-WAN解决方案的核心部分之一。传统的广域…

B站宋红康JAVA基础视频教程个人笔记chapter05

1.一维数组的定义方式 // 方式一(静态初始化) double[] prices; prices new double[]{20, 32, 43};// 方式二:&#xff08;动态初始化&#xff09; String[] foods; foods new String[4]; // 内部声明数组的长度 // String foods new String[4];// 其他方式 int[] prices …

【字符串哈希】

题目 代码 #include<bits/stdc.h> typedef unsigned long long ULL; const int N 1e510; const int P 131; char str[N]; ULL h[N], p[N]; ULL get_hash(int l, int r) {return h[r] - h[l-1] * p[r-l1]; } int n, m; int main() { scanf("%d%d", &n,…

Scrapy | 手动请求发送实现的数据爬取-段子王网站

文章目录 概要爬取流程代码技术细节format%回调函数 小结 概要 爬取段子王网站的标题和内容 核心 Scrapy的手动请求发送实现的数据爬取yield scrapy.Request(url,callback):GET-caL1back指宽解析函数&#xff0c;用于解析数据yield scrapy.ForRequest(url,callback,formdata):…

科普课堂走起 | 什么是网络安全和数据安全?

网络安全和数据安全是现代数字世界中非常重要的两个概念。让我们来详细了解一下这两个领域。 1.网络安全&#xff08;Network Security&#xff09; 网络安全是指保护网络系统免受未经授权的访问、攻击、破坏或滥用的一系列技术和过程。它旨在确保信息的机密性、完整性和可用…

jmeter-beanshell学习16-自定义函数

之前写了一个从文件获取指定数据&#xff0c;用的时候发现不太好用&#xff0c;写了一大段&#xff0c;只能取出一个数&#xff0c;再想取另一个数&#xff0c;再粘一大段。太不好看了&#xff0c;就想到了函数。查了一下确实可以写。 public int test(a,b){return ab; } ctes…

磁盘无法访问的危机与解救:数之寻软件的数据恢复之旅

在数字时代&#xff0c;磁盘作为数据存储的核心&#xff0c;承载着我们的工作文档、珍贵照片、个人视频等无价之宝。然而&#xff0c;当您试图访问某个磁盘时&#xff0c;却遭遇了“磁盘无法访问”的提示&#xff0c;这无疑是一场突如其来的数据危机。本文将深入探讨磁盘无法访…

浮毛季到了,拒绝猫咪变成“蒲公英”,宠物空气净化器去除浮毛

同为铲屎官&#xff0c;面对家中无处不在的猫毛挑战&#xff0c;想必你也深感头疼。衣物、沙发乃至地毯上的明显猫毛尚可通过吸尘器或粘毛器轻松应对&#xff0c;但那些细微漂浮的毛发却成了难以捉摸的“小恶魔”&#xff0c;普通的空气净化器往往力不从心。对于浮毛&#xff0…