排序算法总结

2018年10月8日

/*
本节主要内容:
1、 时间复杂度
2、冒泡排序
3、选择排序
4、插入排序
5、对数器概念和使用
6、递归行为的时间复杂度
7、归并排序
8、快速排序
9、堆排序
10、排序算法的总结和排序稳定性总结
11、工程中的综合排序算法
12、比较器的作用
*/

时间复杂度:

常数时间的操作:
-------------------------------------------------------------------------
一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。

时间复杂度的表示:
-------------------------------------------------------------------------
时间复杂度为一个算法流程中,常数操作数量的指标。常用O (读作big O)来表示。
具体来说,在常数操作数量的表达式中
只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分 如果记为f(N),那么时间复杂度为O(f(N))。

评价一个算法流程的好坏
-------------------------------------------------------------------------
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间

理解额外空间复杂度:

要排序的数组不算做额外的空间,因为我是在提供的数组上进行操作的,就不算作额外的空间
但是像归并排序:需要借助额外的数组,因此归并排序的额外空间复杂度为O(N),选择排序,冒泡排序,插入排序的额外空间复杂度为O(1)

二分查找

在排好序的数组中查询一个数,时间复杂度为O(log2(N)),也可以写作O(logN)

时间复杂度例子:

一个有序数组A,另一个无序数组B,请打印B中的所有不在A中的数,A数组长度为N,B数组长度为M。

算法流程1:对于数组B中的每一个数,都在A中通过遍历的方式找一下;
--------------------------------
时间复杂度为:O(M * N)

算法流程2:对于数组B中的每一个数,都在A中通过二分的方式找一下;
--------------------------------
时间复杂度为:O(M * logN)
因为二分查找的时间复杂度为O(logN)

算法流程3:先把数组B排序,然后用类似外排的方式打印所有不在A中出现的数;
-------------------------------
时间复杂度分析:
①B数组进行排序,假设时间复杂度为O(M * logM)
②类似外排的方式检查,时间复杂度为:O(M + N)
指针a最多滑动N次,指针b最多滑动M次,且每次只滑动a和b中的一个,最差是a和b都走完了这两个数组
即时间复杂度为:O(M + N)
③故而时间复杂度为:O(M * logM) + O(M + N)

如何分析好坏?

如果A数组长,B数组短,则算法3更好,因为此时N更大
如果A数组短,B数组长,则算法2更好,因为此时N小,M大

冒泡排序:

public static void sort(Comparable[] a){
if(a == null || a.length < 2){
return;
}

for(int end = a.length - 1;end > 0;end--){
    for(int i = 0;i < end;i++){
        if(less(a,i+1,i))
            exch(a,i,i+1);
    }
}

}

冒泡排序时间复杂度:

N + (N-1) + (N-2)+ ...... + 1 = O(N2)

冒泡排序额外空间复杂度O(1)

选择排序

public static void sort(Comparable[] a){
if(a == null || a.length < 2){
return;
}

for(int i = 0;i < a.length;i++){
    int min = i;
    for(int j = i+1;j < a.length;j++){
        if(less(a,j,min))
            min = j;
    }
    exch(a,i,min);
}

}

选择排序时间复杂度:

N + (N-1) + (N-2)+ ...... + 1 = O(N2)

选择排序额外空间复杂度O(1)

插入排序

  1. 首先不同于前面冒泡和选择排序的是:冒泡和选择的时间复杂度是与数据状况无关的

    冒泡:总是两两比较,最大的交换到右边
    选择:每次都选出最小的,交换到最前面
    这两个都和数据状况无关,时间复杂度都是O(N2)
  2. 插入排序则不同:

①当数据有序时:1,2,3,4,5
此时时间复杂度为0(N),因为此时对于每一个i,不需要j的移动,因此为O(N)
②当数据完全倒序时:5,4,3,2,1
此时时间复杂度为O(N2),因为此时对于从第二数开始的每一个数,都需要走到最开始才能回到正确位置,
因此要移动1 + 2 + 3 + 4 + 5 + ...... N 也就是O(N2)
③因此插入排序的时间复杂度为:O(N2)


  1. 当数据状况不同产生的算法流程不同时,一律按照最差的来算
    因此插入排序的时间复杂度为:O(N2)
    插入排序额外空间复杂度O(1)

对数器:

验证算法正确性:小样本验证大样本
贪心策略:验证贪心策略的正确性


对数器的概念和使用

0,有一个你想要测的方法a,
1,实现一个绝对正确但是复杂度不好的方法b
2,实现一个随机样本产生器
3,实现比对的方法
4,把方法a和方法b比对很多次来验证方法a是否正确。
5,如果有一个样本使得比对出错,打印样本分析是哪个方法出错
6,当样本数量很多时比对测试依然正确,可以确定方法a已经正确。


代码实现:

/**
 * 0,有一个你想要测的方法a——此时为插入排序的代码
 */
public static void sort(int[] a){
    int N = a.length;

    for(int i = 1;i < N;i++){
        int tmp = a[i];
        int j = i;

        while(j - 1 >= 0 && less(tmp,a[j-1])){
            a[j] = a[j -1];
            j--;
        }

        a[j] = tmp;
    }
}

private static boolean less(Comparable c0, Comparable c1) {
    return c0.compareTo(c1) < 0;
}

/**
 * 1,实现一个绝对正确但是复杂度不好的方法b
 */
 public static void rightMethod(int[] a){
     Arrays.sort(a);
 }

/**
 * 2,实现一个随机样本产生器
 */
public static int[] generateRandomArray(int maxSize,int maxValue){
    //此时该数组中:长度随机,每一个位置上的元素随机
    //生成长度随机的数组
    int[] a = new int[(int) ((maxSize + 1) * Math.random())];    //此时数组的长度为[0,maxSize]整数
    for (int i = 0; i < a.length; i++) {
        a[i] = (int) ((maxValue + 1) * Math.random()) - (int) ((maxValue) * Math.random());   //产生正数或者负数
    }
    return a;
}

// 3,实现比对的方法
private static boolean isEqual(int[] a1, int[] a2) {
if((a1 == null && a2 != null) || (a1 != null && a2 == null))
return false;
if(a1 == null && a2 == null)
return true;
if(a1.length != a2.length)
return false;
for(int i = 0;i < a1.length;i++) {
if(a1[i] != a2[i])
return false;
}
return true;
}
/**
*
* 4,把方法a和方法b比对很多次来验证方法a是否正确。
* for test
*/
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 10; //产生数组长度为10的随机数组
int maxValue = 100;

    boolean succeed = true;
    for (int i = 0; i < testTime; i++) {
        int[] a1 = generateRandomArray(maxSize, maxValue);
        int[] a2 = copyArray(a1);
        int[] a3 = copyArray(a1);
        sort(a1);
        rightMethod(a2);
        if(!isEqual(a1,a2)){
            succeed = false;
            //打印出错的数组,printArray是自己实现的打印的方法
            printArray(a3);
            break;
        }
    }
    System.out.println(succeed ? "Nice":"F@#K");

    int[] arr = generateRandomArray(maxSize,maxValue);
    printArray(arr);
    sort(arr);
    printArray(arr);
}

/**
 * 数组打印
 */
private static void printArray(int[] a) {
    for (int i = 0; i < a.length; i++) {
        System.out.print(a[i] + "  ");
    }
    System.out.println();
}

/**
 * 数组的拷贝
 */
private static int[] copyArray(int[] a) {
    int[] aCopy = new int[a.length];

    for (int i = 0; i < a.length; i++) {
        aCopy[i] = a[i];
    }
    return aCopy;
}
------------------------------------------------------------------------------------

面试时:准备对数器:排序,堆,二叉树

2018年10月9日
剖析递归行为和递归行为时间复杂度的估算
举个例子
/*
在数组中找最大值——递归来实现
*/


public static int getMax(int[] a,int lo,int hi) {
if(lo == hi)
return a[lo];

    int mid = (lo + hi) /2;
    int getMaxLeft = getMax(a,lo,mid);
    int getMaxRight = getMax(a, mid + 1, hi);

    return Math.max(getMaxLeft, getMaxRight);
}

递归函数就是系统在帮你压栈

分析递归函数行为的通式:

master公式:
T(N) = a * T(N/b) + O(N^d)
master公式适用于每一次划分的子问题的规模一致的情况下


其中参数的含义:

N/b:子过程的样本量
a : 子过程量发生的次数
O(N ^ d) :除去调用子过程外,剩下的过程的时间复杂度
T(N):样本量为N时的时间复杂度


如上算法:
样本量为N/2的过程发生了两次,为2 * T(N/2)
子过程发生的次数为2
除去调用子过程,我们剩下的过程的时间复杂度为:O(1)——因为只进行了一个两个数之间求最大值的运算
因此:
此时的a = 2,b = 2,d = 0
则时间复杂度的分析为:
log(b,a) > d ==> 复杂度为O(N ^ log(b,a))
log(b,a) = d ==> 复杂度为O(N^d * logN)
log(b,a) < d ==> 复杂度为O(N^d)
此时我们的算法满足的是:log(b,a) > d,则时间复杂度为:O(N)

归并排序:

public class Merge {
private static Comparable[] aux;

public static void sort(Comparable[] a){
    if(a == null|| a.length < 2)
        return;

    aux = new Comparable[a.length];

    sort(a,0,a.length  - 1);
}

public static void sort(Comparable[] a,int lo,int hi){
    if(hi <= lo){
        return;
    }

    int mid = lo + (hi - lo)/2;
    sort(a,lo,mid);
    sort(a,mid + 1,hi);
    merge(a,lo,mid,hi);
}

private static void merge(Comparable[] a, int lo, int mid, int hi) {
    int i = lo;
    int j = mid + 1;

    for (int k = lo; k <= hi; k++) {
        aux[k] = a[k];
    }

    for (int k = lo; k <= hi; k++) {
        if(i > mid) a[k] = aux[j++];
        else if (j > hi) a[k] = aux[i++];
        else if(less(aux[i],aux[j])) a[k] = aux[i++];
        else a[k] = aux[j++];
    }
}

private static boolean less(Comparable c0, Comparable c1) {
    return c0.compareTo(c1) < 0;
}

}

时间复杂度分析:
①子过程样本量为N/2——>也就是b = 2
②子过程发生次数为2——>也就是a = 2
③除去调用子过程外,剩下的过程为两个有序数组的合并,时间复杂度为:O(N/2 + N/2) = O(N) ——>d = 1
④满足 log(b,a) = d ==> 复杂度为O(N^d * logN)
则时间复杂度:O(N * logN),额外空间复杂度为:O(N)

快速排序:(此时为随机快排,最常用的排序算法)

public static void sort(Comparable[] a){
if(a == null || a.length < 2)
return;
StdRandom.shuffle(a);

sort(a, 0, a.length - 1);

}

private static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) {
return;
}

int pos = partition(a,lo,hi);
sort(a,lo,pos-1);
sort(a, pos + 1, hi);

}

/关键代码/
private static int partition(Comparable[] a, int lo, int hi) {
Comparable v = a[lo];
int i = lo;
int j = hi + 1;

while (true) {
    while(less(a[++i],v)) if(i == hi) break;
    while(less(v,a[--j])) if(j == lo) break;

    if(j <= i)
        break;

    exch(a, i, j);
}

//交换a[lo]和a[j]
exch(a,lo,j);

return j;

}

private static void exch(Comparable[] a, int i, int j) {
Comparable tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}

private static boolean less(Comparable c0, Comparable c1) {
return c0.compareTo(c1) < 0;
}


时间复杂度分析:
关键代码在于:partition
每一次partition均会固定第一个为partition item
-----------------------------------------------
此时的快速排序的算法的时间复杂度是与数据状况有关系的
例如:


情况一:最坏情况1

[0,1,2,3,4,5,6]
partition(a,0,6)
----------------------------------------------------------------------------
当进入partition时,由于i所指向的1大于0,因此i停止移动,此时i == 0
j从6出发,一直大于0,知道j == 0停止
此时j <= i退出循环
最后交换a[0]和a[0]
一共比较N-1次,交换一次
时间复杂度为O(N)
----------------------------------------------------------------------------
partition(a,1,6)
----------------------------------------------------------------------------
此时与上一个是类似的情况,进行了N-2次比较,交换一次
时间复杂度为O(N-1)
----------------------------------------------------------------------------
..................
因此,此时的时间复杂度为N + (N - 1) + (N - 2) + .......... + 1 = o(N2)

情况二:最坏情况2

[6,5,4,3,2,1,0]
与上一种情况类似,时间复杂度也是O(N2)

情况三:最好情况

中间为x,此时时间复杂度为O(NlogN),联系到3-way partition sort

快速排序的时间复杂度:

时间复杂度为:O(NlogN)——此时的时间复杂度是一个长期期望的一个时间复杂度
额外空间复杂度为:O(logN)


理解此时的额外空间复杂度:空间浪费在了划分点pos上
-----------------------------------
int pos = partition(a,lo,hi);
sort(a,lo,pos-1);
sort(a, pos + 1, hi);
-------------------------------------------
sort代码块中,每一次我们都需要记录pos的值,此时,当sort(a,lo,pos-1);执行完后,我们才知道右侧部分在哪儿
要注意:数组能被二分多少次,额外的空间就是多少
------------------------------------------
因此:长期的期望下:额外的空间复杂度为O(logN)

快速排序对于大量的等值存在时的优化:3-way partition

public static void sort(Comparable[] a){
if (a == null || a.length < 2) {
return;
}

    StdRandom.shuffle(a);
    sort(a, 0, a.length - 1);

}

private static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) {
return;
}

Comparable v = a[lo];
int lt = lo;
int gt = hi;
int i = lo;

while (i <= gt) {
    int cmp = a[i].compareTo(v);

    if(cmp < 0) exch(a, lt++, i++);
    else if(cmp > 0) exch(a, i, gt--);
    else i++;
}
sort(a,lo,lt-1);
sort(a,gt+1,hi);

}

private static void exch(Comparable[] a, int i, int j) {
Comparable tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}


快速排序的思考:

有些算法在设计的过程中,我们想要绕过本身的数据状况怎么办?
** 有两个主要的做法:
** ①随机打乱数据状况,那么此时的时间复杂度就是一个概率事件,可能好可能坏
** ②利用哈希进行打乱

三个O(NlogN)的算法:①QickSort ②MergeSort ③HeapSort
最常用的就是快QuickSort:
* 常数项很低

在工程上:其实并不常见递归函数的存在,通常会改为非递归的版本

堆排序——HeapSort

public static void sort(Comparable[] a){
int N = a.length;
//构建二叉堆——时间复杂度为O(N)
for (int k = N/2; k >= 1; k--) {
sink(a,k,N);
}

//进行排序——时间复杂度为:O(NlogN)
while(N > 1){
    exch(a,1,N);
    sink(a,1,--N);                        
}

}

/*

  • 将父节点与较大的的子节点进行交换
  • */
    private static void sink(Comparable[] a, int k, int N) {
    while(2 * k <= N){
    int j = 2 * k;
    if(j < N && less(a,j,j+1)) j = j + 1; //j指向较大的字节点
    if(!less(a,k,j)) break;
    exch(a,j,k); //交换父节点和较大字节点
    k = j; //k继续向下进行判断
    }
    }

private static void exch(Comparable[] a, int i, int j) {
i--;
j--;
Comparable v = a[i];
a[i] = a[j];
a[j] = v;
}

private static boolean less(Comparable[] a, int i, int j) {
return a[--i].compareTo(a[--j]) < 0;
}


时间复杂度分析:

此时利用的是堆这种数据结构:二叉堆
——堆有序的完全二叉树称为二叉堆
——二叉树的高度为logN

堆排序中有两个关键的步骤:
①构建二叉堆——时间复杂度为O(N)
* 代码:
--------------------------------
for (int k = N/2; k >= 1; k--) {
sink(a,k,N);
}
---------------------------------
* 时间复杂度:
----------------------------------------------------------------------------
每一次sink,都是对一条链进行操作,而链的长度是当前堆的高度
因为叶子节点本身就是堆有序的,即有N/2个不需要进行sink
而对于剩下的1-N/2,都需要sink,时间复杂度为:
log(N-1) + log(N - 2) + log(N - 3) + ............ + log(N - N/2)
即此时的时间复杂度为O(N)
----------------------------------------------------------------------------
②进行排序——时间复杂度为:O(NlogN)
* 代码:
--------------------------------
while(N > 1){
exch(a,1,N);
sink(a,1,--N);
}
-------------------------------
* 时间复杂度:
------------------------------------------------------------------------------------------------------
本身sink(1)的时间复杂度为logN,但是每一次数组的最后都是存放最大值,下次便不再参与比较,每一次搞定的都是末尾的数


总结:堆排序时间复杂度为:O(NlogN),额外空间复杂度为:O(1)

堆是一种很重要的数据结构

排序的稳定性及其汇总

排序的稳定性指的是:排序后相同的数在原始数组中的相对位置不变

几大排序算法的稳定性
第一类:时间复杂度为O(N2)的排序算法
①冒泡排序:
——可以实现排序稳定性
* 相同的值不进行交换
②插入排序:
——可以实现排序稳定性
* 向前插入时,遇到相同的值则停止
③选择排序:
——不可以实现排序稳定性
* 例如:5,5,5,4,0,1
此时选择最小的0与第一位5进行交换,此时就已经破坏了稳定性

第二类:时间复杂度为O(NlogN)的排序算法
④归并排序:
——可以实现排序稳定性
* 最后merge中:当左边和右边相等时,就拷贝左边的,就可以保证排序稳定性
⑤快速排序:
——不可以实现排序稳定性
* 因为partition过程无法做到排序稳定性,partition item总是a[lo],可能就导致相同的数顺序被打乱
⑥堆排序
——不可以实现稳定性
* 构建大顶堆的时候排序稳定性就已经被破坏

为什么要追求排序的稳定性?
答:实际业务中,希望原始信息不被抹去
1、如果只是简单的进行数字的排序,那么稳定性将毫无意义。
2、如果排序的内容仅仅是一个复杂对象的某一个数字属性,那么稳定性依旧将毫无意义(所谓的交换操作的开销已经算在算法的开销内了,如果嫌弃这种开销,不如换算法好了?)
3、如果要排序的内容是一个复杂对象的多个数字属性,但是其原本的初始顺序毫无意义,那么稳定性依旧将毫无意义。
4、除非要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法,例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。(当然,如果需求不需要保持初始的排序意义,那么使用稳定性算法依旧将毫无意义)。

介绍一下工程中的综合排序算法

常见的综合排序:
----------------------------------------------------------------------------
Ⅰ 若数组长度很长,则在工程上:
* 首先进行判断:是基本类型还是自定义类型
** 如果为基础类型:则使用快速排序,因为不需要考虑数据稳定性
** 如果为自定义类型,则使用的是归并排序,因为需要考虑数据稳定性
----------------------------------------------------------------------------
Ⅱ 若数组长度短,则直接使用插入排序,不管是基本数据类型还是自定义类型
虽然插入排序时间复杂度为:O(N2),但是在数据量很小的情况下,劣势不会有很大表现
----------------------------------------------------------------------------
数组长度小于60时,直接使用插入排序
此时联系:快排和归并在数据量小的时候的优化

面试:为什么在综合排序中数据量小的部分选择使用插入排序
答:虽然复杂度高,但是常数项很低,在数据量很小的情况下,劣势不会有很大表现
只有在数据量很大的时候,常数项才可以被忽略,插入排序的劣势才表现出来

面试:为什么基本类型选择使用快排,而自定义类型选择归并
答:考虑到的是数据稳定性问题,快排因为partition数据稳定性得不到保证
而归并排序是可以设计为排序稳定性的

面试:数组中:将数组中奇数放左边,偶数放右边,要求原始数组相对次序不变,且时间复杂度为O(N),空间复杂度为O(1)
答:不能,因为此时奇数偶数也是一个0/1问题,对于快速排序中也是0/1标准,而快速排序的时间复杂度为O(NlogN)

0/1 stable sort很难

认识比较器的作用
Arrays.sort(stus,new IdAscendingComparator());
Arrays方法可以传入一个数组和自定义的比较器

利用到比较器的集合:
PriorityQueue TreeMap

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容