一篇文章学完数据结构绪论 线性表 栈和队列

复杂度

复杂度是衡量一个解决某问题的算法效率的标准,分为时间复杂度和空间复杂度。

如果单从执行时间上考虑一个算法的优劣,那么可以对该算法进行运行以后评估时间,也叫作事后统计法,但硬件影响运行时间,所以不科学,我们一般不用。

一般从五个方面评判算法的优劣

正确性:能否获得想要的结果

可读性:是否便于阅读,注释是否齐全,能否让不懂的人一看就知道你的大致想法

健壮性:也叫鲁棒性,就是如果出错了,程序能不能不崩溃

时间复杂度:根据元运算的执行次数判断执行时间

空间复杂度:估算所需占用的存储空间

时间复杂度

时间复杂度有三种,我直接说最常用的用O()来表示的,假设一组数据有n个,我的算法是打印每一个数据,我要对这个数组进行遍历打印,所以打印就是元运算,那么时间复杂度就是O(n)

int n=10;
int a[n]={7,55,6,9,32,42,73,24,9,0};

for(int i=0;i<n;i++)
{
    printf("%d\n",a[i]);
//这是元运算
}

,或者有一组整形数据要冒泡排序,

//为了实现从大到小的冒泡法排序
//假设int a[10]已经有了一组数据 n=10


for(i=0;i<n;i++)
    for(j=i;j<n-1;j++)
        {
            //满足条件就互换

            if(a[j]<a[j+1])
            {
                a[j+1]=a[j]+a[j+1];
                a[j]=a[j+1]-a[j];
                a[j+1]=a[j+1]-a[j];
            }
        }
//在这段代码中影响其运行时间的运算主要是中间的比大小互换的过程,那么这个过程最多会经历多少次呢?
//会经历n*(n-1)/2,我们假设n为无限大,所以常数和n和n方的系数就会被忽略,得到结果时间复杂度是O
//(n^2)

数据结构

1.基本概念:

数据:输入

数据元素:单一不可再分的数据

数据对象:你需要研究的问题就是你的数据对象

抽象数据类型:ADT 是为了解决一个问题,你提出了一个新的数据类型,及其对应的函数功能

2 逻辑结构:

集合 线性 树型 图状

3 存储结构

顺序 链式 索引 散列

基础补充

我们公认在数据结构中经常定义几个常量
OK 1
ERROR 0
OVERFLOW -2 数据溢出

第一章 线性表

顺序表:连续内存存储空间放线性表的个元素,位置相邻,逻辑相邻

首先定义一个线性表

这是c的,CSDN中没找到c的代码段只能找到c++的代码段,就是代码高亮可能不太合适
#define LIST_INIT_SIZE 100
#define LISTINCREMENT 10
typedef struct{
    ElemType *elme;
    int length;
    int listsize;
}SqList;

对于不懂数据结构的初学者我多做一点注释,当初我自学的时候怎么运行也跑不了,因为我不了解c的一些知识,还有这些算法写的原因,所以我运行不了,为了避免你们遇到跟我一样的问题,就做点注释,懂得就不用细看注释了。
define 是声明一个变量
typedef struct 是结构体
ElemType 这个的意思是元素类型,你需要存储什么类型的元素你就让这个Elemrype做什么类型 ,这个ElemType 也可以是结构体 ,需要自己定义,不能空口瞎白活。
listsize 是线性表的最大长度
length 是当前已分配的存储空间,意思就是我已经往线性表里存储了数据的长度

LIST_INIT_SIZE的意思就是定义一个变量 她的作用在后面会提到 就是我定义一个线性表总长度用这个变量来表示,这么做的目的是为了如果我需要改这个表的总长度我不需要看我的代码的逻辑,一步一步找,我直接改这个变量,而且这么定义变量见名知意线性表长度
LISTINCREMENT 就是定义一个变量 加入我的长度不够需要扩容 就扩容这么长 
这个代码希望大家不要直接拿来抄,因为我这个是纯手打的,我怕copy别人的有错,但是我自己手打的,我至少敢肯定基础逻辑是没问题的,大家最好看完逻辑后把学过的逻辑忘掉他的想法自己写,想达到什么功能怎么做,做完以后再反过来跟这个作比较,哪里有优点哪里还有不足,反复咀嚼,才能充分理解,我是懒得背数据结构,因为他会影响我休息的时间。

然后初始化一个线性表

int Init_SqList(SqList &L){
    L.elem=(ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType));
    if(!L.elem)exit (OVERFLOW);
    L.length=0;
    L.listsize=LIST_INIT_SIZE;
    return OK;
}
我的老师教我对函数的注释应该分为如下几个步骤
//功能:初始化一个线性表
//参数:线性表L
//返回值:成功返回OK 失败返回ERROR

以后大家进厂了各个公司都有不同的注释要求,有些好的软件提供了 固定的注释结构,一个公司都用相同的注释模板比如 aa公司 bb 部门 cc时间 dd修改 等等 我的数据结构老师是这么要求我的

补充一下关于引用传递的知识,c和c++中c的参数传递分为两种 一种是值传递 ,比如我现在有个红苹果她是五公斤,值传递如果把这个参数苹果传进一个函数去了,你传的可能是一个绿苹果她也是五公斤,你函数需要的只是苹果的重量,但它不是同一个苹果,而且用完了绿苹果就被丢弃了,只剩下五斤的红苹果,那个红苹果自始至终都没有动过。 应用传递就是把红苹果直接作为参数传进了函数,假设你这个函数里面告诉你苹果被吃掉了四斤他就剩一斤了,那么函数结束后这就是一个一斤的红苹果,这种引用传递必须使用c++的才可以,c是做不到的,当然c语言也可以实现相似的效果,只不过地址传递使用指针太麻烦了,效率太低了,所以大家在创建c的时候一定选择c++

言归正传,这是初始化一个线性表,传入的参数是线性表L 

 L.elem=(ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType));

这一行是申请LIST_INIT_SIZE*sizeof这么长的空间

if(!L.elem)exit (OVERFLOW);

这一行的作用是判断是否申请成功,因为有的时候我们的存储空间可能不够了就出现申请失败这种情况,此事L.elem是空指针 非 空为真所以满足这个条件就说明空间不够了 退出程序返回OVERFLOW
我们在写代码的时候一定要考虑周全,这里到底会不会出错,给出正确的提示,因为需要鲁棒性

    L.length=0;                   因为还没有存储数据所以lenth长度为0 
    L.listsize=LIST_INIT_SIZE;    然后listsize为预设好的LIST_INIT_SIZE常量
    return OK;                    成功 返回ok

插入

int Insert_SqList &L,int i,ElemType x){
    if(i<1||i>L.length+1)return ERROR;
    if(L.length>=L.listsize)return OVERFLOW;
    q=L.elem[i-1];
    for(p=L.elem[L.length-1];p>=q;--p)
    {
        *(p+1)=*p;
    }
    *p=x;
    ++L.length;
    return OK;

}

//功能:插入一个数据x到第i个位置上
//参数:线性表头L 位置 i 数据 x
//返回值:成功返回OK 失败返回ERROR 表满返回OVERFLOW


 if(i<1||i>L.length+1)return ERROR;

这一行的作用是判断输入是否合法如果位置小于1或者比表中有数据的长度达那么返回错误
还有一种写法就是表满了则申请空间扩容,但一般在链表里用的较多,顺序表用的较少

if(L.length>=L.listsize)return OVERFLOW;

这一行是判断是否能继续增加,如果已存数据大于最大表长,那么不可以继续增加,这是前人写的算法,我个人觉得如果该程序中只能通过该插入函数来增加的话我觉得大于没有必要,因为你只要等于了就不加入了怎么可能大于呢?但也许那样写更加严谨,这只是我个人的想法

    q=L.elem[i-1];
    for(p=L.elem[L.length-1];p>=q;--p)
    {
        *(p+1)=*p;
    }

这一行的作用是这样的,你想想,你如果有十个数据,你要插入一个新数据放在第五个位置,那么是不是势必要让原来的第五到第十放到第六到第十一的位置上去呢?这样一想就明白了

     *p=x;
     ++L.length;
    return OK;

    将数据存入,增加表长

删除的算法我不想写了,因为我觉得我的插入写的很细致了,插入你能看懂,删除不就是倒过来吗,我觉得大可不必再要求的特别细致要跟我一样,而是说在实现具体问题的时候具体分析,不要让你学过的数据结构限制你的思维,我的理解他只是一种开拓你思维的东西,让你站在巨人的肩膀上,但并不是让你就长成巨人的样子,你要什么样子你就写成什么样子,数据结构给你的是方式和启发,但不是约束。

链表

先讲存储结构

typedef struct LNode{
    ElemType data;
    struct LNode* next;

}LNode,*LinkList;

我先全局的讲一下链表的整体结构再从小处着手讲代码中的我理解的链表
是这样的,链表就是一环套一环完成的一个表,与之相对应的顺序表就是
地址相连逻辑相连的表,那么链表存储的各个节点之间的地址不相关,但是
逻辑上他们有顺序关系,那么每个节点是通过指针相连,这也就是你们从
各大教科书上说的指针域和数值域,如果指针只能指向他的下一个节点
那么我们称其为单链表,如果指针又能指向前一个节点,又能指向后一个
节点,我们就叫他双链表,单双链表,各有各的优势,单链表我觉得就是
逻辑比较简单,而且他只需要一个指针,肯定比两个指针占据的空间小
但是双链表也有他的优势,他能从链表中的任意一个节点遍历到另一个节点
就是增删改查的时候逻辑远远比单链表要复杂。

同时我在提及两个概念,第一个是循环链表,这个很简单,就是头部能不能指向
尾部,尾部能不能指向头部,都能就是循环链表,循环与否看需要,只要逻辑上
加一点小小的改动就能变成循环链表
第二个是空头和非空头,我个人推荐用空头,先解释什么是空头,什么是非空头
空头就是头部节点是空的,我们从第二个节点开始存储数据,非空头就是这个链表
的第一个节点就开始存储数据,接下来说我推荐空头的原因就是,如果我们在增加
和删除的时候对第一个数据下手了,你用非空头一个不小心你就敢把头结点搞没了,整条
链表的指针都找不到了,而空头,无论第一个数据怎么变,都不会影响整条链表是否存在

以上都是我个人经验理解,没有啥参考,哪里有不妥之处大家多多交流,如果错了我会及时
完善更新,这只是我用的时候的经验,我就是觉得空头好用,你别杠,自己多去试试就知道了

//回到代码

//    ElemType data; 这就是要存储的数
//    struct LNode* next;    这就是指向下一个节点的指针,LNode本身就是一个节点,他可以指向
//    下一个跟她同类型的节点就是链表,双链表就是加一个指向上一个节点的指针,看需要,还有就是
//    重复一个老调重弹的问题,相信很多老师都给学生强调各起名字要见名知意,否则你代码可读性
//    特别差,你进了厂,然后假设你某天删库跑路,你同事只能看着你的代码哭吗?不能啊你得让别人
//    看得懂,注释什么的该写就写,咱不差那一会功夫

链表的创建插入:

链表的插入分为两种,一种是头插法,一种是尾插法

先说头插法 因为尾插法如果想要简便的话需要做一点小小的改动会更舒服一点

头插法,我以一个空头单链表为例,在这里为没有看过我之前代码的小伙计们再补充一下什么是空头

空头就是头结点是空的从第二个节点开始存储信息

假设我已经有了一个链表 L,新节点要存储的信息是 ElemType x

k= L->next;
j=(LNode*)malloc(sizeof(LNode));
j->data=x;
L->next=j;
j->next=k;

我这个写的不是很规范,我只是为了像大家展示一下如何进行一次头插,写的简单也是为了大家能看得明白

下面来对代码进行分析

k是原来的链表的第一个非空节点
那么原来的链表我是不是可以简化成L-k呀 
j存储我要插入的节点的数据
头部是L ,L 的next指向了j,是不是就意味着头结点已经连上了这个新节点j呀?
j的next指向了k就是原来链表的第一个非空节点,是不是就把后面也接上了 现在的链表就是 L -j-k,就完成了头插法,在强调一遍,我极力推荐使用空头,不信的你用非空头写一遍头插你看看麻烦不麻烦就完事儿了

接着咱们讲讲尾插,尾插你要对你的数据结构做一点小小的改动,加一个新的结构体专门存储你这个链表的头结点和尾节点和链表长度
typedef struct k{
    LinkList head;
    LinkList tail;
    int listlength;
}k;
定义一个这样的数据结构每当你对链表进行节点的修改的时候你就更新这个结构体(对k类结构体起个名字叫f),当你选择尾插的时候
f->tail->next= 新节点;
新节点->next=NULL;
f->tail=新节点;

当然你也可以选择不这样,而是遍历到末尾再插入,但是你这样时间复杂度太高了。

或者我给你推荐一个更简便的方法就是搞一个双向循环链表 直接从头部的前指针找到尾节点 然后对其进行修改,只要你想做到,没有你做不到的,解决一个问题的方法有太多种。

插入到特殊位置

对于这个问题我其实是不想用代码去解释的 ,我觉得这样解释记不住,
虽然我肯定可以解释,我换一种方式去说
假设 我有一个单链表 1 3 5 7 9 我想插入6到第四个位置,对于单链表而言,
我想插入6是不是我第一步要找到5 和7 用变量把他们的位置存下来
 然后让6节点的next指向7 5节点的next指向6,
至于代码你想达到这个效果你就按着这个效果来,肯定能实现,
我就不写了,没什么太多的意义

删除某位置节点

这部分我也不上代码了直接假设一个例子我觉得大家都能看明白

一个单链表 有p-q-r 三个节点 想删除q,就得同时保存q的前一个节点p

令p->next=q->next  这样p和r就连在一起了,双链表就是要处理前指针,

但优点是不用存储待删除节点的前一个节点

对于单链表而言,删除的时候使用空头会 更加轻松,对于直接看到这部分的

小伙伴我再解释一遍什么是空头, 空头 就是第一个节点不存数据,永远存在,

第二个节点得存数据,当你删除第一个带数据的节点的时候

你如果有一个空头是不是就方便了,你如果使用非空头我也给你解决办法,就是

令头结点被q先存下来 然后head = head->next 最后再把原来的q释放,这个的

缺点在于原来链表的第一个节点的地址变了,处理不当一个马虎可能这个链表都没了

这就是我为什么喜欢用空头的原因之一,要看其他原因请看其他部分

对顺序表的总结

萌新学线性表可以去写一个很经典的案例学生成绩管系统,网上有很多现成的例子

你会充分的理解链表它指针操作是很容易出错,非常练习debug调试的能力,不会

debug从网上找找自学,实在不行你想偷懒耍滑也可以,你单独加一个printf语句

看看到底变没变 或者打印个整数,看看循环跑的次数,只要你细心解决办法总比问题多

下面讲讲顺序表和线性表的区别,我就是个粗人,也不太会说那些书上文绉绉的话,

直接点,你那个顺序表一般数组是不是直接根据位置可以找啊,数组名[顺序-1] 但是

链表得一个节点一个节点遍历 如果你经常需要查找 尽量用顺序表 如果你经常删除或者插入

你那个顺序表不是经常要前移好多数据 或者后移好多数据 吗?是不是很麻烦 ,而你那个链表

你只需要动动指针 就解决了问题

自我总结

线性表算比较基础的,也是我比较娴熟的,所以讲的比较自信,有些话我能

说的比较通俗,但随着难度加大,我也是比较费劲,我尽量一直保持现在的

习惯,很多时候我也是一瓶子不满,半瓶子晃荡,正好借着写博客顺便巩固

扎实一下自己的数据结构能力,有问题的可以关注我在csdn向我提问,我有

时间都会回答,同时也可以加我qq 305211942 联系比较方便。

第二章 栈和队列

先说栈,根据我的习惯,先上结构体

#define STACK_INIT_SIZE 100
#define STACKINCERMENT 10
typedef struct{
    SElemType* base;
    SElemType* top;
    int stacksize;        
}

先把代码讲一下 ,再大致的讲一下我看的栈
STACK_INIT_SIZE 这个是初始化的栈的长度
STACKINCERMENT 这个是如果对栈扩容一次的增量

    SElemType* base;
    SElemType* top;
    int stacksize; 
对于没有基础的人还是要说一遍这个SElemType 是你需要存储的数据的类型,你要存整形就是整形,你要存结构体就是结构体类型 base 这个指针 行话叫栈底 top 叫栈顶 ,stacksize是栈的长度

下面讲我自己的理解,栈其实就是一个顺序表,只不过他的特别之处在与他只能从同一端输入,同一端输出。空栈时,栈底和栈顶是一样的,存入一个数据到栈顶,然后栈顶上移,提取数据也只能从栈顶提。我举个常用的例子,比如浏览器的后退是不是按一次只能返回上一次的网页,这就是他的特点,同时再补充一点:就是栈顶永远指向存数据的下一个位置,它不是指向数据本身。大多数人称栈为后入先出,或者先入后出,其实都一样,只需要知道他只能从一端进行插入和删除

下面讲栈的基本操作

创建栈我就不写了,建个顺序表 栈顶等于栈底Stacksize就是栈的最大长度
栈的最基本操作有两个 
第一个:压栈,也叫入栈

//功能:入栈,将数据放入栈中
//参数:栈S 待存入数据e
//返回值:成功返回OK 失败返回ERROR

int Push(Stack &S,SElemType e)
{
    if(S.top-S.base>=S.StackSize)
    {
        //这里使用realloc扩容,同时对 malloc realloc calloc有充分理解
    }

    *S.top++=e;

    return OK;

}
其实核心只有一句话就是*S.top++=e 这就是我之前强调过的一句,就是栈顶指针永远指向当前存储数据的下一个
把e这个数据放在S.top上 然后++ 后移指针

    这段是为了判断栈是否满了,满了就扩容,但有一点是可以小小的改一下,就是那个等于号
    因为栈只能通过压栈操作,所以一但满了就扩容就不会存在大于的情况了,我不能李姐呀??
    但是前人都是这么写的,肯定有他的道理,也许是防止创建一个栈的时候指针给错了也不一定呢?
    if(S.top-S.base>=S.StackSize)
    {
        //这里使用realloc扩容,同时对 malloc realloc calloc有充分理解
    }

大多数管这个叫出栈 也叫弹栈

//功能:出栈,删除栈顶数据
//参数:栈S 用e返回被删除数据
//返回值:成功返回OK 失败返回ERROR
int Pop(SqStack &S,SElemType &e)
{
    if(S.top-S.base==0)        //判空 如果空栈就不能删除   
    {
        return ERROR;
    }

    *S.top--;                //删除后,栈顶指针top--,虽然数据还在那,
                            //但是栈顶无了,我们就找不到了,认为它是删除了
    return OK;
}
栈的应用

进制转换
    这个的思路是这样的,举个例子比如十进制转八进制 除8取余 ,余数入栈 直到为0为止 ,然后再
    不断弹栈直到栈为空,就实现了进制转换。

表达式求值
    是这样的计算机它本身没有智力,他很难做评价应该先做那个运算再做那个运算,我们将表达式分为三种,前缀,中缀,和后缀,我们人类易于理解的是中缀表达式,我写过的是后缀表达式的,一般你买到的数据结构辅导书都有这个练习题,或者从网上找也能找到,她会给你一个运算符优先级表,然后写出解决问题的算法,我大致的跟你们解释一下,毕竟我从上次写这个到现在为止快一年了,具体细节还需要自己去试,算法:准备两个栈 一个是运算符栈 一个是运算数栈

按顺序是运算符就放运算符里,是运算数 就放在运算数里,然后根据运算符优先级表 分大于等于小于三种情况,
依次做不同处理


迷宫
    迷宫是这样的,他的核心思想就是穷举遇到问题然后回溯,这个问题我曾经很认真的写过,因为马虎除了好多错,所以印象比较深刻,就详细的讲讲,我是自己在纸上画了一个20*20的迷宫然后用数组来表示 凡是墙体都是-1 凡是能通过的都是5 给一个出口一个入门 有四个方向,我站在某一个位置,我要向下一个位置走的方向就是我在这个位置的赋值是1到4 ,我一旦赋值我就将其入栈,如果这个位置1到4四个方向我都走过了了就回溯到上一个节点,就是退栈。这个结束的条件要么是找到出口了 结束把栈返回 ,要么是栈空了,说明找不到出口,结束。现在回过头来总结一下感觉也不难,当初其实也是摸着石头过河,东一个错,西一个错,思路也不清晰,所以第一次写起来比较费劲,

四皇后八皇后
    这个大家自己去看,对于初学数据结构的人来说肯定是有一点难度,如果能看懂最好,看不懂也别急,后面学的算法你们会学回溯法,所以不着急。但我还是大致讲一讲,因为我已经学过算法了,现在看八皇后问题感觉也不难,大家认真听也能听懂,先说什么是八皇后问题,国际象棋皇后按米字走棋,在8*8的方格里怎么安排八个皇后 且他们不会互相攻击,定义一个数组长度为8,数组中元素的位置表示某一列 数组中元素的结果代表某一行,然后穷举,如果发生了攻击就回溯,直到找到结果为止

队列

啥也不说,先上结构体

#define MAXQSIZE 100
typedef struct{
    QElemType *base;
    int front;
    int rear;
}SqQueue

MAXQSIZE 是队列的最大长度
QElemType 是待存储的数据类型
front 和 rear 相当于 头指针和 尾指针

下面开始讲我自己看的队列,队列就是尾进头出的一个顺序表,也没什么稀奇
因为是顺序表吗 所以我用整形就可以base[rear] base[front]了

至于链队列无非也就是用指针代替顺序表数组而已,还有就是循环队列,这个稍微

有点意思,他利用对头和尾后移然后对队列的最大长度取余来减少空间的浪费,

因为在普通队列的情况下,你头部出队列了 是不是就后移了,尾部入队了也就后移了,

那从开始到头部的地方不就浪费了 ,但是如果你取对队列最大长度求余数后,

就可以重复利用之前被删除的空间,还有一个稍显复杂的问题,其实我看来挺简单,

但是用的不多不过你得知道该用的时候得会用,叫做双端队列,

这个无非就是循环队列,只不过两端开口,但是有些

逻辑看上去就很复杂 我直接讲一个我看来的简单逻辑,你就设一个头一个尾,然后如果

你需要换另一端存储你就把头尾互换,还用之前的入队出队函数就OK。现在你听我讲的

可能比较模糊但是你等我讲完了你再回头看我现在说的这段话,你就会觉得比较容易理解

下面讲队列的基本操作

入队

因为循环队列节省空间,所以普通队列我就不讲了,没什么意思。

入队

//功能:把数据存入队列
//参数:队列Q 待存入数据e
//返回值:成功返回OK 失败返回ERROR

int EnQueue(SeQueue &Q,SElemType e)
{
    if((Q.rear+1)%MAXQSIZE==Q.front)
    {
        return ERROR;
    }

    Q.base[rear]=e;

    Q.rear=(Q.rear+1)%MAXQSIZE;

    return OK;
}

第一部分这个条件语句是判断循环队列是否是满的,至于这么判断的原因:

我在创建队列的时候rear= front 头尾是一样的, 当我这个头部 出队

尾部入队,尾部会不断接近头部,那么什么时候我认为队列满呢?因为

不能和空队列起冲突,所以我牺牲一个空间,我认为尾+1 对总长取余得到的

结果如果等于头就是满的 所以我的判断语句是这样写的

Q.rear=(Q.rear+1)%MAXQSIZE;这句话就是循环队列的条件下尾指针该去的位置

至于为什么他能循环,如果实在不理解你画个队列,你看看你的尾部到了队列最大空间以后

继续插入取余是不是就放到了队列位置的最前端,自己画一下啥都懂了
出队

//功能:将队头数据删除
//参数:队列Q e返回被删除数据
//返回值:成功返回OK 失败返回ERROR

int DeQueue(SqQueue &Q ,QElemType &e)
{
    if(Q.front==Q.rear)
    {
        return ERROR;
    }
    
   e=Q.base[front];
   
   Q.front=(Q.front+1)%MAXQSIZE;

    return OK; 

}

第一个if语句是判断队列是否为空队列,如果是空队列没法删除

如果不是 用e返回被删除的数据 并让前指针后移,删除结束



求队列的元素个数 

(Q.ront-Q.front+MAXQSIZE)%MAXQSIZE 就是元素个数

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

天天开心7788665544

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

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

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

打赏作者

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

抵扣说明:

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

余额充值