找工作知识储备(3)A---从头说12种排序算法:原理、图解、动画视频演示、代码以及笔试面试题目中的应用

作者:寒小阳 时间:2013年9月。
出处:http://blog.csdn.net/han_xiaoyang/article/details/11596001
声明:版权所有,转载请注明出处,谢谢。

0、前言

从这一部分开始直接切入我们计算机互联网笔试面试中的重头戏算法了,初始的想法是找一条主线,比如数据结构或者解题思路方法,将博主见过做过整理过的算法题逐个分析一遍(博主当年自己学算法就是用这种比较笨的刷题学的,囧),不过又想了想,算法这东西,博主自己学的过程中一直深感,基础还是非常重要的,很多难题是基础类数据结构和题目的思想综合发散而来。比如说作为最基本的排序算法就种类很多,而事实上笔试面试过程中发现掌握的程度很一般,有很多题目,包括很多算法难题,其母题或者基本思想就是基于这些经典算法的,比如说快排的partition算法,比如说归并排序中的思想,比如说桶排序中桶的思想。
这里对笔试面试最常涉及到的12种排序算法(包括插入排序、二分插入排序、希尔排序、选择排序、冒泡排序、鸡尾酒排序、快速排序、堆排序、归并排序、桶排序、计数排序和基数排序)进行了详解。每一种算法都有\color{red}{基本介绍、算法原理分析、图解/视频演示、算法代码、笔试面试重点分析、笔试面试题}等板块,希望能帮助大家真正理解这些排序算法,并能使用这些算法的思想解决一些题。不多说了,下面就进入正题了。

一、插入排序

1)算法简介
插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过\color{red}{构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入}。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,\color{red}{需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间}

2)算法描述和分析
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

\color{blue}{1、从第一个元素开始,该元素可以认为已经被排序}

\color{blue}{2、取出下一个元素,在已经排序的元素序列中从后向前扫描}

\color{blue}{3、如果该元素(已排序)大于新元素,将该元素移到下一位置}

\color{blue}{4、重复步骤3,直到找到已排序的元素小于或者等于新元素的位置}

\color{blue}{5、将新元素插入到该位置后}

\color{blue}{6、重复步骤2~5}

如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数减去(n-1)次。平均来说\color{red}{插入排序算法复杂度为O(n^2)}。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

3)算法图解、视频演示

图解:

视频:插入排序舞蹈

)

4)算法代码

void insertion_sort(int array[], int first, int last)
 {
        int i,j;
        int temp;
        for (i = first+1; i<=last;i++)
        {
                temp = array[i];
                j=i-1;
 
                //与已排序的数逐一比较,大于temp时,该数后移
                while((j>=first) && (array[j] > temp))  //当first=0,j循环到-1时,由于[[短路求值]],不会运算array[-1]
                {
                        array[j+1] = array[j];
                        j--;
                }
                array[j+1] = temp;      //被排序数放到正确的位置
 
        }
 }

5)考察点,重点和频度分析
把插入排序放在第一个的原因是因为其出现的频度不高,尤其是这里提到的直接排序算法,基本在笔试的选择填空问时间空间复杂度的时候才可能出现。毕竟排序速度比较慢,因此算法大题中考察的次数比较比较少。

6)笔试面试例题

例题1、

请写出链表的插入排序程序

template<typename T>
struct list_node
{
    struct list_node<T> *next;
    T value;
};
template<typename T>
struct _list
{
    struct list_node<T> *head;
    int size;
};
template<typename T>
void SortLink(struct _list<T> * link) {
    struct list_node<T> *pHead,*pRear,*p,*tp;
    if (!link) return;
    for (pHead=link->head,pRear=0;pHead;pHead=pHead->next) {
        for (tp=pHead,p=pHead->next;p;tp=p,p=p->next)
            if (pHead->value>=p->value)
                tp->next=p->next,p->next=pHead,pHead=p,p=tp;
        if (!pRear) link->head=pHead;
        else pRear->next=pHead;
        pRear=pHead;
    }
}

例题2、

\color{red}{下列排序算法中最坏复杂度不是n(n-1)/2的是?}

A.快速排序 B.冒泡排序 C.直接插入排序 \color{blue}{D.堆排序}

二、二分插入排序

1)算法简介
二分(折半)插入(Binary insert sort)排序是一种在直接插入排序算法上进行小改动的排序算法。其与直接排序算法最大的区别在于\color{red}{查找插入位置时使用的是二分查找的方式},在速度上有一定提升。

2)算法描述和分析
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

\color{blue}{1、从第一个元素开始,该元素可以认为已经被排序}

\color{blue}{2、取出下一个元素,在已经排序的元素序列中二分查找到第一个比它大的数的位置}

\color{blue}{3、将新元素插入到该位置后}

\color{blue}{4、重复上述两步}

    1)稳定

    2)空间代价:O(1)

    3)时间代价:插入每个记录需要O(log i)比较,最多移动i+1次,最少2次。最佳情况O(n log n),最差和平均情况O(n^2)。

二分插入排序是一种稳定的排序。\color{red}{当n较大时,总排序码比较次数比直接插入排序的最差情况好得多}\color{red}{但比最好情况要差,所元素初始序列已经按排序码接近有序时,直接插入排序比二分插入排序比较次数少}。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。

3)算法图解、视频演示

图解:

视频:二分插入排序

4)算法代码

void BinInsertSort(int a[], int n) 
{ 
        int key, left, right, middle; 
        for (int i=1; i<n; i++) 
        { 
                key = a[i]; 
                left = 0; 
                right = i-1; 
                while (left<=right) 
                { 
                        middle = (left+right)/2; 
                        if (a[middle]>key) 
                                right = middle-1; 
                        else 
                                left = middle+1; 
                } 
                 
                for(int j=i-1; j>=left; j--) 
                { 
                        a[j+1] = a[j]; 
                } 
                 
                a[left] = key;        
        } 
}

5)考察点,重点和频度分析
这个排序算法在笔试面试中出现的频度也不高,但毕竟是直接排序算法的一个小改进算法,同时二分查找又是很好的思想,有可能会在面试的时候提到,算法不难,留心一下就会了。

6)笔试面试例题

例题1、

\color{red}{下面的排序算法中,初始数据集的排列顺序对算法的性能无影响的是( )}

A、二分插入排序 \color{blue}{B、堆排序} C、冒泡排序 D、快速排序

例题2、

\color{red}{写出下列算法的时间复杂度。}

(1)冒泡排序;(2)选择排序;(3)插入排序;(4)二分插入排序;(5)快速排序;(6)堆排序;(7)归并排序;

三、希尔排序

1)算法简介
希尔排序,也称递减增量排序算法,因DL.Shell于1959年提出而得名,是插入排序的一种高速而稳定的改进版本。

2)算法描述
\color{blue}{1、先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。}

\color{blue}{2、所有距离为d1的倍数的记录放在同一个组中,在各组内进行直接插入排序。}

\color{blue}{3、取第二个增量d2<d1重复上述的分组和排序,}

\color{blue}{4、直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。}

希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n2),而Hibbard增量的希尔排序的时间复杂度为O(N(5/4)),但是现今仍然没有人能找出希尔排序的精确下界。

3)算法图解、视频演示

图解:




视频:希尔排序Shell Sort 舞蹈

4)算法代码

#include <stdio.h>
 
int main()
{
     const int n = 5;
     int i, j, temp; 
     int gap = 0;
     int a[] = {5, 4, 3, 2, 1}; 
     while (gap<=n)
     {
          gap = gap * 3 + 1;
     } 
     while (gap > 0) 
     {
         for ( i = gap; i < n; i++ )
         {
             j = i - gap;
             temp = a[i];             
             while (( j >= 0 ) && ( a[j] > temp ))
             {
                 a[j + gap] = a[j];
                 j = j - gap;
             }
             a[j + gap] = temp;
         }
         gap = ( gap - 1 ) / 3;
     }    
 }

5)考察点,重点和频度分析
事实上希尔排序算法在笔试面试中出现的频度也不比直接插入排序高,但它的时间复杂度并不是一个定值,所以偶尔会被面试官问到选择的步长和时间复杂度的关系,要稍微有点了解吧。算法大题中使用该方法或者其思想的题也不多。

6)笔试面试例题

例题1、

\color{red}{写出希尔排序算法程序,并说明最坏的情况下需要进行多少次的比较和交换。}

程序略,需要O(n^2)次的比较

例题2、

\color{red}{设要将序列(Q, H, C, Y, P, A, M, S, R, D, F, X)中的关键码按字母序的升序重新排列},则:

冒泡排序一趟扫描的结果是 \color{blue}{ H, C, Q, P, A, M, S, R, D, F, X ,Y }

初始步长为4的希尔(shell)排序一趟的结果是 \color{blue}{P, A, C, S, Q, D, F, X , R, H,M, Y}

二路归并排序一趟扫描的结果是 \color{blue}{H, Q, C, Y,A, P, M, S, D, R, F, X}

快速排序一趟扫描的结果是 \color{blue}{F, H, C, D, P, A, M, Q, R, S, Y,X}

堆排序初始建堆的结果是 \color{blue}{A, D, C, R, F, Q, M, S, Y,P, H, X}

四、选择排序

1)算法简介
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。\color{red}{首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置}\color{red}{然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾}。以此类推,直到所有元素均排序完毕。

2)算法描述和分析
n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果:

\color{blue}{1、初始状态:无序区为R[1..n],有序区为空。}

\color{blue}{2、第i趟排序(i=1,2,3...n-1)}

\color{blue}{第i趟排序开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)}\color{blue}{该趟排序从当前无序区中选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换}\color{blue}{使R[1..i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区}

\color{blue}{3、前n-1趟结束,数组有序化了}

选择排序的交换操作介于0和(n-1)次之间。选择排序的比较操作为n(n-1)/2次之间。选择排序的赋值操作介于0和3(n-1)次之间。比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2。 交换次数O(n),最好情况是,已经有序,交换0次;最坏情况是,逆序,交换n-1次。 交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。

最差时间复杂度 О(n²)
最优时间复杂度 О(n²)
平均时间复杂度 О(n²)
最差空间复杂度 О(n) total, O(1)

3)算法图解、视频演示

图解:

视频:选择排序Select Sort排序舞蹈

4)算法代码

void selection_sort(int *a, int len)
{
    register int i, j, min, t;
    for(i = 0; i < len - 1; i ++)
    {
        min = i;
        //查找最小值
        for(j = i + 1; j < len; j ++)
            if(a[min] > a[j])
                min = j;
        //交换
        if(min != i)
        {
            t = a[min];
            a[min] = a[i];
            a[i] = t;
        }
    }
}

5)考察点,重点和频度分析
就博主看过的笔试面试题而言,选择算法也大多出现在选择填空中,要熟悉其时间和空间复杂度,最好最坏的情况分别是什么,以及在那种情况下,每一轮的比较次数等。

6)笔试面试例题

例题1、

\color{red}{在插入和选择排序中,若初始数据基本正序,则选用}:\color{blue}{ 插入排序}(到尾部) ;\color{red}{若初始数据基本反序,则选用}:\color{blue}{ 选择排序}

例题2、

\color{red}{下述几种排序方法中,平均查找长度()最小的是}
A. 插入排序 \color{blue}{B.快速排序 } C. 归并排序 D. 选择排序

五、冒泡排序

1)算法简介
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

2)算法描述
\color{blue}{1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。}

\color{blue}{2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。}

\color{blue}{3、针对所有的元素重复以上的步骤,除了最后一个。}

\color{blue}{4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。}

冒泡排序是与插入排序拥有相等的执行时间,但是两种法在需要的交换次数却很大地不同。在最坏的情况,冒泡排序需要O(n2)次交换,而插入排序只要最多O(n)交换。冒泡排序的实现(类似下面)通常会对已经排序好的数列拙劣地执行(O(n2)),而插入排序在这个例子只需要O(n)个运算。因此很多现代的算法教科书避免使用冒泡排序,而用插入排序取代之。冒泡排序如果能在内部循环第一次执行时,使用一个旗标来表示有无需要交换的可能,也有可能把最好的复杂度降低到O(n)。在这个情况,在已经排序好的数列就无交换的需要。若在每次走访数列时,把走访顺序和比较大小反过来,也可以稍微地改进效率。有时候称为往返排序,因为算法会从数列的一端到另一端之间穿梭往返。

最差时间复杂度 O(n^2)
最优时间复杂度 O(n)
平均时间复杂度 O(n^2)
最差空间复杂度 总共O(n),需要辅助空间O(1)

3)算法图解、视频演示

图解:

视频:舞动的排序算法 冒泡排序

4)算法代码

 #include <stdio.h>
 void bubbleSort(int arr[], int count)
   {
       int i = count, j;
       int temp;
 
       while(i > 0)
       {
          for(j = 0; j < i - 1; j++)
          {
              if(arr[j] > arr[j + 1])
              {   temp = arr[j];
                  arr[j] = arr[j + 1];
                  arr[j + 1] = temp;
              }
          }
          i--;
      }
 
  }
 
  int main()
  {
      //测试数据
      int arr[] = {5, 4, 1, 3, 6};
      //冒泡排序
      bubbleSort(arr, 5);
      //打印排序结果
      int i;
      for(i = 0; i < 5; i++)
          printf("%4d", arr[i]);
 }

5)考察点,重点和频度分析
一般我们学到的第一个排序算法就是冒泡排序,不得不说,这个还真是一个很常见的考点,平均时间空间复杂度,最好最坏情况下的时间空间复杂度,在不同情况下每一趟的比较次数,以及加标志位减少比较次数等,都是需要注意的地方。

6)笔试面试例题

例题1、

\color{red}{对于整数序列100,99,98,…3,2,1,如果将它完全倒过来,分别用冒泡排序,它们的比较次数和交换次数各是多少?}
\color{blue}{ 答:冒泡排序的比较和交换次数将最大,都是1+2+…+n-1=n(n-1)/2=50×99=4545次。}

例题2、

\color{red}{把一个字符串的大写字母放到字符串的后面,各个字符的相对位置不变,不能申请额外的空间。}

事实上,这道题放到冒泡排序这里不知道是不是特别合适,只是有一种解法是类似冒泡的思想,如下解法一

\color{blue}{解法一、}
每次遇到大写字母就往后冒,最后结果即为所求

#include <stdio.h>
#include <string.h>
//题目以及要求:把一个字符串的大写字母放到字符串的后面,
//各个字符的相对位置不变,不能申请额外的空间。 
//判断是不是大写字母 
int isUpperAlpha(char c){
if(c >= 'A' && c <= 'Z'){
return 1;
}
return 0; 
}
//交换两个字母 
void swap(char *a, char *b){
char temp = *a;
*a = *b;
*b = temp;
} 
char * mySort(char *arr, int len){
if(arr == NULL || len <= 0){
return NULL;
}
int i = 0, j = 0, k = 0;
for(i = 0; i < len; i++){
for(j = len - 1 - i; j >= 0; j--){
if(isUpperAlpha(arr[j])){
for(k = j; k < len - i - 1; k++){
swap(&arr[k], &arr[k + 1]);
}
break;
}
//遍历完了字符数组,但是没发现大写字母,所以没必要再遍历下去
if(j == 0 && !isUpperAlpha(arr[j])){
//结束;
                           return arr;
}
}
}
//over: 
return arr;
}
int main(){
char arr[] = "aaaaaaaaaaaaaaaaaaaaaaaAbcAdeBbDc";
printf("%s\n", mySort(arr, strlen(arr)));
return 0;
}

\color{blue}{解法二}
步骤如下

1、两个指针p1和p2,从后往前扫描

2、p1遇到一个小写字母时停下, p2遇到大写字母时停下,两者所指向的char交换

3、p1, p2同时往前一格

代码如下:

#include <stdio.h>
#include <string.h>
//判断是不是大写字母 
int isUpperAlpha(char c){
if(c >= 'A' && c <= 'Z'){
return 1;
}
return 0; 
}
//交换两个字母 
void swap(char *a, char *b){
char temp = *a;
*a = *b;
*b = temp;
} 
char * Reorder(char *arr, int len){
if(arr == NULL || len <= 0){
return NULL;
}
int *p1 = arr;
int *p2 = arr;
While(p1<arr+len && p2<arr+len){
While( isUpperAlpha(*p1) ){
P1++;
}
While( !isUpperAlpha(*p2) ){
P2++;
}
swap(p1, p2)
}
//结束
return arr;
}
int main(){
char arr[] = "aaaaaaaaaaaaaaaaaaaaaaaAbcAdeBbDc";
printf("%s\n", Reorder(arr, strlen(arr)));
return 0;
}

六、鸡尾酒排序/双向冒泡排序

1)算法简介
鸡尾酒排序等于是冒泡排序的轻微变形。不同的地方在于\color{red}{从低到高然后从高到低},而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能,原因是冒泡排序只从一个方向进行比对(由低到高),每次循环只移动一个项目。

2)算法描述和分析
\color{blue}{1、依次比较相邻的两个数,将小数放在前面,大数放在后面}

\color{blue}{2、第一趟可得到:将最大数放到最后一位}

\color{blue}{3、第二趟可得到:将第二大的数放到倒数第二位}

\color{blue}{4、如此下去,重复以上过程,直至最终完成排序}

鸡尾酒排序最糟或是平均所花费的次数都是O(n^2),但如果序列在一开始已经大部分排序过的话,会接近O(n)。

最差时间复杂度 O(n^2)
最优时间复杂度 O(n)
平均时间复杂度 O(n^2)

3)算法图解、视频演示

图解:


4)算法代码

void CocktailSort(int *a,int nsize)
{
    int tail=nsize-1;
    for (int i=0;i<tail;)
    {
        for (int j=tail;j>i;--j) //第一轮,先将最小的数据排到前面
        {
            if (a[j]<a[j-1])
            {
                int temp=a[j];
                a[j]=a[j-1];
                a[j-1]=temp;
            }
        }
        ++i;                    //原来i处数据已排好序,加1
        for (j=i;j<tail;++j)    //第二轮,将最大的数据排到后面
        {
            if (a[j]>a[j+1])
            {
                int temp=a[j];
                a[j]=a[j+1];
                a[j+1]=temp;
            }    
        }
        tail--;                 //原tail处数据也已排好序,将其减1
    }
}

5)考察点,重点和频度分析
鸡尾酒排序在博主印象中出现的频度也不高,用到它的算法题大题很少,选择填空出现的话多以双向冒泡排序的名称出现,注意注意时间空间复杂度,理解理解算法应该问题就不大了。

6)笔试面试例题
考点基本类似冒泡排序,请参考上一节

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

推荐阅读更多精彩内容