【数据结构初阶(5):排序】

排序的概念及其运用

排序的概念

排序:所谓排序,是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序,如在磁盘中进行的排序。

排序的运用

排序在生活中有着极为广泛的运用,比如购物网站首页导航栏,价格升序或降序,视频网站点击量排序,以及院校排名等,都是与我们日常生活息息相关的,是不可或缺的一种数据结构。

常见的排序算法

我们接下来会依次来实现这些算法并分析他们的时间复杂度、空间复杂度、稳定性等性能指标。

插入排序

直接插入排序

基本思想:
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的数据按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的数据插入完为止,得到一个新的有序序列
实际中我们玩扑克牌时,就用了插入排序的思想
//交换函数
void Swap(int* p1, int* p2)
{
    int* tmp = NULL;
    tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}
// 插入排序
void InsertSort(int* a, int n)
{
//交换思想,能实现但不推荐
    //int j = 0;
    //for (j = 1; j < n; j++)
    //{
    //    int end = j;//end表示数组下标的最后一个值
    //    int i = 0;
    //    for (i = end; i > 0; i--)
    //    {
    //        if (a[i] < a[i - 1])
    //        Swap(&a[i], &a[i - 1]);
    //    }
    //}
//插入思想
    int i = 0;
    for (i = 0; i < n-1; i++)
    {   
        int end = i;
        int tmp = a[end + 1];
        while (end>=0)
        {
            if (tmp < a[end])
            {
                a[end+1] = a[end];
                end--;
            }
            else
            {
                break;
            }
        }
        a[end + 1] = tmp;
    }
}

测试结果如下,即为成功:

直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2),等差数列之和(1+2+...+N-1)=,是典型的O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定,待插入数据从后往前依次比较,若相等则直接插入到比较数之后,相等的数据的相对位置不会发生改变,因此与它是一种稳定的排序算法。

希尔排序(缩小增量排序)

希尔排序法又称缩小增量法。希尔排序法的基本思想是:
先选定一个整数gap,把待排序文件中所有数据分成gap个组,每组有N/gap个数,所有距离为gap的数据分在同一组内,并对每一组内的记录进行排序。然后,取gap=gap/2或gap=gap/3+1重复上述分组和排序的工作。当到达gap=1时,所有数据在统一组内排好序
// 希尔排序
void ShellSort(int* a, int n)
{
    int gap = n;
    while (gap > 1)
    {
        gap = gap / 3 + 1;
        //将N个数据,分成gap组
        for (int j = 0; j < gap; ++j)
        {
            //每组数据,分别排序,每组数据距离是gap,
            //为了不越界,则控制结束条件为n-gap
            for (int i = j; i < n - gap; i += gap)
            {
                //里面逻辑和直接插入排序同理
                int end = i;
                int tmp = a[end + gap];
                while (end >= 0)
                {
                    if (tmp < a[end])
                    {
                        a[end + gap] = a[end];
                        end -= gap;
                    }
                    else
                    {
                        break;
                    }
                }

                a[end + gap] = tmp;
            }
        }
    }
    
//简化版,可以少一层循环
    /*int gap = n;
    while (gap > 1)
    {
        gap = gap / 3 + 1;
        int j = 0;
        for (j = 0; j < n - gap; j++)
        {
            int end = j;
            int tmp = a[end + gap];
                while (end >= 0)
                {
                    if (tmp < a[end])
                    {
                        a[end+gap] = a[end];
                        end-=gap;
                    }
                    else
                    {
                        break;
                    }
                }
                a[end + gap] = tmp;
            
        }
    }*/
}

测试结果如下,即为成功:

希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果,我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定:
将数据分为gap组,则每组有N/gap个数据,每组最坏情况下就是每组的每一个数据都要挪动交换,则一组就是1到N/gap-1以1为公差的等差数列前n项之和,这样的数据共有gap组,则(1+2+3+......+N/gap-1)*gap,但随着gap的减小,数据会越来越接近有序,不会一直都是最坏情况,当gap=1时,相当于直接插入,此时数据基本有序,只需做很小的调整,因此希尔排序时间复杂度不好算,我们只需记住结论即可:
时间复杂度:O(N^1.3)
空间复杂度:O(1)
稳定性:不稳定,在之前预排序时,相同数据的顺序就可能被完全打乱,最后一步的直接插入就无法保证相同数据的相对顺序和最初相同。

性能测试函数介绍:

// 测试排序的性能对比
void TestOP()
{
//创建多个数组,每个数组对应于一种排序
//让每个数组中都是相同顺序的一组随机数,这样才能对比性能
    srand(time(0));
    const int N = 100000;//改变N以此可以看到不同数量数据排序时不同排序算法的效率
    int* a1 = (int*)malloc(sizeof(int) * N);
    int* a2 = (int*)malloc(sizeof(int) * N);
    int* a3 = (int*)malloc(sizeof(int) * N);
    int* a4 = (int*)malloc(sizeof(int) * N);
    int* a5 = (int*)malloc(sizeof(int) * N);
    int* a6 = (int*)malloc(sizeof(int) * N);
    for (int i = 0; i < N; ++i)
    {
        a1[i] = rand();
        a2[i] = a1[i];
        a3[i] = a1[i];
        a4[i] = a1[i];
        a5[i] = a1[i];
        a6[i] = a1[i];
    }
    int begin1 = clock();
    InsertSort(a1, N);
    int end1 = clock();
    int begin2 = clock();
    ShellSort(a2, N);
    int end2 = clock();
    int begin3 = clock();
    SelectSort(a3, N);
    int end3 = clock();
    int begin4 = clock();
    HeapSort(a4, N);
    int end4 = clock();
    int begin5 = clock();
    QuickSort(a5, 0, N - 1);
    int end5 = clock();
    int begin6 = clock();
    MergeSort(a6, N);
    int end6 = clock();
//end-begin计算出每种排序所用时间
    printf("InsertSort:%d\n", end1 - begin1);
    printf("ShellSort:%d\n", end2 - begin2);
    printf("SelectSort:%d\n", end3 - begin3);
    printf("HeapSort:%d\n", end4 - begin4);
    printf("QuickSort:%d\n", end5 - begin5);
    printf("MergeSort:%d\n", end6 - begin6);
    free(a1);
    free(a2);
    free(a3);
    free(a4);
    free(a5);
    free(a6);
}
clock函数(计时函数)
clock_t clock (void) ;
简单而言,就是该程序从启动到函数调用占用CPU的时间。这个函数返回从“开启这个程序进程”到“程序中调用clock()函数”时之间的CPU时钟计时单元(clock tick)数,在MSDN中称之为挂钟时间(wal-clock);若挂钟时间不可取,则返回-1。其中clock_t是用来保存时间的数据类型。
这里用法是用clock函数来计算机器运行一个循环或者处理其它事件到底花了多少时间:

直接插入排序和希尔排序性能对比(可见有100000个数据时,希尔排序效率远高于直接排序):

选择排序

基本思想:每一次从待排序的数据元素中选出最小,最大的两个元素,分别存放在序列的起始位置,和末尾位置,直到全部待排序的数据元素排完 。

直接选择排序

在元素集合array[i]--array[n-1]中选择关键码最大和最小的数据元素
若中间还有元素,则将它与这组元素中的最后一个和第一个元素交换
在剩余的array[i+1]--array[n-2]集合中,重复上述步骤,直到集合剩余1个元素

(该图只表示了选择最小数的过程,选择最大数的过程类似)

//选择排序
void SelectSort(int* a,int n)
{
    int begin = 0;
    int end = n - 1;

    while (begin < end)
    {
        int mini = begin;
        int maxi = begin;
        for (int i = begin+1; i <= end; i++)
        {
            if (a[i] > a[maxi])
            {
                maxi = i;
            }
            if (a[i] < a[mini])
            {
                mini = i;
            }
        }
        Swap(&a[mini], &a[begin]);
//如果开头第一个元素就是最大值,则要单独考虑
        while (maxi==begin)
        {
            maxi = mini;
        }
        Swap(&a[maxi], &a[end]);
        begin++;
        end--;

    }

}
直接选择排序的特性总结:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度:O(N^2),每选择一趟,都要遍历一遍所有元素,对于N个数据,N+(N-2)+(N-4)
+......+1,等差数列前n项和,时间复杂度为O(N^2),思想很简单,但效率并不高。
3. 空间复杂度:O(1)
4. 稳定性:不稳定,相同的数据可能同时为最大值,也可能同时为最小值,不能保证相同数据交换后相对顺序不变,例如当前情况下,若两数同为最大值,那么先出现的反而会被放到后面去。

堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
//堆排序
void AdjustDwon(int* a, int n, int parent)
{
        int child = parent * 2 + 1;
        //建大堆
        while (child < n)
        {
            if (child + 1 < n && a[child] < a[child + 1])
            {
                child++;
            }
            if (a[parent] < a[child])
            {
                Swap(&a[parent], &a[child]);
                parent = child;
                child = parent * 2 + 1;
            }
            else
            {
                break;
            }
        }
}
void HeapSort(int* a, int n)
{
    //向下调整建堆,排升序,建大堆,排降序,建小堆
    int i = (n - 1 - 1) / 2;
    for (i; i >= 0; i--)
    {
        AdjustDwon(a, n, i);
    }
    //排序
    int end = n - 1;
    while (end > 0)
    {
        Swap(&a[0], &a[end]);
        AdjustDwon(a, end, 0);
        --end;
    }
}
堆排序的特性总结:
1. 堆排序使用堆来选数,效率就高了很多。
2. 时间复杂度:O(N*logN),是效率很高的一种算法具体算法见此链接: http://t.csdn.cn/rV6MU
3. 空间复杂度:O(1)
4. 稳定性:不稳定,基于其复杂的向下调整算法,无法保证相同数据相对位置保持不变,因此不稳定。

直接插入排序、希尔排序、直接选择排序、堆排序性能对比(可见有100000个数据时,希尔排序和堆排序效率远高于直接插入排序和直接选排序):

交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

冒泡排序

// 冒泡排序
void BubbleSort(int* a, int n)
{
//一共进行n-1趟冒泡排序
    int j = 0;
    for (j = 0; j < n - 1; j++)
    {
//一趟冒泡排序
        int i = 0;
        for (i = 0; i < n - 1 - j; i++)
        {
            if (a[i] > a[i + 1])
            {
                Swap(&a[i], &a[i + 1]);
            }
        }
    }
}

//优化版本,若原本就有序,则进行一趟冒泡排序即可
void BubbleSort(int* a, int n)
{
    int exchange = 0;
    int j = 0;
    for (j = 0; j < n - 1; j++)
    {
        int i = 0;
        for (i = 0; i < n - 1 - j; i++)
        {
            if (a[i] > a[i + 1])
            {
                Swap(&a[i], &a[i + 1]);
                exchange = 1;
            }
        }
        if (exchange == 0)
            break;
    }

}
冒泡排序的特性总结:
1. 冒泡排序是一种非常容易理解的排序,但实际利用率不高
2. 时间复杂度:O(N^2) ,等差数列前n项和,典型的N^2,即使有优化效率也很低
3. 空间复杂度:O(1)
4. 稳定性:稳定,遇到相等的数,不做交换,下标直接向后++,即相等数的相对位置不变。

快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
 if(left>=right)
   return;
 
 // 按照基准值对array数组的 [left, right)区间中的元素进行划分
 int div = PartSort(array, left, right);
 
 // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
 // 递归排[left, div)
 QuickSort(array, left, div);
 
 // 递归排[div+1, right)
 QuickSort(array, div+1, right);
}

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后续只需分析如何按照基准值来对区间中数据进行划分的方式(即PartSort有哪几种写法)即可。

将区间按照基准值划分为左右两半部分的常见方式有:

Hoare版本
int PartSort1(int* a,int left,int right)
{
    int keyi = left;
    while (left < right)
    {
        //left<right是为了防止右边所有值都比key大,造成越界访问
        while(left<right&& a[right] >=a[keyi])
        {
            right--;
        }
        while (left < right && a[left] <= a[keyi])
        {
            left++;
        }
        Swap(&a[left], &a[right]);
    }
    Swap(&a[left], &a[keyi]);
    keyi = left;
    return keyi;
}

注意:若是左边做key,那就让右边先走,即可保证最后相遇处的值小于key;同理若是右边做key,那就让左边先走,即可保证最后相遇处的值大于key,一趟走完后,可以使key到其正确的位置上,只需递归让key左边以及右边区间分别有序即可。

挖坑法
int PartSort2(int* a, int left, int right)
{
    int holei = left;
    int key = a[left];
    while (left < right)
    {
        while (left < right && a[right] >= key)
        {
            right--;
        }
        a[holei] = a[right];
        holei = right;
        while (left < right && a[left] <= key)
        {
            left++;
        }
        a[holei] = a[left];
        holei = left;

    }
    a[holei] = key;
    return holei;
}

注意:相较于Hoare法,挖坑法更加好理解,左边挖坑,要把右边小的填过来,自然是右边先走,坑到了右边名自然又要把左边大的填过去,最后相遇在坑位处,即是key的正确位置,后续递归步骤同上即可。

前后指针版本
//前后指针法
int PartSort3(int* a, int left, int right)
{
    int keyi = left;
    int prev = left;
    int cur = left + 1;
    while (cur <= right)
    {

        /*if (a[cur] < a[keyi])
        {
            prev++;
            Swap(&a[cur], &a[prev]);
        }*/
        if (a[cur] < a[keyi]&&++prev!=cur)
        {
            Swap(&a[cur], &a[prev]);
        }
        cur++;
    }
    Swap(&a[keyi], &a[prev]);
    keyi = prev;
    return keyi;
}
快速排序优化
  1. 三数取中法选key,在原数组前中后三个数据中,选出数值大小在中间的换到开头做key

  1. 递归到小的子区间时,可以考虑使用插入排序,递归需要建立栈帧,递归到小区间时使用直接插入排序能有效节省空间

优化后代码如下:

//三数取中函数
Getmiddata(int* a, int left, int right)
{
    int mid = (left + right) / 2;
    if (a[left] < a[mid])
    {
        if (a[mid] < a[right])
        {
            return mid;
        }
        else if (a[right] < a[left])
        {
            return left;
        }
        else
        {
            return right;
        }
    }
    else
    {
        if (a[mid] > a[right])
        {
            return mid;
        }
        else if (a[right] > a[left])
        {
            return left;
        }
        else
        {
            return right;
        }
    }
}
//快速排序
void QuickSort(int* a, int left, int right)
{
//小区间用插入排序
    if ((right - left + 1) <= 15)
    {
        InsertSort(a+left, right-left+1);
    }
//三数取中优化
    int mid = Getmiddata(a, left, right);
    Swap(&a[left], &a[mid]);

    if (left >= right)
        return;
    int div = PartSort3(a, left, right);
    QuickSort(a, left, div);
    QuickSort(a, div + 1, right);
}

性能测试比较,得快排的性能还是很高的:

递归需要建立栈帧,递归太深容易出现栈满的问题,上面我们也用小区间变直接插入排序的方式来优化,那么不用递归的方法,我们可以实现快排算法吗?答案是可以的,下面我们要利用栈来实现。

快速排序的非递归实现
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
    Stack sl;
    StackInit(&sl);
    StackPush(&sl, left);
    StackPush(&sl, right);
    while (!StackEmpty(&sl))
    {
        right=StackTop(&sl);
        StackPop(&sl);
        left = StackTop(&sl);
        StackPop(&sl);
        int key = PartSort3(a, left, right);
        if (key - 1 > left)
        {
            StackPush(&sl, left);
            StackPush(&sl, key - 1);
        }
        if (key + 1 < right)
        {
            StackPush(&sl, key + 1);
            StackPush(&sl, right);
        }
        
    }
    StackDestroy(&sl);
}
快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序。
2. 时间复杂度:O(N*logN),快速排序思想类似于二叉树前序遍历,其时间复杂度是典型的N*logN。
3. 空间复杂度:O(logN),递归调用函数会建立栈帧,调用完返回后会逐层销毁,空间复杂度要算的就是最大调用深度,也可以理解成二叉树的深度,因此是O(logN)。
4. 稳定性:不稳定,交换情况多样,无法保证稳定性。

归并排序

基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并,两段有序区间取较小的尾插。 归并排序核心步骤:
void mergesort(int* a, int* tmp, int begin, int end)
{
    if (begin >= end)
        return;

    int mid = (begin + end) / 2;
    // [begin, mid] [mid+1, end] 递归让子区间有序
    mergesort(a,tmp,begin, mid);
    mergesort(a, tmp,mid + 1, end);

    // 归并[begin, mid] [mid+1, end]
    //...

    int begin1 = begin, end1 = mid;
    int begin2 = mid + 1, end2 = end;
    int i = begin;

    while (begin1 <= end1 && begin2 <= end2)
    {
        if (a[begin1] <= a[begin2])
        {
            tmp[i++] = a[begin1++];
        }
        else
        {
            tmp[i++] = a[begin2++];
        }
    }

    while (begin1 <= end1)
    {
        tmp[i++] = a[begin1++];
    }

    while (begin2 <= end2)
    {
        tmp[i++] = a[begin2++];
    }

    memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
// 归并排序递归实现
void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    mergesort(a, tmp, 0, n - 1);
    if (tmp == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    free(tmp);
    tmp = NULL;
}

同样,这里使用递归深度过深时会有满栈的风险,因此,我们还需要实现归并排序的非递归方法。

归并排序的非递归实现
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if (tmp == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    int begin = 0;
    int end = n - 1;
    int rangeN = 1;//每次每组需要排序的数据个数,成2倍增长
    while (rangeN < n)
    {
        int j = 0;
        for (j=begin;j < n;j+=2*rangeN)
        {
            int begin1 = j;
            int end1 = j + rangeN - 1;
            int begin2 = j + rangeN;
            int end2 = j + 2 * rangeN - 1;

//end1、begin2、end2都可能存在越界问题,因此要依次修正
            if (end1 >= n)
            {
                end1 = n - 1;
                // 不存在区间
                begin2 = n;
                end2 = n - 1;
            }
            else if (begin2 >= n)
            {
                // 不存在区间
                begin2 = n;
                end2 = n - 1;
            }
            else if (end2 >= n)
            {
                end2 = n - 1;
            }
            int i = j;

            while (begin1 <= end1 && begin2 <= end2)
            {
                if (a[begin1] <= a[begin2])
                {
                    tmp[i++] = a[begin1++];
                }
                else
                {
                    tmp[i++] = a[begin2++];
                }
            }

            while (begin1 <= end1)
            {
                tmp[i++] = a[begin1++];
            }

            while (begin2 <= end2)
            {
                tmp[i++] = a[begin2++];
            }
// 归并一部分,拷贝一部分
//  memcpy(a + i, tmp + i, sizeof(int)*(end2 - i + 1));
        }
        // 也可以整体归并完了再拷贝
        memcpy(a, tmp, sizeof(int) * (n));

        rangeN *= 2;
    }

    free(tmp);
    tmp = NULL;
}
归并排序的特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN),归并排序思想也类似于二叉树,其时间复杂度仍是典型的N*logN。
3. 空间复杂度:O(N),另外开了一个能存放N个数据的空间。
4. 稳定性:稳定,两个有序序列排序,判断条件可知,遇到相等的数据会按照原来的前后顺序来排,因此稳定。

以上所有排序的性能比较:

以上对比,我们可以知道,快速排序、堆排序、希尔排序、归并排序等都是数据量较大时,效率很高的排序算法,而直接插入排序还有选择排序则更适用于某些特定的情况,效率较低。

计数排序

非比较排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
操作步骤:
1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中
// 计数排序
void CountSort(int* a, int n)
{
    int max = a[0];
    int min = a[0];
    for (int i = 1; i < n; i++)
    {
        if (a[i] < min)
        {
            min = a[i];
        }
        if (a[i] > max)
        {
            max = a[i];
        }
    }

    int N = max - min+1;
    int* c = (int*)calloc(N,sizeof(int));//可以在开辟空间的同时将数据初始化为0,方便计数
    if (c == NULL)
    {
        perror("calloc fail");
        exit(-1);
    }

    for (int i = 0; i < n; i++)
    {
        c[a[i] - min]++;
    }

    //排序
    int i = 0;
    for (int j = 0; j < N; j++)
    {
        while (c[j]--)
        {
            a[i++] = j + min;
        }
    }

    free(c);
    c = NULL;
}

测试结果如下图,则为成功:

计数排序的特性总结:
1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
2. 时间复杂度:O(MAX(N,范围))
3. 空间复杂度:O(范围)
4. 稳定性:稳定

复杂度及稳定性总结:

以上就是我们常用的一些排序算法,当然不是全部,还有人听说过桶排序、基数排序等等,我们不常用,故不做整理,本节相关代码均已整理好,有需要的请查看以下链接,欢迎批评指正!

各种排序算法的实现 · 王哲/practice - 码云 - 开源中国 (gitee.com)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值