Java排序算法

java在内存级别的排序

大致可以分成8类排序

冒泡排序
选择排序
插入排序
希尔排序
归并排序
快速排序
堆排序
桶排序


各种排序的时间复杂度

在这里插入图片描述

1. 冒泡排序

思路:

每一次遍历将最大的那个放到没有排序好的最后,不满足采用交换的方式
比如:
第一次:将最大的数放到最后的位置
第二次:将 0 - (n-1)中最大的数放到 n-1的位置

假如排序的数为 : 5 , 2, -10, 3
第一次排序 :
(1)2, 5, -10, 3 // 因为 2 小于 5 进行交换
(2)2, -10, 5, 3 // -10 小于 5进行交换
(3)2, -10, 3, 5 // 3 小于 5 进行交换
第一轮排序完:最大值5到最后


第二轮排序: 最后一个排好了 不需要排序
(1)-10, 2, 3, 5 // -10小于 2进行交换
(2)-10, 2, 3, 5 // 3 大于2无序排序
第二轮排序完:第二大的值放到了倒数第二位


第三轮排序:
(1)-10, 2, 3, 5 // 2 大于 -10 无序交换
第三轮排序完:就是有序的了


结论:
1、N 个 数需要排序 N - 1次
2、每一次排序都是找到那个区间中最大的值

public static int[] bubblingSort(int[] arr) {
    // 遍历 n - 1次
    for (int i = arr.length - 1; i > 0; i--) {
        // 临近的进行比较 如果不满足进行交换
        for (int j = 0; j < i; j++) {
            if (arr[j] > arr[j + 1]) {
                // 进行交换
                swap(arr, j, j + 1);
            }
        }
    }
    return arr;
}

// 这里可以使用异或的方式
// 但是需要注意 i 和 j 之间的数不可以在同一个内存中
public static void swap(int[] arr, int i, int j) {
    // 使用异或的方式进行交换
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}
// 正常的交换
public static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

2. 选择排序

思路

每次找到最小的数放到相应的值
比如:
第一轮 : 从 1 - n 中找到最小的值放到 1 号的位置
第二轮 : 从 2 - n 中找到最小的值放到 2 号的位置

public static int[] selectSort(int[] arr) {
    // 进行去掉不需要进行排序的数据
    if (arr == null || arr.length < 2) {
        return arr;
    }
    // 选择排序
    for (int i = 0; i < arr.length; i++) {
        // 将第一个值先比作最小的值 这个是假设
        int minIndex = i;
        // 再用一个for找到 i - n中最小的值进行交换
        for (int j = i + 1; j < arr.length; j++) {
            minIndex = arr[j] < arr[minIndex] ? j : minIndex;
        }
        // 到这里minIndex指向的就是最小的值
        // 进行交换 这里可以进行优化 只有不相同的时候再进行交换
        int temp = arr[j];
        arr[j] = arr[minIndex];
        arr[minIndex] = temp;
    }
}


3. 插入排序

思路

第一次保证 0 - 0 是有序的
第二次保证 0 - 1 是有序的
第三层保证 0 - 2 是有序的
第n层保证 0 - (n-1)是有序的
如果发现两个的顺序是不满足的,就进行交换

public static void insertSort(int[] arr) {
    // 插入排序
    // 因为 0-0默认就是有序的 不需要进行排序
    // 这一个for循环 每一次循环保证 0 - i 是有序的
    for (int i = 1; i < arr.length; i++) {
        // 第二次循环 保证是有序的
        // j代表当前数据的前一个数据
        for (int j = i - 1; j >= 0 && arr[j + 1] < arr[j]; j--) {
            // arr[j + 1] < arr[j] 说明后面的一个数小于前面的一个数
            // 进行交换
            swap(arr, j, j + 1);
        }
    }
}
public static void swap(int[] arr, int left, int right) {
    int temp = arr[left];
    arr[left] = arr[right];
    arr[right] = temp;
}

4. 归并排序

介绍:

  • 是一种利用归并的思想实现的一种排序的方式
  • 不断的将一个数组分成两边,只需要保证两边相互是有序的, 然后进行比较两边将小的放到一个额外的数组中即可
  • 不断的递归,最后进行汇总就是有序的了
    在这里插入图片描述
public static void margeSort(int[] arr, int l, int r) {
    // 进行递归
    // 当左边等于右边的时候 可以直接返回
    if (l == r) {
        return;
    }
    // 当不满足上面的时候进行递归
    // 获取中间位置的数
    int mid = l + ((r - l) >> 1);
    // 先不断的向左走
    margeSort(arr, l, mid);
    // 不断的向右走
    margeSort(arr, mid + 1, r);
    // 当走到无法向下面走的时候进行合并
    marge(arr, l, mid, r)
}

/**
     * 进行合并的方法 将数组分成两个部分进行比较
     *
     * @param arr 数组
     * @param l   左边指针
     * @param mid 中间值
     * @param r   右边值
     */
public static void marge(int[] arr, int l, int mid, int r) {
    // 准备一个数组进行存储数据
    int[] temp = new int[r - l + 1];
    // 这个指针给temp进行使用
    int i = 0;
    // 准备两个指针
    int p1 = l;
    int p2 = mid + 1;
    // 将两个数组进行比较小的放入到temp数组中
    while (p1 <= mid && p2 <= r) {
        // 说明两个都没有越界可以进行比较
        // 因为两边是有序的
        // 所以将小的放到temp中
        temp[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
    }
    // 出了这个while 有一个是没有遍历完的
    while (p1 <= mid) {
        // p1没有遍历完
        temp[i++] = arr[p1++];
    }
    while (p2 < r) {
        temp[i++] = arr[p2++];
    }
    // 将排序好的值放入到arr中
    for (int j = 0; j < temp.length; j++) {
        arr[l + j] = temp[j];
    }
}

时间复杂度为 O(NlogN) 空间复杂度: O(N)


小和问题

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。

例子:[1,3,4,2,5] 1左边比1小的数,没有; 3左边比3小的数,1; 4左边比4小的数,1、3; 2左边比2小的数,1;5左边比5小的数,1、3、4、2; 所以小和为1+1+3+1+1+3+4+2=16

思路1:

1、使用暴力匹配法进行遍历,从数组的后面向前面进行遍历,遇到比这个数小的就将其进行相加,放入到数组的指定的位置,最后进行数组的值进行相加就可以。

2、不妨换一种思路,从前面开始进行遍历,如果遇到比自己大的说明自己是这个值的一个小和,进行相加,最后得到的值就是数组的小和。

3、关于第二种方案,可以使用归并排序进行解决,因为归并排序不会重复计算和多计算,当左边的值小于右边的将左边的值放入到备用的数组中,进行记录这个数。

4、需要注意的事项是当两边的值是相同的时候,我们需要将右边的值放入到数组中。

// 小和问题
public static int smallSum(int[] arr, int l, int r) {
    if (l == r) {
        return 0;
    }
    // 获取中间的值
    int mid = l + ((r - l) >> 1);
    return smallSum(arr, l, mid) + smallSum(arr, mid + 1, r)
        + marge(arr, l, mid, r);
}

public static int marge(int[] arr, int l, int mid, int r) {
    // 准备一个数组进行存储数据
    int[] temp = new int[r - l + 1];
    // 这个指针给temp进行使用
    int i = 0;
    // 这个就是计算小和的
    int res = 0;
    // 准备两个指针
    int p1 = l;
    int p2 = mid + 1;
    // 将两个数组进行比较小的放入到temp数组中
    while (p1 <= mid && p2 <= r) {
        // 说明两个都没有越界可以进行比较
        // 因为两边是有序的
        // 所以将小的放到temp中
        // 如果左边的值小于右边的值
        // 将左边的值 存入到res小和中 并且判断有这个
        res += arr[p1] < arr[p2] ? arr[p1] * (r - p2 + 1) : 0;
        temp[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
    }
    // 出了这个while 有一个是没有遍历完的
    while (p1 <= mid) {
        // p1没有遍历完
        temp[i++] = arr[p1++];
    }
    while (p2 <= r) {
        temp[i++] = arr[p2++];
    }
    // 将排序好的值放入到arr中
    for (int j = 0; j < temp.length; j++) {
        arr[l + j] = temp[j];
    }
    return res;
}

逆序对问题

问题:在一个数组中,左边的数比右边的要大,则这两个数构成一个逆序对,打印逆序对的的数量

比如:

[1, 3, 4, 2, 6] 的逆序对的情况 :{4,2} {3,2}

思路:使用归并排序和上面的小和的问题是一致的

public static void reverseOrder(int[] arr, int l, int r) {
    if (l == r) {
        return;
    }
    // 取中间位置
    int mid = l + ((r - l) >> 1);
    // 向左递归
    reverseOrder(arr, l, mid);
    // 向右递归
    reverseOrder(arr, mid + 1, r);
    // 进行合并
    marge(arr, l, mid, r);
}

public static void marge(int[] arr, int l, int mid, int r) {
    // 创建两个指针进行移动
    int p1 = l;
    int p2 = mid + 1;
    // 创建一个临时的数组
    int[] temp = new int[r - l + 1];
    int i = 0;
    while (p1 <= mid && p2 <= r) {
        // 当左边的数大于右边的数
        count += arr[p1] > arr[p2] ? (mid - p1 + 1) : 0;
        // 进行排序
        temp[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
    }
    while (p1 <= mid) {
        temp[i++] = arr[p1++];
    }
    while (p2 <= r) {
        temp[i++] = arr[p2++];
    }
    // 进行数组代替
    for (int j = 0; j < temp.length; j++) {
        arr[l + j] = temp[j];
    }
}

5. 荷兰国旗问题

给定一个数组,和一个数num,请把小于等于num的数放到数组的左边,大于num的数放到数组的右边。要求额外的空间复杂度为O(1),时间复杂度为O(N)

思路:

1、将数组分成两个区域,刚开始小于num的数的区域为0

2、当这个数小于num的时候,和这个区域的后一个进行交换,将这个区域向后移动

3、当这个数大于等于num的,自己向下移动

public static void netherlands1(int[] arr, int nums) {
    // 准备一个指针指向小于区域的左边
    int left = -1;
    // 指向arr的指针
    int index = 0;
    while (index < arr.length) {
        if (arr[index] < nums) {
            // 这个数小于nums的情况 将这个区域向后移动 交换
            swap(arr, ++left, index);
        }
        index++;
    }
}

public static void swap(int[] arr, int l, int r) {
    int temp = arr[l];
    arr[l] = arr[r];
    arr[r] = temp;
}

进阶问题,在之前的问题中,小于num的数放在左边,等于num的数放在中间,大于num的数放在右边

思路:

1、将数组分成三个区域,刚开始小于的指向-1, 大于的指向最后

2、当这个数小于num的时候,左边的区域进行扩展,即这个与小于区域的最后一个进行交换

3、当这个数等于num的时候,直接向下移动

4、当这个数大于num的时候,大于的区域进行左扩进行交换,但是这个值不需要向后移动

5、结束的条件是当这个值和右扩到一起

public static void netherlands2(int[] arr, int nums) {
    // 左区域
    int left = -1;
    // 右区域
    int right = arr.length;
    // 移动的指针
    int index = 0;
    while (index < right) {
        if (arr[index] < nums) {
            swap(arr, ++left, index++);
        } else if (arr[index] > nums) {
            swap(arr, index, --right);
        } else {
            index++;
        }
    }

}
public static void swap(int[] arr, int l, int r) {
    int temp = arr[l];
    arr[l] = arr[r];
    arr[r] = temp;
}

6. 快排排序

**快排1.0:**使用上面的荷兰国旗问题的第一个题目,只是是每次将最后的一个数作为比较的值,将大于这个值放到右边,小于这个值的放到左边,这样这个的值在整体排序中就确定了,然后进行左右的递归,每次确定一个值,最后一定会是有序的

**快排2.0:**和上面问题的区别是采用的荷兰国旗问题进行解决问题,即每次将小于这个数的放到左边,将大于这个数的放到右边,将等于这个值的放到中间,这样的情况是一次性将几个值进行了排序,效率稍微高一点,但是差别差不多。

上面的两种算法,时间复杂度都是O(N^2)的,当 出现一个已经排序好的情况时间复杂度就是 O(N^2)的情况的

**快排3.0:**和2.0的区别是每次不是采用最后的值作为比较的值,而是随机在里面取一个数和最后的一个值进行交换,然后使用交换过的值进行比较,这样在数学的概率中这个算法的时间复杂度为O(N * logN)

// 思路采用的荷兰国旗问题
public static void quickSort(int[] arr, int l, int r) {
    if (arr.length <= 1) {
        // 无序进行排序
        return;
    }
    // 满足条件进行排序
    if (l < r) {
        // 获取随机数和最后的一个值进行交换
        swap(arr, (int) (Math.random() * (r - l + 1)), r);
        // 进行分片即就是荷兰国旗问题
        // 放回的值就是已经排序好的值的索引
        int[] partition = partition(arr, l, r);
        // 进行递归
        quickSort(arr, l, partition[0] - 1);
        quickSort(arr, partition[1] + 1, r);
    }
}

// 返回就是确定值的左右范围的值
public static int[] partition(int[] arr, int L, int R) {
    // 确定左边的最大值的索引 进行移动的
    int l = L - 1;
    // 确定右边界的最小值的索引 进行移动的
    int r = R;
    while (L < r) {
        // 如果遍历到的值小于最小的值
        if (arr[L] < arr[R]) {
            // 将遍历到值和确定左边的区域的地方进行交换
            swap(arr, L++, ++l);
        } else if (arr[L] > arr[R]) {
            swap(arr, L, --r);
        } else {
            L++;
        }
    }
    // 将最后的一个值和最大区域的最前面的值进行交换
    swap(arr, r, R);
    // 返回最大值和最小值的临界值
    // l + 1指向的就是第一个值的坐标
    // 因为进行了交换 r就是最后一个值的坐标
    return new int[]{l + 1, r};
}

// 交换的方法
public static void swap(int[] arr, int l, int r) {
    int temp = arr[l];
    arr[l] = arr[r];
    arr[r] = temp;
}

7. 堆排序

前置知识:

1、堆是一种完全二叉树,完全二叉树的概念是只有最后一层不是满的时候,只需要按照先的排的左边即满足完全二叉树

2、将堆存储到数组中的规律,比如当前存放的节点n的左结点的位置为2 * n + 1, 右节点的坐标为

2 * n + 2当前节点的父节点为 (n - 1) / 2

3、大顶堆的规则是当前树的最大值当前树的根节点,小顶堆的规则是当前树的最小值在当前树的跟节点

堆的两个重要的操作

1、heapInsert:将存入的值变成一个大顶堆,当需要将一个值加入到大顶堆中,并且这个堆还是大顶堆的情况下进行使用。

// 将一个值存入到大顶堆中 并且保持还是大顶堆的情况
// index向上移动 表明当前存入的值存放到数组的index的位置
public static void heapInsert(int[] arr, int index) {
    // 如果当前的数大于其父类的数 将其进行交换
    // 这个while的条件 ① 当前位置的数大于父节点的数
    //               ② 当index到0的时候 arr[0] > arr[0] 也不满足
    while (arr[index] > arr[(index - 1) / 2]) {
        // 交换
        swap(arr, index, (index - 1) / 2);
        // index进行移动 继续和父节点进行比较
        index = (index - 1) / 2;
    }
}

public static void swap(int[] arr, int x, int y) {
    int temp = arr[x];
    arr[x] = arr[y];
    arr[y] = temp;
}

2、heapify:当需要取出大顶堆中最大的值即就是索引为0的位置,即需要保证后面的数还是大顶堆的情况。

思路:

1、当最前面的值就是最大的值,将这个值拿出来后,将最后的一个数复制到最前面的数

2、当复制过来的数先下进行比较,如果有子结点的值大于该节点,即进行交换

/**
     * 将一个值向下进行放值 继续保持是大顶堆的情况
     *
     * @param arr      数组
     * @param index    当前值所在的索引
     * @param heapSize 数组的前几个构成大顶堆
     */
public static void heapify(int[] arr, int index, int heapSize) {
    // 找到左结点
    int leaf = (2 * index) + 1;
    while (leaf < heapSize) {
        // leaf < heapSize 说明存在左结点
        // 判断是左结点大还是右节点大 保留大的节点
        int max = ((leaf + 1) < heapSize
                   && arr[leaf + 1] > arr[leaf]) ? leaf + 1 : leaf;
        // 和当前的节点进行比较保留大的节点
        max = arr[index] > arr[max] ? index : max;
        if (max == index) {
            // 说明当前节点大于子结点 因为下面的满足大顶堆的情况
            break;
        }
        // 说明左右节点大于根节点 交换
        swap(arr, index, max);
        // 将index进行移动
        index = max;
        // 左结点重新定义
        leaf = 2 * index + 1;
    }
}

public static void swap(int[] arr, int x, int y) {
    int temp = arr[x];
    arr[x] = arr[y];
    arr[y] = temp;
}

堆排序的code:

// 进行排序
public static void heapSort(int[] arr) {
    // 去掉错误的数据
    if (arr == null || arr.length < 2) {
        return;
    }
    // 将arr这个变成一个大顶堆
    //        for (int i = 0; i < arr.length; i++) {
    //            heapInsert(arr, i);
    //        }
    // 这里也可以使用heapify解决问题
    // 即分成一个一个的大顶堆最后汇聚成最大的大顶堆
    for (int i = arr.length - 1; i >= 0; i--) {
        heapify(arr, i, arr.length);
    }
    // 记录堆的大小
    int heapSize = arr.length;
    // 将0和最后一个进行交换
    swap(arr, 0, --heapSize);
    // 进行遍历进行交换
    while (heapSize > 0) {
        // 将0号位置进行重新排序
        heapify(arr, 0, heapSize);
        // 重新成大顶堆进行交换
        swap(arr, 0, --heapSize);
    }
}

// 将一个值存入到大顶堆中 并且保持还是大顶堆的情况
// index向上移动 表明当前存入的值存放到数组的index的位置
public static void heapInsert(int[] arr, int index) {
    // 如果当前的数大于其父类的数 将其进行交换
    // 这个while的条件 ① 当前位置的数大于父节点的数
    //               ② 当index到0的时候 arr[0] > arr[0] 也不满足
    while (arr[index] > arr[(index - 1) / 2]) {
        // 交换
        swap(arr, index, (index - 1) / 2);
        // index进行移动 继续和父节点进行比较
        index = (index - 1) / 2;
    }
}

/**
     * 将一个值向下进行放值 继续保持是大顶堆的情况
     *
     * @param arr      数组
     * @param index    当前值所在的索引
     * @param heapSize 数组的前几个构成大顶堆
     */
public static void heapify(int[] arr, int index, int heapSize) {
    // 找到左结点
    int leaf = (2 * index) + 1;
    while (leaf < heapSize) {
        // leaf < heapSize 说明存在左结点
        // 判断是左结点大还是右节点大 保留大的节点
        int max = ((leaf + 1) < heapSize
                   && arr[leaf + 1] > arr[leaf]) ? leaf + 1 : leaf;
        // 和当前的节点进行比较保留大的节点
        max = arr[index] > arr[max] ? index : max;
        if (max == index) {
            // 说明当前节点大于子结点 因为下面的满足大顶堆的情况
            break;
        }
        // 说明左右节点大于根节点 交换
        swap(arr, index, max);
        // 将index进行移动
        index = max;
        // 左结点重新定义
        leaf = 2 * index + 1;
    }
}

public static void swap(int[] arr, int x, int y) {
    int temp = arr[x];
    arr[x] = arr[y];
    arr[y] = temp;
}

时间复杂度 O(NlogN) 空间复杂度O(1) 算法不是稳定的

堆的操作比堆排序更加的重要

8. 基数排序

基数排序是基于桶排序的进行改进的排序

桶排序

当有多少个数进行排序的时候,当都是正数的时候

例如:

1, 4, 2, 5, 8, 2, 3, 7 进行排序

步骤:

1、准备上面数组最大的值长度的数组,例如准备9个长度的数组

2、当出现数据为1的时候,将这个数放到数组索引为1的位置,并且将对应的计数器变成1

3、进行依次的遍历所有的值,在对应的索引的位置,出现一次加一次这个值

4、当所有的遍历完后,从头到尾遍历整个数组将值取出即可

5、这种情况虽然时间复杂度为O(n),如果数组的范围特别的的大,哪需要开辟的数组的长度需要特别的长,并且这种比较只可以使用在数值的类型中

基数排序

1、 在桶排序排序的基础上,不需要创建这么多的数组的长度的值,只需要10个桶即可,从最小位开始取出对应的数,放到对应的桶中,如果将这些桶中的值又放回到数组中(需要满足先进先出)

2、然后继续排序,然后进行10位的判断,一直到最后的一位

下面代码的思路:

1、先找到数组(arr)中最大的值,求出对应的10进制的位数,比如 100 位数为 3

2、准备一个长度为10的数组(count),代表10个桶,即对应位数的值放到对应的数组的位置

3、遍历原始的数组(arr),先取出个位上的数,将count[数]上的值进行加1

4、将count这个数组进行前缀和,例如 [1, 2, 0, 3, 5, 0, 0, 1, 1, 0] ==> [1, 3, 3, 6, 11, 11, 11, 12, 13, 13]

5、这个时候的count数组表示的是比当前的索引小的数有多少个,例如:3这个位置的值为6,代表个位小于等于3的数有6个

6、因为从个位开始,所以需要满足先进先出,所以从后往前遍历原始的数组(arr),求出对应的位置的值,去count中寻找,取到count中的值

7、创建一个和原始数组相同长度的临时数组(temp),6中的count数组中的值 - 1就是这个数存放的位置,即存放到临时数组中

8、最后将临时的数组的值赋值给原始的数组

9、上面的一次操作就是一次入栈和

/**
     * @param arr 需要排序的数组
     * @param l   排序的数组的左位置
     * @param r   排序的数组的右位置
     */
public static void radixSort(int[] arr, int l, int r) {
    if (arr == null || arr.length == 1) {
        // 无需排序
        return;
    }
    // 准备一个临时数组
    int[] temp = new int[r - l + 1];
    // 需要排序
    // 获取最大值的位数
    int digit = getDigit(arr, l, r);
    // for循环次数就是 digit 的个数 一次for循环就是入桶和出桶的操作
    // i ===> 代表取出第几位 个位算第一位
    for (int i = 1; i <= digit; i++) {
        // 1.准备数组10个的长度
        int[] count = new int[10];
        // 2.取出对应i位置的数放入到count中
        for (int j = l; j <= r; j++) {
            // getDigitNum(arr[j], 1) 获取对应的数 在对应的位置加1
            count[getDigitNum(arr[j], i)]++;
        }
        // 3. 进行前缀和的处理
        for (int j = 1; j < count.length; j++) {
            // 等于当前数和前一个数
            count[j] = count[j] + count[j - 1];
        }
        // 4.遍历原始数组 从后向前
        for (int j = r; j >= l; j--) {
            // 取出数中对应的位 getDigitNum(arr[j], i)
            int d = getDigitNum(arr[j], i);
            // count[d] - 1取出的值就是存放的位置
            temp[count[d] - 1] = arr[j];
            // 走了一个减1
            count[d]--;
        }
        // 将临时数组存入到arr中
        for (int j = l, k = 0; j <= r; j++, k++) {
            arr[j] = temp[k];
        }
    }
}


/**
     * 获取num数中 digit位置的数 从个位数开始数
     *
     * @param num   数组
     * @param digit 第几位
     * @return 返回对应的数
     */
public static int getDigitNum(int num, int digit) {
    // 16 第2位
    return (num / (int) Math.pow(10, digit - 1)) % 10;
}

/**
     * 获取这个有效的数组的最大值的最大的长度(10进制)
     *
     * @param arr 数组
     * @param l   左指针
     * @param r   右指针
     * @return 返回最大数的长度
     */
public static int getDigit(int[] arr, int l, int r) {
    // 找到最大值
    // 定义一个初始最大值
    int max = Integer.MIN_VALUE;
    for (int i = l; i <= r; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    // 到这里max中就是最大值
    // 取出最大值的长度
    int count = 1; // 计数
    // 100
    while (max / 10 != 0) {
        count++;
        max /= 10;
    }
    return count;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值