Java实现一些排序:不要再只会冒泡啦

本文详细介绍了五种排序算法:归并排序、快速排序、堆排序、桶排序和基数排序。归并排序通过递归划分和合并实现排序;快速排序则依赖于选取基准元素并分区。堆排序利用二叉堆性质,不断调整堆以得到排序结果。桶排序和基数排序针对整数,前者根据数值分布到桶中再排序,后者按位进行多次桶排序。外部排序则用于处理大型数据文件,通过分段和迭代归并完成排序。
摘要由CSDN通过智能技术生成

目录

一、归并排序

二、快速排序

归并和快速排序的比较

三、堆排序

1、堆的存储

2、添加新结点

3、删除根结点

四、桶排序和基数排序

1、桶排序

2、基数排序

五、外部排序

1、分段排序

2、迭代归并 

        (1)复制f1中前半部分至f2

        (2)归并所有分段

        (3)归并两个片段

3、二合一的全部实现


一、归并排序

归并排序算法将数组分为两半,然后对每个部分持续递归划分子数组。直到每组只有一个元素后开始合并排序,接下来就是不断合并排序合并排序....最终两部分排好序后直接合并即可:

 代码实现如下:  

public class MergeSort {
    public static void mergeSort(int[] list){
        if(list.length > 1) {
            // list前半部分副本 --> firstHalf
            int[] firstHalf = new int[list.length / 2];
            System.arraycopy(list, 0, firstHalf, 0, list.length / 2);
            mergeSort(firstHalf);  // 递归

            int secondHalfLength = list.length - list.length / 2;
            // list后半部分副本 --> secondHalf
            int[] secondHalf = new int[secondHalfLength];
            System.arraycopy(list, list.length / 2, secondHalf, 0, secondHalf.length);
            mergeSort(secondHalf);  // 递归*2

            merge(firstHalf, secondHalf, list); // 两副本排好序后归并成新数组list即可
        }
    }

    // merge方法归并两个数组为temp
    public static void merge(int[] list1, int[] list2, int[] temp){
        int current1 = 0;  // list1 元素下标
        int current2 = 0;  // list2 元素下标
        int current3 = 0;  // temp 元素下标

        while(current1 < list1.length && current2 < list2.length){
            if(list1[current1] < list2[current2]){
                temp[current3++] = list1[current1++]; // 小元素在list1中
            }else{
                temp[current3++] = list2[current2++]; // 小元素在list2中
            }
        }

        while(current1 < list1.length){
            temp[current3++] = list1[current1++];  // list1中有未移元素 --> temp
        }
        while(current2 < list2.length){
            temp[current3++] = list2[current2++];  // list2中有未移元素 --> temp
        }
    }

    public static void main(String[] args){
        int[] list = {2,3,1,-4,-7,9,5,12,0};
        mergeSort(list);
        for (int j : list) {
            System.out.print(j + " ");
        }
    }
}

/*
-7 -4 0 1 2 3 5 9 12 
*/


其中方法merge的归并实现主要是靠下标跟踪,两数组元素逐一比较后,得出较小的元素放入最终数组temp中,被放入元素的所属原数组下标++(为了接着往下走)。通过两两比较最终全部放入temp中实现排序:


 二、快速排序

快速排序主要是通过选出一个pivot基准)元素,然后将数组分为两部分,使得第一部分中的所有元素都<=pivot,第二部分元素都>pivot。然后对两个部分都递归地应用快排算法。

下图总将数组的第一个元素看作pivot,然后逐一递归: 

快排核心是partition方法 --> 以pivot为对比标准,用下标追踪实现元素交换 :

  代码实现:

package QuitSort;

class QuickSort {
    // 常规元素
    public static void quickSort(int[] list) {
        quickSort(list, 0, list.length - 1);
    }

    // 特定元素
    public static void quickSort(int[] list, int first, int last) {
        if (last > first) {
            int pivotIndex = partition(list, first, last);
            quickSort(list, first, pivotIndex - 1);
            quickSort(list, pivotIndex + 1, last);
        }
    }

    // partition方法用low和high分别指向数组两端。
    public static int partition(int[] list, int first, int last) {
        int pivot = list[first];
        int low = first + 1;  // low指向数组第二个元素
        int high = last; // high指向最后一个元素

    // 在数组左逐一寻找 > pivot 的元素,在右逐一寻找第一个 <= pivot 的元素。然后将他俩交换
        while (high > low) {
            while (low <= high && list[low] <= pivot) {
                low++;
            }
            while (low <= high && list[high] > pivot) {
                high--;
            }

            if (high > low) {
                int temp = list[high];
                list[high] = list[low];
                list[low] = temp;
            }
        }
        while (high > first && list[high] >= pivot) {
            high--;
        }

        if (pivot > list[high]) {  // 如果基准元素移动
            list[first] = list[high];
            list[high] = pivot;
            return high; // 返回新pivot下标
        } else return first;  // 否则返回初始pivot下标
    }

    // Test
    public static void main(String[] args) {
        int[] list = {2, 7, 5, 66, 44, 34, -22, -1, 0, 6};
        quickSort(list);
        for (int i = 0; i < list.length; i++) {
            System.out.print(list[i] + " ");
        }
    }
}

动画演示:Quick Sort Animation by Y. Daniel Liang

  • 归并和快速排序的比较

  1. 快速排序de大量工作 --> 将两个线性表归并(排好序后进行)
  2. 归并排序de大量工作 --> 将线性表划分为两个子线性表(排好序前进行)

快速排序的空间效率优于归并排序,因为归并排序需要创建临时数组,快排不需要


三、堆排序

堆排序使用二叉堆,它将所有元素先加到一个堆上,然后逐一移除最大元素来获得一个排好序的线性表

  • 二叉堆:一颗完全二叉树且每个结点  >= 它的任意一个孩子
  • 完全二叉树:二叉树每一层都是满的 / 最后一层没满且最后一层叶子都靠最左放置

注意,这样的二叉树:

b为完全二叉树  c不完全

1、堆的存储

可以通过ArrayList数组存储

2、添加新结点

先将其添加到堆的末尾,然后重建树(最后一个结点当作当前结点):

while(当前结点 > 父结点){

当前结点与父结点交换;

前进一层;

}

 3、删除根结点

当我们删除根结点,也就是最大元素的时候,我们需要重建这颗树来保持堆的属性(根结点为当前结点):

用最后一个结点替换根结点;

while(当前结点有子结点 && 当前结点 < 子结点){

当前结点和较大子结点交换;

当前结点下退一层;

}

假设我现在要删除下面二叉堆中的62: 

 重建堆的过程如下(选择较大子结点交换):

接下来我们设计一个Heap堆类: 

public class Heap<E extends Comparable<E>>{
    // 堆在内部用数组线性表表示
    private java.util.ArrayList<E> list = new java.util.ArrayList<>();

    // 创建一个默认空堆
    public Heap(){
    }

    // 创建一个具有指定对象的堆
    public Heap(E[] objects){
        for(int i = 0; i < objects.length; i++){
            add(objects[i]);
        }
    }

    // 添加一个新的对象到堆中
    public void add(E newObject){
        list.add(newObject);
        int currentIndex = list.size() - 1;

        while(currentIndex > 0){
            int parentIndex = (currentIndex - 1) / 2;
            if(list.get(currentIndex).compareTo(  
                    list.get(parentIndex)) > 0){  // 当前结点 > 父结点 
                // 交换current结点和parent结点
                E temp = list.get(currentIndex);
                list.set(currentIndex, list.get(parentIndex));  // 使用set()替换
                list.set(parentIndex,temp);
            }else break;
            currentIndex = parentIndex;  // 让当前结点指向父结点
        }
    }

    // 将根结点从堆中删除并返回
    public E remove(){
        if(list.size() == 0){
            return null;
        }
        E removeObject = list.get(0);
        list.set(0, list.get(list.size() - 1));  // 用最后一个结点替换当前根结点
        list.remove(list.size() - 1);  // 删掉最后一个结点

        int currentIndex = 0;  // 从根开始
        // 确定maxIndex
        while(currentIndex < list.size()){
            int leftChildIndex = 2 * currentIndex + 1;
            int rightChildIndex = 2 * currentIndex + 2;
            if(leftChildIndex >= list.size()){
                break;
            }
            int maxIndex = leftChildIndex;
            if(rightChildIndex < list.size()){
                if(list.get(maxIndex).compareTo(
                        list.get(rightChildIndex)) < 0){  // 比较左右谁更大
                    maxIndex = rightChildIndex;
                }
            }

            if(list.get(currentIndex).compareTo(  
                    list.get(maxIndex)) < 0) {  // 当前结点 < 较大子结点
                // 交换
                E temp = list.get(maxIndex);
                list.set(maxIndex, list.get(currentIndex));
                list.set(currentIndex, temp);
                currentIndex = maxIndex;
            }else break;
        }
        return removeObject;  // 每次返回最大元素
    }

    // 返回堆的大小
    public int getSize() {
        return list.size();
    }
}

使用Heap类实现堆排:

public class HeapSort {
    public static <E extends Comparable<E>> void heapSort(E[] list) {
        Heap<E> heap = new Heap<>();

        for(int i = 0; i < list.length; i++) {
            heap.add(list[i]);    // 添加元素到堆
        }

        for(int i = list.length - 1; i >= 0; i--) {
            list[i] = heap.remove();  // 然后不断移除最大的根结点
        }
    }

    public static void main(String[] args){
        Integer[] list = {-44,67,89,21,100,7,5,32,2};
        heapSort(list);
        for(int i = 0; i < list.length; i++) {
            System.out.print(list[i] + " ");
        }
    }
}

四、桶排序和基数排序

1、桶排序

桶排和基数排序都是对整数排序的高效算法。

桶排序实现思路是将对应的数字放到下标为自身值的“桶”中。

import java.util.ArrayList;
import java.util.Collections;

public class bucketSort {
    public static void bucketSort(int[] arr) {
        // 逐一找出最大元素和最小元素
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < arr.length; i++) {
            max = Math.max(max, arr[i]);  // 返回二者较大元素
            min = Math.min(min, arr[i]);  // 返回二者较小元素
        }

        //桶数
        int bucketNum = (max - min) / arr.length + 1;
        ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
        for (int i = 0; i < bucketNum; i++) {
            bucketArr.add(new ArrayList<Integer>());
        }

        //将每个元素放入桶
        for (int i = 0; i < arr.length; i++) {
            int num = (arr[i] - min) / (arr.length);
            bucketArr.get(num).add(arr[i]);
        }

        //对每个桶进行排序
        for (int i = 0; i < bucketArr.size(); i++) {
            Collections.sort(bucketArr.get(i));
        }
        System.out.println(bucketArr.toString());
    }

    public static void bucketSort(int[] a, int max) {
        int[] buckets;

        if (a==null || max<1)
            return ;

        // 创建一个容量为max的数组buckets,并且将buckets中的所有数据都初始化为0。
        buckets = new int[max];

        // 1. 计数
        for(int i = 0; i < a.length; i++)
            buckets[a[i]]++;

        // 2. 排序
        for (int i = 0, j = 0; i < max; i++) {
            while( (buckets[i]--) >0 ) {
                a[j++] = i;
            }
        }
        buckets = null;
    }

    public static void main(String[] args) {
        int[] list = {1, 0, 2, 2, 6, 4, 2, 3, -1, -4, -1};
        bucketSort(list);
    }
}

2、基数排序

虽然桶排序比较稳定,但当键值范围过大时,桶排序就不是很可取。基数排序是基于桶排序的,但它只有10个桶。

基数排序的原理是,按照位次逐一对元素的位进行桶排序(0~9)

代码实现如下: 

public class RadixSort {
    // 获取数组a中最大值
    private static int getMax(int[] a) {
        int max;

        max = a[0];
        for (int i = 1; i < a.length; i++) {
            if (a[i] > max){
                max = a[i];
            }
        }
        return max;
    }

    // 对数组按照"某个位数"进行排序(桶排序)
    private static void countSort(int[] a, int exp) {
        int[] output = new int[a.length];    // 存储"被排序数据"的临时数组
        int[] buckets = new int[10];

        // 将数据出现的次数存储在buckets[]中
        for (int i = 0; i < a.length; i++) {
            buckets[(a[i] / exp) % 10]++;
        }
        
        // 让更改后的buckets[i]的值是该数据在output[]中的位置
        for (int i = 1; i < 10; i++) {
            buckets[i] += buckets[i - 1];
        }
        
        // 将数据存储到临时数组output[]中
        for (int i = a.length - 1; i >= 0; i--) {
            output[buckets[ (a[i]/exp)%10 ] - 1] = a[i];
            buckets[ (a[i]/exp)%10 ]--;
        }

        // 将排序好的数据赋值给a[]
        for (int i = 0; i < a.length; i++) {
            a[i] = output[i];
        }
        output = null;
        buckets = null;
    }

    // 基数排序
    public static void radixSort(int[] a) {
        int exp;    // 指数。当对数组按各位进行排序时,exp=1;按十位进行排序时,exp=10
        int max = getMax(a);    // 数组a中的最大值

        // 从个位开始,对数组a按"指数"进行排序
        for (exp = 1; max/exp > 0; exp *= 10)
            countSort(a, exp);
    }

    public static void main(String[] args) {
        int i;
        int a[] = {53, 3, 542, 748, 14, 214, 154, 63, 616};
        radixSort(a);    // 基数排序
        for (i=0; i<a.length; i++) {
            System.out.printf("%d ", a[i]);
        }
    }
}

五、外部排序

 如果对一些大型外部文件排序,首先我们需要将这些数据放入内存中,然后进行排序。但如果咱的文件太大、内存不足以一下子存储文件中所有数据的时候,我们就可以选择外部排序算法。

外部排序算法用到了IO流一块的类,参考:Java(8)二进制IO_颜 然的博客-CSDN博客_java二进制io

首先我们先创建一个包含大量数据的文件:

import java.io.*;

public class CreateLargeFile {
    public static void main(String[] args) throws Exception {
        DataOutputStream output = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream("largedata.dat")));

                for(int i = 0; i < 2_000_000; i++)  // 放入20万个int值数据
                    output.writeInt((int)(Math.random() * 1000000));
        output.close();


        // 先展示100个
        DataInputStream input = new DataInputStream(
                new BufferedInputStream(new FileInputStream("largedata.dat")));
        for(int i = 0; i < 100; i++){
            System.out.print(input.readInt() + " ");
        }
        input.close();
    }
}

外部排序结合归并排序,大致分为两个阶段 --> 分段排序 --> 迭代归并

1、分段排序

重复将数据从文件读入数组中,然后用内部排序sort方法对数组排序,然后将数组输入到一个临时文件中:

// 创建初始有序分段
public static int initializeSegments(int segmentSize, String originalFile, String f1) throws Exception {
    int[] list = new int[segmentSize];  // 创建具有最大尺寸的数组
    // 为原始文件创建一个数据输入流
    DataInputStream input = new DataInputStream( 
            new BufferedInputStream(new FileInputStream(originalFile)));
    // 为原始文件创建一个数据输出流
    DataOutputStream output = new DataOutputStream(
            new BufferedOutputStream(new FileOutputStream(f1)));

    int numberOfSegments = 0;
    while(input.available() > 0){
        numberOfSegments++;
        int i = 0;
        // 从文件中读取一段数据到数组中
        for( ; input.available() > 0 && i < segmentSize; i++){
            list[i] = input.readInt();
        }
        java.util.Arrays.sort(list, 0 , 1);  // 排序

        for(int j = 0; i < i; j++){
            output.writeInt(list[j]);  // 将数组数据写入临时文件中
        }
     }
     input.close();
     output.close();

     return numberOfSegments;  // 返回分段个数
}

2、迭代归并 

将每对有序分段归并到一个更大的有序分段中(二合一,每次归并后分段个数少一半),并将新分段存储到新的临时文件中。然后继续进行迭代归并直到最后只有一个分段结果:

具体操作如下图。先将f1中的一半分段先复制到临时文件f2中,然后对f1和f2中的数据进行归并合成到f3中:

 代码部分如下:

(1)复制f1中前半部分至f2

private static void copyHalfTof2(int numberOfSegments, int segmentSize, DataInputStream f1, DataOutputStream f2) throws Exception{
    for(int i = 0; i < (numberOfSegments / 2) * segmentSize; i++){
        f2.writeInt(f1.readInt());  // 将一个 int 值作为四个字节写入底层流
    }
}

(2)归并所有分段

private static void mergeSegments(int numberOfSegments,int segmentSize, DataInputStream f1, DataInputStream f2, DataOutputStream f3) throws Exception{
    for(int i = 0; i < numberOfSegments; i++){
        mergeTwoSegments(segmentSize, f1, f2, f3);   // 归并两个片段
    }

    while (f1.available() > 0){
        f3.writeInt(f1.readInt());
    }
}

(3)归并两个片段

private static void mergeTwoSegments(int segmentSize, DataInputStream f1, DataInputStream f2, DataOutputStream f3) throws Exception{
    int intFormF1 = f1.readInt();
    int intFormF2 = f2.readInt();
    int f1Count = 1;
    int f2Count = 1;

    // available() 方法可以在读写操作前先得知数据流里有多少个字节可以读取
    while(true){
        // 判断谁的字节数少,就从谁先开始
        if(intFormF1 < intFormF2){  // f1少
            f3.writeInt(intFormF1);
            if(f1.available() == 0 || f1Count++ >= segmentSize){
                f3.writeInt(intFormF2);
                break;
            }else {
                intFormF1 = f1.readInt();
            }
        }else{  // f2少
            f3.writeInt(intFormF2);
            if(f2.available() == 0 || f2Count++ >= segmentSize){
                f3.writeInt(intFormF1);
                break;
            }else{
                intFormF2 = f2.readInt();
            }
        }
    }

    // 如果还有多出的数据,直接写入f3
    while(f1.available() > 0 && f1Count++ < segmentSize){
        f3.writeInt(f1.readInt());
    }
    while(f2.available() > 0 && f2Count++ < segmentSize){
        f3.writeInt(f2.readInt());
    }
}

3、二合一的全部实现

import java.io.*;

public class SortLargeFile {
    public static final int MAX_ARRAY_SIZE = 100000;
    public static final int BUFFER_SIZE = 100000;

    public static void main(String[] args) throws Exception {
        // 从largedata.dat中读取数据,排好序后写入sortedfile.dat中
        sort("largedata.dat", "sortedfile.dat");
        // 显示指定文件的前100个数据
        displayFile("sortedfile.dat");
    }

    /**
     * 排序
     */
    public static void sort(String sourcefile, String targetfile) throws Exception{
        // 创建初始数据,然后将排好序的分段存入f1.dat
        int numberOfSegments = initializeSegments(MAX_ARRAY_SIZE, sourcefile, "f1.dat");

        // 在targetfile中产生一个有序文件
        merge(numberOfSegments,MAX_ARRAY_SIZE,"f1.dat","f2.dat","f3.dat",targetfile);
    }

    /**
     * 1、分段排序
     * 创建初始有序分段
     */
    public static int initializeSegments(int segmentSize, String originalFile, String f1) throws Exception {
        int[] list = new int[segmentSize];  // 创建具有最大尺寸的数组
        // 为原始文件创建一个数据输入流
        DataInputStream input = new DataInputStream(
                new BufferedInputStream(new FileInputStream(originalFile)));
        // 为原始文件创建一个数据输出流
        DataOutputStream output = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream(f1)));

        int numberOfSegments = 0;
        while(input.available() > 0){
            numberOfSegments++;
            int i = 0;
            // 从文件中读取一段数据到数组中
            for( ; input.available() > 0 && i < segmentSize; i++){
                list[i] = input.readInt();
            }
            java.util.Arrays.sort(list, 0 , 1);  // 排序

            for(int j = 0; i < i; j++){
                output.writeInt(list[j]);  // 将数组数据写入临时文件中
            }
        }
        input.close();
        output.close();

        return numberOfSegments;  // 返回分段个数
    }

    /**
     * 归并函数
     */
    // f2辅助f1归并到f3
    public static void merge(int numberOfSegments,int segmentSize,String f1,String f2,String f3,String targetfile) throws Exception{
        if(numberOfSegments > 1){
            mergeOneStep(numberOfSegments,segmentSize,f1,f2,f3);
            // 调用新归并(每归并一次,numberOfSegments减半,有序分段大小segmentSize大小翻倍)
            merge((numberOfSegments + 1) / 2, segmentSize * 2, f3, f1, f2,targetfile);
        }else {       // numberOfSegment = 1时,结束merge递归
            File sortedFile = new File(targetfile);
            if(sortedFile.exists()){
                sortedFile.delete();
            }
            // 这时排好的数据在f1中
            new File(f1).renameTo(sortedFile);  // 重命名为sortedFile
        }
    }

    /**
     * 2、迭代归并
     */
    public static void mergeOneStep(int numberOfSegments, int segmentSize, String f1,String f2,String f3) throws Exception{
        DataInputStream f1Input = new DataInputStream(
                new BufferedInputStream(new FileInputStream(f1), BUFFER_SIZE));
        DataOutputStream f2Output = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream(f2), BUFFER_SIZE));

        // 从f1拷贝一半的段到f2
        copyHalfTof2(numberOfSegments,segmentSize,f1Input,f2Output);
        f2Output.close();

        // 将f1中剩余的数据段与f2中的数据段一起合并到f3中
        DataInputStream f2Input = new DataInputStream(new BufferedInputStream(new FileInputStream(f2),BUFFER_SIZE));
        DataOutputStream f3Output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(f3), BUFFER_SIZE));

        mergeSegments(numberOfSegments / 2, segmentSize, f1Input,f2Input,f3Output);

        f1Input.close();
        f2Input.close();
        f3Output.close();
    }

    /**
     * 复制f1中前半部分至f2
     */
    private static void copyHalfTof2(int numberOfSegments, int segmentSize, DataInputStream f1, DataOutputStream f2) throws Exception{
        for(int i = 0; i < (numberOfSegments / 2) * segmentSize; i++){
            f2.writeInt(f1.readInt());  // 将一个 int 值作为四个字节写入底层流
        }
    }

    /**
     * 归并所有分段
     */
    private static void mergeSegments(int numberOfSegments,int segmentSize, DataInputStream f1, DataInputStream f2, DataOutputStream f3) throws Exception{
        for(int i = 0; i < numberOfSegments; i++){
            mergeTwoSegments(segmentSize, f1, f2, f3);    // 归并两个片段
        }

        while (f1.available() > 0){
            f3.writeInt(f1.readInt());
        }
    }

    /**
     * 归并两个片段
     */
    private static void mergeTwoSegments(int segmentSize, DataInputStream f1, DataInputStream f2, DataOutputStream f3) throws Exception{
        int intFormF1 = f1.readInt();
        int intFormF2 = f2.readInt();
        int f1Count = 1;
        int f2Count = 1;

        // available() 方法可以在读写操作前先得知数据流里有多少个字节可以读取
        while(true){
            // 判断谁的字节数少,就从谁先开始
            if(intFormF1 < intFormF2){  // f1少
                f3.writeInt(intFormF1);
                if(f1.available() == 0 || f1Count++ >= segmentSize){
                    f3.writeInt(intFormF2);
                    break;
                }else {
                    intFormF1 = f1.readInt();
                }
            }else{  // f2少
                f3.writeInt(intFormF2);
                if(f2.available() == 0 || f2Count++ >= segmentSize){
                    f3.writeInt(intFormF1);
                    break;
                }else{
                    intFormF2 = f2.readInt();
                }
            }
        }

        // 如果还有多出的数据,直接写入f3
        while(f1.available() > 0 && f1Count++ < segmentSize){
            f3.writeInt(f1.readInt());
        }
        while(f2.available() > 0 && f2Count++ < segmentSize){
            f3.writeInt(f2.readInt());
        }
    }

    /**
     * 显示指定文件的前100个数据
     */
    public static void displayFile(String filename){
        try{
            DataInputStream input = new DataInputStream(new FileInputStream(filename));
            for(int i = 0; i < 100; i++) {
                System.out.print(input.readInt() + " ");
            }
            input.close();
        } catch(IOException ex) {
            ex.printStackTrace();
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

颜 然

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值