BPlusTree(B+树)的一种实现---转

来自:http://hi.baidu.com/lovelink/item/fd60de0d1c4ca1cf905718aa

以下是C++源代码,拷贝保存为cpp文件即可编译。
该代码实现了B+树的索引文件,可以用于数据库中的稀疏索引和密集索引,具体请看注释。
由于水平所限,本人仅保证其能正常运行,不保证执行效率及合理性。
仅供参考,All Rights Reserved------------------------------------------------------------------------------------------
#include <iostream>
#include <fstream>
#include <string>
#include <windows.h>using namespace std;// 定义B+树的阶数
#define M 4// B+树结点定义
struct BPlusNode
{
int amount;       // 该结点中已保存的键值个数
long key[M];      // 键值数组
long children[M]; // 子结点位置数组
long father;      // 父结点位置
long left;        // 同一层中的左结点位置
long right;       // 同一层中的右结点位置
bool isactive;    // 有效位
bool isleave;     // 标志该结点是否叶结点
};// 函数头定义
bool findkey (long key, long &position);   // 查找包含给定键值的结点位置
bool insertkey (long key, long position); // 插入一个新键值
bool deletekey (long key, long position); // 删除给定的键值
void printkey (long mode);                 // 按照给定模式输出B+树// 全局变量
static long pre;             // 上一个被访问的结点
static long cur;             // 当前指向的结点
static long root;            // 根结点位置
static long smallest;        // 保存最小关键值的叶结点
static long nodenewposition; // 新插入的结点位置// 主函数
void main()
{
string command;
long keynumber;
BPlusNode node;
long position = -1;
int tick1,tick2;
fstream iofile;// 检测索引文件是否存在,若不存在则创建一个初始化的索引文件,其中包含根结点位置,最小键值位置和一个空结点
fstream infile ("BPlusTreeData.dat", ios::binary|ios::in);
if (!infile)
{
   node.amount = 1;
   for (int i = 0; i < M; ++i)
   {
    node.key[i] = 0;
    node.children[i] = 0;
   }
   node.father = 0;
   node.left = 0;
   node.right = 0;
   node.isactive = true;
   node.isleave = true;
   root = 8;
   smallest = 8;
   fstream outfile ("BPlusTreeData.dat", ios::binary|ios::out);
   outfile.seekp(0, ios::beg);
   outfile.write((char *)&root, sizeof(long));
   outfile.write((char *)&smallest, sizeof(long));
   outfile.write((char *)&node, sizeof(BPlusNode));
   outfile.close();
}
infile.close();// 循环获取命令行,执行给定的命令
while (true)
{
   cin >> command >> keynumber;   // 插入一个新的键值,该值不能与已有关键值重复
   if (command == "insert")
   {
    tick1 = GetTickCount();
    if (findkey(keynumber, position))
    {
     cout << keynumber << " is already in this B+ tree" << endl;
    }
    else if (insertkey(keynumber, position))
    {
     tick2 = GetTickCount();
     cout << "Successful inserted in " << tick2 - tick1 << " millisecond" << endl;
    }
    else cout << "Action falled" << endl;
   }   // 删除给定的键值
   else if (command == "delete")
   {
    if (deletekey(keynumber, position))
     cout << "Successful deleted" << endl;
   }   // 按照指定模式输出B+数
   // 模式“1”输出整棵树结构
   // 模式“2”按照从小到大的顺序输出所有键值
   else if (command == "print")
   {
    tick1 = GetTickCount();
    printkey(keynumber);
    tick2 = GetTickCount();
    cout << "Printed in " << tick2 - tick1 << " millisecond" << endl;
   }   // 退出程序
   else if (command == "exit")
    break;
   else
   {
    cout << "Please make sure the command is correct" << endl;
    continue;
   }
}
}// 查找包含给定键值的结点位置,若找到则返回“true”
// “position”保存最后访问的结点位置
bool findkey (long key, long &position)
{
BPlusNode node;
long point;
fstream iofile ("BPlusTreeData.dat", ios::binary|ios::in|ios::out);
iofile.seekp(0, ios::beg);
iofile.read((char *)&root, sizeof(long));
iofile.seekp(root, ios::beg);
while (true)
{
   cur = iofile.tellp();
   iofile.read((char *)&node, sizeof(BPlusNode));
   if(!node.isactive) continue;
 
   // B+树只在叶结点保存记录信息
   // 所以查找必须执行到叶结点
   if(!node.isleave)      // 如果该结点不是叶结点,则根据键值决定访问哪个子结点
   {
    for (int i = 1; i < node.amount; ++i)
    {
     point = -1;
     if (node.key[i] > key)
     {
      point = node.children[i-1];
      break;
     }
    }
    if (point == -1) point = node.children[node.amount-1];
    iofile.seekp(point, ios::beg);
    pre = cur;
   }
   else                 // 如果该结点是叶结点,则顺序访问结点中的键值
   {
    for (int i = 1; i < node.amount; ++i)
    {
     if (node.key[i] == key)
     {
      position = cur;
      iofile.close();
      return true;
     }
    }
    position = cur;
    iofile.close();
    return false;
   }
}
}// 按照给定格式输出B+数
void printkey (long mode)
{
BPlusNode node;
int i = 1, k = 1;
fstream iofile("BPlusTreeData.dat", ios::binary|ios::in|ios::out);
/*/ for debug ///
BPlusNode rootnode;
iofile.read((char *)&root, sizeof(long));
iofile.seekp(root, ios::beg);
iofile.read((char *)&rootnode, sizeof(BPlusNode));
cout << "root's children: ";
for (int m = 0; m < rootnode.amount; ++m)
   cout << rootnode.children[m] << " ";
cout << endl;
///*/
// 从根结点开始广度遍历,输出整棵B+树结构
if (mode == 1)
{
   iofile.seekp(0, ios::beg);
   iofile.read((char *)&root, sizeof(long));
   iofile.seekp(root, ios::beg);
   do
   {
    cur = iofile.tellp();
    cout << "level " << k << ":";
    do
    {
     iofile.read((char *)&node, sizeof(BPlusNode));
     cout << "   node " << i << ": ";
     for (int j = 1; j < node.amount; ++j)
      cout << node.key[j] << " ";
     if (node.right == 0)
     {
      i = 1;
      cout << endl;
      break;
     }
     iofile.seekp(node.right, ios::beg);
     ++i;
    }
    while(true);
    iofile.seekp(cur, ios::beg);
    iofile.read((char *)&node, sizeof(BPlusNode));
    if (node.children[0] == 0)
     break;
    iofile.seekp(node.children[0], ios::beg);
    ++k;
   }
   while (true);
   iofile.close();
}// 从包含最小键值的叶结点开始按照从小到大的顺序输出所有键值
else if (mode == 2)
{
   iofile.seekp(4, ios::beg);
   iofile.read((char *)&smallest, sizeof(long));
   iofile.seekp(smallest, ios::beg);
   do
   {
    iofile.read((char *)&node, sizeof(BPlusNode));
    for (int l = 1; l < node.amount; ++l)
     cout << node.key[l] << " ";
    if (node.right == 0)
    {
     cout << endl;
     break;
    }
    iofile.seekp(node.right, ios::beg);
   }
   while(true);
   iofile.close();
}
}// 在位于“position”的结点中插入一个新键值“key”
// 按照B+树的规则,根据情况分裂结点
bool insertkey (long key, long position)
{
fstream iofile;
iofile.open("BPlusTreeData.dat", ios::binary|ios::in|ios::out);
BPlusNode node;
BPlusNode nodenew;
BPlusNode nodetemp1, nodetemp2;
long keytemp[M];
long childrentemp[M+1];
iofile.seekp(0, ios::end);
long posEnd = iofile.tellp();//根节点分裂之后新建根节点
if (position == 0)
{
   iofile.seekp(0, ios::beg);
   iofile.read((char *)&root, sizeof(long));
   iofile.seekp(root, ios::beg);
   iofile.read((char *)&node, sizeof(BPlusNode));
   nodenew.amount = 2;
   nodenew.key[1] = key;
   nodenew.children[0] = root;
   nodenew.children[1] = nodenewposition;
   nodenew.father = 0;
   nodenew.left = 0;
   nodenew.right = 0;
   nodenew.isactive = true;
   nodenew.isleave = false;
   iofile.seekp(8, ios::beg);
   do
   {
    cur = iofile.tellp();
    iofile.read((char *)&nodetemp2, sizeof(BPlusNode));
   }
   while(nodetemp2.isactive && iofile.tellp() < posEnd);
   if (nodetemp2.isactive)
   {
    nodenewposition = iofile.tellp();
    iofile.write((char *)&nodenew, sizeof(BPlusNode));
   }
   else
   {
    iofile.seekp(cur, ios::beg);
    nodenewposition = cur;
    iofile.write((char *)&nodenew, sizeof(BPlusNode));
   }
   root = nodenewposition;
   iofile.seekp(0, ios::beg);
   iofile.write((char *)&root, sizeof(long));
   for (int i = 0; i <= 1; ++i)
   {
    iofile.seekp(nodenew.children[i], ios::beg);
    iofile.read((char *)&nodetemp1, sizeof(BPlusNode));
    nodetemp1.father = nodenewposition;
    iofile.seekp(nodenew.children[i], ios::beg);
    iofile.write((char *)&nodetemp1, sizeof(BPlusNode));
   }
   iofile.close();
   return true;
}// 没有分裂到根结点
else
{
   int insertposition = 0;
   iofile.seekp(position, ios::beg);
   iofile.read((char *)&node, sizeof(BPlusNode));
   if (node.amount < M)         // 结点中还有空位保存新插入的键值,不需要分裂
   {
    // 按照从小到大的顺序重新排列原有键值和新插入的键值
    bool issort1 = false;
    for (int i = 1; i < node.amount; ++i)
    {
     if (node.key[i] > key)
     {
      for (int j = node.amount - i, k = 0; j > 0; --j, ++k)
      {
       node.key[node.amount-k] = node.key[node.amount-k-1];
      }
      node.key[i] = key;
      insertposition = i;
      issort1 = true;
      break;
     }
    }
    if (!issort1)
    {
     node.key[node.amount] = key;
     insertposition = node.amount;
    }
    node.amount++;
    if (!node.isleave)
    {
     for (int p = node.amount-1; p > insertposition; --p)
     {
      node.children[p] = node.children[p-1];
     }
     node.children[insertposition] = nodenewposition;
    }
    iofile.seekp(position, ios::beg);
    iofile.write((char *)&node, sizeof(BPlusNode));
    iofile.close();
    return true;
   }
   else         // 结点中没有空位保存新插入的键值,必须分裂成两个结点
   {
    long nextinsertkey = 0;    // 按照从小到大的顺序重新排列原有键值和新插入的键值
    // 并且按照键值的顺序重新排列保存子结点位置的数组
    bool issort2 = false;
    for (int m = 1, n = 0, o = 0; m < M; ++m, ++o)
    {
     if (node.key[m] < key || issort2)
     {
      keytemp[m-1+n] = node.key[m];
      childrentemp[o+n] = node.children[o];
     }
     else
     {
      keytemp[m-1] = key;
      childrentemp[o] = node.children[o];
      childrentemp[o+1] = nodenewposition;
      n = 1;
      --m;
      issort2 = true;
     }
    }
    if (!issort2)
    {
     keytemp[M-1] = key;
     childrentemp[M-1] = node.children[M-1];
     childrentemp[M] = nodenewposition;
    }
    /*/ for debug //
    cout << "keytemp: ";
    for (int a = 0; a < M; ++a)
     cout << keytemp[a] << " ";
    cout << endl;
    /*/
    node.amount = M/2+1;
    if (!node.isleave)      // 按照内部结点的方式创建新结点
    {
     nodenew.amount = M/2;
     for (int i = 0, j = 1; i <= M; ++i)
     {
      if (i < M/2)
      {
       node.key[i+1] = keytemp[i];
       node.children[i] = childrentemp[i];
      }
      else if (i == M/2)
      {
       nextinsertkey = keytemp[i];
       node.children[i] = childrentemp[i];
      }
      else if (i < M)
      {
       nodenew.key[j] = keytemp[i];
       nodenew.children[j-1] = childrentemp[i];
       node.key[i] = 0;
       ++j;
      }
      else if (i == M)
      {
       nodenew.children[j-1] = childrentemp[i];
      }
     }
     nodenew.isleave = false;
    }
    else      // 按照叶结点的方式创建新结点
    {
     nodenew.amount = M/2+1;
     for (int i = 0, j = 1; i < M; ++i)
     {
      if (i < M/2)
      {
       node.key[i+1] = keytemp[i];
      }
      else
      {
       nodenew.key[j] = keytemp[i];
       if (i < M-1)
        node.key[i+1] = 0;
       ++j;
      }
     }
     nextinsertkey = nodenew.key[1];
     nodenew.isleave = true;
     for (int n = 0; n < M; ++n)
      nodenew.children[n] = 0;
    }
    nodenew.key[0] = 0;
    nodenew.father = node.father;
    nodenew.left = position;
    nodenew.right = node.right;
    nodenew.isactive = true;    // 查找新结点的插入位置
    // 若索引文件中存在一个曾经被删除的结点,则用新结点覆盖掉这个结点
    // 若不存在这样的结点,则将新结点添加到索引文件尾部
    iofile.seekp(8, ios::beg);
    do
    {
     cur = iofile.tellp();
     iofile.read((char *)&nodetemp2, sizeof(BPlusNode));
    }
    while(nodetemp2.isactive && iofile.tellp() < posEnd);
    if (nodetemp2.isactive)
    {
     nodenewposition = iofile.tellp();
     iofile.write((char *)&nodenew, sizeof(BPlusNode));
    }
    else
    {
     iofile.seekp(cur, ios::beg);
     nodenewposition = cur;
     iofile.write((char *)&nodenew, sizeof(BPlusNode));
    }
    node.right = nodenewposition;
    iofile.seekp(position, ios::beg);
    iofile.write((char *)&node, sizeof(BPlusNode));
    iofile.close();
    if (insertkey(nextinsertkey, nodenew.father))      // 递归调用插入算法将分裂后需要插入到父结点的键值插入到父结点中
     return true;
    else return false;
   }
}
}// 删除给定的键值
// 该算法不符合B+树的删除规则
// 只是简单地将除被删除键值外的其它键值重新插入一遍
bool deletekey (long key, long position)
{
fstream iofile;
iofile.open("BPlusTreeData.dat", ios::binary|ios::in|ios::out);
BPlusNode node;
long *keynumbertemp = new long[];
long number = 0;
long posEnd;
iofile.seekp(0, ios::end);
posEnd = iofile.tellp();
iofile.seekp(4, ios::beg);
iofile.read((char *)&smallest, sizeof(long));
iofile.seekp(smallest, ios::beg);
do
{
   iofile.read((char *)&node, sizeof(BPlusNode));
   for (int i = 1; i < node.amount; ++i)
   {
    keynumbertemp[number] = node.key[i];
    ++number;
   }
   if (node.right == 0)
   {
    --number;
    break;
   }
   iofile.seekp(node.right, ios::beg);
}
while(true);
/*/ for debug /
cout << "smallest: " << smallest << endl;
for (int x = 0; x <= number; ++x)
{
   cout << keynumbertemp[x] << " " << endl;
}
/*/
node.amount = 1;
for (int j = 0; j < M; ++j)
{
   node.key[j] = 0;
   node.children[j] = 0;
}
node.father = 0;
node.left = 0;
node.right = 0;
node.isactive = true;
node.isleave = true;
root = 8;
smallest = 8;
iofile.seekp(0, ios::beg);
iofile.write((char *)&root, sizeof(long));
iofile.write((char *)&smallest, sizeof(long));
iofile.write((char *)&node, sizeof(BPlusNode));
for (;iofile.tellp() < posEnd;)
{
   iofile.read((char *)&node, sizeof(BPlusNode));
   node.isactive = false;
   iofile.seekp(-long(sizeof(BPlusNode)), ios::cur);
   iofile.write((char *)&node, sizeof(BPlusNode));
}
iofile.close();
for (int k = 0; k <= number; ++k)
{
   if (keynumbertemp[k] == key)
    continue;
   findkey(keynumbertemp[k], position);
   insertkey(keynumbertemp[k], position);
}
return true;
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
BPlusTree_Java实现 package bplustree; import java.util.*; import com.xuedi.IO.*; import com.xuedi.maths.*; ////// DisposeRoot ///////中的key参数有些问题 public class BTree { //用于记录每个节点中的键值数量 public int keyAmount; //树的根节点 public Node root; public BTree(int keyAmount) { this.keyAmount = keyAmount; this.root = new Node(keyAmount); } //在B树中插入叶节点///////////////////////////////////////////////////////////// public void insert(long key,Object pointer) { //找到应该插入的节点 Node theNode = search(key,root); //在叶节点中找到空闲空间,有的话就把键放在那里 if( !isFull(theNode) ) { putKeyToNode(key,pointer,theNode); }else{ //如果在适当的叶节点没有空间,就把该叶节点分裂成两个,并正确分配键值 Node newNode = separateLeaf(key,pointer,theNode); //如果分裂的是根节点,就新建一个新的根节点将新建的节点作为他的字节点 if( isRoot(theNode) ) { DisposeRoot(theNode,newNode,newNode.keys[0]); }else{ //将新建立的节点的指针插入到上层节点 insertToInnerNode(theNode.parent,newNode,newNode.keys[0]); } } } //lowerNode是下级节点分离后新建立的那个节点/////////////////////////////////////// //upperNode是lowerNode的上层节点 private void insertToInnerNode(Node upperNode,Node lowerNode,long key) { //上层节点有空位就直接插入 if( !isFull(upperNode) ) { putKeyToNode(key,lowerNode,upperNode); //重置父节点指针 pointerRedirect(upperNode); return; }else{ //如果分裂的是根节点,就新建一个新的根节点将新建的节点作为他的子节点 Node newNode; if( isRoot(upperNode) ) { newNode = separateInnerNode(key,lowerNode,upperNode); Node newRoot = new Node(this.keyAmount); newRoot.pointer[0] = upperNode; newRoot.pointer[1] = newNode; upperNode.parent = newRoot; newNode.parent = newRoot; newRoot.keyAmount = 1; newRoot.keys[0] = key; root = newRoot; //重置父节点指针 pointerRedirect(upperNode); return; }else{ //上层非根节点没有空位进行分裂和插入操作 newNode = separateInnerNode(key,lowerNode,upperNode); //重置父节点指针 pointerRedirect(upperNode); //记录要向上插入的键值在源节点中的位置(该键值在separateInnerNode()被保留在srcNode中) int keyToUpperNodePosition = upperNode.keyAmount; //向上递归插入 insertToInnerNode(upperNode.parent,newNode,upperNode.keys[keyToUpperNodePosition]); //重置父节点指针 pointerRedirect(newNode); } } } //将对应的内部节点进行分裂并正确分配键值,返回新建的节点 private Node separateInnerNode(long key,Object pointer,Node srcNode) { Node newNode = new Node(this.keyAmount); //因为我在Node中预制了一个位置用于插入,而下面的函数(putKeyToLeaf())不进行越界检查 //所以可以将键-指针对先插入到元节点,然后再分别放到两个节点中 putKeyToNode(key,pointer,srcNode); //先前节点后来因该有(n+1)/2取上界个键-值针对 int ptrSaveAmount = (int)com.xuedi.maths.NumericalBound.getBound(0,(double)(this.keyAmount+1)/2); int keySaveAmount = (int)com.xuedi.maths.NumericalBound.getBound(0,(double)(this.keyAmount)/2); int keyMoveAmount = (int)com.xuedi.maths.NumericalBound.getBound(1,(double)(this.keyAmount)/2); //(n+1)/2取上界个指针和n/2取上界个键留在源节点中 //剩下的n+1)/2取下界个指n/2取下界个键留在源节点中 for (int k = ptrSaveAmount; k < srcNode.keyAmount; k++) { newNode.add(srcNode.keys[k], srcNode.pointer[k]); } newNode.pointer[newNode.keyAmount] = srcNode.pointer[srcNode.pointer.length-1]; srcNode.keyAmount = keySaveAmount; return newNode; } //将对应的叶节点进行分裂并正确分配键值,返回新建的节点/////////////////////////////// private Node separateLeaf(long key,Object pointer,Node srcNode) { Node newNode = new Node(this.keyAmount); //兄弟间的指针传递 newNode.pointer[this.keyAmount] = srcNode.pointer[this.keyAmount]; //因为我在Node中预制了一个位置用于插入,而下面的函数(putKeyToLeaf())不进行越界检查 //所以可以将键-指针对先插入到元节点,然后再分别放到两个节点中 putKeyToNode(key,pointer,srcNode); //先前节点后来因该有(n+1)/2取上界个键-值针对 int oldNodeSize = (int)com.xuedi.maths.NumericalBound.getBound(0,(double)(this.keyAmount+1)/2); for(int k = oldNodeSize; k <= this.keyAmount; k++) { newNode.add(srcNode.keys[k],srcNode.pointer[k]); } srcNode.keyAmount = oldNodeSize; //更改指针--让新节点成为就节点的右边的兄弟 srcNode.pointer[this.keyAmount] = newNode; return newNode; } //把键值放到叶节点中--这个函数不进行越界检查//////////////////////////////////////// private void putKeyToNode(long key,Object pointer,Node theNode) { int position = getInsertPosition(key,theNode); //进行搬迁动作--------叶节点的搬迁 if( isLeaf(theNode) ) { if(theNode.keyAmount <= position) { theNode.add(key,pointer); return; } else{ for (int j = theNode.keyAmount - 1; j >= position; j--) { theNode.keys[j + 1] = theNode.keys[j]; theNode.pointer[j + 1] = theNode.pointer[j]; } theNode.keys[position] = key; theNode.pointer[position] = pointer; } }else{ //内部节点的搬迁----有一定的插入策略: //指针的插入比数据的插入多出一位 for (int j = theNode.keyAmount - 1; j >= position; j--) { theNode.keys[j + 1] = theNode.keys[j]; theNode.pointer[j + 2] = theNode.pointer[j+1]; } theNode.keys[position] = key; theNode.pointer[position+1] = pointer; } //键值数量加1 theNode.keyAmount++; } //获得正确的插入位置 private int getInsertPosition(long key,Node node) { //将数据插入到相应的位置 int position = 0; for (int i = 0; i < node.keyAmount; i++) { if (node.keys[i] > key) break; position++; } return position; } //有用的辅助函数//////////////////////////////////////////////////////////////// //判断某个结点是否已经装满了 private boolean isFull(Node node) { if(node.keyAmount >= this.keyAmount) return true; else return false; } //判断某个节点是否是叶子结点 private boolean isLeaf(Node node) { //int i = 0; if(node.keyAmount == 0) return true; //如果向下的指针是Node型,则肯定不是叶子节点 if(node.pointer[0] instanceof Node) return false; return true; } private boolean isRoot(Node node) { if( node.equals(this.root) ) return true; return false; } //给内部节点中的自己点重新定向自己的父亲 private void pointerRedirect(Node node) { for(int i = 0; i <= node.keyAmount; i++) { ((Node)node.pointer[i]).parent = node; } } //新建一个新的根节点将新建的节点作为他的字节点 private void DisposeRoot(Node child1,Node child2,long key) { Node newRoot = new Node(this.keyAmount); newRoot.pointer[0] = child1; newRoot.pointer[1] = child2; newRoot.keyAmount = 1; newRoot.keys[0] = key; root = newRoot; //如果两个孩子是叶节点就让他们两个相连接 if( isLeaf(child1) ) { //兄弟间的指针传递 child2.pointer[this.keyAmount] = child1.pointer[this.keyAmount]; child1.pointer[this.keyAmount] = child2; } pointerRedirect(root); return; } /////////////////////////////////////////////////////////////////////////////// //用于寻找键值key所在的或key应该插入的节点 //key为键值,curNode为当前节点--一般从root节点开始 public Node search(long key,Node curNode) { if (isLeaf(curNode)) return curNode; for (int i = 0; i < this.keyAmount; i++) { if (key < curNode.keys[i]) //判断是否是第一个值 return search(key, (Node) curNode.pointer[i]); else if (key >= curNode.keys[i]) { if (i == curNode.keyAmount - 1) //如果后面没有值 { //如果key比最后一个键值大,则给出最后一个指针进行递归查询 return search(key,(Node) curNode.pointer[curNode.keyAmount]); } else { if (key < curNode.keys[i + 1]) return search(key, (Node) curNode.pointer[i + 1]); } } } //永远也不会到达这里 return null; } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值