前端JS实现八大排序(含原理及改进)

冒泡排序

原理

两相邻的数依次比较
若要求升序排列 两两比较时前一个数比后一个数大则互换位置
相互比较完一轮最大的数就会到最后面,并且不再参与比较
循环比较 直到比较完成

一张网图
冒泡排序

实现

    var arr =[2,0,1,15,6];
    function bubbleSort(arr){
        for(var i=0;i<arr.length-1;i++){
            for (var j=0;j<arr.length-1-i;j++){
                if(arr[j]>arr[j+1]){
                    var temp =arr[j];
                    arr[j]=arr[j+1];
                    arr[j+1]=temp;
                }
            }
        }
        console.log(arr);
    }
    bubbleSort(arr);//(5) [0, 1, 2, 6, 15]

改进

以上是普通版的冒泡排序,每次排完一轮后又要重头开始,没有其特殊性,那么对于一部分已经排好序的数组来说用此方法无疑增添了没必要的比较环节,因此对冒泡可用以下两种方法进行改进:

  1. 增设final值表示上一轮中最后发生交换的位置,当final=0那么就说明已经排好序了,否则从上次未排好序的位置进行即可。
 var arr =[2,0,1,15,6];
    function bubbleSort(arr){
        var i =arr.length -1;//最开始的位置
        while(i>0){
            var final = 0;//每轮循环开始 标志位归0
            for(var j = 0;j<i;j++){
                if(arr[j]>arr[j+1]){
                    final = j 
                    var temp = arr[j];
                    arr[j]=arr[j+1];
                    arr[j+1]=temp;
                }
            }
            i=final;//当跳出此循环时final之后的已经排序完成
        }
        console.log(arr);
    }
    bubbleSort(arr);//(5) [0, 1, 2, 6, 15]

若不设立确切的标志位,也可以用布尔值表示上一轮是否发生了数据交换,初始设为ture,while(ture)中每一轮比较都将该标志设为false,若发生交换则置false,和上面思路一样只是上面还会改变每一轮排序结束的位置,在一些情况下更加优化。

  1. 每轮排序正冒泡 反冒泡同时进行,每次将排好后最大数向前移一位、最小数往后一位。由于每次排完后都确定了最小值和最大值,这样可以让排序次数减少近一半。
    var arr =[2,0,1,15,6];
    function bubbleSort(arr){
        var high = arr.length-1;
        var low = 0;//最初定义最高位和最低位
        while(low<high){
            for(var i = low ;i<high;i++){
                if(arr[i]>arr[i+1]){
                    var temp =arr[i];
                    arr[i]=arr[i+1];
                    arr[i+1]=temp;
                }
            }
            high--;
            for(var i = high;i>low;i--){
                if(arr[i]<arr[i-1]){
                    var temp=arr[i];
                    arr[i]=arr[i-1];
                    arr[i+1]=temp;               
                }
            }
            low++;
        }
        console.log(arr);
    }
    bubbleSort(arr);//(5) [0, 1, 2, 6, 15]

快速排序

原理

快排的基本思想就是分治法。
(1)在待排序数组中,选择一个元素作为"基准"(pivot)。
(2)所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边。
(3)对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个或零个元素为止。(从这一步可以看出递归的思想)

实现

此处提供两种比较常见的实现方式
第一种比较好理解 就是原理直接的实现方法
1.

    var arr =[2,0,1,15,6];
    function quickSort(arr){
        //递归结束条件
        if(arr.length<=1){
            return arr;
        }
        //选择"基准"(pivot),并将其与原数组分离,再定义两个空数组,用来存放一左一右的两个子集。
        var pivotIndex = Math.floor(arr.length / 2) ;
        var pivot = arr.splice(pivotIndex, 1)[0];
        var left = [];
        var right = [];
        //遍历数组 小的放left 其余放right
        for (var i = 0; i < arr.length; i++){
            if (arr[i] < pivot) {
                left.push(arr[i]);
            } else {
                right.push(arr[i]);
            }
        }
        //递归
        return quickSort(left).concat([pivot], quickSort(right));
    }
    console.log(quickSort(arr));//(5) [0, 1, 2, 6, 15]

2.仍是使用递归,原出处这里,下面是根据自己的理解写的注释,原处可能更详细一些

    var arr = [2, 0, 1, 15, 6];
    function quickSort(arr, left, right) {
        /*
         * left为需要数组中参与排序的起始点;right为数组中参与排序的终止点;
         * left如果有传数字就为left,没传参则为0;
         * right如果有传参就为right,没传参则为len-1;
         * 有传参可能会部分排序可能不会排序,没传参默认排序整个数组;
         * partitionIndex为分组界限;
         */
        var len = arr.length,
            partitionIndex,
            left = typeof left !== 'number' ? 0 : left,
            right = typeof right !== 'number' ? len - 1 : right;

        //进行递归条件
        if (left < right) {
            // 得出分界索引,函数具体实现在下方
            partitionIndex = partition(arr, left, right);
            // 数组中小于arr[partitionIndex]的部分再次使用quickSort排序;
            quickSort(arr, left, partitionIndex - 1);
            // 数组中大于arr[partitionIndex]的部分再次使用quickSort排序;
            quickSort(arr, partitionIndex + 1, right);
        }
        // 递归终止,返回本身,此时已排好序
        return arr;
    }

    //此方法用于得出分界索引 并改变数组排序
    function partition(arr, left, right) {
        /*
         * pivotIndex为参照位,index位上的值都与其比较
         * 最终实现参照位的数左边都是小数 右边都是大数
         */
        var pivotIndex = left,
            index = pivotIndex + 1;
        // for循环从参照物一个元素arr[index]开始一直比较到子数组结束arr[right]
        for (var i = index; i <= right; i++) {
            // 循环中如果有任何小于参照物的,就将他交换到index的位置,然后index向右移动到下一个位置;
            //得到index所经过的位置 其上的数都比参照数小
            if (arr[i] < arr[pivotIndex]) {
                swap(arr, i, index);
                index++;
            }
        }
        /*
         * 因为每次都是交换完后index移动到下一个位置,所以在循环结束时,index仍为待交换的位置;
         * 此时索引pivotIndex+1到index-1的元素都小于参照物
         */

        // 交换pivotIndex和index-1索引的值之后得到index-1位左边的数都是小数
        swap(arr, pivotIndex, index - 1);
        // 返回index-1作为拆分子数组的分界线;
        return index - 1;
    }
    //交换arr中索引为i j的两个数
    function swap(arr, i, j) {
        var temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    console.log(quickSort(arr));//(5) [0, 1, 2, 6, 15]

改进

用“三路快排”的方法将数组分成三段, 即小于基准元素、 等于基准元素和大于基准元素, 这样可以比较高效的处理数组中存在相同元素的情况,其它特征与快速排序基本相同。

    var arr3 = [2, 1, 6, 5, 7, 16, 12, 17, 11, 10];
    function quickSort3(arr) {
        if (arr.length == 0) {
            return [];
        }
        var left = [];
        var center = [];
        var right = [];
        var pivot = arr[0];
        for (var i = 0; i < arr.length; i++) {
            if (arr[i] < pivot) {
                left.push(arr[i]);
            } else if (arr[i] == pivot) {
                center.push(arr[i]);
            } else {
                right.push(arr[i]);
            }
        }
        return [...quickSort3(left), ...center, ...quickSort3(right)];
    }
    console.log(quickSort3(arr3));//(10) [1, 2, 5, 6, 7, 10, 11, 12, 16, 17]

另外,如果考虑到优化的话,基准值的选取也有讲究,贴个链接 快排优化 这位大佬也写得很详细

直接插入排序

原理

将无序序列插入有序序列中,每次得到一个新的、记录数加一的新的有序序列。最开始则规定arr[0]为有序序列

实现

    var arr = [2, 0, 1, 15, 6];
    function insertSort(arr){
        //i=1开始插入
        for(var i = 1; i<arr.length;i++){
            var temp = arr[i];//选择临时变量保存该数,这样当排序改变时我们比较的还是初始这个i对应的值
            //符合条件 进行插入
           if(temp<arr[i-1]){
               for(var j=i-1;arr[j]>temp;j--){
                   arr[j+1]=arr[j];//每个都往后挪一位
               }
               arr[j+1]=temp;//将新数加入排好序的数组
           }
        }
        return arr;
    }
    console.log(insertSort(arr)); //(5) [0, 1, 2, 6, 15]

改进

二分插入排序:相较于上面的排序区别在于在寻找插入位置时采用二分法的思想

    var arr = [2, 0, 1, 15, 6];
    function insertSort(arr) {
        for (var i=1;i<arr.length; i++) {
            var temp=arr[i],
            left=0,
            right=i-1;
			//二分查找
            while(left<=right){ 
              var mid= parseInt((left+right)/2); //中间位
              if(temp<arr[mid]){ //当前值比中间值小 则在左边继续寻找   
                right = mid-1;
              }else{
                left=mid+1;//当前值比中间值大 则在右边继续寻找
              }
            }              
            for(var j=i-1;j>=left;j--){
              arr[j+1]=arr[j];
            }
            arr[left]=temp;
          }
        return arr;
     }
     console.log(insertSort(arr)); //(5) [0, 1, 2, 6, 15]

以及下方的希尔排序也能够优化直接插入排序,一定程度上也能减少直接插入的工作量。

希尔排序

原理

希尔排序又叫缩小增量排序。将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

实现

var arr =[2,0,1,15,6];
function shellSort(arr) {
    var len = arr.length;
    // 增量每次都/2
    for (var step = Math.floor(len / 2); step> 0; step = Math.floor(step / 2)) {
        //从增量那组开始进行插入排序,直至末尾
        for (var i = step; i < len; i++) {
            var j = i;
            var temp = arr[i];
            // j - step就是代表与它同组隔壁的元素 就相当于和需要比较的前一个数相比
            //while中就是插入排序的思想
            while (j - step >= 0 && temp < arr[j - step]) {
                 arr[j] = arr[j - step];
                 j = j - step;
            }
            arr[j] = temp;
        }
    }
    return arr;
}
console.log(shellSort(arr)); //(5) [0, 1, 2, 6, 15] 

改进

一些例子中希尔排序甚至比直接插入排序更慢,那是因为我们选取的增量(就是上述的step)是无差异的。而为了给希尔排序提供更有效的增量方式,就应该保证分组粗调没有盲区,每一轮的增量需要彼此“互质”,也就是没有除1之外的公约数。
于是,人们相继提出了很多种增量方式,其中最具代表性的是Hibbard增量和Sedgewick增量。
Hibbard的增量序列如下:
1,3,7,15…
通项公式 2^k-1
利用此种增量方式的希尔排序,最坏时间复杂度是O(n^(3/2))
Sedgewick的增量序列如下:
1, 5, 19, 41, 109…
通项公式 94^k - 92^k + 1 或者 4^k - 3*2^k + 1
利用此种增量方式的希尔排序,最坏时间复杂度是O(n^(4/3))
关于这两种增量方式的时间复杂度,有些需要很复杂的数学证明,有些是人们的大致猜想,大家暂时不用纠结。

此处改进参考漫画解释希尔排序 如果难以理解代码可以看这位大佬画的漫画,小仓鼠也很可爱(๑‾ ꇴ ‾๑)

简单选择排序

原理

在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。

实现

var arr =[2,0,1,15,6];
function selectSort(arr) {
    var len = arr.length;
    var temp;
    for (var i = 0; i < len - 1; i++) {
        for (var j = i + 1; j < len; j++) {
            if (arr[j] < arr[i]) {
                temp = arr[j];
                arr[j] = arr[i];
                arr[i] = temp;
            }
        }
    }
    return arr;
}

console.log(selectSort(arr)); //(5) [0, 1, 2, 6, 15] 

改进

类似前面一些排序方法中存在的问题,简单选择排序每一趟只能选出一个最大(小)值,因此我们可以考虑每趟同时选出最大和最小值。
二元选择排序

var arr =[2,0,1,15,6];
function selectSort(arr) {
    var len = arr.length;
    var temp,min,max;
    //只有len/2趟
    for (var i = 0; i < Math.floor(len/2); i++) {
        //最开始最大最小值都设在起点
        min=i;
        max=i;
        for (var j = i; j < len-i; j++) {
            if(arr[j]>arr[max]){
                max=j;
                continue;
            }
            if(arr[j]<arr[min]){
                min=j;
            }
        }
        //一轮结束后 找到本趟最大 最小数
        //分别按序放到最前i 和最后len-i-1
        temp = arr[min];
        arr[min]=arr[i];
        arr[i]=temp;
        temp = arr[max];
        arr[max]=arr[len-i-1];
        arr[len-i-1]=temp;
    }
    return arr;
}

console.log(selectSort(arr)); //(5) [0, 1, 2, 6, 15]

堆排序

原理

堆排序是一种树形选择排序,是对直接选择排序的有效改进。
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。
初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程就为堆排序。

实现

var arr =[2,0,1,15,6];
var len;// len设为全局变量

//建立大顶堆函数
function buildMaxHeap(arr) {
    len = arr.length;
    //Math.floor((len-1)/2) 得到最后一个有孩子的堆顶
    for (var i = Math.floor((len-1)/2); i >= 0; i--) {
        heapify(arr, i);
    }
}
//调整堆函数  i为需要调整的子堆堆顶元素索引
function heapify(arr, i) {     // 堆调整
    var left = 2*i + 1,
        right = 2*i + 2,
        largest = i;//假设堆顶是子堆最大元素

    if (left < len && arr[left] > arr[largest]) {
        largest = left;
    }
    if (right < len && arr[right] > arr[largest]) {
        largest = right;
    }
 
    if (largest != i) {
        swap(arr, i, largest); //将孩子中的最大元素和子堆堆顶交换
        heapify(arr, largest);//开始调整largest作为根结点的子堆
    }
}

//交换arr中i j位置的数
function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
//堆排序
function heapSort(arr) {
    //先构造大顶堆
    buildMaxHeap(arr);
    //接下来进行堆排序
    for (var i = arr.length - 1; i > 0; i--) {
        swap(arr, 0, i);
        len--;
        heapify(arr, 0);
    }
    return arr;
}

console.log(heapSort(arr)); //(5) [0, 1, 2, 6, 15] 

改进

由于堆排序本身就是对直接选择排序的改进,在此就不考虑堆排序的进一步改进

归并排序

原理

将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的,然后再把有序子序列合并为整体有序序列。

实现

归并也是分治的思想,先将数组分为最小有序单元(只有一个数),再两两合并,使得最终数组有序。这里提供两种方法:
1.

 var arr =[2,0,1,15,6];
    function mergeSort(arr) {
        if (arr.length == 1) {
            return arr;
        } else {
            var mid = parseInt(arr.length / 2);
            var left = arr.slice(0, mid);
            var right = arr.slice(mid);
            return merge(mergeSort(left), mergeSort(right));
        }
    }
    //合并的过程
    function merge(left, right) {
        var temp = new Array();
        while (left.length > 0 && right.length > 0) {
            if (left[0] < right[0]) {
                temp.push(left.shift());//取出第一个push进去
            } else {
                temp.push(right.shift());
            }
        }
        //left 或者 right一方已经全部装入,则另一方剩下的装入temp
        return temp.concat(left, right);
    }

console.log(mergeSort(arr)); //(5) [0, 1, 2, 6, 15] 
var arr =[2,0,1,15,6];
var temp = new Array();
mergeSort(arr, 0, arr.length - 1, temp);
console.log(arr); //(5) [0, 1, 2, 6, 15]
//分治思想
function mergeSort(arr, left, right, temp) {
    if (left < right) {
        let mid = Math.floor((left + right) / 2);
        //向左递归进行分解
        mergeSort(arr, left, mid, temp);
        //向右递归进行分解
        mergeSort(arr, mid + 1, right, temp);
        //合并
        merge(arr, left, mid, right, temp);
    }
}

//合并的方法
function merge(arr, left, mid, right, temp) {
    var i = left;
    var j = mid + 1;
    var t = 0;
    //先把左右两边有序的数组按规则传入temp 类比第一个方法的思想
    while (i <= mid && j <= right) {
        if (arr[i] <= arr[j]) {
            temp[t] = arr[i];
            t++;
            i++;
        } else {
            temp[t] = arr[j];
            t++;
            j++;
        }
    }
    //把有剩余数据的一边一次全部填充到temp
    while (i <= mid) {
        temp[t] = arr[i];
        t++;
        i++;
    }
    while (j <= right) {
        temp[t] = arr[j];
        t++;
        j++;
    }
    //将temp排好序的数字存入arr中
    t = 0;
    let tempLeft = left;
    while (tempLeft <= right) {
        arr[tempLeft] = temp[t];
        t++;
        tempLeft++;
    }
}

改进

归并算法原本针对两个有序表形成一个新的有序表的算法,那么只需要用到上面的合并函数(即merge函数),后面拓展到对任一无序表进行排序,就可采用上诉二路归并的办法。若要基于上述排序进一步优化则可以从:

  1. 对小规模子数组使用插入排序
  2. 测试待排序序列中左右半边是否已有序(如果左右半边出现有序的情况就可以减少不必要的“分”“治”操作)
  3. 去除原数组序列到辅助数组的拷贝

改进参照【算法】归并排序算法的编码和优化 里面有很详细的介绍

基数排序

原理

数排序是使用空间换时间的经典算法
将整数按位数切割成不同的数字,然后按每个位数分别比较。
这张动图很直观了
在这里插入图片描述
总结下来就是三步:
取得数组中的最大数,并取得位数;
arr为原始数组,从最低位开始取每个位组成radix数组;
对radix进行计数排序(利用计数排序适用于小范围数的特点)

实现

    var arr = [2, 0, 1, 15, 6];
    function radixSort(arr) {
        //定义二维数组 一维表示桶 装每位的数字
        let bucket = new Array(10);//可以理解为横向 桶的个数
        for (let i = 0; i < bucket.length; i++) {
            bucket[i] = new Array(arr.length);//可以理解为纵向 桶的深度
        }
        //定义一维数组来记录每个桶的每次放入的数据个数
        //bucketElementCounts[0]记录bucket[0]桶的放入数据个数
        let buckeElementCounts = new Array(10).fill(0);//全置0
        //得到数组中最大数
        let max = arr[0];
        for (let i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i]
            }
        }
        //得到最大是几位数
        let maxLength = (max + '').length;
        //外层for循环 :从个位开始拿 然后开始排序 直到最高位
        for (let i = 0, n = 1; i < maxLength; i++, n = n * 10) {
            //每一轮,对每个元素的各个位数进行排序处理,
            //第一次是个位,第二次是十位,第三次是百位
            //内层循环 遍历数组每个数 存入对应桶中
            for (let j = 0; j < arr.length; j++) {
                //取出每个元素的当前排序需要存入位的值
                let digitOfElement = Math.floor(arr[j] / n) % 10;
                //第digitOfElement号桶 第buckeElementCounts[digitOfElement]个位置
                bucket[digitOfElement][buckeElementCounts[digitOfElement]] = arr[j];
                buckeElementCounts[digitOfElement]++;
            }
            //按桶从左到右 桶中从下到上的顺序存入原数组
            //index 为原数组下标
            let index = 0;
            //遍历每桶,并将桶中的数据放入原数组
            for (let k = 0; k < buckeElementCounts.length; k++) {
                //如果桶中有数据,才放入原数组
                if (buckeElementCounts[k] !== 0) {
                    //循环该桶即第k个桶,即第k个一维数组,放入
                    for (let l = 0; l < buckeElementCounts[k]; l++) { 
                        //取出第k号桶第l个元素放入arr
                        arr[index] = bucket[k][l];
                        //arr下标后移
                        index++;
                    }
                    //每轮处理后,下标要清0!!!
                    buckeElementCounts[k] = 0;
                }
            }
        }
    }
    
radixSort(arr);
console.log(arr);//[0, 1, 2, 6, 15]

动图及实现参考 基数排序

改进

基数排序对于含有负数存在的数组是无法正常排序的。
我的想法是(如有不严谨之处欢迎指正)可以给每个数加上最小值的绝对值,排完序后再减去即可。

    var arr = [20, -10, 0, 10, -6];
    //作全局变量
    let min = 0;
    function radixSort(arr) {
        //定义二维数组 一维表示桶 装每位的数字
        let bucket = new Array(10);//可以理解为横向 桶的个数
        for (let i = 0; i < bucket.length; i++) {
            bucket[i] = new Array(arr.length);//可以理解为纵向 桶的深度
        }
        //定义一维数组来记录每个桶的每次放入的数据个数
        //bucketElementCounts[0]记录bucket[0]桶的放入数据个数
        let buckeElementCounts = new Array(10).fill(0);//全置0
        initialization(arr);
        //得到数组中最大数
        let max = arr[0];
        for (let i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i]
            }
        }
        //得到最大是几位数
        let maxLength = (max + '').length;
        //外层for循环 :从个位开始拿 然后开始排序 直到最高位
        for (let i = 0, n = 1; i < maxLength; i++, n = n * 10) {
            //每一轮,对每个元素的各个位数进行排序处理,
            //第一次是个位,第二次是十位,第三次是百位
            //内层循环 遍历数组每个数 存入对应桶中
            for (let j = 0; j < arr.length; j++) {
                //取出每个元素的当前排序需要存入位的值
                let digitOfElement = Math.floor(arr[j] / n) % 10;
                //第digitOfElement号桶 第buckeElementCounts[digitOfElement]个位置
                bucket[digitOfElement][buckeElementCounts[digitOfElement]] = arr[j];
                buckeElementCounts[digitOfElement]++;
            }
            //按桶从左到右 桶中从下到上的顺序存入原数组
            //index 为原数组下标
            let index = 0;
            //遍历每桶,并将桶中的数据放入原数组
            for (let k = 0; k < buckeElementCounts.length; k++) {
                //如果桶中有数据,才放入原数组
                if (buckeElementCounts[k] !== 0) {
                    //循环该桶即第k个桶,即第k个一维数组,放入
                    for (let l = 0; l < buckeElementCounts[k]; l++) { 
                        //取出第k号桶第l个元素放入arr
                        arr[index] = bucket[k][l];
                        //arr下标后移
                        index++;
                    }
                    //每轮处理后,下标要清0!!!
                    buckeElementCounts[k] = 0;
                }
            }
        }
        revivification(arr);
    }
//初始化数组
function initialization(arr){
    for(let i = 0;i<arr.length;i++){
        if(arr[i]<min){
            min=arr[i];
        }
    }
    if(min != 0){
        for(let i =0;i<arr.length;i++){
        arr[i] += Math.abs(min);
        }
    }
}
//还原数组
function revivification(arr){
    if(min!=0){
        for(let i =0;i<arr.length;i++){
        arr[i] -= Math.abs(min);
        }
    }
}
radixSort(arr);
console.log(arr);//(5) [-10, -6, 0, 10, 20]
  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值