[Java DS] 那就浅说一下排序吧(学习总结)

目录

排序(sorting)

1.概念

1.1排序

1.2稳定性

1.3算法时间复杂度、空间复杂度

1.4区间表示法

2.七大基于比较的排序-总览

3.插入排序

3.1直接插入排序

3.2希尔排序(Shell sort)

4.选择排序

4.1直接选择排序

4.2堆排序

5.交换排序

5.1冒泡排序

5.2快速排序

1、快速排序过程:

2、快速排序核心思路总结:

3.快速排序的性能分析

4.第二种partition

5.第三种partition

6.partition 进阶

7.快排的几个常见优化手段

6.归并排序

1.归并排序的基本思路:

2.归并排序的性能分析

7. 小结


排序(sorting)

1.概念

1.1排序

通常意义上的排序,都是指的原地排序 (in place sort)。数组的排序。

1.2稳定性

经过排序算法处理之后,相同元素的相对位置不会变化——具备稳定性。反之,如果保证不了,就是不具备稳定性。{1, 3a, 9, 4, 5, 3b, 8} => {1, 3, 3, 4, 5, 8, 9}如果排序算法能保证3a, 3b的顺序就是具有稳定性。

1.3算法时间复杂度、空间复杂度

两者都有最好情况、最坏情况、平均情况

1.4区间表示法

元素类型使用 long 类型、下标类型使用 int 类型。long[] array,int from,int to

左闭右闭 size = from - to + 1;左闭右开size= to - from。

2.七大基于比较的排序-总览

3.插入排序

3.1直接插入排序

1、记取出来进行排序的这个数为k ,插入合适的位置,依次向前比较,所谓合适的位置就是第一次遇到 k >= x 的位置。

2、代码实现:

 public static void insertSort(long[] array){
        // 一共要取多少个元素来进行插入过程(无序区间里有多少个元素)
        for(int i = 0;i < array.length - 1;i++){
            // 有序区间:[0,i];无序区间:[i + 1,n)
            // 取出无序区间的第一个数
            long k = array[i + 1];
            // 从后往前,遍历有序区间,找到第一次k >= array[j] 的位置
            int j;
            for(j = i;j >=0 && k < array[j];j--){
                array[j + 1] = array[j];    // 不符合条件的情况
            }
            array[j + 1] = k;
        }
    }

3、时间复杂度: 最好O(n);平均和最坏O(n^2); 空间复杂度:O(1);具备稳定性

插排和冒泡从性能分析和稳定性方面完全一致。但插排远快于冒泡的,所以冒泡基本没人用。

3.2希尔排序(Shell sort)

1.在插排的基础上做优化。先预排序 -> 虽然不能让数据完全有序,但可以让数据解决有序,提高跨度的插排。

        逻辑上按照固定长度的间隔做分组,比如用 3 间隔,就可以把10 个数分成3 组。

        3 1 0 4 2 5 7 8 6 9;3 4 7 9看成一组,1 2 8看成一组,0 5 6 看成一组。各自在各自的分组内部做插排。

2.如何进行各自分组的插排

//gap:带间隔 / 一共多少组
    public static void insertSortWithGap(long[] array,int gap){
        //外围的循环次数是 n - gap 次
        for(int i = 0; i < array.length - gap;i++){
            // 一共有gap 个分组
            //有序:[0.i + gap];无序:[i + gap,n)
            long k = array[i + gap];

            int j;
            // j 下标只去找同一组的元素比较,所以每次跳过间隔
            for(j = i;j >= 0 && k < array[j];j = j - gap){
                array[j + gap] = k;
            }
        }
    }

使用比较大的gap,gap大小减少,直到gap 趋于1,当gap 为1 的时候就是插排,结果就是有序的。通常情况下,gap = array.length / 2  、 gap = gap / 2;收敛为1

3.希尔排序

 public static void shellSort(long[] array){
        int gap = array.length / 2;
        while(gap > 1){
            insertSortWithGap(array,gap);

            gap = gap / 2;
        }

        //最后再执行一次插排
        insertSort(array);
    }

时间复杂度:O(n^1.3);不具备稳定性(相等的元素可能分不到一个组里)

4.选择排序

4.1直接选择排序

每次遍历无序区间,找到最大的数的位置,把他和无序区间最后一个数进行交换,其实就是保证最大的数永远出现在最后,下次再去无序区间找下一个最大的数,交换到最后。也可以每次选最小的数放前面,或者选出一个最大一个最小往两边放。

public static void selectSort(long[] array){
        //每次选择出最大的数,放到最后去
        // 一共要选择出 array.length - 1 个数
        for(int i = 0;i < array.length - 1;i++){
            // 通过遍历无序区间只找打最大的数所在的位置就可以了,不换
            // 无序区间[0,array.length - i)
            int maxIndex = 0;//假设最大数放在0 号位置
            for(int j = 1;j < array.length - i;j++){
                if(array[j] > array[maxIndex]){
                    // 说明无需区间找到了新的最大的数
                    // 所以记录下最大数的下标
                    maxIndex = j;
                }
            }

            // 遍历完之后,无序区间的最大的数就放在maxIndex所在下标处
            // array[maxIndex] 是最大数

            // 交换[maxIndex] 和 无序区间最后一个元素[array.length - i -1]
            swap(array,maxIndex,array.length - i- 1);
        }
    }

时间复杂度:O(n^2);不具备稳定性

4.2堆排序

选出无序区间的最大的元素交换到最后面去——找最值(用堆)——把无序区间逻辑上看成堆,构建一个大堆。

代码实现:

    public static void heapSort(long[] array){
        //1.建立大堆
        createBigHeap(array);

        //2.遍历n - 1次
        for(int i = 0 ;i < array.length - 1;i++ ){
            // 2.1交换之前的无序区间[0,n - 1)
            swap(array,0,array.length - i -1);
            // 交换之后的无序区间[0,n - i - 1),元素个数n - i- 1 个
            // 2.2 对堆的[0]进行向下调整,堆里的元素个数就是无序区间的元素个数
            shiftDown(array,array.length - i - 1,0);
        }
    }
    // 向下调整
    private static void shiftDown(long[] array,int size,int index){
        while(2 * index + 1 < size){
            int maxIndex = 2 * index + 1;
            int right = maxIndex + 1;
            if(right < size && array[right] > array[maxIndex]){
                maxIndex = right;
            }

            if(array[index] >= array[maxIndex]){
                return;
            }

            swap(array,index,maxIndex);

            index = maxIndex;
        }
    }
    // 建堆操作
    private static void createBigHeap(long[] array){
        //从最后一个元素的双亲开始
        for(int i = (array.length - 2) / 2;i >= 0;i--){
            shiftDown(array,array.length,i);
        }
    }

时间复杂度:O(n*log(n));空间复杂度:O(1);不具备稳定型(因为堆的调整过程中,如果相等的元素在不同的子树中,无法控制)

5.交换排序

5.1冒泡排序

1、冒泡排序(bubble sort):减治算法。减:每次解决一个问题之后,问题规模在减少。治:采用相同的方式处理相同的问题。

2、对  n 个的数组做冒泡排序,每次经过冒泡,都可以将一个最大的数冒泡到最后去,n -> n - 1 -> n - 2 -> 1...  -> 0

3、代码实现:

//冒泡排序
public class Sort {
    // 对整个数组做排序
    public static void bubbleSort(long[] array){
        // 一个数组经过 array.length - 1 次 这个数组就有序了
        for(int i = 0;i< array.length;i++){
            // 每次将最大的数经过冒泡的方式,放到最后
            // 整个数组 = 【无序区间】【有序区间】
            // 无序区间:[0,array.length - i]
            // 有序区间:[array.length - i,array.length]
            for(int j = 0;j < array.length - i - 1;j++){
                if(array[j] > array[j + 1]){
                    swap(array,j,j + 1);
                }
            }
        }
    }
    private static void swap(long[] array, int i, int j){
        long temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

4、测试的完备性问题——对于排序来说:

1.正常情况:[9, 1, 3, 5, 2, 4, 8 ,0 ,7]

2.一些特殊情况——边界法:

        数组中一个元素都没有[];数组中的所有元素都一样[3, 3, 3, 3, 3, 3];数组已经有序了[1, 2, 3, 4, 5, 6, 7];数组是逆序[7, 6, 5, 4, 3, 2, 1];数组中的元素特别大的情况

5、时间复杂度:最好情况是O(n);平均、最坏情况是O(n^2)

        空间复杂度:O(1)        具备稳定性

5.2快速排序

1、快速排序过程:

1.选择一个元素 pivot(支点)(如何选择不重要),通过遍历的方式,比较给定元素和区间内其他元素的大小关系。

2.在遍历期间,通过算法设计,让区间划分为三段:pivot、左边小于等于pivot、右边大于等于pivot。注意:pivot 不一定刚刚好在中间,因为 pivot 是任意选择的。两边区间内的元素互相之间的顺序没有任何保证。

3.第二步完成之后,pivot 就是处于最终有序后应该在的位置,认为 pivot 已经排好顺序了

4.对左右两个小区间进行排序(可能其中一个区间里面一个元素都没有)

最开始,可能我们要对10个元素的区间进行排序,经过一番处理后。变成两个小问题(问题性质一样,但数据规模在减小),变成了对 7 个元素 和 2 个元素的区间分别进行排序。

2、快速排序核心思路总结:

0.如果区间内的元素个数 <= 1个,则不用做任何处理,因为天然有序。

1.从区间中(这里区间不代表整个数组,所以需要通过[from, to] 来限定)中挑选一个pivot ,挑选方式随意。我们暂时选用区间最右边的一个元素。

2.遍历区间,将每个元素都和 pivot 进行比较,并且进行必要的位置移动,使得区间遍历完成之后满足区间被分为三个部分,[ <= pivot ]  [ pivot ]  [ >=pivot ]。整个过程一般被称为 partitoin(分割区间)。注意:partition 只是快排中的一个小步骤而已

3.继续对左右两个小区间,按照同样的方式进行处理。

        下面讨论的区间,很大可能性是处于原始数组中间的一段区间。array 中的[from,to] from 很大概率不是 0,虽然一开始是 0;同理,to 很大概率也不是 array.length - 1 ,虽然一开始是。

1.第一种 partition

两边向中间的逼近

        整个待区分的区间,在分区的过程中,大体又可以分为 4 个小区域;除了pivot 之外,应该还有 3 个区域,分别是 1.所有 <= pivot 的元素;2.所有 >= pivot 的元素;3.所有还未和 pivot 比较过的元素。

建议:当array[to] = pivot 时,先从 array[left] 开始和 pivot 比较

比较结果:array[left] <= pivot:直接让 left 朝右走一步,left++;继续比较array[left]

                 array[left] > pivot:array[left] 和 pivot 的比较结束,该让array[right] 和 pivot

                 比较,当array[right] >= pivot 时,right--;

      

 array[left] > pivot; array[right] < pivot,这个时候就卡住了,所以我们把right 和 left 进行交换

 交换完成之后仍然保持:【from , left) <= pivot;【right , to】 >= pivot;【left , right) 未参与比较。

然后,再让array【left】 参与比较

比较结果:array[left] <= pivot:直接让 left 朝右走一步,left++;继续比较array[left]

                 array[left] > pivot:array[left] 和 pivot 的比较结束,该让array[right] 和 pivot

                 比较,当array[right] >= pivot 时,right--;

实际上就是:先让array[left] 参与比较,直到 array【left】 > pivot;再让array【right】 参与比较,直到array【right】 < pivot;当两边都卡住时,交换【left】和【right】处的元素,进行这样的循环。出口:【left,right)区间里没有元素了,也就是 right = left 时结束循环,这个时候我们的partition 就结束了。所以只要 left < right 那么我们的循环就继续。

         因为最后我们要把pivot 放在中间,所以最后一步当 left = right 时,把left 位置的元素和 pivot 进行交换。

 2.partition代码:

 /**
     * 以区间最右边的元素 array[to] 作为pivot。遍历整个区间,从 from 到 to
     * 移动必要的元素,进行分区
     * @param array
     * @param from
     * @param to
     * @return
     */
    private static int partitionMethodA(long[] array,int from,int to){
        // 1.先找出pivot
        long pivot = array[to];
        // 2.定义left 和 right 两个下标
        int left = from;
        int right = to;
        //[from,left) 都是 <= pivot
        //[left,right)  都是未参与比较的
        //[right,to] 都是 >= pivot 的

        //循环,保证每个元素都参与了和 pivot 的比较
        // 也就是,只要[left,right)区间内还有元素,循环就应该继续
        while (left < right){
            //左边先比较

            // 随着 left 在循环过程中一直在++,left < right 的条件不能一直保证
            // 所以要时刻进行left < right 的保证
            // 且 得现有 left < right 后面的比较才有意义
            while(left < right && array[left] <= pivot){
                left++;
            }
            //循环停止时,说明array[left] > pivot
            while (left < right && array[right] >= pivot){
                right--;
            }
            //循环停止时,说明array[right] < pivot
            // 两边都卡住时,交换[left] 和 [right] 位置的元素
            long t = array[left];
            array[left] = array[right];
            array[right] = t;
        }

        // 走到这里,说明 left == right 
        // 待比较区间中已经没有元素了,都已经在各自应该的位置上了
        long t = array[to];
        array[to] = array[left];
        array[left] = t;

        //返回 pivot 最终所在下标
        return left;
    }

  3.快速排序代码:

public class Sort {
    public static void quickSort(long[] array){
        quickSortRange(array, 0, array.length - 1);
    }

    //为了代码书写方便,我们选择使用左闭右闭的区间表示形式
    // 让我们对 array 中从 from 到 to 的位置进行排序,其他地方不用管
    // 其中, from 和 to 都算在区间中
    // 左闭右闭的情况下,区间内的元素个数 = to - from + 1
    private static void quickSortRange(long[] array,int from,int to){
        if(to - from + 1 <= 1){
            //区间中元素个数 <= 1个,自然有序
            return;
        }

        //挑选出区间中最右边的元素array[to]
        int pi = partitionMethodA(array,from,to);
        // 小于等于 pivot 的元素所在的区间如何表示 array,from,pi - 1
        // 大于等于pivot的元素所在的区间如何表示 array,pi + 1,to

        // 按照分治算法的思路,使用相同的方式,处理相同性质的问题,只是问题的规模在变小
        quickSortRange(array,from,pi - 1);      // 针对小于等于 pivot 的区间做处理
        quickSortRange(array,pi + 1,to);      // 针对大于等于 pivot 的区间做处理
    }

    private static int partitionMethodA(long[] array,int from,int to){
         //此处省略
    }
}

3.快速排序的性能分析

1.针对partition 这个单独的步骤,它的时间复杂度是多少?

        1)n 所代表的数据规模是什么?   该区间的元素个数        区间中的每个元素都和partition 做过一次(并且只有一次)比较,其他的工作,几乎可以忽略。

时间复杂度:O(n)   空间复杂度:O(1)

2.快排的时间复杂度

把快排看成是一个二叉树

时间复杂度:最好、平均:O(n * log(n) )        最坏:O(n * n)

时间复杂度:partition 的时间复杂度O(n)  * 树的高度

方法的执行过程中,要避开调用栈,调用栈是对一段内存空间的抽象。调用栈开辟的越多,对内存的使用越多。调用栈的多少是快排唯一和 n 有关的数据。所以空间复杂度主要看 这个关系。最终还是表现为二叉树的高度,在log(n) 到 n 之间变化。

空间复杂度:最好、平均:O( log(n) )        最坏:O(n)

空间复杂度:partition 的空间复杂度O(1)  * 树的高度

快排是唯一一个空间复杂度也分情况讨论的排序算法

稳定性:保证不了,因为没有可以在做到O(n)的情况下,还能保证稳定性的 partition 算法。

4.第二种partition

还是分为三种情况,但比第一种的性能略好一些,是一种挖坑。

第二种 partition 的代码:

   private static int partitionMethodB(long[] array,int from,int to){
        long pivot = array[to];
        int left= from;
        int right =to;

        while(left < right){
            while (left < right && array[left] <= pivot){
                left++;
            }
            
            array[right] = array[left];

            while (left < right && array[right] >= pivot){
                right--;
            }
            
            array[left] = array[right];
            
            array[left] = pivot;
        }

        return left;
    }

5.第三种partition

 【from,s) 元素 <= pivot        【s,i)元素 >= pivot        【i,to)元素待比较

i 的遍历范围 【from,to)

array【i】 < pivot:交换【i】和【s】的元素,同时i++;s++;

array【i】 > pivot:i++,s不动

 array【i】 < pivot:交换【i】和【s】的元素,同时i++;s++;

 走到 i = to 时,说明走完了,交换 i 和 s 的值

 第三种 partition 代码实现:

private static int partitionMethodC(long[] array,int from,int to){
        int s = from;
        long pivot = array[to];
        for(int i = from;i < to;i++){   //遍历【from,to)
            if(array[i] < pivot){
                long t = array[i];
                array[i] = array[s];
                array[s] = t;

                s++;
            }
        }
        array[to] = array[s];
        array[s] = pivot;

        return s;
    }

6.partition 进阶

希望完成partition 之后,我们的区间被分成3 部分:【<pivot】【==pivot】【>pivot】

与之前不同的是,把 == pivot 的全部聚集在一起

partition 的过程中,应该 4 各部分:小于pivot、等于pivot、大于pivot和未参与比较的

 array【i】 < pivot:交换【i】和【s】;s++;i++

array【i】 == pivot:i++;

array【i】 > pivot:交换【i】和【g】;g--;i不动

 这里需要返回类型有点不同:需要同时返回两个位置1、< pivot的尾巴 2、> pivot 的开头。

java的方法是无法一次性返回两个值:因此1)专门定义个类用于返回 2)两个位置都是int 型(类型相同)int[] 作为返回类型,我们的 partition 一定返回一个int [] ,元素一定只有两个,一个是小于pivot 的结尾,一个是 大于pivot 的开头。

    // partition 进阶
    private static int[] partitionMethodD(long[] array,int from,int to){
        int s = from;
        int i = from;
        int g = to;
        long pivot = array[to];

        // 只要有元素还没有比较过,循环继续
        while (g - i + 1 > 0){
            if(array[i] == pivot){
                i++;
            } else if(array[i] < pivot){
                swap(array,i,s);
                i++;
                s++;
            } else {
                swap(array,i,g);
                g--;
            }
        }

        return new int[] {s - 1,g + 1};
    }

7.快排的几个常见优化手段

1.前提结论:再待排序区间元素比较少的情况下,快排的速度低于插排。所以,待排序区间的元素低于一个阈值(比如说取一个20),直接使用插排完成排序动作。

代码体现:

    // 【from,to】 是左闭右闭的
    private static void insertSortRange(long[] array,int from,int to){
        int size = to - from;
        for(int i = 0;i < size;i++){
            // 有序区间【from,from + 1】
            // 无序区间【from + i + 1,to】
            // 选中的无序区间的第一个元素 array【from + i + 1】
            long key = array[from + i + 1];
            int j;
            for(j = from + i;j >= from && array[j] > key;j--){
                array[j + 1] = array[j];
            }
            array[j + 1] = key;
        }
    }

    private static void quickSortRange(long[] array,int from,int to){
        // 待排序元素个数<= 1 的情况下,什么都不需要做
        if(to - from + 1 <= 1){
            return;
        }
        
        //待排序元素个数 <= 20 的情况下,使用插排完成排序
        if(to - from + 1 <= 20){
            insertSortRange(array,from,to);
            return;
        }

        int[] indices = partitionMethodD(array,from,to);
        int lessIndex = indices[0];
        int greatIndex = indices[1];

        //小于pivot的区间【from,lessIndex】
        quickSortRange(array,from,lessIndex);
        //大于pivot的区间【greatIndex,to】
        quickSortRange(array,greatIndex,to);
    }

    public static void quickSort(long[] array){
        quickSortRange(array, 0, array.length - 1);
    }

2.partition 上的算法优化(比如,把 == pivot 的提前找出来),其实可以同时选择多个基准值,比如三个p1 < p2 < p3,整个区间就分为了7个部分。

3.选择 pivot 的方式上进行优化

        我们选择是区间的最右边(最左边),最大的缺点是,如果区间已经有序或逆序的情况下,会称为最坏的情况。

        选择基准值的方式,可以优化成:1.随机选取法:每次随机一个位置,把这个位置作为基准值。生成随机数在计算机本身中就是最高成本操作。虽然最坏的情况仍然存在,但落在最坏情况的期望降低了。2.几个数中选择基准值。暂用三数取中法——取区间的最开始、最中间、最结尾分别取这三个数。取这三个数大小上是中间的那个值作为基准值。这样第一就避免了最坏情况的发生,第二有序、逆序避免成为了最坏情况。

6.归并排序

也是一种分治算法的思想,目前介绍的是两路归并的方式

左边的区间还可以再细分,右边也可以再细分。

1.归并排序的基本思路:

long[] array,int from,int to ,这里使用【from,to)左闭右开的形式,代码更简单

情况一:如果待排序区间已经有序(区间内的元素个数 <= 1),则排序操作可以直接返回

情况二:其他

        1.确定中间位置的下标 mid    mid = from + (size / 2)。所以【from,to)的区间,被我们从逻辑上视为是左右两个小区间组成的【from,mid)和 【mid,to)

        2.然后,对左右两个小区间使用相同的方式,进行排序

        3.当左右两个小区间已经有序时,执行合并两个有序区间的操作,得到一个最终的有序大区间

 代码表示:

public class MergeSort {
    public static void mergeSort(long[] array) {
        mergeSortRange(array,0,array.length);
    }

    private static void mergeSortRange(long[] array,int from,int to){
        int size = to - from;
        // 情况1:如果区间内的元素个数小于等于 1 个什么都不用干
        if(size <= 1){
            return;
        }
        // 其他情况
        // 1.找到区间中间位置的下标
        int mid = from + (size / 2);

        //2.优先对左【from,mid)和右【mid,to)两个小区间先进行排序
        //(使用同样的方式处理:分治思想)
        mergeSortRange(array,from,mid);
        mergeSortRange(array,mid,to);

        // 3.有了两个分别各自有序的小区间【from,mid)和【mid,to)
        // 通过一定方式,将【from,mid)[mid,to)合并到【from,to)都是有序的
        // 两个有序数组合并到一起(需要放回原地)
        merge(array,from,mid,to);
    }

    private static void merge(long[] array, int from, int mid, int to) {
        //先计算出来额外空间需要多少个,也就是计算两个空间合起来多大
        int size = to - from;
        //申请一个额外的数组,作为临时保存的地方
        long[] other = new long[size];

        int left = from; //左边小区间下标
        int right = mid;  // 右边小区间下标
        int dest = 0;    // 临时空间下标

        // 只要左右两个小区间还有元素要参与比较
        while (left < mid && right < to){
            if(array[left] <= array[right]){
                other[dest] = array[left];
                dest++;
                left++;
            } else {
                other[dest] = array[right];
                dest++;
                right++;
            }
        }

        //其中一个区间的元素取完了,另一个区间一定还有元素
        // 把剩余的元素都放入other中
        while (left < mid){
            other[dest++] = array[left++];
        }
        while (right < to){
            other[dest++] = array[right++];
        }

        // 把other 中的有序元素,放回array 中,注意下标问题
        for(int i = 0;i < size;i++){
       //array的下标基准是从【from】开始,other 下标的基准是从【0】开始
            array[from + i] = other[i]; 
        }
    }

2.归并排序的性能分析

merge (合并有序区间)操作,时间复杂度:O(n);空间复杂度:O(n)

归并排序时间复杂度:O(n * log(n) )

归并排序空间复杂度:O(n)

稳定性:具备

7. 小结

按照不同的分类标准,划分这些排序。

1.具备稳定性的排序:冒泡、排序、归并

2.平均情况下,执行速度分成两组:

       1)慢:冒泡、插排、选择——排序的元素个数在 10万 级别左右

        2)快:快排、归并、希尔、堆排——个数在 1亿 级别

3.空间角度:

        1)空间复杂度是O(1):冒泡、插排、希尔、选择、堆排

        2)空间复杂度是O(log(n))~ O(n):快排

        3)空间复杂度是O(n):归并

        4)最好情况下(如果数组有序情况下),有两种排序可以达到O(n)的时间复杂度:冒泡、插排

        5)属于减治算法(每次问题规模减少1):冒泡、插排、希尔、选择、堆排

              属于分治算法(把问题分成多个子问题分别处理):快排、归并

        6)外部排序:针对二级存储(硬盘)上的数据进行排序(特点:数据很大。一般认为内存放不下)只有归并支持外部排序。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值