伸展树(Splay tree)图解与实现

原文地址: https://blog.csdn.net/u014634338/article/details/49586689


一、伸展树 

本文介绍了二叉查找树的一种改进数据结构–伸展树(Splay Tree)。它的主要特点是不会保证树一直是平衡的,但各种操作的平摊时间复杂度是O(log n),因而,从平摊复杂度上看,二叉查找树也是一种平衡二叉树。另外,相比于其他树状数据结构(如红黑树,AVL树等),伸展树的空间要求与编程复杂度要小得多。

伸展树的出发点是这样的:考虑到局部性原理(刚被访问的内容下次可能仍会被访问,查找次数多的内容可能下一次会被访问),为了使整个查找时间更小,被查频率高的那些节点应当经常处于靠近树根的位置。这样,很容易得想到以下这个方案:每次查找节点之后对树进行重构,把被查找的节点搬移到树根,这种自调整形式的二叉查找树就是伸展树。每次对伸展树进行操作后,它均会通过旋转的方法把被访问节点旋转到树根的位置。

为了将当前被访问节点旋转到树根,我们通常将节点自底向上旋转,直至该节点成为树根为止。“旋转”的巧妙之处就是在不打乱数列中数据大小关系(指中序遍历结果是全序的)情况下,所有基本操作的平摊复杂度仍为O(log n)。

在AVL树中我我们知道有4种旋转方式(实际是两种)RR型,LL型,RL型,LR型,如果不清楚AVL树旋转方式的可以参考平衡二叉树(AVL)图解与实现,个人觉还是说得很明白了,这里我们还是已图解的方式来讲解伸展树的操作,对伸展树的旋转操作中我沿用了AVL树中的部分名称,当然这个不是很严谨,主要是明白其中的原理,至于怎么称呼这个都是个人习惯而已。

首先给出伸展树的结构定义:
  1. typedef struct SplayNode *Tree;
  2. typedef int ElementType;
  3. struct SplayNode
  4. {
  5. Tree parent; //该结点的父节点,方便操作
  6. ElementType val; //结点值
  7. Tree lchild;
  8. Tree rchild;
  9. SplayNode( int val= 0) //默认构造函数
  10. {
  11. parent= NULL;
  12. lchild=rchild= NULL;
  13. this->val=val;
  14. }
  15. };



二、伸展树的旋转操作


什么叫单R型呢,在上图中,我们查找的元素是9,其父节点是7,并且7是根结点,查找结点是其父节点的右孩子,而且把9变成根结点只需一次左旋转即可(即将9提升一层),这样的情况我们叫单R型,经过一次左旋转后结点9替代了原来的根结点7,变成新的根结点(注意这里因为图简单,9最终变成了根结点,在树复杂的情况,一般不会一次就变成了根结点,但肯定会变成原子树的根,这也就是程序中说的当前子树中的新根)。为了后面更加轻松,这里把单左旋代码贴出,可以对比图示和代码分析分析,便于理解

  1. //单左旋操作
  2. //参数:根,旋转结点(旋转中心)
  3. //返回:当前子树中的新根
  4. Tree left_single_rotate(Tree &root,Tree node)
  5. {
  6. if (node== NULL)
  7. return NULL;
  8. Tree parent=node->parent; //其父结点
  9. Tree grandparent=parent->parent; //其祖父结点
  10. parent->rchild=node->lchild; //设置其父节点的右孩子
  11. if (node->lchild) //如果有左孩子则更新node结点左孩子的父节点信息
  12. node->lchild->parent=parent;
  13. node->lchild=parent; //更新node结点的左孩子信息
  14. parent->parent=node; //更新原父节点的信息
  15. node->parent=grandparent;
  16. if (grandparent) //更新祖父孩子结点的信息
  17. {
  18. if (grandparent->lchild==parent)
  19. grandparent->lchild=node;
  20. else
  21. grandparent->rchild=node;
  22. }
  23. else //不存在祖父节点,则原父节点为根,那么旋转后node为根
  24. root=node;
  25. return node;
  26. }

(2)单L型

单L型和单R型是对称的,也就是说查找结点3是其父节点的左子树,并且其父节点是根结点,这样一次右旋转后3就是根结点了。

  1. //单右旋操作
  2. //参数:根,旋转结点(旋转中心)
  3. //返回:当前子树中的新根
  4. Tree right_single_rotate(Tree &root,Tree node)
  5. {
  6. if (node== NULL)
  7. return NULL;
  8. Tree parent,grandparent;
  9. parent=node->parent;
  10. grandparent=parent->parent;
  11. parent->lchild=node->rchild;
  12. if (node->rchild)
  13. node->rchild->parent=parent;
  14. node->rchild=parent;
  15. parent->parent=node;
  16. node->parent=grandparent;
  17. if (grandparent)
  18. {
  19. if (grandparent->lchild==parent)
  20. grandparent->lchild=node;
  21. else
  22. grandparent->rchild=node;
  23. }
  24. else
  25. root=node;
  26. return node;
  27. }

(3)RR型



所谓RR型,简单点说就是两次R型,两次左旋转,这种情况是查找结点有父节点,同时也有祖父结点,并且三则在同右侧,这种就是RR型,针对这种情况,先把查找结点的父节点旋转一次,即提升一层,然后再以查找结点再次旋转,这样查找结点就到了根结点了,都是左旋转,只是旋转对象不一样罢了。

  1. //两次单左旋操作
  2. //参数:根,最后将变成子树根结点的结点
  3. void left_double_rotate(Tree &root,Tree node)
  4. {
  5. left_single_rotate(root,node->parent);
  6. left_single_rotate(root,node);
  7. }


(4)LL型



  1. //两次单右旋操作
  2. //参数:根,最后将变成子树根结点的结点
  3. void right_double_rotate(Tree &root,Tree node)
  4. {
  5. right_single_rotate(root,node->parent); //先提升其父节点
  6. right_single_rotate(root,node); //最后提升自己
  7. }

LL型和RR型是对称的,经过一次双右旋结果如上图,但是这样就结束了吗?回想一下,伸展树的旋转操作目的是干什么,不是为了把查找结点推送至树根么,是的,但是现在这种情况结点9还不是树根,但是这种情况不是我们前面讲过的单R型吗?所以再来次左旋就可以了,也就是下面这个样子:



(5)RL型


  1. //双旋操作(RL型),于AVL树类似
  2. //参数:根,最后将变成子树根结点的结点
  3. void RL_rotate(Tree&root,Tree node)
  4. {
  5. right_single_rotate(root,node); //先右后左
  6. left_single_rotate(root,node);
  7. }


这个和AVL树中的RL是一样的,旋转完成后,还需要一步左旋:


OK,到位了。


(6)LR型


  1. //双旋操作(LR型),于AVL树类似
  2. //参数:根,最后将变成子树根结点的结点
  3. void LR_rotate(Tree &root,Tree node)
  4. {
  5. left_single_rotate(root,node); //先左
  6. right_single_rotate(root,node); //后右
  7. }

OK,到这里伸展树的几种情况就介绍完了,怎么这么多旋转方式,其实大可不必这样,这个只是我学习的时候自己总结的,和网上的也打不相同,主要是自己理解了它的旋转方式后,就好了,至于命名这些都影响不大,我这里把它们分为这几种的方式,主要是为了封装成函数,方便我的调用,这样逻辑更清楚一下,自己懂了以后就可以根据自己理解来组织代码了。

三、伸展树的操作
伸展树的操作和AVL树一样无非就是查,插,删,下面我们分别来介绍它们。
(1)先看看查找函数search:
  1. //查找函数,带调整功能
  2. //参数:根结点,需要查找的val
  3. //返回:true or false
  4. bool search(Tree &root,ElementType val)
  5. {
  6. Tree parent= NULL;
  7. Tree *temp= NULL;
  8. temp=search_val(root,val, parent);
  9. if (*temp && *temp!=root)
  10. {
  11. SplayTree(root,*temp);
  12. return true;
  13. }
  14. return false;
  15. }

查找函数中里面有另一个具体的查找函数,我们先不管它,先梳理逻辑,首先我们通过内部的查找函数,查找值为val的结点,找到后返回结点给temp,如果查找成功,并且当前结点不是根结点,那么我们将进行树的调整,将结点temp推到树根,否则直接退出,这就是search的功能,简单明了。

  1. //具体的查找函数
  2. //参数:根,需要查找的val,父节点指针
  3. //成功:返回其结点
  4. //失败:返回其引用,方便后面的插入操作
  5. Tree *search_val(Tree &root,ElementType val,Tree &parent)
  6. {
  7. if (root== NULL)
  8. return &root;
  9. if (root->val>val)
  10. return search_val(root->lchild,val,parent=root);
  11. else if(root->val<val)
  12. return search_val(root->rchild,val,parent=root);
  13. return &root;
  14. }

这里我们有必要介绍一下内部的查找函数,因为这是一个通用的接口,后面都会用到它,这个查找函数,如果查找成功则返回结点的引用,否则返回它该插入地方的引用,也就是其最后的parent的某个孩子,parent是查找成功或失败结点的父节点,也是引用类型。OK,这就是我们的查找函数,这里没有强化它的查找功能,只是方便我们后面的插入和删除工作。

(2)插入

  1. //插入函数
  2. //参数:根,需要插入的val
  3. //返回:true or false
  4. bool insert(Tree &root,ElementType val)
  5. {
  6. Tree *temp= NULL;
  7. Tree parent= NULL;
  8. //先查找,如果成功则无需插入,否则返回该结点的引用。
  9. temp=search_val(root,val,parent);
  10. if (*temp== NULL) //需要插入数据
  11. {
  12. Tree node= new SplayNode(val);
  13. *temp=node; //因为是引用型,所以这里直接赋值,简化了很多了。
  14. node->parent=parent; //设置父节点。
  15. return true;
  16. }
  17. return false;
  18. }

可以看到这个插入函数也是很短的,注意观察,里面有我们熟悉的东西,没错就是前面所讲的内部查找函数,这里对插入结点,我们先进行查找,如果查找成功就不进行插入,否则返回该插入地址的引用,这样我们直接让*temp=node,便完成了插入工作,简化了很多工作,然后设置父节点信息,插入成功。

(3)伸展
当我们查找一个val后,我们需要对树进行伸展,下面就是我们的伸展函数
  1. //Splay调整操作
  2. void SplayTree(Tree &root,Tree node)
  3. {
  4. while (root->lchild!=node && root->rchild!=node && root!=node) //当前结点不是根,或者不是其根的左右孩子,则根据情况进行旋转操作
  5. up(root, node);
  6. if (root->lchild==node) //当前结点为根的左孩子,只需进行一次单右旋
  7. root=right_single_rotate(root, node);
  8. else if(root->rchild==node) //当前结点为根的右孩子,只需进行一次单左旋
  9. root=left_single_rotate(root, node);
  10. }

可以看到,里面有个up函数,在这个函数外,还有单独的if判断结构,这两个if就是判断特殊情况的,也就是我们只需进行一个单旋便可以晋级为根结点的情况,这个很简单,结合一下图就可以看出来了。OK,看看我们的up函数
  1. //根据情况,选择不同的旋转方式
  2. void up(Tree &root,Tree node)
  3. {
  4. Tree parent,grandparent;
  5. int i,j;
  6. parent=node->parent;
  7. grandparent=parent->parent;
  8. i=grandparent->lchild==parent ? -1: 1;
  9. j=parent->lchild==node ? -1: 1;
  10. if (i== -1 && j== -1) //AVL树中的LL型
  11. right_double_rotate(root, node);
  12. else if(i== -1 && j== 1) //AVL树中的LR型
  13. LR_rotate(root, node);
  14. else if(i== 1 && j== -1) //AVL树中的RL型
  15. RL_rotate(root, node);
  16. else //AVL树中的RR型
  17. left_double_rotate(root, node);
  18. }

up顾名思义就是往上,也就是把查找结点往上推送,在这个函数里面我们判断了旋转类型,是LL型,还是RR型,还是LR型,亦或是RL型,然后再调用我们前面展示过的旋转函数。只需旋转函数最好结合图然后再看代码,这样很容易理解,不要只看代码。

到这里我们的查找和插入,以及伸展过程我们都展示了,这里很重要一个函数就是查找函数,还有就是几种旋转方式。

(4)删除

  1. //删除操作
  2. void remove(Tree &root,ElementType val)
  3. {
  4. Tree parent= NULL;
  5. Tree *temp;
  6. Tree *replace;
  7. Tree replace2;
  8. temp=search_val(root,val, parent); //先进行查找操作
  9. if(*temp) //如果查找到了
  10. {
  11. if (*temp!=root) //判断是否是根结点,不是根结点,则需要调整至根结点
  12. SplayTree(root, *temp);
  13. //调至根结点或者本来就是根结点后进行删除,先查看是否有替代元素
  14. if (root->rchild)
  15. {
  16. //有替代元素
  17. replace=Find_Min(root->rchild); //找到替换元素
  18. root->val=(*replace)->val; //替换
  19. if ((*replace)->lchild== NULL) //左子树为空
  20. {
  21. replace2=*replace;
  22. *replace=(*replace)->rchild; //重接其右孩子
  23. delete replace2;
  24. }
  25. else if((*replace)->rchild== NULL) //右子树为空
  26. {
  27. replace2=*replace;
  28. *replace=(*replace)->lchild; //重接其左孩子
  29. delete replace2;
  30. }
  31. }
  32. else
  33. {
  34. //无替代元素,则根直接移向左子树,不管左子树是否为空都可以处理
  35. replace2=root;
  36. root=root->lchild;
  37. delete replace2;
  38. }
  39. }
  40. }

在删除函数中,我们首先进行了查找,查找失败就退出,查找成功后,我们便把它推送到根结点,然后再用我们BST删除方式,找替代元素,这样化繁为简,只是这里统一采用引用方式,要简单很多。
下面是这是我们的熟悉的找替代元素的函数:
  1. //操作当前子树的最小结点
  2. //返回:其最小结点的引用
  3. Tree *Find_Min(Tree &root)
  4. {
  5. if (root->lchild)
  6. return Find_Min(root->lchild);
  7. return &root;
  8. }

OK,到这里我们的伸展树就介绍完了,在这里可以看到我们伸展树里面的函数和AVL树里面的函数差别很大,在AVL树里面,我们即采用了引用(部分),同时又可以通过返回值来设置,再加上手生,写得有点杂乱,这里的伸展树,我就统一采用引用方式,能不返回值就返回值,这样可以简化很多操作,加之伸展树本来就比AVL树简单,不同判断平衡因子,因此写起来就更加简单了。

下面就是我们总的代码:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <iostream>
  4. using namespace std;
  5. typedef struct SplayNode *Tree;
  6. typedef int ElementType;
  7. struct SplayNode
  8. {
  9. Tree parent; //该结点的父节点,方便操作
  10. ElementType val; //结点值
  11. Tree lchild;
  12. Tree rchild;
  13. SplayNode( int val= 0) //默认构造函数
  14. {
  15. parent= NULL;
  16. lchild=rchild= NULL;
  17. this->val=val;
  18. }
  19. };
  20. bool search(Tree &,ElementType);
  21. Tree *search_val(Tree&,ElementType,Tree&);
  22. bool insert(Tree &,ElementType);
  23. Tree left_single_rotate(Tree&,Tree);
  24. Tree right_single_rotate(Tree &,Tree );
  25. void LR_rotate(Tree&,Tree );
  26. void RL_rotate(Tree&,Tree );
  27. void right_double_rotate(Tree&,Tree );
  28. void left_double_rotate(Tree&,Tree );
  29. void SplayTree(Tree &,Tree);
  30. void up(Tree &,Tree );
  31. Tree *Find_Min(Tree &);
  32. void remove(Tree &,ElementType);
  33. //查找函数,带调整功能
  34. //参数:根结点,需要查找的val
  35. //返回:true or false
  36. bool search(Tree &root,ElementType val)
  37. {
  38. Tree parent= NULL;
  39. Tree *temp= NULL;
  40. temp=search_val(root,val, parent);
  41. if (*temp && *temp!=root)
  42. {
  43. SplayTree(root,*temp);
  44. return true;
  45. }
  46. return false;
  47. }
  48. //具体的查找函数
  49. //参数:根,需要查找的val,父节点指针
  50. //成功:返回其结点
  51. //失败:返回其引用,方便后面的插入操作
  52. Tree *search_val(Tree &root,ElementType val,Tree &parent)
  53. {
  54. if (root== NULL)
  55. return &root;
  56. if (root->val>val)
  57. return search_val(root->lchild,val,parent=root);
  58. else if(root->val<val)
  59. return search_val(root->rchild,val,parent=root);
  60. return &root;
  61. }
  62. //插入函数
  63. //参数:根,需要插入的val
  64. //返回:true or false
  65. bool insert(Tree &root,ElementType val)
  66. {
  67. Tree *temp= NULL;
  68. Tree parent= NULL;
  69. //先查找,如果成功则无需插入,否则返回该结点的引用。
  70. temp=search_val(root,val,parent);
  71. if (*temp== NULL) //需要插入数据
  72. {
  73. Tree node= new SplayNode(val);
  74. *temp=node; //因为是引用型,所以这里直接赋值,简化了很多了。
  75. node->parent=parent; //设置父节点。
  76. return true;
  77. }
  78. return false;
  79. }
  80. //单左旋操作
  81. //参数:根,旋转结点(旋转中心)
  82. //返回:当前子树中的新根
  83. Tree left_single_rotate(Tree &root,Tree node)
  84. {
  85. if (node== NULL)
  86. return NULL;
  87. Tree parent=node->parent; //其父结点
  88. Tree grandparent=parent->parent; //其祖父结点
  89. parent->rchild=node->lchild; //设置其父节点的右孩子
  90. if (node->lchild) //如果有左孩子则更新node结点左孩子的父节点信息
  91. node->lchild->parent=parent;
  92. node->lchild=parent; //更新node结点的左孩子信息
  93. parent->parent=node; //更新原父节点的信息
  94. node->parent=grandparent;
  95. if (grandparent) //更新祖父孩子结点的信息
  96. {
  97. if (grandparent->lchild==parent)
  98. grandparent->lchild=node;
  99. else
  100. grandparent->rchild=node;
  101. }
  102. else //不存在祖父节点,则原父节点为根,那么旋转后node为根
  103. root=node;
  104. return node;
  105. }
  106. //单右旋操作
  107. //参数:根,旋转结点(旋转中心)
  108. //返回:当前子树中的新根
  109. Tree right_single_rotate(Tree &root,Tree node)
  110. {
  111. if (node== NULL)
  112. return NULL;
  113. Tree parent,grandparent;
  114. parent=node->parent;
  115. grandparent=parent->parent;
  116. parent->lchild=node->rchild;
  117. if (node->rchild)
  118. node->rchild->parent=parent;
  119. node->rchild=parent;
  120. parent->parent=node;
  121. node->parent=grandparent;
  122. if (grandparent)
  123. {
  124. if (grandparent->lchild==parent)
  125. grandparent->lchild=node;
  126. else
  127. grandparent->rchild=node;
  128. }
  129. else
  130. root=node;
  131. return node;
  132. }
  133. //双旋操作(LR型),于AVL树类似
  134. //参数:根,最后将变成子树根结点的结点
  135. void LR_rotate(Tree &root,Tree node)
  136. {
  137. left_single_rotate(root,node); //先左
  138. right_single_rotate(root,node); //后右
  139. }
  140. //双旋操作(RL型),于AVL树类似
  141. //参数:根,最后将变成子树根结点的结点
  142. void RL_rotate(Tree&root,Tree node)
  143. {
  144. right_single_rotate(root,node); //先右后左
  145. left_single_rotate(root,node);
  146. }
  147. //两次单右旋操作
  148. //参数:根,最后将变成子树根结点的结点
  149. void right_double_rotate(Tree &root,Tree node)
  150. {
  151. right_single_rotate(root,node->parent); //先提升其父节点
  152. right_single_rotate(root,node); //最后提升自己
  153. }
  154. //两次单左旋操作
  155. //参数:根,最后将变成子树根结点的结点
  156. void left_double_rotate(Tree &root,Tree node)
  157. {
  158. left_single_rotate(root,node->parent);
  159. left_single_rotate(root,node);
  160. }
  161. //Splay调整操作
  162. void SplayTree(Tree &root,Tree node)
  163. {
  164. while (root->lchild!=node && root->rchild!=node && root!=node) //当前结点不是根,或者不是其根的左右孩子,则根据情况进行旋转操作
  165. up(root, node);
  166. if (root->lchild==node) //当前结点为根的左孩子,只需进行一次单右旋
  167. root=right_single_rotate(root, node);
  168. else if(root->rchild==node) //当前结点为根的右孩子,只需进行一次单左旋
  169. root=left_single_rotate(root, node);
  170. }
  171. //根据情况,选择不同的旋转方式
  172. void up(Tree &root,Tree node)
  173. {
  174. Tree parent,grandparent;
  175. int i,j;
  176. parent=node->parent;
  177. grandparent=parent->parent;
  178. i=grandparent->lchild==parent ? -1: 1;
  179. j=parent->lchild==node ? -1: 1;
  180. if (i== -1 && j== -1) //AVL树中的LL型
  181. right_double_rotate(root, node);
  182. else if(i== -1 && j== 1) //AVL树中的LR型
  183. LR_rotate(root, node);
  184. else if(i== 1 && j== -1) //AVL树中的RL型
  185. RL_rotate(root, node);
  186. else //AVL树中的RR型
  187. left_double_rotate(root, node);
  188. }
  189. //操作当前子树的最小结点
  190. //返回:其最小结点的引用
  191. Tree *Find_Min(Tree &root)
  192. {
  193. if (root->lchild)
  194. return Find_Min(root->lchild);
  195. return &root;
  196. }
  197. //删除操作
  198. void remove(Tree &root,ElementType val)
  199. {
  200. Tree parent= NULL;
  201. Tree *temp;
  202. Tree *replace;
  203. Tree replace2;
  204. temp=search_val(root,val, parent); //先进行查找操作
  205. if(*temp) //如果查找到了
  206. {
  207. if (*temp!=root) //判断是否是根结点,不是根结点,则需要调整至根结点
  208. SplayTree(root, *temp);
  209. //调制根结点或者本来就是根结点后进行删除,先查看是否有替代元素
  210. if (root->rchild)
  211. {
  212. //有替代元素
  213. replace=Find_Min(root->rchild); //找到替换元素
  214. root->val=(*replace)->val; //替换
  215. if ((*replace)->lchild== NULL) //左子树为空
  216. {
  217. replace2=*replace;
  218. *replace=(*replace)->rchild; //重接其右孩子
  219. delete replace2;
  220. }
  221. else if((*replace)->rchild== NULL) //右子树为空
  222. {
  223. replace2=*replace;
  224. *replace=(*replace)->lchild; //重接其左孩子
  225. delete replace2;
  226. }
  227. }
  228. else
  229. {
  230. //无替代元素,则根直接移向左子树,不管左子树是否为空都可以处理
  231. replace2=root;
  232. root=root->lchild;
  233. delete replace2;
  234. }
  235. }
  236. }
  237. //前序
  238. void PreOrder(Tree root)
  239. {
  240. if (root== NULL)
  241. return;
  242. printf( "%d ",root->val);
  243. PreOrder(root->lchild);
  244. PreOrder(root->rchild);
  245. }
  246. //中序
  247. void InOrder(Tree root)
  248. {
  249. if (root== NULL)
  250. return;
  251. InOrder(root->lchild);
  252. printf( "%d ",root->val);
  253. InOrder(root->rchild);
  254. }
  255. int main()
  256. {
  257. Tree root= NULL;
  258. insert(root, 11);
  259. insert(root, 7);
  260. insert(root, 18);
  261. insert(root, 3);
  262. insert(root, 9);
  263. insert(root, 16);
  264. insert(root, 26);
  265. insert(root, 14);
  266. insert(root, 15);
  267. search(root, 14);
  268. printf( "查找14:\n");
  269. printf( "前序:");
  270. PreOrder(root);
  271. printf( "\n");
  272. printf( "中序:");
  273. InOrder(root);
  274. printf( "\n");
  275. // remove(root,16);
  276. // remove(root,26);
  277. // remove(root,11);
  278. remove(root, 16);
  279. printf( "删除16:\n");
  280. printf( "前序:");
  281. PreOrder(root);
  282. printf( "\n");
  283. printf( "中序:");
  284. InOrder(root);
  285. printf( "\n");
  286. return 0;
  287. }
程序我反复测试过,目前还没有发现问题,当然也是还存在bug,不管是代码还是思考过程,如果有问题,还希望各位不吝指教。感谢。
main函数中某测试数据的图:

测试结果:



最后也许伸展树讲的比较简略,因为和AVL树有很大相识部分,可以参考一下: 平衡二叉树AVL图解与实现

二叉搜索树的中的删除结点参考: 二叉搜索树

如果对指针感到疑惑可以参看一下: C语言指针初探

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值