数据结构入门(8) ——查找

数据结构入门 ——查找


前言

  本系列文章将简要介绍数据结构课程入门知识,文章将结合我们学校(吉大)数据结构课程内容进行讲述。


一、线性表查找

  • 查找亦称检索
  • 一个文件包含n个记录(或称元素、结点),每个记录都有一个关键词域。一个查找算法,就是对给定的值K,在文件中找关键词等于K的那个记录 。
  • 平均查找长度:查找一个元素所作的关键词平均比较次数。衡量一个查找算法优劣的主要标准。

顺序查找

无序表的顺序查

从线性表的起始结点开始,逐个检查每个结点Ri(1 ≤ i ≤ n) ,或者找到关键词Ki = K,或者i > n(i为表中记录的下标,n为线性表包含的元素个数)查找以失败告终。

  • 查找成功的平均查找长度: E ( n ) = ∑ i = 1 n P i × C i = n + 1 2 E(n)=\sum_{i=1}^nP_i×C_i=\frac {n+1}{2} E(n)=i=1nPi×Ci=2n+1
  • 查找失败的查找长度:n+1
  • 顺序查找的时间复杂度:O(n)

有序表的顺序查找

  • 查找成功:1 ≤ i ≤ n,性能与无序表查找一致
  • 查找失败:i = n+1, 性能比无序表查找好
  • 时间复杂度O(n)
  • 利用了有序表的序关系,但还没有充分利用

有序表的二分查找

选取一个位置i (1≤ i ≤ n),比较K和Ki,我们有:

  • K < Ki,[ K一定在Ri左侧,不必考虑Ri, …,Rn]
  • K = Ki ,[ 查找成功结束 ]
  • K > Ki,[K一定在Ri右侧,不必考虑R1, … ,Ri]

使用不同的规则确定 i,可得到不同的二分查找方法:对半查找、一致对半查找、斐波那契查找和插值查找等。

对半(折半)查找

K与待查表的中间记录的关键词 Kn/2比较,其结果有三:
①K<Kn/2 ,②K= Kn/2 ,③K>Kn/2
由情况 ① 和 ③ 能确定下一次应到表的哪一半中去查找,即将查找范围缩小一半,由情况 ② 知查找成功结束;

int Bsearch(int R[],int n,int K){//K为要查找的值
	int s=0,e=n-1;
	while(s<=e){
		int i=(s+e)/2;
		if(K<R[i]) e=i-1;
		if(k==R[i])return i;
		if(k>R[i])s=i+1;
	}
	return -1;//未找到
}
二叉判定树

对于有序表Rs, Rs+1,…, Re,对半查找的二叉判定树T(s,e)的递归定义如下:

  • 当e-s+1 ≤ 0时:T(s, e)为空;
  • 当e-s+1 > 0时:
    T(s, e)的根结点是 ⌊ ( e + s ) / 2 ⌋ \lfloor(e+s)/2\rfloor (e+s)/2
    根结点的左子树是Rs, Rs+1,…, R ⌊ ( e + s ) / 2 ⌋ R_{\lfloor(e+s)/2\rfloor} R(e+s)/2 -1相对应的二叉判定树;
    在这里插入图片描述
  • 对半查找算法的每次成功查找正好对应判定树的一个内结点,关键词的比较次数该结点的深度加1,即从根到该结点所经过的结点数。
  • 每次不成功的查找对应判定树的一个外结点,关键词的比较次数恰好为该结点深度,即根到该节点所经过的内结点数。

成功的平均查找长度: ASLSUCC =(11+22+43+34)/10 =29/10 =2.9
**查找失败的平均查找长度:**ASLUNSUCC= (53+64)/11= 39/11

对半查找总结

优点:查找效率为O(logn) ,比顺序查找高。
缺点:只适用于有序表,且限于顺序存储结构,对线性链表难以进行二分查找。

一致对半查找

伪代码
算法U (R, n, K . i ) 
i = ceil(n / 2) . m = n / 2 .//ceil为取上界
WHILE K ≠ R[i] DO
( 	IF m = 0 THEN RETURN FALSE.
	δ = ceil(m / 2) .
	IF K < R[i] THEN i = i − δ . //i指向左区间中间位置
	IF K > R[i] THEN i = i + δ . //i指向右区间中间位置
	m = m / 2 .
) 
RETURN TRUE.

在这里插入图片描述

  • 算法U之所以被称为是一致的,是因为,在层k上的一个结点的编号与在层k-1上其父结点的编号之差的绝对值,均有一致的常数δ .
    例如,上图中,对第1层均有一致的δ = 3,对第2层均有一致的δ =1,对第3层均有一致的δ = 1

  • 当查找失败时,U 可能在结束前作一次冗余的比较。如图中阴影结点所示。

斐波那契查找

斐波那契(Fibonacci)查找: 对半查找的替代,以Fibonacci序列的分划代替对半查找的均匀分划

假定表中元素的个数是某个Fibonacci数减1,即n=Fk+1-1. 把K与 K F k K_{F_k} KFk作首次比较,从而产生
如下的结果:

  1. K = K F k K_{F_k} KFk :则查找成功;
  2. K < K F k K_{F_k} KFk :继续在1到Fk-1区间内查找;
  3. K > K F k K_{F_k} KFk :继续在Fk+1到Fk+1-1区间内查找

Fibonacci查找算法在最坏情况下的时间复杂性为O(log2n),且平均复杂性亦为O(log2n)

插值查找

  • 为了简便,假定有序表中记录的关键词是数值,且K1<K2<…<Kn呈均匀分布。
  • 给定K,可用线性插值来决定K的期望地址
  • 对于子文件Ks,…, Ke,则K与Ki的比对位置为:
    i = s + K − K s K e − K s ( e − s ) i=s+\frac {K-K_s}{K_e-K_s}(e-s) i=s+KeKsKKs(es)

插值查找性能

  • 插值查找算法每经一次迭代,平均情况下待查找区间长度由n缩至 n \sqrt n n
  • 关键词比较的次数 = 迭代的次数
  • 假定k次迭代,则 n ( 1 2 ) k = 2 n^{(\frac 12)^k}=2 n(21)k=2
  • 平均时间复杂度O(loglogn)
  • 最坏情况时间复杂度O(n)

综合评价

  1. 从O(logn)到O(loglogn)优势并不明显(除非查找表极长,或比较操作成本极高)
  2. 需引入乘除法运算
  3. 元素分布不均匀时效率受影响。

二、查找树

之前介绍的有序表查找算法,有序表采用顺序存储。
若表中元素频繁增删,插入、删除元素后仍维持表中元素有序,需要O(n)的时间,二分查找失去意义

二叉查找树

定义: 二叉查找树(亦称二叉搜索树、二叉排序树)是一棵二叉树,且各结点关键词互异,其中根序列按其关键词递增排列

结点结构

leftkeyright
struct BSTnode {
int key;
BSTnode* left;
BSTnode* right;
};

二叉查找树基本操作

查找

在二叉查找树中查找关键词为K的元素

BSTnode* search(BSTnode*t, int K){ //递归版本
	if (t == NULL || t->key == K) return t;
	if (K < t->key) return search(t->left, K);
	else return search(t->right, K);
}

BSTnode* search2(BSTnode*t, int K){ //迭代版本
	BSTnode* p = t;
	while (p != NULL) {
		if (K < p->key) p = p->left;
		else if (K > p->key) p = p->right;
		else return p;
	}
	return NULL;
}


插入

将关键词为K的元素插入查找树中,如果K已存在,则不插入
在以t为根的二叉查找树中插入关键词值为K的结点,在t中查找K,在查找失败处插入

void insert(BSTnode* &t, int K){
	if (t == NULL){ 
		t = new BSTnode(); 
		t->key = K; 
	}
	else if (K < t->key) insert(t->left, K);
	else if (K > t->key) insert(t->right, K);
}
删除

删除关键词为K的元素
在t中找到关键词值为K的结点,分3种情况删除

  1. 若K是叶结点,则直接删除。

  2. 单分支:K只有1个子结点,则让其子结点替换K
    在这里插入图片描述
    在这里插入图片描述

  3. K有2个子结点,则让其右子树最小结点s替换K , s的右子树作为其父结点r的左子树。在这里插入图片描述

void remove(BSTnode* &t, int K) {
	if (t == NULL) return;
	if (K < t->key) remove(t->left, K);
	else if (K > t->key) remove(t->right, K);
	else if (t->left==NULL||t->right==NULL){
		BSTnode* temp = t; // K有0或1个子结点
		t=(t->left!=NULL)? t->left : t->right;
		delete temp;
	}
	else if (t->right->left==NULL){ //K有2个子结点且右孩子的左孩子为空
		BSTnode *s=t->right; s->left=t->left; delete t; t=s; 
	}
	else { //K有2个子结点且右孩子的左孩子不空
		BSTnode *r, *s = t->right;
		while(s->left!=NULL){r=s; s=s->left;}
		r->left=s->right; s->left=t->left;
		s->right=t->right; delete t; t=s;
	}
}

二叉查找树总结

  • 查找、插入、删除平均时间复杂度O(logn)
  • 最坏时间复杂度O(n)

高度平衡树(AVL树)

二叉查找树的操作时间复杂度由树高度决定,所以希望控制树高,左右子树尽可能平衡。

称一棵二叉查找树称为高度平衡树,当且仅当或由单一外结点组成,或由两个子树形 Tl 和 Tr 组成,并且满足:

  1. | h (Tl) − h (Tr) | ≤ 1,其中h ( T )表示树T的高度
  2. Tl 和 Tr 都是高度平衡树

AVL树即为每个结点的左子树和右子树的高度最多差1的二叉查找树

设T为高度平衡树,q是T之内结点,qL 和 qR 是 q 的左、右子树,hL 和 hR 分别是 qL 和 qR 的高度,q 的平衡系数(或曰平衡因子)BF(q)定义为hR − hL
高度平衡树所有结点的平衡系数只可能为:-1,0,1

高度平衡树的实现

struct AVLnode {
	int key; 
	int height; //以该结点为根的子树高度
	AVLnode* left;
	AVLnode* right;
	AVLnode(int k, int h, AVLnode *l, AVLnode*r){ 
		key = k; height = h; left = l; right = r; 
	}
};
int height(AVLnode *t){return(t==NULL)?-1:t->height;}

高度平衡树的查找算法与二叉查找树一致

高度平衡树的插入

AVL树是一种二叉查找树,故可使用二叉查找树的插入方法插入结点,但插入一个新结点时,有可能破坏AVL树的平衡性

  • 平衡系数为+1的结点,如果在它的右子树插入新结点,可能使它的右子树变得更高
  • 平衡系数为 −1的结点,如果在它的左子树插入新结点,可能使它的左子树变得更高

此时必须调整树的结构,使之平衡化

在插入结点后对平衡树进行调整,恢复平衡的性质。实现这种调整的操作称为“旋转”。


高度平衡树的旋转

  • 每插入一个新结点时, AVL 树中相关结点的平衡状态会可能发生改变。
  • 在插入一个新结点X后,应调整失去平衡的最小子树,即从插入点到根的路径向上找第一个不平衡结点A。
  • 不平衡意味着A的两棵子树的高度相差2
  • 与二叉查找树插入算法相比,高度平衡树的插入算法要多做一件事,即插入结点后,检查从插入点到根的路径上是否有失衡的点,若有则做相应调整
  • 从发生不平衡的结点A起,沿刚才的路径取该结点下两层的结点B和C

在这里插入图片描述
在这里插入图片描述

  • 情况1、2:即LL/RR型:
    A、B、C处于一条直线上
    平衡方案:单旋转
  • 情况3、4:即LR/RL型:
    A、B、C处于一条折线上
    平衡方案:双旋转
单旋转

一、LL型
在这里插入图片描述

左子树高,则右转
抓住结点B往上拽使之成为根结点(以B为轴将A右转/顺时针旋转),使A成为B的右孩子,BL,AR的性质不变,BR成为A的左子树
在这里插入图片描述

void LL(AVLnode* &A) {
	AVLnode *B = A->left;
	A->left = B->right;
	B->right = A;
	A->height = max(height(A->left),height(A->right))+1;
	B->height = max(height(B->left), height(B->right)) + 1;
	A = B;
} 

二、RR型

在这里插入图片描述
右子树高,则左转
抓住结点B往上拽使之成为根结点(以B为轴将A左转/逆时针旋转),使得,A成为B的左孩子,BR, AL的性质不变,BL成为A的右子树

在这里插入图片描述

void RR(AVLnode* &A) {
	AVLnode *B = A->right;
	A->right = B->left;
	B->left = A;
	A->height = max(height(A->left), height(A->right)) + 1;
	B->height = max(height(B->left), height(B->right)) + 1;
	A = B;
}

双旋转

一、LR型
以C为轴先左转后右转

在这里插入图片描述
看以B为根的子树,其右子树高,那就以C为轴,将B左转。
旋转后ABC在一条直线上
在这里插入图片描述
看以A为根的子树,其左子树高,那就以C为轴,将A右转。
旋转后平衡系数为0,高度仍为h+1。
在这里插入图片描述

void LR(AVLnode* &A) {
	RR(A->left);
	LL(A);
}

二、RL型
以C为轴先右转后左转
在这里插入图片描述
看以B为根的子树,其左子树高,那就以C为轴,将B右转。旋转后ABC在一条直线上。
在这里插入图片描述
看以A为根的子树,其右子树高,那就以C为轴,将A左转。
旋转后平衡系数为0,高度为h+1
在这里插入图片描述

void RL(AVLnode* &A) {
	LL(A->right);
	RR(A);
}

插入实现

void insert(AVLnode* &t, int K) {
	if(t==NULL){t=new AVLnode(K,0,NULL,NULL);}
	else if (K<t->key) { //在左子树插入
		insert(t->left, K);
		if(height(t->left)-height(t->right)==2)
			if(K<t->left->key) LL(t);
			else LR(t);
	}	
	else if (K>t->key){ //在右子树插入
		insert(t->right, K);
		if(height(t->right)-height(t->left)==2)
			if (K>t->right->key) RR(t);
			else RL(t);
	}
	t->height=max(height(t->left),height(t->right))+1;
}

高度平衡树的删除

可采用二叉查找树的删除算法进行删除。
删除某结点X后,沿从X到根结点的路径上考察沿途结点的平衡系数,若第一个不平衡点为A,平衡以 A 为根的子树。平衡后,可能使子树A高度变小。这样可能导致A的父结点不满足平衡性。所以要继续向上考察结点的平衡性,最远可能至根结点,即最多需要做O(logn)次旋转。

高度平衡树的优缺点

  • 优点:AVL树引入平衡因子,很好的限制了树的平均高度为O(logn) ,插入、删除、查找的最坏时间复杂性均为O(logn);
  • 缺点:AVL树插入和删除实现比较复杂,平衡代价高

三、散列

  • 一个确定的函数h
    ◆以结点的关键词K 为自变量
    ◆函数值h(K) 作为结点的存储地址(散列地址)
  • 查找时根据函数h计算存储位置
    ◆通常散列表的存储空间是一个一维数组
    ◆散列地址是数组的下标
  • 按散列存储方式构造的存储结构称为散列表。

散列函数

把关键词值映射到存储位置的函数,通常用h来表示。

散列函数的选取原则:

  1. 运算尽可能简单
  2. 函数的值域必须在散列表长的范围内
  3. 极少出现冲突:尽可能使得关键词不同时,其散列函数值亦不相同
  4. 通常h的值域为{0, 1, 2, … , M-1},即0≤ h(K) < M. M为散列表长度。

散列函数(哈希函数)常用方法

直接定址法
例如:有一个从1到100岁的人口数字统计表,其中,年龄作为关键字,哈希函数取关键字自身。

数字分析法
有学生的生日数据如下:
年.月.日
75.10.03
75.11.23
76.03.02
76.07.12
75.04.21
76.02.15

经分析,第一位,第二位,第三位重复的可能性大,取这三位造成冲突的机会增加,所以尽量不取前三位,取后三位比较好

平方取中法
取关键字平方后的中间几位为哈希地址。

折叠法
将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址,这方法称为折叠法。
例如:每一种西文图书都有一个国际标准图书编号,它是一个10位的十进制数字,若要以它作关键字建立一个哈希表,当馆藏书种类不到10,000时,可采用此法构造一个四位数的哈希函数。

除留余数法
取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址。
H(key)=key MOD p (p<=m)

随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即
H(key)=random(key),其中random为随机函数。通常用于关键字长度不等时采用此法。
若已知哈希函数及冲突处理方法,哈希表的建立步骤如下:
Step1. 取出一个数据元素的关键字key,计算其在哈希表中的存储地址D=H(key)。若存储地址为D的存储空间还没有被占用,则将该数据元素存入;否则发生冲突,执行Step2。
Step2. 根据规定的冲突处理方法,计算关键字为key的数据元素之下一个存储地址。若该存储地址的存储空间没有被占用,则存入;否则继续执行Step2,直到找出一个存储空间没有被占用的存储地址为止。


常用冲突处理方法

无论哈希函数设计有多么精细,都会产生冲突现象,也就是2个关键字处理函数的结果映射在了同一位置上,因此,有一些方法可以避免冲突。

拉链法
拉出一个动态链表代替静态顺序存储结构,可以避免哈希函数的冲突,不过缺点就是链表的设计过于麻烦,增加了编程复杂度。此法可以完全避免哈希函数的冲突。
若链表很长,可以将其替代为跳表或查找树。
在C++与Java标准库中的哈希表均采用拉链解决冲突,当链表长度大于8时会自动转换为红黑树

拉链法实现一个简单哈希表
LeetCode 706.设计哈希映射

class MyHashMap {
public:
    MyHashMap():data(base) {}
    
    void put(int key, int value) {
        int h=hashkey(key);
        for(auto it=data[h].begin(),end=data[h].end();it!=end;++it){
            if((*it).first==key){
                (*it).second=value;
                return;
            }
        }
        data[h].push_back(make_pair(key,value));
    }
    
    int get(int key) {
        int h=hashkey(key);
        for(auto it=data[h].begin(),end=data[h].end();it!=end;++it){
            if((*it).first==key){
                return (*it).second;
            }
        }
        return -1;
    }
    
    void remove(int key) {
        int h=hashkey(key);
        for(auto it=data[h].begin(),end=data[h].end();it!=end;++it){
            if((*it).first==key){
                data[h].erase(it);
                return;
           }
        }
    }
private:
    static const int base=769;
    vector<list<pair<int ,int>>> data;
    static int hashkey(int key){
        return key%base;
    }
};

线性探查
线性探查完全废除链接,当发生冲突时,以固定的次序查找表中的结点,直到找到一个关键词为K的结点或者找到一个空位置。

其循环探查路径:h(K), h(K)+1, …, M-1, 0, 1, …, h(K)-1

缺点:聚集现象,即很多元素连成一片,使探查次数增加。

定义:设散列表空间大小为M,填入表中的元素个数是N,则称α= N / M为散列表的“装载(填)因子”。
➢ 装载因子α=9 / 11 ≈ 0.82。
➢ 实际应用时,常将散列表大小设计为 α=0.5~0.8为宜。
➢ Java HashMap的α=0.75,超过此值将自动进行表扩容。

二次探查
探测序列: hi = ( h(K) ± i2) mod M 其中i=1, 2, …, (M-1)/2, M为散列表表长
其具体循环探查路径:h(K), h(K)+12, h(K)-12, h(K)+22, h(K)-22, h(K)+32, h(K)-32, …以上各值均在 mod M意义下

双重探查
从h(K)开始,寻找空地址时,所前进的步长不是固定的,而与K有关,即用δ(K)代替线性探查的前进步长1 (1≤ δ(K) <M) 。
循环探查路径:h(K), h(K)+ δ(K), h(K)+ 2δ(K), …以上各值均在 mod M意义下
为了确保表中的每一个地址都能探查到,要求δ(K)和M互质,因此M应选作质数


查找长度

虽然散列在关键词与记录存储位置之间建立了直接映射,但由于“冲突”的存在,仍有必要以平均查找长度来衡量散列表查找效率。

散列表的平均查找长度ASL:平均探测次数

线性探查的平均查找长度

在这里插入图片描述

查找成功:查找的元素在散列表中

  1. 输入个数等于表中实际元素个数,即9种
  2. 第一种输入:查找的元素K等于20,元素比较1次
  3. 第二种输入:查找的元素K等于1,元素比较1次

ASLSUCC= 1 9 ( 1 + 1 + 1 + 1 + 3 + 1 + 1 + 3 + 3 ) \frac 19(1+ 1+ 1+ 1+ 3+ 1+ 1+ 3+ 3) 91(1+1+1+1+3+1+1+3+3)

查找不成功:查找的元素不在散列表中。

  1. 输入数据经散列函数计算后只可能映射到0…9的位置,故可能的输入有10种(散列函数值域)。
  2. 第一种输入:输入的K满足h(K)=0,探测6次
  3. 第二种输入:输入的K满足h(K)=1,探测5次

ASLUNSUCC= 1 10 ( 6 + 5 + 4 + 3 + 2 + 1 + 5 + 4 + 3 + 2 ) \frac 1{10}(6+ 5+ 4+ 3+ 2+ 1+ 5+ 4+ 3 +2) 101(6+5+4+3+2+1+5+4+3+2)

拉链法平均查找长度

关键词19, 01, 23, 14, 55, 68, 11, 82, 36
散列函数h(K)=K mod 7

在这里插入图片描述
ASLSUCC=(61+22+1*3)/9 =13/9
ASLUNSUCC=(2+3+2+1+2+4+2)/7


散列表的删除

拉链法:链表删除

其他方法,以线性探查法为例,讨论散列表的删除过程。

元素不能直接删除,因为相应的位置可能引起过冲突,数据记录绕过该位置存在了别处,该位置作为查找其他元素的占位符。
方案1: 懒惰删除(Lazy Deletion)。并不真正的删除元素,而是将删除的位置做一个标记,其状态置为“已删除”,表中每个位置有3个状态:空的、已占用的、已删除的。

  • 当查找一个关键词时,将跳过“已删除”的位置,就像它们是“已占用”的一样。
  • 插入关键词时,则可被插入到“已删除” 或者“空的”位置中。

缺点: 一旦表的位置变为“已占用”的,则它们就绝不再次地变空。
在进行大量删除和插入操作后,“空的”位置将越来越少,每次不成功的查找将花费更长的查找长度,甚至为表长M。
方案2: 平时采用Lazy Deletion,每隔一段时间定期执行一次真正删除,把标记为“已删除”的结点真正删去:

  • 可以保存每一记录的访问次数。
  • 定期将标记为“已删除”的位置清空。把记录按访问次数递减的顺序依次重新插入散列表,保证上一周期被频繁访问的元素更有机会存储在接近其散列函数值的位置。

**方案3:**将位置j清空后,其余元素按刚才的插入顺序重新插入散列表

扫描位置j以后,下一个空位置之前的每一个位置i,看将位置j清空后,会不会阻碍查找T[i]的路径,若会则将T[i]移至空位。

伪代码
算法LineDectectionDelete ( T, M, j )
/*对由线性探查算法建立的散列表T, 删除T[j]处的记录*/
whiletrue{ //T[j]置空后可能导致后面一系列元素前移
	T[j]置为空 . i = j .
	do //考查 位置j以后,下一个空位置之前 的元素
	{ 	i = (i+1) mod M .
		if T[i]为空 THEN return.
		else r = h(T[i])} while (j < r <= i or i < j < r or r <= i < j) .
	//r 沿探查方向先遇到i(情况2、3、4),无需调整
	T[j] = T[i]. j = i .
	//T[i]前移使位置i变空,需继续考虑后面元素是否需要前移
)

散列表的性能分析

解决冲突的策略查找成功查找不成功
拉链法 1 + n 2 1 + \frac n2 1+2nn+e-n
线性探查 1 2 ( 1 + 1 1 − n ) \frac 12(1+\frac 1{1-n}) 21(1+1n1) 1 2 ( 1 + 1 ( 1 − n ) 2 ) \frac 12(1+\frac 1{(1-n)^2}) 21(1+(1n)21)
二次探查
双重探查
1 n l n 1 1 − n \frac 1nln\frac 1{1-n} n1ln1n1 1 1 − n \frac 1{1-n} 1n1
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值