B树

B树
       即二叉搜索树:
       1.所有非叶子结点至多拥有两个儿子(Left和Right);
       2.所有结点存储一个关键字;
       3.非叶子结点的左指针指向小于其关键字的子树,右指针指向大于其关键字的子树;
       如:
       
       B树的搜索,从根结点开始,如果查询的关键字与结点的关键字相等,那么就命中;否则,如果查询关键字比结点关键字小,就进入左儿子;如果比结点关键字大,就进入右儿子;如果左儿子或右儿子的指针为空,则报告找不到相应的关键字;
       如果B树的所有非叶子结点的左右子树的结点数目均保持差不多(平衡),那么B树的搜索性能逼近二分查找;但它比连续内存空间的二分查找的优点是,改变B树结构(插入与删除结点)不需要移动大段的内存数据,甚至通常是常数开销;
       如:
      
    但B树在经过多次插入与删除后,有可能导致不同的结构:
   右边也是一个B树,但它的搜索性能已经是线性的了;同样的关键字集合有可能导致不同的树结构索引;所以,使用B树还要考虑尽可能让B树保持左图的结构,和避免右图的结构,也就是所谓的“平衡”问题;      
       实际使用的B树都是在原B树的基础上加上平衡算法,即“平衡二叉树”;如何保持B树结点分布均匀的平衡算法是平衡二叉树的关键;平衡算法是一种在B树中插入和删除结点的策略;
B-树
       是一种多路搜索树(并不是二叉的):
       1.定义任意非叶子结点最多只有M个儿子;且M>2;
       2.根结点的儿子数为[2, M];
       3.除根结点以外的非叶子结点的儿子数为[M/2, M];
       4.每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)
       5.非叶子结点的关键字个数=指向儿子的指针个数-1;
       6.非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
       7.非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
       8.所有叶子结点位于同一层;
       如:(M=3)
       B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点;
B-树的特性:
       1.关键字集合分布在整颗树中;
       2.任何一个关键字出现且只出现在一个结点中;
       3.搜索有可能在非叶子结点结束;
       4.其搜索性能等价于在关键字全集内做一次二分查找;
       5.自动层次控制;
       由于限制了除根结点以外的非叶子结点,至少含有M/2个儿子,确保了结点的至少利用率,其最底搜索性能为:
    
       其中,M为设定的非叶子结点最多子树个数,N为关键字总数;
       所以B-树的性能总是等价于二分查找(与M值无关),也就没有B树平衡的问题;
       由于M/2的限制,在插入结点时,如果结点已满,需要将结点分裂为两个各占M/2的结点;删除结点时,需将两个不足M/2的兄弟结点合并;
B+树
       B+树是B-树的变体,也是一种多路搜索树:
       1.其定义基本与B-树同,除了:
       2.非叶子结点的子树指针与关键字个数相同;
       3.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间);
       5.为所有叶子结点增加一个链指针;
       6.所有关键字都在叶子结点出现;
       如:(M=3)
   B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;
       B+的特性:
       1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
       2.不可能在非叶子结点命中;
       3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
       4.更适合文件索引系统;
B*树
       是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针;
   B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2);
       B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
       B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;
       所以,B*树分配新结点的概率比B+树要低,空间使用率更高;
       B树:二叉树,每个结点只存储一个关键字,等于则命中,小于走左结点,大于走右结点;
       B-树:多路搜索树,每个结点存储M/2到M个关键字,非叶子结点存储指向关键字范围的子结点;
       所有关键字在整颗树中出现,且只出现一次,非叶子结点可以命中;
       B+树:在B-树基础上,为叶子结点增加链表指针,所有关键字都在叶子结点中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中;
       B*树:在B+树基础上,为非叶子结点也增加链表指针,将结点的最低利用率从1/2提高到2/3;
实现:

1、B+树索引的总体结构
①B+树索引是一个多级索引,但是其结构不同于多级顺序索引;
②B+树索引采用平衡树结构,即每个叶结点到根的路径长度都相同;
③每个非叶结点有到n个子女,n对特定的树是固定的;
④B+树的所有结点结构都相同,它最多包含n-1个搜索码值K1、K2、…、Kn-1,以及n个指针P1、P2、…、Pn,每个结点中的搜索码值按次序存放,即如果i<j,那么Ki<Kj,如图1所示。

图1 B+树的结点结构 
2、B+树索引的叶结点
①指针Pi(i=1,2,…,n-1)指向具有搜索码值Ki的一个文件记录或一个指针(存储)桶,桶中的每个指针指向具有搜索码值Ki的一个文件记录。指针桶只在文件不按搜索码顺序物理存储时才使用。指针Pn具有特殊的作用;
②每个叶结点最多可有n-1个搜索码值,最少也要有个搜索码值。各个叶结点中搜索码值的范围互不相交。要使B+树索引成为稠密索引,数据文件中的各搜索码值都必须出现在某个叶结点中且只能出现一次;
③由于各叶结点按照所含的搜索码值有一个线性顺序,所以就可以利用各个叶结点的指针Pn将叶结点按搜索码顺序链接在一起。这种排序能够高效地对文件进行顺序处理,而B+树索引的其他结构能够高效地对文件进行随机处理,如图2所示。
图2 B+树索引的叶结点结构示例 
3、B+树索引的非叶结点
①B+树索引的非叶结点形成叶结点上的一个多级(稀疏)索引;
②非叶结点的结构和叶结点的结构相同,即含有能够存储n-1个搜索码值和n个指针的存储单元的数据结构。只不过非叶结点中的所有指针都指向树中的结点;
③如果一个非叶结点有m个指针,则≤m≤n。若m<n,则非叶结点中指针Pm之后的所有空闲空间作为预留空间,与叶结点的区别在于结点的最后一个指针Pm和Pn的位置与指向不同,如图3所示;
图3 B+树索引的非叶结点结构 
④在一个含有m个指针的非叶结点中,指针Pi(i=2,…,m-1)指向一棵子树,该子树的所有结点的搜索码值大于等于Ki-1而小于Ki。指针Pm指向子树中所含搜索码值大于等于Km-1的那一部分,而指针P1指向子树中所含搜索码值小于K1的那一部分,如图4所示。
图4 B+树索引的非叶结点中指针Pi的指向
4、B+树索引的根结点
①根结点的结构也与叶结点相同;
②根结点包含的指针数可以小于。但是,除非整棵树只有一个结点,否则根结点必须至少包含两个指针。图5给出一个B+树结构的示意图。
图5 account关系的B+树索引结构

5、部分操作的C语言实现
/* btrees.h */ 
/* 
* 平衡多路树的一种重要方案。 
* 在 1970 年由 R. Bayer 和 E. McCreight 发明。 
*/ 
#define M 1 
/* B 树的阶,即非根节点中键的最小数目。 
* 有些人把阶定义为非根节点中子树的最大数目。 
*/ 
typedef int typekey; 
typedef struct btnode { /* B-Tree 节点 */ 
int d; /* 节点中键的数目 */ 
typekey k[2*M]; /* 键 */ 
char *v[2*M]; /* 值 */ 
struct btnode *p[2*M+1]; /* 指向子树的指针 */ 
} node, *btree; 
/* 
* 每个键的左子树中的所有的键都小于这个键, 
* 每个键的右子树中的所有的键都大于等于这个键。 
* 叶子节点中的每个键都没有子树。 
*/ 

/* 当 M 等于 1 时也称为 2-3 树 
* +----+----+ 
* | k0 | k1 | 
* +-+----+----+--- 
* | p0 | p1 | p2 | 
* +----+----+----+ 
*/ 
extern int btree_disp; /* 查找时找到的键在节点中的位置 */ 
extern char * InsValue; /* 与要插的键相对应的值 */ 

extern btree search(typekey, btree); 
extern btree insert(typekey,btree); 
extern btree delete(typekey,btree); 
extern int height(btree); 
extern int count(btree); 
extern double payload(btree); 
extern btree deltree(btree); 
/* end of btrees.h */ 

/*******************************************************/ 

/* btrees.c */ 
#include 
#include 
#include "btrees.h" 

btree search(typekey, btree); 
btree insert(typekey,btree); 
btree delete(typekey,btree); 
int height(btree); 
int count(btree); 
double payload(btree); 
btree deltree(btree); 

static void InternalInsert(typekey, btree); 
static void InsInNode(btree, int); 
static void SplitNode(btree, int); 
static btree NewRoot(btree); 

static void InternalDelete(typekey, btree); 
static void JoinNode(btree, int); 
static void MoveLeftNode(btree t, int); 
static void MoveRightNode(btree t, int); 
static void DelFromNode(btree t, int); 
static btree FreeRoot(btree); 

static btree delall(btree); 
static void Error(int,typekey); 

int btree_disp; /* 查找时找到的键在节点中的位置 */ 
char * InsValue = NULL; /* 与要插的键相对应的值 */ 
static int flag; /* 节点增减标志 */ 
static int btree_level = 0; /* 多路树的高度 */ 
static int btree_count = 0; /* 多路树的键总数 */ 
static int node_sum = 0; /* 多路树的节点总数 */ 
static int level; /* 当前访问的节点所处的高度 */ 
static btree NewTree; /* 在节点分割的时候指向新建的节点 */ 
static typekey InsKey; /* 要插入的键 */ 

btree search(typekey key, btree t) 
{ 
int i,j,m; 
level=btree_level-1; 
while (level >= 0){ 
for(i=0, j=t->d-1; i t->k[m])?(i=m+1):(j=m)); 
if (key == t->k){ 
btree_disp = i; 
return t; 
} 
if (key > t->k) /* i == t->d-1 时有可能出现 */ 
i++; 
t = t->p; 
level--; 
} 
return NULL; 
} 

btree insert(typekey key, btree t) 
{ 
level=btree_level; 
InternalInsert(key, t); 
if (flag == 1) /* 根节点满之后,它被分割成两个半满节点 */ 
t=NewRoot(t); /* 树的高度增加 */ 
return t; 
} 

void InternalInsert(typekey key, btree t) 
{ 
int i,j,m; 

level--; 
if (level < 0){ /* 到达了树的底部: 指出要做的插入 */ 
NewTree = NULL; /* 这个键没有对应的子树 */ 
InsKey = key; /* 导致底层的叶子节点增加键值+空子树对 */ 
btree_count++; 
flag = 1; /* 指示上层节点把返回的键插入其中 */ 
return; 
} 
for(i=0, j=t->d-1; i t->k[m])?(i=m+1):(j=m)); 
if (key == t->k) { 
Error(1,key); /* 键已经在树中 */ 
flag = 0; 
return; 
} 
if (key > t->k) /* i == t->d-1 时有可能出现 */ 
i++; 
InternalInsert(key, t->p); 

if (flag == 0) 
return; 
/* 有新键要插入到当前节点中 */ 
if (t->d < 2*M) {/* 当前节点未满 */ 
InsInNode(t, i); /* 把键值+子树对插入当前节点中 */ 
flag = 0; /* 指示上层节点没有需要插入的键值+子树,插入过程结束 */ 
} 
else /* 当前节点已满,则分割这个页面并把键值+子树对插入当前节点中 */ 
SplitNode(t, i); /* 继续指示上层节点把返回的键值+子树插入其中 */ 
} 

/* 
* 把一个键和对应的右子树插入一个节点中 
*/ 
void InsInNode(btree t, int d) 
{ 
int i; 
/* 把所有大于要插入的键值的键和对应的右子树右移 */ 
for(i = t->d; i > d; i--){ 
t->k = t->k[i-1]; 
t->v = t->v[i-1]; 
t->p[i+1] = t->p; 
} 
/* 插入键和右子树 */ 
t->k = InsKey; 
t->p[i+1] = NewTree; 
t->v = InsValue; 
t->d++; 
} 
/* 
* 前件是要插入一个键和对应的右子树,并且本节点已经满 
* 导致分割这个节点,插入键和对应的右子树, 
* 并向上层返回一个要插入键和对应的右子树 
*/ 
void SplitNode(btree t, int d) 
{ 
int i,j; 
btree temp; 
typekey temp_k; 
char *temp_v; 
/* 建立新节点 */ 
temp = (btree)malloc(sizeof(node)); 
/* 
* +---+--------+-----+-----+--------+-----+ 
* | 0 | ...... | M | M+1 | ...... |2*M-1| 
* +---+--------+-----+-----+--------+-----+ 
* |<- M+1 ->|<- M-1 ->| 
*/ 
if (d > M) { /* 要插入当前节点的右半部分 */ 
/* 把从 2*M-1 到 M+1 的 M-1 个键值+子树对转移到新节点中, 
* 并且为要插入的键值+子树空出位置 */ 
for(i=2*M-1,j=M-1; i>=d; i--,j--) { 
temp->k[j] = t->k; 
temp->v[j] = t->v; 
temp->p[j+1] = t->p[i+1]; 
} 
for(i=d-1,j=d-M-2; j>=0; i--,j--) { 
temp->k[j] = t->k; 
temp->v[j] = t->v; 
temp->p[j+1] = t->p[i+1]; 
} 
/* 把节点的最右子树转移成新节点的最左子树 */ 
temp->p[0] = t->p[M+1]; 
/* 在新节点中插入键和右子树 */ 
temp->k[d-M-1] = InsKey; 
temp->p[d-M] = NewTree; 
temp->v[d-M-1] = InsValue; 
/* 设置要插入上层节点的键和值 */ 
InsKey = t->k[M]; 
InsValue = t->v[M]; 

} 
else { /* d <= M */ 
/* 把从 2*M-1 到 M 的 M 个键值+子树对转移到新节点中 */ 
for(i=2*M-1,j=M-1; j>=0; i--,j--) { 
temp->k[j] = t->k; 
temp->v[j] = t->v; 
temp->p[j+1] = t->p[i+1]; 
} 
if (d == M) /* 要插入当前节点的正中间 */ 
/* 把要插入的子树作为新节点的最左子树 */ 
temp->p[0] = NewTree; 
/* 直接把要插入的键和值返回给上层节点 */ 
else { /* (d /* 把节点当前的最右子树转移成新节点的最左子树 */ 
temp->p[0] = t->p[M]; 
/* 保存要插入上层节点的键和值 */ 
temp_k = t->k[M-1]; 
temp_v = t->v[M-1]; 
/* 把所有大于要插入的键值的键和对应的右子树右移 */ 
for(i=M-1; i>d; i--) { 
t->k = t->k[i-1]; 
t->v = t->v[i-1]; 
t->p[i+1] = t->p; 
} 
/* 在节点中插入键和右子树 */ 
t->k[d] = InsKey; 
t->p[d+1] = NewTree; 
t->v[d] = InsValue; 
/* 设置要插入上层节点的键和值 */ 
InsKey = temp_k; 
InsValue = temp_v; 
} 
} 
t->d =M; 
temp->d = M; 
NewTree = temp; 
node_sum++; 
} 

btree delete(typekey key, btree t) 
{ 
level=btree_level; 
InternalDelete(key, t); 
if (t->d == 0) 
/* 根节点的子节点合并导致根节点键的数目随之减少, 
* 当根节点中没有键的时候,只有它的最左子树可能非空 */ 
t=FreeRoot(t); 
return t; 
} 

void InternalDelete(typekey key, btree t) 
{ 
int i,j,m; 
btree l,r; 
int lvl; 

level--; 
if (level < 0) { 
Error(0,key); /* 在整个树中未找到要删除的键 */ 
flag = 0; 
return; 
} 
for(i=0, j=t->d-1; i t->k[m])?(i=m+1):(j=m)); 
if (key == t->k) { /* 找到要删除的键 */ 
if (t->v != NULL) 
free(t->v); /* 释放这个节点包含的值 */ 
if (level == 0) { /* 有子树为空则这个键位于叶子节点 */ 
DelFromNode(t,i); 
btree_count--; 
flag = 1; 
/* 指示上层节点本子树的键数量减少 */ 
return; 
} else { /* 这个键位于非叶节点 */ 
lvl = level-1; 
/* 找到前驱节点 */ 
r = t->p; 
while (lvl > 0) { 
r = r->p[r->d]; 
lvl--; 
} 
t->k=r->k[r->d-1]; 
t->v=r->v[r->d-1]; 
r->v[r->d-1]=NULL; 
key = r->k[r->d-1]; 
} 
} 
else if (key > t->k) /* i == t->d-1 时有可能出现 */ 
i++; 
InternalDelete(key,t->p); 
/* 调整平衡 */ 
if (flag == 0) 
return; 
if (t->p->d < M) { 
if (i == t->d) /* 在最右子树中发生了删除 */ 
i--; /* 调整最右键的左右子树平衡 */ 
l = t->p; 
r = t->p[i+1]; 
if (r->d > M) 
MoveLeftNode(t,i); 
else if(l->d > M) 
MoveRightNode(t,i); 
else { 
JoinNode(t,i); 
/* 继续指示上层节点本子树的键数量减少 */ 
return; 
} 
flag = 0; 
/* 指示上层节点本子树的键数量没有减少,删除过程结束 */ 
} 
} 

/* 
* 合并一个节点的某个键对应的两个子树 
*/ 
void JoinNode(btree t, int d) 
{ 
btree l,r; 
int i,j; 
l = t->p[d]; 
r = t->p[d+1]; 

/* 把这个键下移到它的左子树 */ 
l->k[l->d] = t->k[d]; 
l->v[l->d] = t->v[d]; 
/* 把右子树中的所有键值和子树转移到左子树 */ 
for (j=r->d-1,i=l->d+r->d; j >= 0 ; j--,i--) { 
l->k = r->k[j]; 
l->v = r->v[j]; 
l->p = r->p[j]; 
} 
l->p[l->d+r->d+1] = r->p[r->d]; 
l->d += r->d+1; 
/* 释放右子树的节点 */ 
free(r); 
/* 把这个键右边的键和对应的右子树左移 */ 
for (i=d; i < t->d-1; i++) { 
t->k = t->k[i+1]; 
t->v = t->v[i+1]; 
t->p[i+1] = t->p[i+2]; 
} 
t->d--; 
node_sum--; 
} 
/* 
* 从一个键的右子树向左子树转移一些键,使两个子树平衡 
*/ 
void MoveLeftNode(btree t, int d) 
{ 
btree l,r; 
int m; /* 应转移的键的数目 */ 
int i,j; 
l = t->p[d]; 
r = t->p[d+1]; 
m = (r->d - l->d)/2; 

/* 把这个键下移到它的左子树 */ 
l->k[l->d] = t->k[d]; 
l->v[l->d] = t->v[d]; 
/* 把右子树的最左子树转移成左子树的最右子树 
* 从右子树向左子树移动 m-1 个键+子树对 */ 
for (j=m-2,i=l->d+m-1; j >= 0; j--,i--) { 
l->k = r->k[j]; 
l->v = r->v[j]; 
l->p = r->p[j]; 
} 
l->p[l->d+m] = r->p[m-1]; 
/* 把右子树的最左键提升到这个键的位置上 */ 
t->k[d] = r->k[m-1]; 
t->v[d] = r->v[m-1]; 
/* 把右子树中的所有键值和子树左移 m 个位置 */ 
r->p[0] = r->p[m]; 
for (i=0; id-m; i++) { 
r->k = r->k[i+m]; 
r->v = r->v[i+m]; 
r->p = r->p[i+m]; 
} 
r->p[r->d-m] = r->p[r->d]; 
l->d+=m; 
r->d-=m; 
} 
/* 
* 从一个键的左子树向右子树转移一些键,使两个子树平衡 
*/ 
void MoveRightNode(btree t, int d) 
{ 
btree l,r; 
int m; /* 应转移的键的数目 */ 
int i,j; 
l = t->p[d]; 
r = t->p[d+1]; 

m = (l->d - r->d)/2; 
/* 把右子树中的所有键值和子树右移 m 个位置 */ 
r->p[r->d+m]=r->p[r->d]; 
for (i=r->d-1; i>=0; i--) { 
r->k[i+m] = r->k; 
r->v[i+m] = r->v; 
r->p[i+m] = r->p; 
} 
/* 把这个键下移到它的右子树 */ 
r->k[m-1] = t->k[d]; 
r->v[m-1] = t->v[d]; 
/* 把左子树的最右子树转移成右子树的最左子树 */ 
r->p[m-1] = l->p[l->d]; 
/* 从左子树向右子树移动 m-1 个键+子树对 */ 
for (i=l->d-1,j=m-2; j>=0; j--,i--) { 
r->k[j] = l->k; 
r->v[j] = l->v; 
r->p[j] = l->p; 
} 
/* 把左子树的最右键提升到这个键的位置上 */ 
t->k[d] = l->k; 
t->v[d] = l->v; 
l->d-=m; 
r->d+=m; 
} 
/* 
* 把一个键和对应的右子树从一个节点中删除 
*/ 
void DelFromNode(btree t, int d) 
{ 
int i; 
/* 把所有大于要删除的键值的键左移 */ 
for(i=d; i < t->d-1; i++) { 
t->k = t->k[i+1]; 
t->v = t->v[i+1]; 
} 
t->d--; 
} 
/* 
* 建立有两个子树和一个键的根节点 
*/ 
btree NewRoot(btree t) 
{ 
btree temp; 
temp = (btree)malloc(sizeof(node)); 
temp->d = 1; 
temp->p[0] = t; 
temp->p[1] = NewTree; 
temp->k[0] = InsKey; 
temp->v[0] = InsValue; 
btree_level++; 
node_sum++; 
return(temp); 
} 
/* 
* 释放根节点,并返回它的最左子树 
*/ 
btree FreeRoot(btree t) 
{ 
btree temp; 
temp = t->p[0]; 
free(t); 
btree_level--; 
node_sum--; 
return temp; 
} 

void Error(int f,typekey key) 
{ 
if (f) 
printf("Btrees error: Insert %d!/n",key); 
else 
printf("Btrees error: delete %d!/n",key); 
} 

int height(btree t) 
{ 
return btree_level; 
} 

int count(btree t) 
{ 
return btree_count; 
} 
double payload(btree t) 
{ 
if (node_sum==0) 
return 1; 
return (double)btree_count/(node_sum*(2*M)); 
} 
btree deltree (btree t) 
{ 
level=btree_level; 
btree_level = 0; 
return delall(t); 

} 
btree delall(btree t) 
{ 
int i; 
level--; 
if (level >= 0) { 
for (i=0; i < t->d; i++) 
if (t->v != NULL) 
free(t->v); 
if (level > 0) 
for (i=0; i<= t->d ; i++) 
t->p=delall(t->p); 
free(t); 
} 
return NULL; 
} 

/* end of btrees.c */ 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值