基于C++11的无序容器分析

基本内容

1.概念上,unordered容器以一种随意顺序包含你安插进去的所有元素。也就是说,你可以把这个容器想成一个袋子(bag):你可以放元素进去,但当你打开袋子对所有元素做某些事,你是以一种随机的次序来访问它们。所以,相比于(multi)set和(multi)map,这里不需要排序准则。

就像各个associative容器那样,这里的个别class也互不相同:

Unordered set 和 multiset 存放的是某特定类型的个别value,而 unordered map 和 maltinae存放的元素都是key/value pair,其中key 被用来作为“存放和查找某特定元素(包含相应的value)”的依据。

Unordered set 和map都不允许元素重复,而 unordered multiset和multimap都允许。

2.欲使用一个unordered set 或 unordered multiset,你必须首先包含头文件<unordered_set>欲使用一个 unordered map或 multimap,你必须首先包含头文件<unordered_map>:

#include <unordered_set>
#include <unordered_map>

3.在那里,上述四种类型分别被定义为namespace std内的class template:

namespace std (
        template <typename T,typename Hash = hash<T>,typename EqPred = equal_to<T>,typename Allocator = allocator<T> >
        class unordered set;
        template <typename T,typename Hash = hash<T>typename EqPred = equal_to<T>,typename Allocator = allocator<T> >
        class unordered multiset;
        template <typename Key, typename T,typename Hash = hash<T>,typename EqPred = equal_to<T>,typename Allocator = allocator<pair<const Key, T>>>
        class unordered map;
        template <typename Key, typename T,typename Hash = hash<T>,typename EqPred = equal_to<T>,typename Allocator = allocator<pair<const Key, T>>>
        class unordered multimap;

一个 mordered set 或 unordered multiset 的元素类型,可以是任意指派的T,只要它是可比的。

对于unordered map和 unordered multimap,第一个template 参数是元素的key类型,第二个template 参数是元素的value 类型。一个 unordered map 或unordered multimap 的元素可拥有任何类型的Key和T,只要它们满足以下两个条件:

1. Key 和value 都必须可被复制或可被搬移。(满足拷贝构造函数和移动构造函数)

2. Key 必须可被“等价准则”拿来比较。(重构了相等操作符)

可有可无的第二或第三template参数用来定义hash function。如果没有指明使用哪个hash function,就使用默认的hash<>,这是个 function object,定义于<functional>,可用于所有整数类型、浮点数类型、pointer、string及若干特殊类型。至于其他value类型,你必须传入你自己的hash function

可有可无的第三或第四template 参数用来定义等价准则:这是个predicate,用来查找元素。它用来判断“两个 value 是否相等”。如果没有指定,就使用默认的equal_to<>,它会以operator==比较两个元素。

可有可无的第四或第五template参数用来定义内存模型。默认的内存模型是allocator,由C++标准库提供。

Unordered容器的能力

所有标准化unordered container class 都以hash table 为基础。尽管如此,仍允许种种实现选择。通常C++标准库并不指明所有实现细节,这样才能允许种种可能的选择,但 unordered容器仍有若干被具体指明的性质,基于以下假设:

1.这些hash table 使用chaining做法,于是一个hash code 将被关联至一个 linked list。

2.上述那些 linked list是单链或双链,取决于实现。C++standard只保证它们的iterator“至

少”是forward iterator。

3.关于 rehashing(重新散列),有各式各样的实现策略:

A.传统做法是,在单一insert或erase动作出现时,有时会发生一次内部数据重新组织。-

B.所谓递进式做法是,渐进改变 bucket或slot的数量,这对即时环境特别有用,因为在其中“突然放大hash table”的代价也许太高。

Unordered容器允许上述二种策略。

无序容器的内部实现

以上显示了unordered set 或unordered multiset 的典型内部布局,依据C++标准库给予的最低保证。对于每个将被存放的value,hash function 会把它映射至hash table内某个bucket(slot)中。每个bucket管理一个单向 linked list,内含所有会造成hash function产出"相同数值"元素。

以上显示了unordere map 或unordered multimap的典型内部布局,依据C++标准库给予的最低保证。对于每个将被存放的元素,hash function 会把key映射至hash table内的某个bucket(slot)中。每个bucket 管理一个单向 linked list,内含所有会造成hash function 产出"相同数值”的元素。

使用hash table作用

内部使用hashtable,其主要优点是,它惊人的运行期行为。假设拥有良好的hashing策

略,并且有良好的实现,你可以保证在安插、删除、查找元素时获得摊提常量时间(之所以是摊提的,因为偶尔发生的rehashing可能是个大型操作,带着线性复杂度)。

Unordered容器的几乎所有操作一一包括拷贝构造和赋值元素的安插和寻找,以及等价比较一一的预期行为都取决于hash function 的质量。如果hash function对不同的元素竟产生相等数值。hash table的任何操作都会导致低下的执行效率。这个缺点不完全是数据结构本身,也因为客户对此么有足够的意识。

无序容器相对于一般关联式容器的缺点

Unordered 容器比起寻常的associative容器,也有若干缺点:

1.Unordered 容器不提供operator<、>、<=和>=用以安排布置这些容器的多重实例。然而提供了==和!=。

2.不提供lower_bound()和upper_bound()。

3.由于 iterator 只保证至少是个forward iterator,因此反向iterator 包括rbegin()、rend()、crbegin()和crend()都不提供,你不能够使用那种要求获得bidirectional iterator的算法。

无序容器和一般的关联式容器的相似点

由于元素的(key)value具体关系到元素的位置——这里指的是bucket entry——你不可以直接改动元素的(key)value。因此,很像associative容器那样,欲改动一个元素的value,你必须先移除拥有旧日value的元素,然后安插一个拥有新value的新元素。这个接口反映出以下行为:

1.Unordered容器不提供“直接元素访问操作”。

2.通过 iterator 进行的间接访问有其束缚:从iterator的角度观之,元素的(key)value是常

量。

身为一个程序员,你可以指定若干会影响hash table行为的参数:

1.你可以指定 bucket的最小数量。

2.你可以提供你自己的hash function。

3.你可以提供你自己的等价准则:它必须是个

predicate,用来在bucket list的所有数据项中找出准确的元素。。

4.你可以指定一个最大负载系数,一旦超过就会自动rehashing.

5.你可以强迫rehashing。

但是你不能够影响以下行为:

1.成长系数(growthfactor),那是“自动rehashing”时用来成长或缩小 list of buckets的系

数。

2.最小负载系数(minimum load factor),用来强制进行rehashing(当容器中的元素个数缩

减)。

注意,rehashing 只可能发生在以下调用之后:insert()、rehash()、reserve()或clear()。这是以下保证的自然结果:erase()绝不会造成指向元素的iterator、reference 和 pointer 失效。因此,如果你删除数百个元素,bucket 的大小并不会改变。但如果你在那之后安插一个元素,bucket的大小就有可能缩小。

也请注意,在那些支持等价key 的容器内,也就是说在unordered multiset 和multimap 内,带有等价 key 的元素将会被相邻排列。Rehashing 以及其他“可能于内部改变元素次序”的操作,都会维持“带有等价key”的元素的相对次序

创建和控制Unordered容器

创建,复制和销毁(Creat ,Copy,and Destroy)

unordered容器的构造函数和析构函数

Unord c//Default构造函数,建立一个empty unordered容器,不含任何元素
Unord c(bnum)//建立一个empty unordered容器,内部使用至少bnum个 bucket
Unord c(bnum,hf)//建立一个empty unordered容器,内部使用至少bnum个bucket并以hf作为hash function
Unord c(bnum,hf,cmp)//建立一个empty unordered容器,内部使用至少bnum个bucket,以hf为hash function,并以cmp作为predicate 用来鉴定等价 value
Unord c(c2)//Copy 构造函数,建立某个unordered容器的拷贝,类型相同(所有元素都被复制一份)
Unord c = c2//Copy构造函数,建立某个unordered容器的拷贝,类型相同(所有元素都被复制一份)
Unord c(rv)//Move构造函数,建立一个unordered容器,取rv之中的值
Unord c = rv//Move构造函数,建立一个unordered容器,取rv之中的值
Unord c(beg,end)建立一个unordered容器,以区间[beg,end)内的元素为初值
Unord c(beg,end,bnum)//建立一个unordered容器,以区间[beg,end)内的元素为初值,内部使用至少bnum个bucket
Unord c(beg,end,bnum,hf)//建立一个unordered容器,以区间[(beg,end)内的元素为初值,内部使用至少bnum个 bucket,以hf为hash function
Unord c(beg,end,bnum,hf,cmp)//建立一个unordered容器,以区间[beg,end)内的元素为初值,内部使用至少bnum个 bucket,以h为hash function,并以cmp作为predicate 用来鉴定等价value
Unord c(initlist)//建立一个unordered容器,以初值列initlist中的元素为初值
Unord c = initlist//建立一个unordered容器,以初值列initlist中的元素为初值
c.~Unord()//销毁所有元素并释放内存

unordered容器unord的所有可能类型

unordered_set<Elem>               //一个unordered set,使用hash<>作为默认的hash 函数,使用equal_to<>(operator==)作为默认的比较函数
unordered_set<Elem, Hash>         //一个 unordered set,使用Hash作为默认的hash函数,使用equal_to<>(operator==)作为默认的比较函数
unordered_set<Elem,Hash,Cmp>      //一个unordered set,使用Hash作为默认的hash函数,使用Cmp作为默认的比较函数
unordered_multiset<Elem>          //一个unordered multiset,使用hash<>作为默认的hash 函数,使用equal_to<>(operator==)作为默认的比较函数
unordered_multiset<Elem, Hash>    //一个unordered multiset,使用Hash作为默认的hash函数,使用equal_to<>(operator==)作为默认的比较函数
unordered_multiset<Elem,Hash,Cmp> //一个unordered multiset,使用Hash作为默认的hash函数,使用Cmp作为默认的比较函数
unordered_map<Key, T>             //一个unordered map,使用hash<>作为默认的hash函数,使用equal_to<>(operator==)作为默认的比较函数
unordered_map<Key,T,Hash>         //一个unordered map,使用Hash作为默认的hash函数,使用equal_to<>(operator==)作为默认的比较函数
unordered_map<Key, T,Hash,Cmp>    //一个个unordered map,使用Hash作为默认的hash函数,使用Cmp作为默认的比较函数
unordered_multimap<Key, T>        //一个unordered multimap,使用hash<>作为默认的hash函数,使用equal_to<>(operator==)作为默认的比较函数
unordered_multimap<Key,T,Hash>    //一个unordered multimap,使用Hash作为默认的hash函数,使用equal_to<>(operator==)作为默认的比较函数
unordered _multimap<Key,T,Hash, Cmp>//一个unordered multimap,使用Hash作为默认的hash函数,使用Cmp作为默认的比较函数

关于构建,有很多种实参传递形式。一方面,你可以传递众多value成为初始元素:

1.来自一个相同类型的既有容器。

2.来自一个区间[begin,end)的所有元素。

3.来自一个初值列内的所有元素。

另一方面,你可以传递若干实参,用来影响unordered容器的行为:

1.Hash 函数(作为构造函数实参)。

2.等价准则(作为构造函数实参)。

3.Bucket的最初数量(作为构造函数实参)。

布局操作(Layout Operation)

c.hash_function()     //返回hash函数
c.key_eq()            //返回“相等性判断式”
c.bucket_count()      //返回当前的bucket个数
c.max_bucket_count()  //返回bucket的最大可能数量
c.load_factor()       //返回当前的负载系数
c.max_load_factor()   //返回当前的最大负载系数
c.max_load_factor(val)//设定最大负载系数为val
c.rehash(bnum)        //将容器rehash,使其 bucket个数至少为bnum
c.reserve(num)        //将容器rehash,使其空间至少可拥有num个元素

提供你自己的Hash函数

所有hash table都需要一个hash函数,把你放进去的元素的value映射至某个相关的bucket。它的目标是,两个相等的value总是导致相同的bucket 索引,而不同的value理应导致不同的bucket 索引。对于任何范围的(被传入的) value, hash 函数应该提供良好的hash value分布。

Hash函数必须是个函数,或function object,它接受一个元素类型下的value作为参数,并返回一个类型为std::size_t的value。因此,bucket的当前数量并未被考虑。将其返回值映射至合法的bucket 索引范围内,是由容器内部完成。因此,你的目标是提供一个函数,可把不同的元素值均匀映射至区[0,size_t)内。

下面示范如何提供你自己的hash函数:

#include <functional>
class Customer {...};

class CustomerHash
public:
std::size_t operator()(const Customer& c) const {
return ..
}
};
std::unordered_set<Customer,CustomerHash> custset;

在这里,CustomerHash是一个function object,为class Customer定义出hash函数。

如果不愿意传递一个function object成为容器类型的一部分,你也可以传递一个hash函数作为构造函数实参。然而请注意,hash 函数相应的template 类型也必须对应设妥:

std::size_t customer _hash_func (const Customer& c)
{
return...
};

std::unordered_set<Customer,std::size_t(*)(const Customer&)>
custset(20,customer_hash_func);

在这里,customer_hash_func()被传递为构造函数第二实参,其类型为“一个pointer,指向某函数,该函数接受一个Customer并返回一个std::size_t”,作为第二template实参。

如果没有给予特殊的hash函数,默认的hash 函数是hash<>,那是<functional>提供的一个function object,可对付常见类型:包括所有整数类型、所有浮点数类型、pointer, string,以及若干特殊类型。这些之外的其他类型,你就必须提供你自己的hash 函数。提供一个好的hash函数,说来容易做来难。就像搭便车一样,你可以使用默认的hash函数来完成你自己的hash函数。一个天真的做法是,单纯把那些数据栏“由默认之hash函数产生”的所有hash value加起来。举个例子:

class CustomerHash
{
public:
std::size_t operator()(const Customer& c) const {
       return std::hash<std::string>()(c.fname)+
              std::hash<std::string>()(c.lname)+
              std::hash<long>()(c.no);
}
};

这里,返回的hash value 只不过是Customer的数据栏fname、lname和no的hash value总和。如果预定义的所有hash函数对这些数据栏的类型以及所给予的值都能运作良好,那么三个值的总和必然也在[0,std::size_t)范围内。根据一般溢出规则,上述结果值应该也能够有良好的分布。然而专家认为,这仍然是个粗劣的hash 函数。提供一个良好的hash函数可能是十分棘手的事,似乎不如想象中那么轻松。

提供你自己的等价准则

供你自己的等价准则为unordered容器类型的第三或第四template 参数,你可以传递等价准则,那应该是个predicate,用以找出同一个bucket内的相等value。默认使的是equal_to<>,它以operator==进行比较。基于此,提供合法等价准则的最方便做送是为你自己的类型提供operator==。

例如:

class Customer {
};
bool operator == (const Customer& c1, const Customer& c2) {
...
}
std::unordered_multiset<Customer,CustomerHash> custmset;
std::unordered_map<Customer,String,CustomerHash> custmap;

在全局范围内重载一个等价运算符用作Customer

然而,你也可以提供你自己的等价准则,

例如:

#include <functional>
class Customer {...};
class CustomerEqual
{
public:
bool operator() (const Customer& c1, Customer& c2) const {
return...
}
};
std::unordered_set<Customer,CustomerHash,CustomerEqual> custset;
std::unordered_multimap<Customer,String,CustomerHash,CustomerEqual> custmmap;

这里针对类型Customer定义了一个function object,你必须在其中实现operator()使它能够比较两个元素并返回一个bool指指示它们是否相等。

Unordered容器的其他操作

非更易型操作(Nonmodifying Operation)

c.empty() //返回是否容器为空(相当于size()==0但也许较快)
c.size()  //返回元素的数量
c.max_size() //返回元素个数之最大可能量
c1==c2   //返回c1是否等于c2
c1!=c2   //返回c1是否不等于c2

再次注意,对于比较而言,unordered容器只提供操作符==和!=。

特殊的查找操作(Special Search Operation)

Unordered 容器对于快速查找元素有着优化设计。为了从这种行为中得利,这种容器提供特殊的查找函数。这些函数是寻常算法的特殊版本,有着相同名称。你应该总是选择这些针对unordered容器的优化版本以达到常量时间复杂度,而不是寻常算法的线性复杂度,前提是hash value 均匀分布。举个例子,在一个拥有1000个元素的集合中查找,平均只需1次比较动作,而不是associative 容器的10次,也不是sequence 容器的500次。

c.count(val)//返回“元素值为val”的元素个数
c.find(val)//返回“元素值为val”的第一个元素,如果找不到就返回val
c.equal_range(val)//返回val可被安插的第一个位置和最后一个位置,也就是“元素值==val”的元素区间

赋值(Assignment)

c=c2//将c2的全部元素赋值给c
c=rv//将rvalue rv的所有元素以move assign方式给予c(始自C++11)
c=initlist//将初值列initlist的所有元素赋值给c(始自C++11)
c1.swap(c2)//置换c1和c2的数据
swap(c1, c2)//置换c1和c2的数据

Unordered 容器的赋值操作这些操作的左右两端容器必须有相同类型,更明确地说是其hash function的类型以及等价准则必须相同——即使hash函数本身可能不同。

迭代器相关函数和元素访问

c.begin()      //返回一个指向首处 forward iterator
c.cbegin()     //返回一个const forward iterator
c.end()        //返回一个指向末尾下一个位置的 forward iterator
c.cend()       //返回一个指向末尾下一个位置的const forward iterator
c.rbegin()     //返回一个reverse iterator,指向末尾的下一个位置
c.rend()       //返回一个reverse iterator,指向首处的位置
c.crend()      //返回一个const reverse iterator,指向首处的位置
c.crbegin()    //返回一个const reverse iterator,指向末尾的下一个位置

Unordered 容器并不允许“直接元素访问”,所以你必须使用range-based for循环或iterator。由于这些iterator 只保证是forward iterator,不支持bidirectional iterator或random-access iterator 。因此,你不能够在“只为bidirectional iterator或random-access iterator而提供”的算法中使用它们,那些算法往往是针对 sorting 或random shuffling的。

对于unordered(multi)set,从 iterator的角度观之,所有元素都被视为const。unordered(multi)map,所有元素的key 都被视为const。这是必要的,确保你无法(不会)因为改变元素的value 而危及元素位置。虽然元素之间“无定序",但 value会在当前hash函数的作用下决定其所属的bucket位置。基于这个因素,你不可以为元素调用任何更易型(modifying)算法。例如你不可以调用算法remove(),因为它的移除动作是“以后继元素覆盖掉被移除元素”。

安插和移除元素(Inserting and Removing Element)

c.insert(val)//安插一个val拷贝,返回新元素位置,不论是否成功
c.insert(pos,val)//安插一个val拷贝,返回新元素位置(pos是个提示,指出安插动作的查找起点。若提示恰当可加快速度)
c.insert(beg,end)//将区间[beg,end)内所有元素的拷贝安插到c(无返回值)
c.insert(initlist)//安插初值列initlist内所有元素的一份拷贝(无返回值;始自C++11)
c.emplace (args...)//安插一个以args为初值的元素,并返回新元素的位置,不论是否成功
c.erase(val)//移除“与val相等”的所有元素,返回被移除的元素个数
c.erase(pos)//移除iterator 位置pos上的元素,无返回值
c.erase(beg,end)//移除区间[beg,end)内的所有元素,无返回值
c.clear()//移除所有元素,将容器清空

一般而言,抹除(erasing)函数并不会令“指向至其他元素”的iterator 和reference失效。然而成员函数insert()和emplace()有可能令所有iterator失效。Rehashing之所以发生,是由于一次安插动作造成最终的元素个数大于等于bucket 数量乘以最大负载系数(也就是说,当最大负载系数提供的保证被打破)。insert()和emplace()成员函数不会影响指向容器元素的reference的有效性。

注意,安插函数insert()和emplace()的返回类型差别如下:

Unordered set 提供以下接口:

pair<iterator,bool> insert(const value_type& val);

iterator insert(iterator posHint,const value_type& val);

template <typename.. . Args>
pair<iterator,bool> emplace(Args&&... args);

Unordered multiset 提供以下接口:

iterator insert(const value_type& val);

iterator insert(iterator posHint,const value_type& val);

template <typename... Args>
iterator emplace(Args&&... args);

Bucket接口

我们可能通过一个特定的bucket接口访问个别bucket,用以暴露整个hash table的内部状态。

c.bucket_count()      //返回当前的bucket个数
c.bucket(val)         //返回val将(或可能)被找到的那个bucket的编号
c.bucket_size(buckidx)//返回第buckidx个bucket所含的元素个数
c.begin(buckidx)      //返回一个forward iterator,指向第buckidx个bucket中的第一元素
c.end(buckidx)        //返回一个forward iterator,指向第buckidx个bucket 中的最末元素的下一位置
c.cbegin(buckidx)     //返回一个const forward iterator,指向第buckidx个bucket中的第一元素
c.cend(buckidx)       //返回一个const forward iterator,指向第buckidx个bucket中的最末元素的下一位置

使用 Unordered Map 作为 Associative Array

就像map一样,unordered map 也提供一个 subscript(下标)操作符用来直接元素访问,还提供一个相应的成员函数at()。

c[key]     //安插一个带着key的元素——如果尚未存在于容器内。返回一个reference 指向带着 key 的元素(only for nonconstant unordered maps)
c.at(key)  //返回一个reference 指向带着key的元素

at()会根据它所收到的key 产出对应元素的value,如果该元素不存在则会抛出一个类型为out_of_range的异常。

对于操作符[],其索引也就是用来识别元素的key。这意味着操作符[]的索引可能是任何类型,而非只是整数类型。这样的接口,也就是所谓的associative array接口。关于操作符[],索引类型并不是它和寻常C-style array 的唯一不同。请注意,你不能给出错误的索引,如果你指定某个 key 为索引,却找不到对应的元素,会有一个新元素被自动安插到map内。该新元素的value 将由其类型的default 构造函数始化。因此,欲使用这一特性,你就不能指定一个没有default构造函数的value类型。注,所有基础类型都提供有一个default 构造函数,将初值设为0。

异常处理(Exception Handling)

Unordered 容器是一种以节点为基础的容器,所以,构建节点一旦失败,不过就是使容器回到原先状态而已。然而由于“可能发rehashing”,下列保证适用于所有unordered容器:

1.“单元素安插”拥有要么成功要么无效行为,前提是hash 函数和“等价准则”不抛出异常。而如果它们不抛出异常,它们要么成功,要么就“无作用”。

2.erase()不抛出异常,前提是 hash 函数和“等价准则”不抛出异常——那正是默认使用之函数的情况。

3.没有任何clear()函数会抛出异常。

4.没有任何swap()函数会抛出异常,前提是hash的“copy 构造函数”或“copy assignment

操作符”或“等价比较函数”不抛出异常。

5.rehash()有着要么成功要么无效行为,前提是hash函数和“等价

比较函数”不抛出异常。而如果它们不抛出异常,它们要么成功,要么“无作用”。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
unordered_map是C++标准库中提供的一个无序关联容器,它基于哈希表实现,用于存储键值对。unordered_map中的键和值可以是任意类型,且键值对之间的映射关系是唯一的。 unordered_map的特点如下: 1. 无序性:unordered_map中的元素是无序存储的,与元素插入的顺序无关。 2. 快速查找:由于使用了哈希表,unordered_map提供了快速的查找操作。平均情况下,插入、删除和查找操作的时间复杂度为O(1)。 3. 内存消耗较大:由于哈希表的特性,unordered_map在存储上需要更多的内存空间。 4. 迭代器失效:由于哈希表的重新哈希操作,插入和删除元素可能导致迭代器失效。 unordered_map提供了以下常用的内置函数: 1. insert(pair): 向unordered_map中插入一个键值对。 2. erase(key): 从unordered_map中移除指定的键key及其对应的值。 3. size(): 返回unordered_map中键值对的个数。 4. empty(): 判断unordered_map是否为空,如果为空则返回true,否则返回false。 5. clear(): 清空unordered_map中的所有键值对。 6. find(key): 查找unordered_map中是否存在指定的键key,如果存在则返回指向该键值对的迭代器,否则返回unordered_map::end()。 7. count(key): 返回unordered_map中与指定键key相等的键值对的个数。 除了上述常用的函数外,还可以使用迭代器遍历unordered_map中的所有键值对。需要注意的是,unordered_map中的键是唯一的,如果插入具有相同键的元素,新元素将会替换旧元素。希望对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Reol520

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值