数据结构-Array

一 java中的数组

1. 数组是学习java接触到的第一个数据结构,也是最简单的数据结构;

2. 数组基础

  • 把数据码成一排进行存放就叫做数组;
  • 数组中有一个很重要的概念叫做索引(注意:索引从0开始);
  • 使用数组的优点:可以根据索引快速查询;
  • 数组最好应用于有“索引有语意”的情况;
  • 并非所有有语意的索引都适用于数组,比如说使用身份证号查询某人的工资情况,这样的情况就需要考虑其他的数据结构了或者考虑使用索引没有语意的情况;
  • 如果索引没有语意就会出现新的问题比如说:

    1⃣️如上图所示,在索引没有语意的情况下,如何表示没有的元素?
    2⃣️如何添加元素,如何删除元素?


二 基于java提供的数组二次封装出一个属于自己的数组

1. 封装一个基本数组

2. 示例代码

public class ArrayTest {
    // 声明一个私有的int类型数组
    private int[] data;
    // 声明数组的大小
    private int size;

    /**
     * 构造函数,传入数组的容量capacity构造ArrayTest
     * @param capacity
     */
    public ArrayTest(int capacity){
        // 创建一个数组,容量为capacity
        data = new int[capacity];
        // 初始长度为0
        size = 0;
    }

    /**
     * 无参数的构造函数,默认数组的容量capacity=10
     */
    public ArrayTest(){
        // 默认数组长度为10
        this(10);
    }

    /**
     * 获取数组中的元素个数
     * @return
     */
    public int getSize(){
        return size;
    }

    /**
     * 获取数组容量
     * @return
     */
    public int getCapacity(){
        return data.length;
    }

    /**
     * 判断数组中是否有元素
     * @return
     */
    public boolean isEmpty(){
        return size == 0;
    }
}

参数说明:

  • size:表示数组中元素的个数(后续的添加 删除等操作需要维护此属性);
  • data:声明的数组;
  • capacity:不需要维护,因为当用户创建数组是需要传入数组的默认长度;

2. 向数组中添加元素

2.1 向空数组添加元素


2.2 代码

/**
 * 向所有元素后添加一个新元素
 * @param e
 */
public void addlast(int e){
    // 判断数组长度与容量是否相等
    if (size == data.length){
        throw new IllegalArgumentException("AddLast failed. Array is full.");
    }
    // 向最后一个元素后边添加一个元素
    data[size] = e;
    // 维护数组长度
    size++;
}

2.3 向数组中任意一个位置添加新的元素

由上边图示可以看出,向指定索引添加元素的过程需要对数组其他位置的元素进行移动的操作,而且是从后向前移动(如果从前往后移动会覆盖掉下一个索引位置的元素),最后还需要维护一下size属性;

2.4 代码

    /**
     * 向指定索引位置插入元素
     * @param index
     * @param e
     */
    public void add(int index, int e){
        // 判断数组长度与容量是否相等
        if (size == data.length){
            throw new IllegalArgumentException("AddLast failed. Array is full.");
        }
        // 判断索引是否合法
        if (index < 0 || index > size){
            throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
        }
        // 从后往前遍历遍历数组,实现数组中元素的位移
        for (int i = size - 1; i >= index; i--){
            // 将当前索引位置的元素移动至下一个索引位置上
            data[i + 1] = data[I];
        }
        // 向指定索引位置插入元素
        data[index] = e;
        // 维护数组长度
        size++;
    }

2.5 复用核心添加逻辑改造方法

/**
     * 向所有元素后添加一个新元素
     * @param e
     */
    public void addlast(int e){
        // 复用核心的添加逻辑
        add(size,e);
    }

    /**
     * 向数组的第一个位置添加一个元素
     * @param e
     */
    public void addFirst(int e){
        // 复用核心的添加逻辑
        add(0,e);
    }

    /**
     * 向指定索引位置插入元素
     * @param index
     * @param e
     */
    public void add(int index, int e){
        // 判断数组长度与容量是否相等
        if (size == data.length){
            throw new IllegalArgumentException("AddLast failed. Array is full.");
        }
        // 判断索引是否合法
        if (index < 0 || index > size){
            throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
        }
        // 从后往前遍历遍历数组,实现数组中元素的位移
        for (int i = size - 1; i >= index; i--){
            // 将当前索引位置的元素移动至下一个索引位置上
            data[i + 1] = data[I];
        }
        // 向指定索引位置插入元素
        data[index] = e;
        // 维护数组长度
        size++;
    }

3. 重写toString

/**
     * toString
     * @return
     */
    @Override
    public String toString(){
        // 创建新的容器
        StringBuilder stringBuilder = new StringBuilder();
        // 设置格式
        stringBuilder.append(String.format("Array: size = %d , capacity = %d\n", size, data.length));
        // 开始拼接返回值
        stringBuilder.append("[");
        // 使用循环进行拼接
        for (int i = 0; i < size; i++){
            // 向容器中装载元素
            stringBuilder.append(data[I]);
            // 判断是否是最后一个元素
            if (i != size - 1){
                // 拼接分隔符
                stringBuilder.append(", ");
            }
        }
        // 闭合
        stringBuilder.append("]");
        // 返回
        return stringBuilder.toString();
    }

4 获取以及修改指定索引位置的元素

/**
     * 获取指定索引位置上的元素
     * @param index
     * @return
     */
    public int get(int index){
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Get failed, index is illegal.");
        }
        return data[index];
    }

    /**
     * 修改指定索引位置的元素
     * @param index
     * @param e
     */
    public void set(int index, int e){
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Set failed, index is illegal.");
        }
        data[index] = e;
    }

注意:get方法对data进行了封装,调用者是无法调用到没有元素的索引位置的。

5 包含与搜索

/**
     * 查看数组中是否包含某个元素
     * @param e
     * @return
     */
    public boolean contains(int e){
        // 遍历数组查询元素
        for (int i = 0; i < size; i++){
            // 判断当前位置元素是否是要找的元素
            if (data[i] == e){
                // 返回结果
                return true;
            }
        }
        // 返回结果
        return false;
    }

    /**
     * 查找数组中元素e所在的索引,如果不存在元素e,则返回-1
     * @param e
     * @return
     */
    public int find(int e){
        // 遍历数组查询元素
        for (int i = 0; i < size; i++){
            // 判断当前位置元素是否是要找的元素
            if (data[i] == e){
                // 返回结果
                return I;
            }
        }
        return -1;
    }

6 删除操作

6.1 删除操作过程图示

从以上图示中可以看出,删除操作与添加操作相似,删除操作是一个从后向前赋值的操作,通过将后一个元素赋值给前一个元素从而到达删除元素的目的;值得注意的是最后一个元素也就是size所在位置的元素在操作完成以后并没有被置为默认值,但是调用者是无法调用到这个位置的,所以可以不做处理;

6.2 代码

/**
     * 删除指定索引位置的元素,并返回被删除的元素
     * @param index
     * @return
     */
    public int remove(int index){
        // 判断索引是否合法
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Remove failed, index is illegal.");
        }
        // 记录要被删除的元素
        int ret = data[index];
        // 通过遍历找到要被删除的元素
        for (int i = index + 1; i < size; i++){
            // 从后向前进行赋值操作
            data[i - 1] = data[i];
        }
        // 维护数组长度
        size--;
        // 返回结果
        return ret;
    }

    /**
     * 删除数组中的第一个元素
     * @return
     */
    public int removeFirst(){
        return remove(0);
    }

    /**
     * 删除数组中的最后一个元素
     * @return
     */
    public int removelast(){
        return remove(size - 1);
    }

    /**
     * 删除指定索引
     * @param e
     */
    public void removeElement(int e){
        // 调用查询方法查找要删除元素的索引
        int index = find(e);
        // 判断索引是否合法
        if (index != -1){
            // 调用删除方法执行删除操作
            remove(index);
        }
    }

在删除操作中也使用了类似于添加操作的复用思想。

7 使用泛型改造数组
截止目前为止,封装的数组是int型的(也就是说只能存放int型的数据),在java的概念中关于数组我们一般习惯性的称之为容器;所以我们需要对数组进行一定的改造,让它可以存放任何类型(这些类型包含java中的对象以及我们自己创建的数据类型比如:student、car)
下面看一下改造以后的代码:

package com.arrays.array;

/**
 * Created by yangxiaokai on 2018/5/27.
 */
public class Array<E> {

    private E[] data;
    private int size;

    /**
     * 构造函数,传入数组的容量capacity构造Array
     * @param capacity
     */
    public Array(int capacity){
        // 创建数据,长度为capacity
        data = (E[]) new Object[capacity];
        // 长度为0
        size = 0;
    }

    /**
     * 无参数的构造函数,默认数组的容量capacity=10
     */
    public Array(){
        // 默认数组长度为10
        this(10);
    }

    /**
     * 获取数组的容量
     * @return
     */
    public int getCapacity(){
        return data.length;
    }

    /**
     * 获取数组中的元素个数
     * @return
     */
    public int getSize(){
        return size;
    }

    /**
     * 返回数组是否为空
     * @return
     */
    public boolean isEmpty(){
        return size == 0;
    }

    /**
     * 向所有元素后添加一个新元素
     * @param e
     */
    public void addLast(E e){
        add(size,e);
    }

    /**
     * 在所有元素前添加一个新元素
     * @param e
     */
    public void addFirst(E e){
        add(0,e);
    }

    /**
     * 在index索引的位置插入一个新元素e
     * @param index
     * @param e
     */
    public void add(int index, E e){
        // 判断索引是不是合法
        if (index < 0 || index > size){
            throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
        }
        // 通过遍历将数组中的元素进行位移
        for (int i = size - 1; i >= index; i--){
            data[i + 1] = data[i];
        }
        // 将新添加的元素放入到数组指定的索引位置上
        data[index] = e;
        // 操作数组长度增加1
        size++;
    }

    /**
     * 获取索引index上的元素
     * @param index
     * @return
     */
    public E get(int index){
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Get failed, index is illegal.");
        }
        return data[index];
    }

    /**
     * 设置索引index上的元素
     * @param index
     * @param e
     */
    public void set(int index, E e){
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Set failed, index is illegal.");
        }
        data[index] = e;
    }

    /**
     * 查询数组中是否包含元素e
     * @param e
     * @return
     */
    public boolean contains(E e){
        // 遍历数组查找元素
        for (int i = 0; i < size; i++){
            if (data[i].equals(e)){
                return true;
            }
        }
        return false;
    }

    /**
     * 查找数组中元素e所在的索引,如果不存在元素e,则返回-1
     * @param e
     * @return
     */
    public int find(E e){
        // 遍历数组查找元素
        for (int i = 0; i < size; i++){
            if (data[i].equals(e)){
                // 返回索引
                return i;
            }
        }
        return -1;
    }

    /**
     * 从数组中删除index位置的元素, 返回删除的元素
     * @param index
     * @return
     */
    public E remove(int index){
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Remove failed, index is illegal.");
        }
        // 记录被删除的元素
        E res = data[index];
        // 通过遍历将数组中的元素进行位移
        for (int i = index + 1; i < size; i++){
            // 索引考后的元素覆盖索引靠前的元素
            data[i - 1] = data[i];
        }
        // 操作数组长度
        size--;
        // 将数组的最后一个位置上的元素置为空
        data[size] = null;
        // 返回记录结果
        return res;
    }

    /**
     * 从数组中删除第一个索引上的元素并返回被删除的元素
     * @return
     */
    public E removeFirst(){
        return remove(0);
    }

    /**
     * 从数组中删除最后一个索引上的元素并返回被删除的元素
     * @return
     */
    public E removeLast(){
        return remove(size - 1);
    }

    /**
     * 从数组中删除指定元素
     * @param e
     */
    public void removeElement(E e){
        // 查询数组中是否有此元素
        int index = find(e);
        // 根据结果判断
        if (index != -1){
            remove(index);
        }
    }

    /**
     * toString
     * @return
     */
    @Override
    public String toString(){
        // 创建新的容器
        StringBuilder builder = new StringBuilder();
        // 设置数组格式
        builder.append(String.format("Array: size = %d , capacity = %d\n", size, data.length));
        // 拼接返回值
        builder.append("[");
        for (int i = 0; i < size; i++){
            // 向容器中添加元素
            builder.append(data[i]);
            // 判断是否大于容器长度
            if (i != size -1){
                // 拼接分隔符
                builder.append(", ");
            }
        }
        // 闭合
        builder.append("]");
        // 返回
        return builder.toString();
    }
}

由于现在数组使用了泛型可以存放引用的数据类型,所以我们在删除的方法中需要将最后一个位置上的元素置为空,如果不置空的话将会影响垃圾回收,因为此位置上一直存在一个引用。

8 动态数组改造
截止目前为止,我们自己封装的数据还是一个静态的数组,但是静态数组有一个问题:每一次在初始化的时候对于数组的初始容量没有办法进行准确的预估,这就导致数组空间开辟过大会产生空间的浪费,开辟的过小又不够用的情况;所以我们需要一种自动的可伸缩的机制来实现数组容量的自动化管理来解决我们的这个问题。

8.1 动态数组图示

通过以上图示可以看出,当数组空间达到上限的时候我们可以创建一个容量更大的新数组,然后使用循环将之前数组中的元素放入新数组中,这样就实现了数组的扩容,就可以解决上面我们提到的静态数组空间的问题;对于删除的操作我们也可以采用这样的机制来实现;不过这个过程需要循环来将原来数组中的数据添加到新的数组中可能对于性能有一定的损耗,我们将在文章的最后来一起探究这个数组的性能问题。

8.2 代码

/**
     * 动态数组逻辑(扩容缩容)
     * @param newCapacity
     */
    private void resize(int newCapacity){
        // 创建新的容器
        E[] newData = (E[]) new Object[newCapacity];
        // 复制数组元素到新的数组
        for (int i = 0; i < size; i++){
            newData[i] = data[i];
        }
        // 改变数组索引
        data = newData;
    }
/**
     * 从数组中删除index位置的元素, 返回删除的元素
     * @param index
     * @return
     */
    public E remove(int index){
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Remove failed, index is illegal.");
        }
        // 记录被删除的元素
        E res = data[index];
        // 通过遍历将数组中的元素进行位移
        for (int i = index + 1; i < size; i++){
            // 索引考后的元素覆盖索引靠前的元素
            data[i - 1] = data[i];
        }
        // 操作数组长度
        size--;
        // 将数组的最后一个位置上的元素置为空
        data[size] = null;
        // 数组缩容逻辑
        if (size == data.length / 4 && data.length / 2 != 0){
            // 执行数组缩容
            resize(data.length / 2);
        }

        // 返回记录结果
        return res;
    }
/**
     * 在index索引的位置插入一个新元素e
     * @param index
     * @param e
     */
    public void add(int index, E e){
        // 判断索引是不是合法
        if (index < 0 || index > size){
            throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
        }
        // 判断当前长度是不是大于数组长度,如果大于则进行扩容
        if (size >= data.length){
            resize(data.length * 2);
        }
        // 通过遍历将数组中的元素进行位移
        for (int i = size - 1; i >= index; i--){
            data[i + 1] = data[i];
        }
        // 将新添加的元素放入到数组指定的索引位置上
        data[index] = e;
        // 操作数组长度增加1
        size++;
    }

三 完整版代码

package com.arrays.array;

/**
 * Created by yangxiaokai on 2018/5/27.
 */
public class Array<E> {

    private E[] data;
    private int size;

    /**
     * 构造函数,传入数组的容量capacity构造Array
     * @param capacity
     */
    public Array(int capacity){
        // 创建数据,长度为capacity
        data = (E[]) new Object[capacity];
        // 长度为0
        size = 0;
    }

    /**
     * 无参数的构造函数,默认数组的容量capacity=10
     */
    public Array(){
        // 默认数组长度为10
        this(10);
    }

    /**
     * 获取数组的容量
     * @return
     */
    public int getCapacity(){
        return data.length;
    }

    /**
     * 获取数组中的元素个数
     * @return
     */
    public int getSize(){
        return size;
    }

    /**
     * 返回数组是否为空
     * @return
     */
    public boolean isEmpty(){
        return size == 0;
    }

    /**
     * 向所有元素后添加一个新元素
     * @param e
     */
    public void addLast(E e){
        add(size,e);
    }

    /**
     * 在所有元素前添加一个新元素
     * @param e
     */
    public void addFirst(E e){
        add(0,e);
    }

    /**
     * 在index索引的位置插入一个新元素e
     * @param index
     * @param e
     */
    public void add(int index, E e){
        // 判断索引是不是合法
        if (index < 0 || index > size){
            throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
        }
        // 判断当前长度是不是大于数组长度,如果大于则进行扩容
        if (size >= data.length){
            resize(data.length * 2);
        }
        // 通过遍历将数组中的元素进行位移
        for (int i = size - 1; i >= index; i--){
            data[i + 1] = data[i];
        }
        // 将新添加的元素放入到数组指定的索引位置上
        data[index] = e;
        // 操作数组长度增加1
        size++;
    }

    /**
     * 获取索引index上的元素
     * @param index
     * @return
     */
    public E get(int index){
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Get failed, index is illegal.");
        }
        return data[index];
    }

    /**
     * 设置索引index上的元素
     * @param index
     * @param e
     */
    public void set(int index, E e){
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Set failed, index is illegal.");
        }
        data[index] = e;
    }

    /**
     * 查询数组中是否包含元素e
     * @param e
     * @return
     */
    public boolean contains(E e){
        // 遍历数组查找元素
        for (int i = 0; i < size; i++){
            if (data[i].equals(e)){
                return true;
            }
        }
        return false;
    }

    /**
     * 查找数组中元素e所在的索引,如果不存在元素e,则返回-1
     * @param e
     * @return
     */
    public int find(E e){
        // 遍历数组查找元素
        for (int i = 0; i < size; i++){
            if (data[i].equals(e)){
                // 返回索引
                return i;
            }
        }
        return -1;
    }

    /**
     * 从数组中删除index位置的元素, 返回删除的元素
     * @param index
     * @return
     */
    public E remove(int index){
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Remove failed, index is illegal.");
        }
        // 记录被删除的元素
        E res = data[index];
        // 通过遍历将数组中的元素进行位移
        for (int i = index + 1; i < size; i++){
            // 索引考后的元素覆盖索引靠前的元素
            data[i - 1] = data[i];
        }
        // 操作数组长度
        size--;
        // 将数组的最后一个位置上的元素置为空
        data[size] = null;
        // 数组缩容逻辑
        if (size == data.length / 4 && data.length / 2 != 0){
            // 执行数组缩容
            resize(data.length / 2);
        }

        // 返回记录结果
        return res;
    }

    /**
     * 从数组中删除第一个索引上的元素并返回被删除的元素
     * @return
     */
    public E removeFirst(){
        return remove(0);
    }

    /**
     * 从数组中删除最后一个索引上的元素并返回被删除的元素
     * @return
     */
    public E removeLast(){
        return remove(size - 1);
    }

    /**
     * 从数组中删除指定元素
     * @param e
     */
    public void removeElement(E e){
        // 查询数组中是否有此元素
        int index = find(e);
        // 根据结果判断
        if (index != -1){
            remove(index);
        }
    }

    /**
     * toString
     * @return
     */
    @Override
    public String toString(){
        // 创建新的容器
        StringBuilder builder = new StringBuilder();
        // 设置数组格式
        builder.append(String.format("Array: size = %d , capacity = %d\n", size, data.length));
        // 拼接返回值
        builder.append("[");
        for (int i = 0; i < size; i++){
            // 向容器中添加元素
            builder.append(data[i]);
            // 判断是否大于容器长度
            if (i != size -1){
                // 拼接分隔符
                builder.append(", ");
            }
        }
        // 闭合
        builder.append("]");
        // 返回
        return builder.toString();
    }

    /**
     * 动态数组逻辑(扩容缩容)
     * @param newCapacity
     */
    private void resize(int newCapacity){
        // 创建新的容器
        E[] newData = (E[]) new Object[newCapacity];
        // 复制数组元素到新的数组
        for (int i = 0; i < size; i++){
            newData[i] = data[i];
        }
        // 改变数组索引
        data = newData;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,000评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,745评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,561评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,782评论 1 298
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,798评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,394评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,952评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,852评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,409评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,483评论 3 341
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,615评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,303评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,979评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,470评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,571评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,041评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,630评论 2 359

推荐阅读更多精彩内容