数据结构基础详解

基本概念

  1. 数据是描述客观事物的数值、字符以及能输入机器且能被处理的各种符号集合。数据的含义非常广泛,除了通常的数值数据、字符、字符串是数据以外,声音、图像等一切可以输入计算机并能被处理的都是数据。例如除了表示人的姓名、身高、体重等的字符、数字是数据,人的照片、指纹、三维模型、语音指令等也都是数据。
  2. 数据元素是数据的基本单位,是数据集合的个体,在计算机程序中通常作为一个整体来进行处理。例如一条描述一位学生的完整信息的数据记录就是一个数据元素;空间中一点的三维坐标也可以是一个数据元素。数据元素通常由若干个数据项组成,例如描述学生相关信息的姓名、性别、学号等都是数据项;三维坐标中的每一维坐标值也是数据项。数据项具有原子性,是不可分割的最小单位。
  3. 数据对象是性质相同的数据元素的集合,是数据的子集。例如一个学校的所有学生的集合就是数据对象,空间中所有点的集合也是数据对象。
  4. 数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。是组织并存储数据以便能够有效使用的一种专门格式,它用来反映一个数据的内部构成,即一个数据由那些成分数据构成,以什么方式构成,呈什么结构。
    由于信息可以存在于逻辑思维领域,也可以存在于计算机世界,因此作为信息载体的数据同样存在于两个世界中。表示一组数据元素及其相互关系的数据结构同样也有两种不同的表现形式,一种是数据结构的逻辑层面,即数据的逻辑结构;一种是存在于计算机世界的物理层面,即数据的存储结构
    数据的逻辑结构按照数据元素之间相互关系的特性来分,可以分为以下四种结构:集合、线性结构、树形结构和图状结构。这里讨论的数据结构主要有线性表、栈、队列、树,其中线性表、栈、队列属于线性结构,树和图属于非线性结构。
    数据的存储结构主要包括数据元素本身的存储以及数据元素之间关系表示。通过数据元素的定义可以看出,我们可以很容易的使用 Java 中的一个类来实现它,数据元素的数据项就是类的成员变量。
    数据元素之间的关系在计算机中主要有两种不同的表示方法:顺序映像非顺序映像,并由此得到两种不同的存储结构:顺序存储结构和链式存储结构。顺序存储结构的特点是:数据元素的存储对应于一块连续的存储空间,数据元素之间的前驱和后续关系通过数据元素在存储器中的相对位置来反映。链式存储结构的特点是:数据元素的存储对应的是不连续的存储空间,每个存储节点对应一个需要存储的数据元素。元素之间的逻辑关系通过存储节点之间的链接关系反映出来。
    由于我们是在 Java 这种计算机高级程序设计语言的基础上来讨论数据结构,因此,我们在讨论数据的存储结构时不会在真正的物理地址的基础上去讨论顺序存储和链式存储,而是在 Java 语言提供的一维数组以及对象的引用的基础上去讨论和实现数据的存储结构。

线性表

  1. 线性表定义
    线性表(linear list)是n个类型相同数据元素的有限序列,通常记作(a0, a1, …ai-1, ai, ai+1 …, an-1 )。
    在这里特别需要注意的是线性表和数组的区别。从概念上来看,线性表是一种抽象数据类型;数组是一种具体的数据结构。线性表与数组的逻辑结构是不一样的,线性表是元素之间具有1对1的线性关系的数据元素的集合,而数组是一组数据元素到数组下标的一一映射。并且从物理性质来看,数组中相邻的元素是连续地存储在内存中的;线性表只是一个抽象的数学结构,并不具有具体的物理形式,线性表需要通过其它有具体物理形式的数据结构来实现。在线性表的具体实现中,表中相邻的元素不一定存储在连续的内存空间中,除非表是用数组来实现的。对于数组,可以利用其下标在一个操作内随机存取任意位置上的元素;对于线性表,只能根据当前元素找到其前驱或后继,因此要存取序号为i的元素,一般不能在一个操作内实现,除非表是用数组实现的。
    线性表是一种非常灵活的数据结构,线性表可以完成对表中数据元素的访问、添加、删除等操作,表的长度也可以随着数据元素的添加和删除而变化。
  2. 线性表的顺序存储与实现
    线性表的顺序存储是用一组地址连续的存储单元依次存储线性表的数据元素。假设线性表的每个数据元素需占用K个存储单元,并以元素所占的第一个存储单元的地址作为数据元素的存储地址。则线性表中序号为i的数据元素的存储地址LOC(ai)与序号为i+1 的数据元素的存储地址LOC(ai+1)之间的关系为
    LOC(ai+1) = LOC(ai) + K
    通常来说,线性表的i号元素ai的存储地址为
    LOC(ai) = LOC(a0) + i×K
    其中LOC(a0)为 0 号元素a0的存储地址,通常称为线性表的起始地址。
    线性表的这种机内表示称作线性表的顺序存储。它的特点是,以数据元素在机内存储地址相邻来表示线性表中数据元素之间的逻辑关系。每一个数据元素的存储地址都和线性表的起始地址相差一个与数据元素在线性表中的序号成正比的常数。由此,只要确定了线性表的起始地址,线性表中的任何一个数据元素都可以随机存取,因此线性表的顺序存储结构是一种随机的存储结构。
    在这里插入图片描述图1
    由于高级语言中的数组具也有随机存储的特性,因此在抽象数据类型的实现中都是使用数组来描述数据结构的顺序存储结构。通过图1,我们看到线性表中的数据元素在依次存放到数组中的时候,线性表中序号为 i 的数据元素对应的数组下标也为 i,即数据元素在线性表中的序号与数据元素在数组中的下标相同。
    在这里需要注意的是,如果线性表中的数据元素是对象时,数组存放的是对象的引用,即线性表中所有数据元素的对象引用是存放在一组连续的地址空间中。
    在这里插入图片描述
    图2
    由于线性表的长度可变,不同的问题所需的最大长度不同,那么在线性表的具体实现中我们是使用动态扩展数组大小的方式来完成线性表长度的不同要求的。
    代码 线性表的数组实现
public class ListArray implements List { 
	 private final int LEN = 8; //数组的默认大小
	 private Strategy strategy; //数据元素比较策略
	 private int size; //线性表中数据元素的个数
	 private Object[] elements; //数据元素数组
	 //构造方法
	 public ListArray () { 
		 this(new DefaultStrategy()); 
	 } 
	 public ListArray (Strategy strategy){ 
		 this.strategy = strategy; 
		 size = 0; 
		 elements = new Object[LEN]; 
	 } 
 
	 //返回线性表的大小,即数据元素的个数。
	 public int getSize() { 
		 return size; 
	 } 
	 //如果线性表为空返回 true,否则返回 false。
	 public boolean isEmpty() { 
	 	return size==0; 
	 } 
	 //判断线性表是否包含数据元素 e 
	 public boolean contains(Object e) { 
		for (int i=0; i<size; i++) 
		 	if (strategy.equal(e,elements[i])) return true; 
	 	return false; 
	 } 
	 //返回数据元素 e 在线性表中的序号
	 public int indexOf(Object e) { 
		 for (int i=0; i<size; i++) 
			if (strategy.equal(e,elements[i])) return i; 
		 return -1; 
	 } 
	 //将数据元素 e 插入到线性表中 i 号位置
	 public void insert(int i, Object e) throws OutOfBoundaryException { 
		 if (i<0||i>size) 
			 throw new OutOfBoundaryException("错误,指定的插入序号越界。"); 
		 if (size >= elements.length) 
		 	expandSpace(); 
		 for (int j=size; j>i; j--) 
		 	elements[j] = elements[j-1]; 
		 elements[i] = e; size++; 
		 return; 
	 } 
	 
	 private void expandSpace(){ 
		 Object[] a = new Object[elements.length*2]; 
		 for (int i=0; i<elements.length; i++) 
		 	a[i] = elements[i]; 
		 elements = a; 
	 } 
	 //将数据元素 e 插入到元素 obj 之前
	 public boolean insertBefore(Object obj, Object e) { 
		 int i = indexOf(obj); 
		 if (i<0) return false; 
		 insert(i,e); 
		 return true; 
	 } 
	 //将数据元素 e 插入到元素 obj 之后
	 public boolean insertAfter(Object obj, Object e) { 
		 int i = indexOf(obj); 
		 if (i<0) return false; 
		 insert(i+1,e); 
		 return true; 
	 } 
	 //删除线性表中序号为 i 的元素,并返回之
	 public Object remove(int i) throws OutOfBoundaryException { 
		 if (i<0||i>=size) 
		 	throw new OutOfBoundaryException("错误,指定的删除序号越界。"); 
		 Object obj = elements[i]; 
		 for (int j=i; j<size-1; j++) 
		 	elements[j] = elements[j+1]; 
		 elements[--size] = null; 
		 return obj; 
	 } 
	 //删除线性表中第一个与 e 相同的元素
	 public boolean remove(Object e) { 
		 int i = indexOf(e); 
		 if (i<0) return false; 
		 remove(i); 
		 return true; 
	 } 
	 //替换线性表中序号为 i 的数据元素为 e,返回原数据元素
	 public Object replace(int i, Object e) throws OutOfBoundaryException { 
		 if (i<0||i>=size) 
		 	throw new OutOfBoundaryException("错误,指定的序号越界。"); 
		 Object obj = elements[i]; 
		 elements[i] = e; 
		 return obj; 
	 } 
	 //返回线性表中序号为 i 的数据元素
	 public Object get(int i) throws OutOfBoundaryException { 
		 if (i<0||i>=size) 
			 throw new OutOfBoundaryException("错误,指定的序号越界。"); 
		 return elements[i]; 
	 } 
}
  1. 线性表的链式存储与实现
    实现线性表的另一种方法是链式存储,即用指针将存储线性表中数据元素的那些单元依次串联在一起。这种方法避免了在数组中用连续的单元存储元素的缺点,因而在执行插入或删除运算时,不再需要移动元素来腾出空间或填补空缺。然而我们为此付出的代价是,需要在每个单元中设置指针来表示表中元素之间的逻辑关系,因而增加了额外的存储空间的开销。
    3.1 单链表
    链表是一系列的存储数据元素的单元通过指针串接起来形成的,因此每个单元至少有两个域,一个域用于数据元素的存储,另一个域是指向其他单元的指针。这里具有一个数据域和多个指针域的存储单元通常称为结点(node)。
    在这里插入图片描述
    图3
代码  单链表结点定义
public class SLNode implements Node { 
	 private Object element; 
	 private SLNode next; 
	 
	 public SLNode() { 
	 	this(null,null); 
	 } 
	 public SLNode(Object ele, SLNode next){ 
		 this.element = ele; 
		 this.next = next; 
	 } 
	 public SLNode getNext(){ 
	 	 return next; 
	 } 
	 public void setNext(SLNode next){ 
		 this.next = next; 
	 } 
	 /**************** Methods of Node Interface **************/ 
	 public Object getData() { 
	 	return element; 
	 } 
	 public void setData(Object obj) { 
	 	element = obj; 
	 } 
} 

单链表是通过上述定义的结点使用 next 域依次串联在一起而形成的。
在这里插入图片描述
图4
链表的第一个结点和最后一个结点,分别称为链表的首结点和尾结点。尾结点的特征是其 next 引用为空(null)。链表中每个结点的 next 引用都相当于一个指针,指向另一个结点,借助这些 next 引用,我们可以从链表的首结点移动到尾结点。如此定义的结点称为单链表(single linked list)。在单链表中通常使用 head 引用来指向链表的首结点,由 head 引用可以完成对整个链表中所有节点的访问。有时也可以根据需要使用指向尾结点的 tail 引用来方便某些操作的实现。
在单链表结构中还需要注意的一点是,由于每个结点的数据域都是一个 Object 类的对象,因此,每个数据元素并非真正如图 3-4 中那样,而是在结点中的数据域通过一个 Object类的对象引用来指向数据元素的。
与数组类似,单链表中的结点也具有一个线性次序,即如果结点 P 的 next 引用指向结点 S,则 P 就是 S 的直接前驱,S 是 P 的直接后续。单链表的一个重要特性就是只能通过前驱结点找到后续结点,而无法从后续结点找到前驱结点。在单链表中通常需要完成数据元素的查找、插入、删除等操作。下面我们逐一讨论这些操作的实现。
在单链表中进行查找操作,只能从链表的首结点开始,通过每个结点的 next 引用来一次访问链表中的每个结点以完成相应的查找操作。例如需要在单链表中查找是否包含某个数据元素 e,则方法是使用一个循环变量 p,起始时从单链表的头结点开始,每次循环判断 p所指结点的数据域是否和 e 相同,如果相同则可以返回 true,否则继续循环直到链表中所有结点均被访问,此时 p 为 null。该过程如图5所示。
在这里插入图片描述
图5
在单链表中数据元素的插入,是通过在链表中插入数据元素所属的结点来完成的。对于链表的不同位置,插入的过程会有细微的差别。图 6(a)、6(b)、6(c)分别说明了在单链表的表头、表尾以及链表中间插入结点的过程。
在这里插入图片描述
图6
类似的,在单链表中数据元素的删除也是通过结点的删除来完成的。在链表的不同位置删除结点,其操作过程也会有一些差别。图 7(a)、7(b)、7(c)分别说明了在单链表的表头、表尾以及链表中间删除结点的过程。
在这里插入图片描述
在这里插入图片描述
图7
3.2 双向链表
单链表的一个优点是结构简单,但是它也有一个缺点,即在单链表中只能通过一个结点的引用访问其后续结点,而无法直接访问其前驱结点,要在单链表中找到某个结点的前驱结点,必须从链表的首结点出发依次向后寻找,但是需要Ο(n)时间。为此我们可以扩展单链表的结点结构,使得通过一个结点的引用,不但能够访问其后续结点,也可以方便的访问其前驱结点。扩展单链表结点结构的方法是,在单链表结点结构中新增加一个域,该域用于指向结点的直接前驱结点。扩展后的结点结构是构成双向链表的结点结构,如图 8 所示。
在这里插入图片描述
图8

代码 双向链表结点定义
public class DLNode implements Node { 
  private Object element; 
  private DLNode pre; 
  private DLNode next; 
  public DLNode() { 
 	 this(null,null,null); 
  } 
  public DLNode(Object ele, DLNode pre, DLNode next){ 
 	 this.element = ele; 
 	 this.pre = pre; 
 	 this.next = next; 
  } 
  public DLNode getNext(){ 
  	return next; 
  } 
  public void setNext(DLNode next){ 
  t	his.next = next; 
  } 
  public DLNode getPre(){ 
  	return pre; 
  } 
  public void setPre(DLNode pre){ 
 	 this.pre = pre; 
  } 
  /****************Node Interface Method**************/ 
  public Object getData() { 
  	return element; 
  } 
  public void setData(Object obj) { 
  	element = obj; 
  } 
} 

单链表的插入操作,除了首结点之外必须在某个已知结点后面进行,而在双向链表中插入操作在一个已知的结点之前或之后都可以进行。例如在某个结点 p 之前插入一个新结点的过程如图 9所示。
在这里插入图片描述
图9
单链表的删除操作,除了首结点之外必须在知道待删结点的前驱结点的基础上才能进行,而在双向链表中在已知某个结点引用的前提下,可以完成该结点自身的删除。图 10表示了删除 p 的过程。
在这里插入图片描述
图10
3.3 线性表的单链表实现

代码  线性表的单链表实现
public class ListSLinked implements List { 
    private Strategy strategy; //数据元素比较策略
    private SLNode head; //单链表首结点引用
    private int size; //线性表中数据元素的个数
    
    public ListSLinked () { 
   	 this(new DefaultStrategy()); 
    } 
    public ListSLinked (Strategy strategy) { 
   	 this.strategy = strategy; 
   	 head = new SLNode(); 
   	 size = 0; 
    } 
    
    //辅助方法:获取数据元素 e 所在结点的前驱结点
    private SLNode getPreNode(Object e){ 
   	 SLNode p = head; 
   	 while (p.getNext()!=null) 
   	 if (strategy.equal(p.getNext().getData(),e)) return p; 
   	 else p = p.getNext(); 
   	 return null; 
    } 
    //辅助方法:获取序号为 0<=i<size 的元素所在结点的前驱结点
    private SLNode getPreNode(int i){ 
   	 SLNode p = head; 
   	 for (; i>0; i--) p = p.getNext(); 
   		 return p; 
    } 
    //获取序号为 0<=i<size 的元素所在结点
    private SLNode getNode(int i){ 
   	 SLNode p = head.getNext(); 
   	 for (; i>0; i--) p = p.getNext(); 
   	 	return p; 
    } 
    //返回线性表的大小,即数据元素的个数。
    public int getSize() { 
    	return size; 
    } 
    //如果线性表为空返回 true,否则返回 false。
    public boolean isEmpty() { 
    	return size==0; 
    } 
    //判断线性表是否包含数据元素 e 
    public boolean contains(Object e) { 
   	 SLNode p = head.getNext(); 
   	 while (p!=null) 
   		 if (strategy.equal(p.getData(),e)) return true; 
   	 	else p = p.getNext(); 
   	 return false; 
    } 
    //返回数据元素 e 在线性表中的序号
    public int indexOf(Object e) { 
   	 SLNode p = head.getNext(); 
   	 int index = 0; 
   	 while (p!=null) 
   		 if (strategy.equal(p.getData(),e)) return index; 
   		 else {index++; p = p.getNext();} 
   	 return -1; 
    } 
    //将数据元素 e 插入到线性表中 i 号位置
    public void insert(int i, Object e) throws OutOfBoundaryException { 
   	 if (i<0||i>size) 
   	 	throw new OutOfBoundaryException("错误,指定的插入序号越界。"); 
   	 SLNode p = getPreNode(i); 
   	 SLNode q = new SLNode(e,p.getNext()); 
   	 p.setNext(q); 
   	 size++; 
   	 return; 
    } 
    //将数据元素 e 插入到元素 obj 之前
    public boolean insertBefore(Object obj, Object e) { 
   	 SLNode p = getPreNode(obj); 
   	 if (p!=null){ 
   	 	SLNode q = new SLNode(e,p.getNext()); 
   		 p.setNext(q); 
   		 size++; 
   		 return true; 
   	 } 
   	 return false; 
    } 
    //将数据元素 e 插入到元素 obj 之后
    public boolean insertAfter(Object obj, Object e) { 
   	 SLNode p = head.getNext(); 
   	 while (p!=null) 
   		 if (strategy.equal(p.getData(),obj)){ 
   			 SLNode q = new SLNode(e,p.getNext()); 
   			 p.setNext(q); 
   			 size++; 
   			 return true; 
   		 } 
   		 else p = p.getNext(); 
   	return false; 
    } 
    //删除线性表中序号为 i 的元素,并返回之
    public Object remove(int i) throws OutOfBoundaryException { 
   	 if (i<0||i>=size) 
   	 	throw new OutOfBoundaryException("错误,指定的删除序号越界。"); 
   	 SLNode p = getPreNode(i); 
   	 Object obj = p.getNext().getData(); 
   	 p.setNext(p.getNext().getNext()); 
   	 size--; 
   	 return obj; 
    } 
    //删除线性表中第一个与 e 相同的元素
    public boolean remove(Object e) { 
   	 SLNode p = getPreNode(e); 
   	 if (p!=null){ 
   		 p.setNext(p.getNext().getNext()); 
   		 size--; 
   		 return true; 
   	 } 
   	 return false; 
    } 
    //替换线性表中序号为 i 的数据元素为 e,返回原数据元素
    public Object replace(int i, Object e) throws OutOfBoundaryException { 
   	 if (i<0||i>=size) 
   	 	throw new OutOfBoundaryException("错误,指定的序号越界。"); 
   	 SLNode p = getNode(i); 
   	 Object obj = p.getData(); 
   	 p.setData(e); 
   	 return obj; 
    } 
    //返回线性表中序号为 i 的数据元素
    public Object get(int i) throws OutOfBoundaryException { 
   	 if (i<0||i>=size) 
   	 throw new OutOfBoundaryException("错误,指定的序号越界。"); 
   	 SLNode p = getNode(i); 
   	 return p.getData(); 
    } 
} 

TO BE CONTINUE…

重要意义   一般认为,一个数据结构是由数据元素依据某种逻辑联系组织起来的。对数据元素间逻辑关系的描述称为数据的逻辑结构;数据必须在计算机内存储,数据的存储结构是数据结构的实现形式,是其在计算机内的表示;此外讨论一个数据结构必须同时讨论在该类数据上执行的运算才有意义。   在许多类型的程序的设计中,数据结构的选择是一个基本的设计考虑因素。许多大型系统的构造经验表明,系统实现的困难程度和系统构造的质量都严重的依赖于是否选择了最优的数据结构。许多时候,确定了数据结构后,算法就容易得到了。有些时候事情也会反过来,我们根据特定算法来选择数据结构与之适应。不论哪种情况,选择合适的数据结构都是非常重要的。   选择了数据结构,算法也随之确定,是数据而不是算法是系统构造的关键因素。这种洞见导致了许多种软件设计方法和程序设计语言的出现,面向对象的程序设计语言就是其中之一。 编辑本段研究内容   在计算机科学中,数据结构是一门研究非数值计算的程序设计问题中计算机的操作对象(数据元素)以及它们之间的关系和运算等的学科,而且确保经过这些运算后所得到的新结构仍然是原来的结构类型。   “数据结构”作为一门独立的课程在国外是从1968年才开始设立的。 1968年美国唐·欧·克努特教授开创了数据结构的最初体系,他所著的《计算机程序设计技巧》第一卷《基本算法》是第一本较系统地阐述数据的逻辑结构和存储结构及其操作的著作。“数据结构”在计算机科学中是一门综合性的专业基础课。数据结构是介于数学、计算机硬件和计算机软件三者之间的一门核心课程。数据结构这一门课的内容不仅是一般程序设计(特别是非数值性程序设计)的基础,而且是设计和实现编译程序、操作系统、数据库系统及其他系统程序的重要基础。   计算机是一门研究用计算机进行信息表示和处理的科学。这里面涉及到两个问题:信息的表示,信息的处理 。   而信息的表示和组织又直接关系到处理信息的程序的效率。随着计算机的普及,信息量的增加,信息范围的拓宽,使许多系统程序和应用程序的规模很大,结构又相当复杂。因此,为了编写出一个“好”的程序,必须分析待处理的对象的特征及各对象之间存在的关系,这就是数据结构这门课所要研究的问题。众所周知,计算机的程序是对信息进行加工处理。在大多数情况下,这些信息并不是没有组织,信息(数据)之间往往具有重要的结构关系,这就是数据结构的内容。数据的结构,直接影响算法的选择和效率。   计算机解决一个具体问题时,大致需要经过下列几个步骤:首先要从具体问题中抽象出一个适当的数学模型,然后设计一个解此数学模型的算法(Algorithm),最后编出程序、进行测试、调整直至得到最终解答。寻求数学模型的实质是分析问题,从中提取操作的对象,并找出这些操作对象之间含有的关系,然后用数学的语言加以描述。计算机算法与数据的结构密切相关,算法无不依附于具体的数据结构数据结构直接关系到算法的选择和效率。运算是由计算机来完成,这就要设计相应的插入、删除和修改的算法 。也就是说,数据结构还需要给出每种结构类型所定义的各种运算的算法。   数据是对客观事物的符号表示,在计算机科学中是指所有能输入到计算机中并由计算机程序处理的符号的总称。   数据元素是数据的基本单位,在计算机程序中通常作为一个整体考虑。一个数据元素由若干个数据项组成。数据项是数据的不可分割的最小单位。有两类数据元素:一类是不可分割的原子型数据元素,如:整数"5",字符 "N" 等;另一类是由多个款项构成的数据元素,其中每个款项被称为一个数据项。例如描述一个学生的信息的数据元素可由下列6个数据项组成。其中的出生日期又可以由三个数据项:"年"、"月"和"日"组成,则称"出生日期"为组合项,而其它不可分割的数据项为原子项。   关键字指的是能识别一个或多个数据元素的数据项。若能起唯一识别作用,则称之为 "主" 关键字,否则称之为 "次" 关键字。   数据对象是性质相同的数据元素的集合,是数据的一个子集。数据对象可以是有限的,也可以是无限的。   数据处理是指对数据进行查找、插入、删除、合并、排序、统计以及简单计算等的操作过程。在早期,计算机主要用于科学和工程计算,进入八十年代以后,计算机主要用于数据处理。据有关统计资料表明,现在计算机用于数据处理的时间比例达到80%以上,随着时间的推移和计算机应用的进一步普及,计算机用于数据处理的时间比例必将进一步增大。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值