使用Java语言深度探索数据结构中的数组算法:完美结合详解与示例代码

版本说明

当前版本号[20231004]。

版本修改说明
20231004初版

目录

二. 基础数据结构

2.1 数组

1) 概述

定义

在计算机科学中,数组是由一组元素(值或变量)组成的数据结构,每个元素有至少一个索引或键来标识

因为数组内的元素是连续存储的,所以数组中元素的地址,可以通过其索引计算出来,例如:

int[] array = {1,2,3,4,5}

知道了数组的数据起始地址 B a s e A d d r e s s BaseAddress BaseAddress,就可以由公式 B a s e A d d r e s s + i ∗ s i z e BaseAddress + i * size BaseAddress+isize 计算出索引 i i i 元素的地址

  • i i i 即索引,在 Java、C 等语言都是从 0 开始
  • s i z e size size 是每个元素占用字节,例如 i n t int int 4 4 4 d o u b l e double double 8 8 8

小测试

byte[] array = {1,2,3,4,5}

已知 array 的数据的起始地址是 0x7138f94c8,那么元素 3 的地址是什么?

答:0x7138f94c8 + 2 * 1 = 0x7138f94ca

空间占用

Java 中数组结构为

  • 8 字节 markword
  • 4 字节 class 指针(压缩 class 指针的情况)
  • 4 字节 数组大小(决定了数组最大容量是 2 32 2^{32} 232
  • 数组元素 + 对齐字节(java 中所有对象大小都是 8 字节的整数倍[^12],不足的要用对齐字节补足)

例如

int[] array = {1, 2, 3, 4, 5};

的大小为 40 个字节,组成如下

8 + 4 + 4 + 5*4 + 4(alignment)

随机访问性能

即根据索引查找元素,时间复杂度是 O ( 1 ) O(1) O(1)

2) 动态数组

1)插入
addlast 方法

意在把新增的元素加入到最后一个到动态数组里。

public class 动态数组
{
    private int size = 0;//逻辑大小
    private int capacity = 8;//所创建数组的数组容量
    private int[] array = new int[capacity];

       /**
     * 向最后位置 [size] 添加元素
     *
     * @param element 待添加元素
     */
    public void addlast(int element)
    {
        array[size] = element;
        size++;
    }
}

这个类具有以下特点:

  • size变量表示当前数组中已存储的元素数量。
  • capacity变量表示数组的容量,即可以容纳的最大元素数量。
  • array是一个整型数组,用于存储元素。
  • addlast(int element)方法用于在数组的末尾添加一个元素,并将size增加1。
测试: addlast 方法

​ 这段代码作为一个JUnit测试类,在测试方法test1中,首先创建了一个动态数组对象,然后向数组中添加了5个元素,最后遍历数组并打印出每个元素的值。

package SuanFa.test;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import SuanFa.初始算法.第一章数组.动态数组;
import java.util.Arrays;

public class 动态数组Test {

    @Test
    public void test1()
    {
        动态数组 dynamicarray = new 动态数组();
        dynamicarray.addlast(1);
        dynamicarray.addlast(2);
        dynamicarray.addlast(3);
        dynamicarray.addlast(4);
        dynamicarray.addlast(5);

        for(int i = 0; i < 5; i++)
        {
            System.out.println(dynamicarray.get(i));
        }
    }
}

测试结果如下:

image-20231004104127182

注:接下来的所有方法中将只保留最主要的代码。

add 方法

意在向某一个索引里加入新值。

     /**
     * 向 [0 .. size] 位置添加元素
     *
     * @param index   索引位置
     * @param element 待添加元素
     */
    public void add(int index, int element)
    {
        if(index >=0 && index < size)
        {
            System.arraycopy(array, index, array, index+1, size - index);
            array[index] = element;
            size++;
        }
    }

这个类具有以下特点:

  • size变量表示当前数组中已存储的元素数量。
  • capacity变量表示数组的容量,即可以容纳的最大元素数量。
  • array是一个整型数组,用于存储元素。
  • add(int index, int element)方法用于在指定索引位置插入到指定索引位置,并确保索引在合法范围内(大于等于0且小于当前数组大小)。
  • 如果索引合法,使用System.arraycopy()方法将原数组中从指定索引位置开始的元素向后移动一个位置,腾出空间来插入新元素。(复制 array 数组中从下标为 index 的元素复制到 array 数组的下标为 index+1 的元素上,并且只复制 size - index 的元素)
  • 将新元素赋值给指定索引位置。
  • 增加size变量的值,表示数组中已存储的元素数量增加了1。
  • 如果索引不合法,该方法不会执行任何操作。
测试:add方法

​ 这段代码作为一个JUnit测试类,在测试方法test1中,首先创建了一个动态数组对象,然后向数组中添加了4个元素,接着使用add方法在索引为2的位置插入了一个值为9的元素。最后遍历数组并打印出每个元素的值。

    @Test
    public void test2()
    {
        动态数组 dynamicarray = new 动态数组();
        dynamicarray.addlast(1);
        dynamicarray.addlast(2);
        dynamicarray.addlast(3);
        dynamicarray.addlast(4);
        //dynamicarray.addlast(5);

        dynamicarray.add(2, 9);

        for(int i = 0; i < 5; i++)
        {
            System.out.println(dynamicarray.get(i));
        }
    }
}

测试结果如下:

image-20231004104452737

addlast 方法与 add 方法合并版

由于addlast与add方法中,都有 array[下标] = element; size++; 这两段代码,于是也可以对他们进行优化,放在一起

     /**
     * 向 [0 .. size] 位置添加元素
     *
     * @param index   索引位置
     * @param element 待添加元素
     */
    public void add(int index, int element)
    {
        // 添加逻辑
        if(index >=0 && index < size)
        {
            // 向后挪动, 空出待插入位置
            System.arraycopy(array, index,
                    array, index+1, size - index);
        }
        array[index] = element;
        size++;
    }

       /**
     * 向最后位置 [size] 添加元素
     *
     * @param element 待添加元素
     */
    public void addlast(int element)
    {
        add(size, element);
    }

这个类具有以下特点:

  • size变量表示当前数组中已存储的元素数量。
  • capacity变量表示数组的容量,即可以容纳的最大元素数量。
  • array是一个整型数组,用于存储元素。
  • add(int index, int element)方法用于在指定索引位置插入一个元素。
  • addlast(int element)方法用于在数组的末尾添加一个元素。
  • add(int index, int element)方法将传入的元素插入到指定索引位置,并确保索引在合法范围内(大于等于0且小于当前数组大小)。
  • 如果索引合法,使用System.arraycopy()方法将原数组中从指定索引位置开始的元素向后移动一个位置,腾出空间来插入新元素。
  • 将新元素赋值给指定索引位置。
  • 增加size变量的值,表示数组中已存储的元素数量增加了1。
  • 如果索引不合法,该方法不会执行任何操作。
  • addlast(int element)方法调用add(int index, int element)方法,将元素添加到数组的末尾
  • 数组会自动调整大小以适应新元素的添加。
get 方法

意在接收一个整数参数 index ,返回数组中下标为 index 的元素

        /**
     * 查询元素
     *
     * @param index 索引位置, 在 [0..size) 区间内
     * @return 该索引位置的元素
     */
    public int get(int index)
    {
        return array[index];
    }

这个类具有以下特点:

  • size变量表示当前数组中已存储的元素数量。
  • capacity变量表示数组的容量,即可以容纳的最大元素数量。
  • array是一个整型数组,用于存储元素。
  • 查询方法get接收一个整数参数 index ,返回数组中下标为 index 的元素
测试:get 方法

与测试:addlast 方法相同

    @Test
    public void test1()
    {
        动态数组 dynamicarray = new 动态数组();
        dynamicarray.addlast(1);
        dynamicarray.addlast(2);
        dynamicarray.addlast(3);
        dynamicarray.addlast(4);
        dynamicarray.addlast(5);

        for(int i = 0; i < 5; i++)
        {
            System.out.println(dynamicarray.get(i));
        }
    }
2) 遍历
遍历方法1:consumer 遍历

​ 这段代码定义了一个名为 foreach 的方法,该方法接收一个 Consumer 类型的参数consumer。在方法内部,使用for循环遍历数组array,**对于每个元素,调用consumer的accept方法执行传入的操作。**最后,forEach方法没有返回值(void)。

    /**
     * 遍历方法1
     *
     * @param consumer 遍历要执行的操作, 入参: 每个元素
     */
 public void foreach(Consumer<Integer> consumer)
    {
        for(int i = 0; i<size ; i++)
   	   // 提供 array[i]
        // 返回 void
        {
            consumer.accept(array[i]);
        }
    }
测试:consumer 遍历

​ 在该测试方法中,首先创建了一个动态数组对象dynamicarray,并向其中添加了5个元素。然后调用forEach方法遍历数组,对于每个元素,使用Lambda表达式打印出该元素的值。

public class 动态数组Test {

    @Test
    public void test3()
    {
        动态数组 dynamicarray = new 动态数组();
        dynamicarray.addlast(1);
        dynamicarray.addlast(2);
        dynamicarray.addlast(3);
        dynamicarray.addlast(4);

        dynamicarray.forEach((element->
        {
            System.out.println(element);
        }));
    }
}

测试结果如下:

image-20231004163648866

遍历方法2:迭代器遍历

​ 该动态数组类实现了Iterable接口,并重写了iterator方法

在类中重写了Iterable接口的iterator方法,返回一个Iterator对象,该对象实现了Iterator接口的方法hasNext和next。其中,hasNext方法用于判断是否还有下一个元素,next方法用于**返回当前元素并将指针移动到下一个元素。**由于没有给出数组元素的添加和删除操作,因此该类的迭代器只能从头到尾遍历一遍数组。

public class 动态数组 implements Iterable<Integer>
{
    private int size = 0;//逻辑大小
    private int capacity = 8;//容量
    private int[] array = new int[capacity];

        /**
     * 遍历方法2 - 迭代器遍历
     */
    @Override
    public Iterator<Integer> iterator(){
        return new Iterator<Integer>() {
            int i = 0;
            @Override
            public boolean hasNext() { // 有没有下一个元素
                return i < size;
            }

            @Override
            public Integer next() { // 返回当前元素,并移动到下一个元素
                return array[i++];
            }
       };
}
测试:迭代器遍历

​ 使用增强for循环遍历数组,对于每个元素,打印出该元素的值。

@Test
    public void test4()
    {
        动态数组 dynamicarray = new 动态数组();
        dynamicarray.addlast(1);
        dynamicarray.addlast(2);
        dynamicarray.addlast(3);
        dynamicarray.addlast(4);

        for(Integer element: dynamicarray)
        {
            System.out.println(element);
        }
    }

测试结果如下:

image-20231004112314866

遍历方法3:stream 遍历

​ 这段代码定义了一个名为stream的公共方法,该方法返回一个IntStream类型的对象。在方法内部,使用IntStream.of()方法创建一个包含数组array中前size个元素的IntStream流并返回该流。

    /**
     * 遍历方法3 - stream 遍历
     *
     * @return stream 流
     */
public IntStream stream(){
        return IntStream.of(Arrays.copyOfRange(array,0,size));
    }
测试:stream 遍历
 @Test
    public void test5()
    {
        动态数组 dynamicarray = new 动态数组();
        dynamicarray.addlast(7);
        dynamicarray.addlast(2);
        dynamicarray.addlast(4);
        dynamicarray.addlast(1);

        dynamicarray.stream().forEach(element->{
            System.out.println(element);
        });
    }

测试结果如下:

image-20231004164731217

插入与遍历方法汇总

​ 以下这段代码定义了一个名为动态数组的类,汇总了所有方法,实现了Iterable接口。该类方法分别有:

  1. add(int index, int element):向指定索引位置添加元素的方法。如果索引有效(即大于等于0且小于当前大小),则将该位置及之后的元素向后移动一位,然后将新元素放置在指定索引位置上,并将大小加1。
  2. addlast(int element):向数组末尾添加元素的方法。调用add(size, element)实现。
  3. get(int index):获取指定索引位置的元素的方法。
  4. foreach(Consumer<Integer> consumer):遍历数组并对每个元素执行给定的消费者操作。
  5. stream():返回一个IntStream流,其中包含数组中的所有元素。使用Arrays.copyOfRange(array, 0, size)创建一个包含数组元素的新数组,并将其转换为IntStream流。
package SuanFa.初始算法.第一章数组;

import java.util.Arrays;
import java.util.Iterator;
import java.util.function.Consumer;
import java.util.stream.IntStream;

public class 动态数组 implements Iterable<Integer> {
    private int size = 0;//逻辑大小
    private int capacity = 8;//容量
    private int[] array = new int[capacity];

    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            int i = 0;

            @Override
            public boolean hasNext() { // 有没有下一个元素
                return i < size;
            }

            @Override
            public Integer next() { // 返回当前元素,并移动到下一个元素
                return array[i++];
            }
        };
    }

    public void add(int index, int element) {
        if (index >= 0 && index < size) {
            System.arraycopy(array, index,
                    array, index + 1, size - index);
        }
        array[index] = element;
        size++;
    }

    public void addlast(int element) {
        add(size, element);
    }

    public int get(int index) {
        return array[index];
    }

    public void foreach(Consumer<Integer> consumer) {
        for (int i = 0; i < size; i++) {
            consumer.accept(array[i]);
        }
    }

    public IntStream stream() {
        return IntStream.of(Arrays.copyOfRange(array, 0, size));
    }
}
插入与遍历测试类汇总

该类包含了5个测试方法:

  1. test1():测试了向动态数组末尾添加元素的功能。首先创建了一个空的动态数组对象dynamicarray,然后连续调用addlast()方法向数组末尾添加了5个元素(1到5)。最后使用循环遍历数组并打印每个元素的值。
  2. test2():测试了在指定位置插入元素的功能。首先创建了一个空的动态数组对象dynamicarray,然后连续调用addlast()方法向数组末尾添加了5个元素(1到5)。接着调用add()方法在索引为2的位置插入了一个新元素9。最后使用循环遍历数组并打印每个元素的值。
  3. test3():测试了使用IntStream流遍历动态数组的功能。首先创建了一个空的动态数组对象dynamicarray,然后连续调用addlast()方法向数组末尾添加了5个元素(1到5)。接着调用stream()方法获取一个包含数组元素的IntStream流,并使用forEach()方法对每个元素执行打印操作。
  4. test4():测试了使用for-each循环遍历动态数组的功能。首先创建了一个空的动态数组对象dynamicarray,然后连续调用addlast()方法向数组末尾添加了5个元素(1到5)。接着使用for-each循环遍历数组,并在每次迭代中打印当前元素的值。
  5. test5():测试了在动态数组中间插入元素的功能。首先创建了一个空的动态数组对象dynamicarray,然后连续调用addlast()方法向数组末尾添加了4个元素(7、2、4、1)。最后使用流
package SuanFa.test;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;


import SuanFa.初始算法.第一章数组.动态数组;

import java.lang.annotation.ElementType;
import java.util.Arrays;


public class 动态数组Test {

    @Test
    public void test1() {
        动态数组 dynamicarray = new 动态数组();
        dynamicarray.addlast(1);
        dynamicarray.addlast(2);
        dynamicarray.addlast(3);
        dynamicarray.addlast(4);
        dynamicarray.addlast(5);

        for (int i = 0; i < 5; i++) {
            System.out.println(dynamicarray.get(i));
        }
    }

    @Test
    public void test2() {
        动态数组 dynamicarray = new 动态数组();
        dynamicarray.addlast(1);
        dynamicarray.addlast(2);
        dynamicarray.addlast(3);
        dynamicarray.addlast(4);

        dynamicarray.add(2, 9);

        for (int i = 0; i < 5; i++) {
            System.out.println(dynamicarray.get(i));
        }
    }

    @Test
    public void test3() {
        动态数组 dynamicarray = new 动态数组();
        dynamicarray.addlast(1);
        dynamicarray.addlast(2);
        dynamicarray.addlast(3);
        dynamicarray.addlast(4);

        dynamicarray.stream().forEach(element -> {
            System.out.println(element);
        });
    }

    @Test
    public void test4() {
        动态数组 dynamicarray = new 动态数组();
        dynamicarray.addlast(1);
        dynamicarray.addlast(2);
        dynamicarray.addlast(3);
        dynamicarray.addlast(4);

        for (Integer element : dynamicarray) {
            System.out.println(element);
        }
    }

    @Test
    public void test5() {
        动态数组 dynamicarray = new 动态数组();
        dynamicarray.addlast(7);
        dynamicarray.addlast(2);
        dynamicarray.addlast(4);
        dynamicarray.addlast(1);

        dynamicarray.stream().forEach(element -> {
            System.out.println(element);
        });
    }
}
3)删除

​ 该方法接受一个整数参数 index,表示要删除元素的索引位置。

​ 首先,它将要删除的元素保存在变量 removed 中。然后,使用 System.arraycopy() 方法将索引位置后的元素向前移动一位,覆盖掉要删除的元素。

​ 接下来,通过将数组的大小减一来更新数组的大小。

​ 最后,返回被删除的元素。

/**
 * 从 [0 .. size) 范围删除元素
 *
 * @param index 索引位置
 * @return 被删除元素
 */
public int remove(int index){
    // 保存要删除的元素
    int removed = array[index];
    
    // 将索引位置后的元素向前移动一位
    System.arraycopy(array, index + 1, array, index, size - index - 1);
    
    // 更新数组的大小
    size--;
    
    // 返回被删除的元素
    return removed;
}
测试:删除方法

​ 这是一个J用于测试动态数组的测试类。首先创建一个dynamicarray的动态数组对象,然后使用addlast方法向数组中添加元素1、2、3、4和5。接着**调用remove方法移除索引为2的元素,并将其值赋给变量removed。**最后,使用stream方法和forEach遍历打印数组中的所有元素。

@Test
    public void test6() {
        动态数组 dynamicarray = new 动态数组();
        dynamicarray.addlast(1);
        dynamicarray.addlast(2);
        dynamicarray.addlast(3);
        dynamicarray.addlast(4);
        dynamicarray.addlast(5);

        int removed = dynamicarray.remove(2);
        System.out.println(removed);
        System.out.println("-----");
        dynamicarray.stream().forEach(element -> {
            System.out.println(element);
        });
    }

测试结果如下:

image-20231004201626597

4)扩容
  • ​ 该方法首先进行容量检查。
  • ​ 如果当前数组的大小为0,即数组为空,那么将创建一个新的数组,其容量为capacity
  • ​ 如果当前数组的大小等于容量,即数组已满,那么将通过将容量增加至原来的两倍来扩展数组。
  • ​ 然后,创建一个新的数组newArray,其容量为新的容量值。
  • ​ 接下来,使用System.arraycopy()方法将原数组中的元素复制到新数组中。
  • ​ 最后,将新数组赋值给原数组,完成数组的扩展操作。
  • ​ 这个方法通常用于动态数组的管理,以确保数组在插入元素时能够自动调整其容量大小,以适应不断增长的数据量。
  private void checkAndGrow() {
        // 容量检查
        if (size == 0) {
            array = new int[capacity];
        } else if (size == capacity) {
            capacity += capacity >> 1;
            int[] newArray = new int[capacity];
            System.arraycopy(array, 0,
                    newArray, 0, size);
            array = newArray;
        }
    }
5)插入或删除性能

部位置,时间复杂度是 O ( n ) O(n) O(n)

间位置,时间复杂度是 O ( n ) O(n) O(n)

部位置,时间复杂度是 O ( 1 ) O(1) O(1)(均摊来说)

3) 二维数组

int[][] array = {
    {11, 12, 13, 14, 15},
    {21, 22, 23, 24, 25},
    {31, 32, 33, 34, 35},
};
  • 二维数组占 32 个字节,其中 array[0],array[1],array[2] 三个元素分别保存了指向三个一维数组的引用

  • 三个一维数组各占 40 个字节

  • 它们在内层布局上是连续

更一般的,对一个二维数组 A r r a y [ m ] [ n ] Array[m][n] Array[m][n]

  • m m m 是外层数组的长度,可以看作 row 行
  • n n n 是内层数组的长度,可以看作 column 列
  • 当访问 A r r a y [ i ] [ j ] Array[i][j] Array[i][j] 0 ≤ i < m , 0 ≤ j < n 0\leq i \lt m, 0\leq j \lt n 0i<m,0j<n时,就相当于
    • 先找到第 i i i 个内层数组(行)
    • 再找到此内层数组中第 j j j 个元素(列)
测试

Java 环境下(不考虑类指针和引用压缩,此为默认情况),有下面的二维数组

byte[][] array = {
    {11, 12, 13, 14, 15},
    {21, 22, 23, 24, 25},
    {31, 32, 33, 34, 35},
};

已知 array 对象起始地址是 0x1000,那么 23 这个元素的地址是什么?

答:

  • 起始地址 0x1000
  • 外层数组大小:16字节对象头 + 3元素 * 每个引用4字节 + 4 对齐字节 = 32 = 0x20
  • 第一个内层数组大小:16字节对象头 + 5元素 * 每个byte1字节 + 3 对齐字节 = 24 = 0x18
  • 第二个内层数组,16字节对象头 = 0x10,待查找元素索引为 2
  • 最后结果 = 0x1000 + 0x20 + 0x18 + 0x10 + 2*1 = 0x104a

4) 局部性原理

这里只讨论空间局部性

  • cpu 读取内存(速度慢)数据后,会将其放入高速缓存(速度快)当中,如果后来的计算再用到此数据,在缓存中能读到的话,就不必读内存了
  • 缓存的最小存储单位是缓存行(cache line),一般是 64 bytes,一次读的数据少了不划算啊,因此最少读 64 bytes 填满一个缓存行,因此读入某个数据时也会读取其临近的数据,这就是所谓空间局部性
对效率的影响

比较下面 ij 和 ji 两个方法的执行效率

int rows = 1000000;
int columns = 14;
int[][] a = new int[rows][columns];

StopWatch sw = new StopWatch();
sw.start("ij");
ij(a, rows, columns);
sw.stop();
sw.start("ji");
ji(a, rows, columns);
sw.stop();
System.out.println(sw.prettyPrint());

ij 方法

public static void ij(int[][] a, int rows, int columns) {
    long sum = 0L;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < columns; j++) {
            sum += a[i][j];
        }
    }
    System.out.println(sum);
}

ji 方法

public static void ji(int[][] a, int rows, int columns) {
    long sum = 0L;
    for (int j = 0; j < columns; j++) {
        for (int i = 0; i < rows; i++) {
            sum += a[i][j];
        }
    }
    System.out.println(sum);
}

执行结果

0
0
StopWatch '': running time = 96283300 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
016196200  017%  ij
080087100  083%  ji

可以看到 ij 的效率比 ji 快很多,为什么呢?

  • 缓存是有限的,当新数据来了后,一些旧的缓存行数据就会被覆盖
  • 如果不能充分利用缓存的数据,就会造成效率低下

以 ji 执行为例,第一次内循环要读入 [ 0 , 0 ] [0,0] [0,0] 这条数据,由于局部性原理,读入 [ 0 , 0 ] [0,0] [0,0] 的同时也读入了 [ 0 , 1 ] . . . [ 0 , 13 ] [0,1] ... [0,13] [0,1]...[0,13],如图所示

image-20221104164329026

但很遗憾,第二次内循环要的是 [ 1 , 0 ] [1,0] [1,0] 这条数据,缓存中没有,于是再读入了下图的数据

image-20221104164716282

这显然是一种浪费,因为 [ 0 , 1 ] . . . [ 0 , 13 ] [0,1] ... [0,13] [0,1]...[0,13] 包括 [ 1 , 1 ] . . . [ 1 , 13 ] [1,1] ... [1,13] [1,1]...[1,13] 这些数据虽然读入了缓存,却没有及时用上,而缓存的大小是有限的,等执行到第九次内循环时

image-20221104164947154

缓存的第一行数据已经被新的数据 [ 8 , 0 ] . . . [ 8 , 13 ] [8,0] ... [8,13] [8,0]...[8,13] 覆盖掉了,以后如果再想读,比如 [ 0 , 1 ] [0,1] [0,1],又得到内存去读了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

放下华子我只抽RuiKe5

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

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

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

打赏作者

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

抵扣说明:

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

余额充值