《算法导论读书笔记》第二部分 第六章 堆排序

第二部分 排序和顺序统计量

这一部分介绍了几种解决如下排序问题的算法:

即输入n个数的序列,输出排列好的序列

数据的结构

名词:记录(record),关键字(key),卫星数据(satellite data) …

为什么要排序

很多计算机科学家认为排序是算法研究中最基础的问题,其原因有很多:

有时应用本身就需要对信息进行排序。

很多算法通常把排序作为关键子程序。

现有的排序算法数量非常庞大,其中所使用的技术也非常丰富。

我们可以证明排序问题的一个非平凡下界(在第8章中,我们会给出证明)。

...(详见书)
排序算法

在第2章中已经介绍了两种排序算法。插入和归并排序

如果输入数组中仅有常数个元素需要在排序过程中存储在数组之外,则称排序算法是原址的(in place)

归并算法有更好的渐进运行时间Θ(nlgn),但它所使用的MERGE过程并不是原址的。

在这一部分中,我们将介绍两种新的排序算法,它们可以排序任意的实数。(6~8章)

下面有个统计的表:

在这里插入图片描述

顺序统计量

一个n个数的集合的第i个顺序统计量就是集合中第i小的数。

当然,我们可以通过将输入集合排序,取输出的第i个元素来选择第i个顺序统计量。

当不知道输入数据的分布时,这种方法的运行时间为Ω(nlgn), 即第8章中所证明的比较排序算法的下界。

背景

没有很复杂的数学知识,有一点概率

第六章 堆排序

heapsort.

时间复杂度是O(nlgn),且具有空间原址性。

因此,堆排序V是集合了我们目前已经讨论的两种排序算法优点的一种排序算法。

我们用"堆"这种数据结构来进行信息管理。

虽然堆这一词源自堆排序,但是目前它已经被引申为“垃圾收集存储机制”,例如在Java和Lisp语言中所定义的。

注意本书中任何部分堆都是指堆数据结构

6.1 堆

在这里插入图片描述

如图,(二叉)堆是一个数组,可以看成一个近似的完全二叉树(见B.5.2节)。树上每个结点堆对应数组中的一个元素。

除了最底层,树是完全充满的,自左到右填充。

  • 表示堆的数组A包括两个属性:

    A.length(通常)给出数组元素的个数

    A.heap-size表示有多少堆元素存储在该数组中。

也就是说,虽然A[1…A.length]可能都存有数据,但只有A[1…A.heap-size]中存放的是堆的有效元素,0 <=A.heap-size<=A.length。

给定下标i我们很容易得到:

PARENT(i) return ⌊i/2⌋

LEFT(i) return 2i

RIGHT(i) return 2i + 1

在堆排序好的实现中,这三个函数通常是以“宏”或者“内联函数”的方式实现的。

二叉堆可以分为两种形式:最大堆和最小堆。

在最大堆中,最大堆性质是指除了根以外的所有结点i都要满足:

A [ P A R E N T ] ≥ A [ i ] A[PARENT] \ge A[i] A[PARENT]A[i]

也就是某个结点的值至多与其父节点一样大。

最小堆性质即相反

A [ P A R E N T ] ≤ A [ i ] A[PARENT] \le A[i] A[PARENT]A[i]

最小元素放在根结点

在堆排序算法中,我们使用的是最大堆。

堆中的结点高度就是该结点到叶节点的最长简单路径上边的数目;

堆的高度可定义为根结点的高度。

如果是n个元素的完全二叉树,则高度为 lg ⁡ n \lg n lgn

我们会发现,堆结构上的一些基本操作的运行时间至多与树的高度成正比,即时间复杂度为O(lgn)

一些基本过程:

MAX-HEAPIFY过程: O(lgn), 是维护最大堆性质的关键。

BUILD-MAX-HEAP过程: 具有线性时间复杂度,功能是从无序的输入数据数组中构造一个最大堆。

HEAPSORT过程:O(nlgn), 功能是对一个数组进行原址排序。

MAX-HEAP-INSERT、HEAP-EXTRACT-MAX、HEAP-INCREASE-KEY和 HEAP-MAXIMUM过程:O(lgn), 功能是利用堆实现一个优先队列。

练习

TODO: <26-09-20, wgc> >

6.2 维护堆的性质

MAX-HEAPIFY过程。

它的输入为一个数组A和一个下标i

我们假定LEFT(i)和RIGHT(i)的二叉树都是最大堆,但A[i]可能小于它的孩子,此时:

我们调用MAX-HEAPIFY, 通过让A[i]的值在最大堆中“逐级下降”,从而使得以下标i为根结点的子树重新遵循最大堆的性质。

MAX-HEAPIFY(A, i)

1   l = LEFT(i)
2   r = RIGHT(i)
3   if l <= A.heap-size and A[l] > A[i]
4       largest = l
5   else largest = i
6   if r <= A.heap-size and A[r] > A[largest]
7       largest = r
8   if largest != i
9       exchange A[i] with A[largest]
10      MAX-HEAPIFY(A, largest)

孩子比父亲大就交换,交换后,下标为largest的结点的值是原来的A[i],于是堆该结点为根结点的子树递归调用MAX-HEAPIFY

运行时间分析:

对于以i为根结点、大小为n的子树:

交换父子关系时间代价为Θ(1), 加上递归调用发生的运行时间,
因此每个孩子子树的大小至多为2n/3(最坏情况发生在树的最底层恰好半满的时候)

于是:

T ( n ) ≤ T ( 2 n 3 ) + Θ ( 1 ) T(n) \le T(\frac{2n}{3}) + \Theta(1) T(n)T(32n)+Θ(1)

由主定理解得T(n) = O(lgn)

也就是说,对于一个树高为h的结点来说,MAX-HEAPIFY的时间复杂度是O(h)

练习

6.2-1 参照图6-2的方法,说明MAX-HEAPIFY(A, 3)在数组A = <27, 17, 3, 16, 13, …>上的操作过程

图示的话,没啥好说的

6.2-2 参照过程MAX-HEAPIFY, 写出能够维护相应最小堆的MIN-HEAPIFY(A, i)的伪代码,并比较二者的运行时间

MAX-HEAPIFY(A, i)

1   l = LEFT(i)
2   r = RIGHT(i)
3   if l <= A.heap-size and A[l] > A[i]
4       largest = l
5   else largest = i
6   if r <= A.heap-size and A[r] > A[largest]
7       largest = r
8   if largest != i
9       exchange A[i] with A[largest]
10      MAX-HEAPIFY(A, largest)

MIN-HEAPIFY(A, i)

1   l = LEFT(i)
2   r = RIGHT(i)
3   if l <= A.heap-size and A[l] < A[i]
4       least = l
5   else least = i
6   if r <= A.heap-size and A[r] < A[least]
7       least = r
8   if least != i
9       exchange A[i] with A[least]
10      MIN-HEAPIFY(A, least)

时间不变

6.2-3 当元素A[i]比其孩子都大时,调用M-H会有什么结果?

就9,10行不执行了,树也不会动

6.2-4 当i>A.heap-size/2时,调用MAX-HEAPIFY(A, i)会有什么结果?

does not cause any changes to the heap tree.

因为此时的i没有孩子

does not cause any effect

6.2-5 想用循环控制结构取代递归,重写M-HEAPIFY代码

Heapify(A, i)

1    do
2    p = i
...  ...

11      Exchange A[i] with A[largest]
12      i = largest
13   while p != i
6.2-6 证明:对一个大小为n的堆,MAX-HEAPIFY的最坏情况运行时间为Ω(lgn)

上面其实证过了,就每个孩子都换一次,就是树高O(lgn)

6.3 建堆

我们可以用自底而上的方法利用过程MAX-HEAPIFY把一个大小为n = A.length 的数组A[1…n]转换为最大堆。

也知道子数组A[⌋n/2⌊+1…n]中的元素都是树的叶结点。

每个叶结点都可以看成只包含一个元素的堆。

过程 BUILD-MAX-HEAP对树中的其他结点都调用一次MAX-HEAPIFY

BUILD-MAX-HEAP(A)

1   A.heap-size = A.length
2   for i = ⌊A.length/2⌋ downto 1
3       MAX-HEAPIFY(A, i)

为了证明其正确性,我们使用如下的循环不变量:

在第2~3行中每一次for循环的开始,结点i+1, i+2, ..., n 都是一个最大堆的根结点。

我们证明这一变量第一次循环前为真,且每次循环迭代都维持不变。

循环结束时,这一不变量可以用于证明正确性。

具体证明略,细想就好。

简单估算 BUILD-MAX-HEAP 运行时间的上界:

每次调用MAX-HEAPIFY的时间复杂度是O(lgn) BUILD-MAX-HEAP需要O(n)次这样的调用,

因此得到总时间复杂度O(nlgn)。

当然这个上界正确但不是渐进准确的。

我们可以进一步得到更紧确的界。

观察到,不同结点运行MAX-HEAPIFY的时间与该结点树高相关,而大部分结点的高度都很小。

因此,利用如下性质:

n个元素的堆高⌊lgn⌋

高h的堆最多包含⌈n/2h+1⌉个结点

所以BUILD-MAX-HEAP的总代价可以表示为:

∑ h = 0 ⌊ lg ⁡ n ⌋ ⌈ n 2 h + 1 ⌉ O ( h ) = O ( n ∑ h = 0 ⌊ lg ⁡ n ⌋ h 2 h ) \sum_{h= 0}^{\left\lfloor \lg n \right\rfloor}\left\lceil \frac{n}{2^{h+ 1}} \right\rceil O(h) = O(n \sum_{h= 0}^{\left\lfloor \lg n \right\rfloor}\frac{h}{2^{h}}) h=0lgn2h+1nO(h)=O(nh=0lgn2hh)

由x=1/2的公式A.8得到最后一个累积和,(自行查阅)有:

∑ h = 0 ∞ h 2 h = 1 2 ( 1 − 1 2 ) 2 = 2 \sum_{h= 0}^{\infty}\frac{h}{2^{h}}= \frac{\frac{1}{2}}{(1- \frac{1}{2})^{2}}= 2 h=02hh=(121)221=2

于是得到BUILD-MAX-HEAP时间复杂度:

O ( n ∑ h = 0 ⌊ l g n ⌋ h 2 h ) = O ( n ∑ h = 0 ∞ h 2 h ) = O ( n ) O(n\sum_{h= 0}^{\left\lfloor lgn \right\rfloor}\frac{h}{2^{h}})= O(n\sum_{h= 0}^{\infty}\frac{h}{2^{h}})= O(n) O(nh=0lgn2hh)=O(nh=02hh)=O(n)

我们可以在线性时间内把一个无序数组构造成为一个最大堆。

类似的BUILD-MIN-HEAP构造最小堆结构相同,都是线性时间。

练习

6.3-1 说明 BUILD-MAX-HEAP操作过程
6.3-2 对于BUIlD-MAX-HEAP中第2行的循环控制变量i来说,为什么我们要求它是从⌊A.length/2⌋到1递减,而不是从1到⌊A.length/2⌋递增呢?

使用MAX-HEAPIFY(A,i)函数时左右子树都应该是最大堆,从i = 1 开始就可能出现不满足的情况。

6.3-3 证明:对于任一包含n个元素的堆中,至多有⌈n/2h+1⌉个高度为h的结点?

以H为堆的高度,有两个细节要注意:

结点的高度和深度要注意分清楚;

如果不是完全二叉树,结点高度不一定相同;

n 个 元 素 的 堆 , 高 度 为 H , 则 2 0 + 2 1 + . . . + 2 H = n → 2 H + 1 − 1 = n n个元素的堆,高度为H,则2^{0}+ 2^{1}+ ...+ 2^{H}= n \quad \to 2^{H+ 1}- 1 = n nH20+21+...+2H=n2H+11=n

利用数学归纳法:

当高度h=0:

n 2 0 + 1 = 2 H + 1 − 1 2 = 2 H − 1 2 = ⌊ 2 H − 1 2 ⌋ = 2 H \frac{n}{2^{0+ 1}}= \frac{2^{H+ 1}- 1}{2}= 2^{H}- \frac{1}{2}= \left\lfloor 2^{H}- \frac{1}{2} \right\rfloor = 2^{H} 20+1n=22H+11=2H21=2H21=2H

这是完全二叉树的结点数,向下取整得到正确个数

所以当h = 0时满足;

假设h = k, 除了k=0时结点可能不满,其余层都满。

h = k+1 时,结点数正好是h = k的一半,代入后依然等式成立,得证。

6.4 堆排序算法

HEAPSORT(A)

1   BUILD-MAX-HEAP(A)
2   for i = A.length downto 2
3       exchange A[1] with A[i]
4       A.heap-size = A.heap-size - 1
5       MAX-HEAPIFY(A, 1)

将最大元素A[1]与A[n]互换,再取出结点n

再用MAX-HEAPIFY维护最大堆

循环至堆大小从n-1降到2

如此排序,准确的循环不变量定义见练习6.4-2

在这里插入图片描述

在这里插入图片描述

图6-4

习题

TODO: <30-09-20, wgc> >

6.5 优先队列

以下几乎百分百还原原书内容

堆排序是个优秀的算法,但在实际应用中,第7章将要介绍的快速排序的性能一般会优于堆排序。

尽管如此,堆这一数据结构仍然有很多应用。

本节将介绍一个常见应用:作为高效的优先队列。

和堆一样,优先队列也有两种形式:最大和最小优先队列。

这里,我们关注于如何基于最大堆实现最大优先队列。

练习6.5-3将会编写最小优先队列。

优先队列(priority queue)是一种用来维护由一组元素构成的集合S的数据结构,
其中每个元素都有一个相关的值,称为关键字(key)。一个最大优先队列支持以下操作:

INSERT(S, x): 把元素x插入到集合S中。等价于S=S⋃{x}。

MAXIMUM(S): 返回S中具有最大关键字的元素。

EXTRACT-MAX(S): 去掉并返回S中的具有最大关键字的元素。

INCREASE-KEY(S, x, k): 将元素x的关键字值增加到k,这里假设k的值不小于x的原关键字值。

相应的最小优先队列也有四个操作。

关于最大优先队列的应用,其中一个就是在共享计算机系统的作业调度。

最小优先队列可以被用于基于事件驱动的模拟器。

队列中保存要模拟的事件,每个事件都有一个发生时间作为其关键字

事件必须按照发生的时间顺序进行模拟,因为某一事件的模拟结果可能会出发对其他事件的模拟。

在每一步,模拟程序调用EXTRACT-MIN来选择下一个要模拟的事件。

当一个新事件产生时,模拟器通过调用INSERT将其插入最小优先级队列中。

在第23章和第24章的内容中,我们将看到其他用途,特别是对DECREASE-KEY操作的使用。

显然,优先队列可以用堆来实现。

对一个像作业调度或事件驱动模拟器这样的应用程序来说,优先队列的元素对应着应用程序中的对象。

通常我们需要确定哪个对象对应一个给定的优先队列元素,反之亦然。

因此,在用堆实现队列时,需要在堆中的每个元素里存储对应对象的句柄(handle)。

句柄(如一个指针或一个整数型等)的准确含义依赖于具体的应用程序。

同样,在应用程序的对象中,我们也需要存储一个堆中对应元素的句柄。通常这一句柄是数组的下标。

由于在堆的操作过程中,元素会改变其在数组中的位置,

因此,在具体的实现中,在重新确定堆元素位置时,我们也需要更新相应应用程序对象中的数组下标。

因为对应用程序对象的访问细节强烈依赖于应用程序及其实现方式,所以这里我们不做详细讨论。需强调的是,这些句柄也需要被正确地维护。


现在,我们来讨论实现最大优先队列的操作。

过程 HEAP-MAXIMUM可以在Θ(1)时间内实现MAXIMUM操作。

HEAP-MAXIMUM(A)

1   return A[1]

过程HEAP-EXTRACT-MAX实现EXTRACT-MAX操作。

它与HEAPSORT过程中的for循环体部分(第3~5行)很相似。

HEAP-EXTRACT-MAX(A)

1   if A.heap-size < 1
2       error "heap underflow"
3   max = A[1]
4   A[1] = A[A.heap-size]
5   A.heap-size = A.heap-size - 1
6   MAX-HEAPIFY(A, 1)
7   return max

HEAP-EXTRACT-MAX的时间复杂度为O(lgn)。

因为除了O(lgn)的MAX-HEAPIFY以外,其它操作都是常数阶。

HEAP-INCREASE-KEY能够实现INCREASE-KEY操作。

在优先队列中我们希望增加关键字的优先队列元素由对应的数组下标i来标识。

这一操作需要首先将元素A[i]的关键字更新为新值。

因为增大A[i]的关键字可能会违反最大堆的性质,所以上述操作采用类似于2.1节INSERTION-SORT中插入循环(5~7行)的方式,
在从当前结点到根结点的路径上,为新增的关键字寻找恰当的插入位置。

在HEAP-INCREASE-KEY的操作过程中,当前元素会不断地与其父节点进行比较,如果当前元素的关键字较大,则当前元素与其父节点进行交换。

不断重复这一过程,直到当前元素的关键字小于其父节点时终止,因为此时已经重新符合了最大堆的性质。(准确的循环不变量表示见练习6.5-5。)

HEAP-INCREASE-KEY(A, i, key)

1   if key < A[i]
2       error "new key is smaller than current key"
3   A[i] = key
4   while i > 1 and A[PARENT(i)] < A[i]
5       exchange A[i] with A[PARENT(i)]
6       i = PARENT(i)

在这里插入图片描述

图6-5

增大i结点并排序

在包含n个元素的堆上,HEAP-INCREASE-KEY的时间复杂度是O(lgn)。

因为第3行做了关键字更新的结点到根结点的路径长度为O(lgn)

MAX-HEAP-INSERT能够实现INSERT操作。它的输入是要被插入到最大堆A中的新元素的关键字。

MAX-HEAP-INSERT首先通过增加一个关键字为-∞的叶结点来扩展最大堆。

然后调用HEAP-INCREASE-KEY为新结点设置对应的关键字,同时保持最大堆的性质

MAX-HEAP-INSERT(A, key)

1   A.heap-size = A.heap-size + 1
2   A[A.heap-size] = -∞
3   HEAP-INCREASE-KEY(A, A.heap-size, key)

在包含n个元素的堆上,MAX-HEAP-INSERT的运行时间为O(lgn)。

总之,在一个包含n个元素的堆中,所有优先队列的操作都可以在O(lgn)时间内完成。

练习

6.5-1 试说明HEAP-EXTRACT-MAX在堆A=<15, 13, …,>的操作过程

6.5-2 试说明MAX-HEAP-INSER(A, 10)在堆A=…上的操作过程

6.5-3 要求用最小堆实现最小优先队列,请写出HEAP-MINIMUM、HEAP-EXTRACT-MIN、HEAP-DECREASE-KEY和MIN-HEAP-INSERT的伪代码

HEAP-MINIMUM

1   return A[1]

HEAP-EXTRACT-MIN

1   if A.heap-size < 1
2       then error "heap underflow"
3   min = A[1]
4   A[1] = A[A.heap-size]
5   A.heap-size = A.heap-size - 1
6   MIN-HEAPIFY(A, 1)
7   return min

HEAP-DECREASE-KEY(A, i, key)

1   if key > A[i]
2       then error "new key is larger than current key"
3   A[i] = key
4   while i > 1 and A[PARENT(i)] > A[i]
5       do exchange A[i] with A[PARENT(i)]
6   i = PARENT(i)

MIN-HEAP-INSERT(A, key)

1   A.heap-size = A.heap-size + 1
2   A[A.heap-size] = ∞
3   HEAP-DECREASE-KEY(A, A.heap-size, key)

MIN-HEAPIFY(A, i)

l = LEFT(i)
r = RIGHT(i)
if l <= A.heap-size and A[l] < A[i]
    then smallest = 1
else smallest = i
if r <= heap-size[A] and A[r] < A[smallest]
    then smallest = r
if smallest != i
    then exchange A[i] with A[smallest]
    MIN-HEAPIFY(A, smallest)
6.5-4 在MAX-HEAP-INSERT的第二行,为什么我们要把关键字设为-∞,然后又将其增加到所需的值呢?

Since the heap data structure is represented by an array and deletions are implemented by reducing the size of the array there may be undefined values in the array past the end of the heap.

Therefore it is essential that the MAX-HEAP-INSERT sets the key of the inserted node to -∞ such that HEAP-INCREASE-KEY does not fail.

因为堆数据结构由数组表示,且删除是通过减小数组大小来实现的,数组中可能出现堆末尾之前的未定义的值

因此有必要设置-∞使得HEAP-INCREASE-KEY 不会失败

6.5-5 试分析在使用下列循环不变量时,HEAP-INCREASE-KEY的正确性

在算法的第4~6行while 循环每次迭代开始的时候,子数组A[1…A.heap-size]要满足最大堆的性质。如果有违背,只有一种可能:
A[i]大于A[PARENT(i)]

这里你可以假定在调用HEAP-INCREASE-KEY时,A[1…A.heap-size]是满足最大堆性质的。

初始:在循环开始前,除了刚增加的A[i]=key不满足最大堆性质,其他元素都满足

保持:在循环过程中,通过不断交换key与其parent值,并不断更新parent值来使增加元素满足最大堆性质。

终止:当i=1或A[PARENT(i)]>=A[i]时,表示所有元素已经排好,并且满足最大堆,那么循环自然终止

6.5-6 在HEAP-INCREASE-KEY的第五行的交换操作中,一般需要三次赋值来完成。想一想如何利用INSERTION-SORT内循环部分的思想,只用一次赋值就完成这一交换操作?

HEAP-INCREASE-KEY(A, i, key)

1   if key < A[i]
2       error "new key is smaller than current key"
3   A[i] = key
4   while i > 1 and A[PARENT(i)] < A[i]
5       exchange A[i] with A[PARENT(i)]
6       i = PARENT(i)

INSERTION SORT(A)

1   for j = 2 to A.length
2       key = A[j]
3       i = j - 1
4       while i > 0 and A[i] > key
5           A[i+1] = A[i]
6           i = i - 1
7       A[i+1] = key

三次赋值应该就是以temp中介完成交换


HEAP-INCREASE-KEY(A, i, key)

if key < A[i]
    error "new key is smaller than current key"
A[i] = key
while i>=0 and A[PARENT(i)] < A[i]
    A[i] = A[PARENT(i)] // insertion sort inner loop concept is used 
    i = PARENT(i)
A[i] = key   // 即A[PARENT(i)] = key
6.5-7 试说明如何使用优先队列来实现一个先进先出队列,以及如何使用优先队列来实现栈(队列和栈的定义见10.1节)

不是很懂队列和栈,先写这点,建议跳过

先进先出队列可以用最低优先级队列定义(min-priority Queue)

  • 用k 表示队列元素数,初始化为0

  • 用l 表示移除出队列的元素数,初始化为0

  • 每当一个新元素进队,指配优先级为k。

    k ← k+1

    运行时间为Θ(1),因此,新进的元素有最高优先级而且处于最后的位置

  • 每当一个元素移除出队列:l ← l + 1

    最坏情况运行时间为Θ(log(k-1)),因为要执行一次MIN-HEAPIFY(A)

6.5-8 HEAP-DELETE(A, i)操作能够将结点i从堆A中删除。对于一个包含n个元素的堆,请设计一个能够在O(lgn)时间内完成的HEAP-DELETE操作

HEAP-DELETE(A, i)

Exchange A[i] with A[A.heap-size]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A, i)

简单粗暴

6.5-9 请设计一个时间复杂度为O(nlgk)的算法,它能够将k个有序链表合并为一个有序链表,这里n所有输入链表包含的总的元素个数。(提示:使用最小堆来完成k路归并。)

插入所有处于位置1的k个元素到堆里,用EXTRACT-MAX 得到合并列表的第一个元素。

插入位置2的来自列表的最大元素到堆中。

重复这一过程,显然运行时间是O(nlgk)


思考题

(用插入的方式建堆) 我们可以通过反复调用MAX-HEAP-INSERT实现向一个堆中插入元素,考虑BUILD-MAX-HEAP的如下实现方式:

BUILD-MAX-HEAP`(A)

1   A.heap-size = 1
2   for i = 2 to A.length
3       MAX-HEAP-INSERT(A, A[i])
a. 当输入数据相同时,BUILD-MAX-HEAP和BUILD-MAX-HEAP`生成的堆是否总是一样?如果是,请证明;否则,请举出一个反例。

不总一样,叶结点顺序可能不同

b.证明:在最坏的情况下,调用BUILD-MAX-HEAP'建立一个包含n个元素的堆的时间复杂度是O(nlgn)

每次插入花费最多O(lgn),共有n次插入,所以时间复杂度为O(nlgn)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值