排序算法總結

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閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容