leetcode實(shí)戰(zhàn)——二分搜索及其變形(尋找左右邊界、查找插入位置)

二分查找

前言

說到二分查找很多人都是耳熟能詳,這個(gè)算法基本是每個(gè)工科生(不僅僅是計(jì)算機(jī)相關(guān)專業(yè))的必備知識(shí)點(diǎn),在各種算法的題目中出現(xiàn)的頻率也是極高的。然而很多考題并不會(huì)簡簡單單的去讓你實(shí)現(xiàn)是個(gè)二分算法,而是通過各種變形來考驗(yàn)同學(xué)們對(duì)二分查找算法的理解程度,比如在在排序數(shù)組中查找元素的第一個(gè)和最后一個(gè)位置以及數(shù)組中的第K個(gè)最大元素這兩道題里面就要用到二分搜索來尋找邊界點(diǎn)和逼近最后的正確答案。我猜大多數(shù)人可能和我以前一樣也是僅僅大概知道二分搜索這個(gè)東西并且能夠簡單實(shí)現(xiàn),但是對(duì)于二分搜索的變形并不清楚。這篇文章將從最簡單的二分查找開始講起,然后用兩個(gè)簡單的二分搜索的變形的題目來加深對(duì)二分法的理解,希望能夠讓大家透徹的理解二分搜索這個(gè)重要的算法。

簡介

在計(jì)算機(jī)科學(xué)中,二分搜索,也稱為半間隔搜索、對(duì)數(shù)搜索或二分截?cái)啵且环N搜索算法,用于查找排序數(shù)組中目標(biāo)值的位置。二分搜索將目標(biāo)值與數(shù)組的中間元素進(jìn)行比較。如果它們不相等,則消除目標(biāo)不能位于其中的那一半,并在剩余的一半上繼續(xù)搜索,再次將中間元素與目標(biāo)值進(jìn)行比較,并重復(fù)此過程直到找到目標(biāo)值。如果搜索以其余一半為空結(jié)束,則目標(biāo)不在數(shù)組中。

在最壞的情況下,二分搜索的時(shí)間復(fù)雜度為O(logN),其中N為有序數(shù)組的長度。有專門為快速搜索而設(shè)計(jì)的專用數(shù)據(jù)結(jié)構(gòu),例如哈希表,可以比二分搜索更有效地進(jìn)行搜索。但是,二分搜索可用于解決范圍更廣的問題,例如,在數(shù)組中查找相對(duì)于目標(biāo)數(shù)字的下一個(gè)最小或下一個(gè)最大的元素。

基礎(chǔ)——二分查找

與其空講不如直接用一個(gè)題目來演示,這是LeetCode第704題二分查找

給定一個(gè)n個(gè)元素有序的(升序)整型數(shù)組nums和一個(gè)目標(biāo)值target,寫一個(gè)函數(shù)搜索nums中的target,如果目標(biāo)值存在返回下標(biāo),否則返回-1

示例1:

輸入: nums = [-1,0,3,5,9,12], target = 9
輸出: 4
解釋: 9 出現(xiàn)在 nums 中并且下標(biāo)為 4

示例2:

輸入: nums = [-1,0,3,5,9,12], target = 2
輸出: -1
解釋: 2 不存在 nums 中因此返回 -1

這是最基本的二分搜索,看到這道題,幾乎本能地寫下了答案:

public int search(int[] nums, int target) {
    int left = 0 , right = nums.length-1, mid;
    while (left <= right) {           // 循環(huán)結(jié)束條件 ?
        mid = (left+right) / 2;
        if (target == nums[mid])      // 發(fā)現(xiàn)目標(biāo)元素
            return mid;               // ?
        else if (target < nums[mid])  // 目標(biāo)在左半?yún)^(qū)
            right = mid-1;            // ?
        else                          // 目標(biāo)在右半?yún)^(qū)
            left = mid+1;             // ?
    }
    return -1;                        // 找不到目標(biāo)
}

簡單解釋一下代碼的邏輯:

while (left <= right)

循環(huán)結(jié)束的條件,當(dāng)left==right的時(shí)候,說明范圍已經(jīng)縮減到了最后一個(gè)能夠?qū)ふ业闹担还苡袥]有找到,結(jié)束了這次循環(huán)之后,整個(gè)搜索都應(yīng)該結(jié)束(left>right沒有意義了)

mid = (left+right) / 2;

每次都取中間的一個(gè)元素,在這里左右相加可能出現(xiàn)溢出的情況,可以用mid = left+(right-left) / 2;來代替。注意這個(gè)地方可能出現(xiàn)left==mid的情況,在后面會(huì)提到。

if (target == nums[mid]) return mid;

當(dāng)找到這個(gè)元素的時(shí)候就返回這個(gè)位置的索引。

else if (target < nums[mid]) right = mid-1;

目標(biāo)元素比中間位置元素的位置還要小,那么就以mid-1作為右邊界繼續(xù)搜索。注意這里的mid是包含在原來的查找范圍內(nèi)的,所以需要排除mid繼續(xù)搜索。

else left = mid+1;

目標(biāo)元素比中間的大,把mid元素排除掉,再從mid右邊一個(gè)元素mid+1開始尋找。

當(dāng)循環(huán)終止的時(shí)候,如果找不到目標(biāo)元素,一定是left>right,從邏輯內(nèi)的計(jì)算可以發(fā)現(xiàn)一定是left==right+1。因?yàn)樽詈笠粋€(gè)循環(huán)一定是left==right==mid,在經(jīng)過下面的right=mid-1或者left=mid+1計(jì)算之后,得到left==right+1

OK,現(xiàn)在我們花了大概30秒的時(shí)間把代碼寫了下來,然后測(cè)試通過————有沒有感覺這是一段藏在我們工科生內(nèi)心里面的代碼,不用多想就能寫出來!不過先別高興,這里我就有點(diǎn)疑惑,如果數(shù)組中存在重復(fù)的目標(biāo)元素,那么這段代碼返回的索引是左邊元素還是右邊的還是中間的?!我們來試一下:

兩個(gè)9:

輸入:nums=[-1,0,3,5,9,9,12] target=9
輸出:5

三個(gè)9:

輸入:nums=[-1,0,3,5,9,9,9,12] target=9
輸出:5

四個(gè)9:

輸入:nums=[-1,0,3,5,9,9,9,9,12] target=9
輸出:4

四個(gè)9(修改其他元素):

輸入:nums=[-1,9,9,9,9,10,11,12,13] target=9
輸出:4

所以從上面的例子可以看出來,隨著9的增加,最后返回的索引的位置可能是左邊也可能是中間的也有可能是右邊的,至于到底應(yīng)該是哪個(gè)位置的取決于9的數(shù)量以及在數(shù)組中的位置,上面這段代碼唯一能做的就是就到目標(biāo)元素的其中一個(gè)索引(出現(xiàn)重復(fù)則不能確定索引位置相對(duì)于其他重復(fù)元素的位置),如果找不到就返回-1。

那么現(xiàn)在新的挑戰(zhàn)過來了,如何找到元素在排序數(shù)組中第一個(gè)位置和最后一個(gè)呢?

進(jìn)階——在排序數(shù)組中查找元素的第一個(gè)和最后一個(gè)位置

在排序數(shù)組中查找元素的第一個(gè)和最后一個(gè)位置是leetcode第34題

給定一個(gè)按照升序排列的整數(shù)數(shù)組nums,和一個(gè)目標(biāo)值target。找出給定目標(biāo)值在數(shù)組中的開始位置和結(jié)束位置。

你的算法時(shí)間復(fù)雜度必須是O(logn)級(jí)別。

如果數(shù)組中不存在目標(biāo)值,返回[-1, -1]

示例1:

輸入: nums = [5,7,7,8,8,10], target = 8
輸出: [3,4]

示例2:

輸入: nums = [5,7,7,8,8,10], target = 6
輸出: [-1,-1]

一看這個(gè)題目就是二分法來做,最簡單直接的方法就是用上面一個(gè)案例的二分法找到這個(gè)元素,然后從這個(gè)元素位置同時(shí)向左向右開始遍歷,直到直到第一個(gè)不是這個(gè)目標(biāo)值的元素,就是邊界了。雖說這個(gè)方法用了二分法并且在尋找元素的時(shí)候時(shí)間復(fù)雜度做到了O(logn),但是如果這個(gè)數(shù)組里面所有元素都是目標(biāo)元素,比如把第一個(gè)示例改成[8,8,8,8,8,8],那么在找到這個(gè)元素之后,還是要遍歷整個(gè)數(shù)組,時(shí)間復(fù)雜度就降低到了O(n)了,那么要如何只用兩次二分搜索就找到上下邊界呢?

二分搜索是有一套自己的模板,前一個(gè)案例用到的是基本的套用,在上一題的代碼里面有四個(gè)?,而這個(gè)四個(gè)就是整個(gè)二分搜索的關(guān)鍵點(diǎn),修改了這幾個(gè)值就是修改了邏輯。

那么我們?cè)谶壿嬌显僦匦率崂硪幌逻@個(gè)情況下二分搜索邏輯,現(xiàn)在我們要尋找左邊界:

  1. 如果target==nums[mid],那么左邊界可能就是mid,也有可能在mid的左邊
  2. 如果target<nums[mid],那么左邊界一定在mid左邊
  3. 如果target>nums[mid],那么左邊界一定在mid右邊

看上面的第一個(gè)和第二個(gè)條件,歸納一下可以得出:如果target<=nums[mid]的時(shí)候,target可能在mid這個(gè)位置,也可能在mid的左邊,下一步的right直接用mid就行了,不用減一。所以我們可以把while循環(huán)內(nèi)部的邏輯縮減成下面這個(gè)樣子:

mid = (left+right) / 2;
if (target <= nums[mid])
    right = mid;            // 兩個(gè)條件合二為一
else
    left = mid+1;        // 保持不變

每次循環(huán)都能夠縮小范圍并且始終把左邊界的位置控制在leftright中間,直到循環(huán)結(jié)束。

似乎找到了一絲曙光,有點(diǎn)頭緒了,不過我們好像剛才把return語句給刪掉了,返回的值到底在哪?回答是返回值在循環(huán)結(jié)束之后處理,根據(jù)邊界賦值的條件,我們?cè)谘h(huán)里面沒有辦法判斷是否已經(jīng)找到了這個(gè)元素,能使用的只是循環(huán)結(jié)束之后的leftright兩個(gè)值(其實(shí)只有一個(gè),因?yàn)檠h(huán)結(jié)束之后left==right)。

可是這個(gè)循環(huán)真的能夠結(jié)束嗎?我們有循環(huán)結(jié)束的條件left<=right,如果left==right==mid并且target==nums[mid],這個(gè)循環(huán)不就死循環(huán)了?所以這里為了不讓循環(huán)死在這個(gè)地方,修改一下條件為left<right,分析一下這個(gè)循環(huán):如果left+1==right,那么mid=(left+right)/2=left,則有:

  • 如果target<=nums[mid]right=mid=left,循環(huán)結(jié)束
  • 如果target>nums[mid]left=mid+1=left+1=right,循環(huán)也結(jié)束

所以如果把循環(huán)結(jié)束的條件變成left<right,無論如何循環(huán)都會(huì)結(jié)束。那么現(xiàn)在要根據(jù)循環(huán)結(jié)束之后的leftright這個(gè)兩個(gè)值,找到邊界值。根據(jù)前面的篩選條件,我們到了最后一層的循環(huán),left+1=right,范圍縮小到了最后兩個(gè)元素,只有幾種結(jié)果,我們定義num1<target<num2num1num2都是數(shù)組里面的元素:

  1. [num1, target] 此時(shí)nums[mid]=nums[left]=num1,判斷條件target>nums[mid]成立,left=mid+1,得到最后的循環(huán)結(jié)束的left的位置的值就是target
  2. [target, num2] 此時(shí)同樣nums[mid]=nums[left]=target,判斷條件target<=nums[mid]成立,right=mid,最后得到nums[left]=nums[right]=target
  3. [target, target] 這個(gè)情況和第二種情況一樣,判斷條件target<=nums[mid]成立,得到nums[left]=nums[right]=target

所以從上面的討論可以看出,當(dāng)數(shù)組中存在一個(gè)或者多個(gè)target元素的時(shí)候,一定能夠通過這個(gè)方法找到target的左邊界為left;如果不存在的時(shí)候,循環(huán)終止,這個(gè)left位置的值肯定不是target

所以最后尋找左邊界的代碼如下:

public int searchRangeLeft(int[] nums, int target) {
    int left = 0 , right = nums.length-1, mid;
    while (left < right) {         // 注意
        mid = (left+right) / 2;
        if (target <= nums[mid])  // 在左側(cè)
            right = mid;
        else                      // 在右側(cè)
            left = mid+1;
    }

    return nums[left]==target?left:-1;
}

這個(gè)時(shí)候從上面這個(gè)代碼一定能夠找到元素的左邊界,同樣的我們根據(jù)上面的分析過程,完成右邊界的代碼:

// 錯(cuò)誤代碼
public int searchRangeRight(int[] nums, int target) {
    int left = 0 , right = nums.length-1, mid;
    while (left < right) {
        mid = (left+right) / 2 + 1 ;
        if (target >= nums[mid])  // 右邊界在右側(cè) 和上面代碼不同的地方
            left = mid;           // 這里變成了left
        else                      // 右邊界在左側(cè)
            right = mid-1;        // 這里變成了right
    }

    return nums[left]==target?left:-1;
}

然后把運(yùn)行一下nums = [5,7,7,8,8,10], target = 8這個(gè)測(cè)試用例發(fā)現(xiàn)出現(xiàn)了死循環(huán)!WTF!發(fā)生了什么!明明和尋找左邊界的代碼一樣啊!問題就出在mid = (left+right) / 2 ;這行代碼里面。我們回頭看一下循環(huán)的終止條件的分析:

  • 如果target<=nums[mid](改成了target<nums[mid]),right=mid=left(根據(jù)mid=(left+right)/2來的,所以此時(shí)right=mid-1=left-1),循環(huán)結(jié)束
  • 如果target>nums[mid](改成了target>=nums[mid]),left=mid+1=left+1=right(變成了left=mid=right-1,此時(shí)left<right仍然成立,所以循環(huán)會(huì)一直進(jìn)行下去),循環(huán)也結(jié)束

在尋找左邊界的時(shí)候如果left=right-1,計(jì)算出來的mid值一直是left,這樣能夠讓mid的位置始終靠左;但是在計(jì)算右邊界的時(shí)候,需要讓mid的位置向右靠,終止循環(huán),所以此時(shí)mid的計(jì)算公式要改成mid = (left+right) / 2 + 1,最后讓mid=right才能最后讓left==right。修改之后代碼如下:

// 正確代碼
public int searchRangeRight(int[] nums, int target) {
    int left = 0 , right = nums.length-1, mid;
    while (left < right) {
        mid = (left+right) / 2 + 1 ;   // 注意
        if (target >= nums[mid])
            left = mid;
        else
            right = mid-1;
    }

    return nums[left]==target?left:-1;
}

組合上面兩個(gè)方法就可以得到我們最后的結(jié)果了。上面講的兩個(gè)方法,也可以用來代替案例一中的二分搜索,畢竟在案例一只要找到了這個(gè)元素就可以了,不在乎是左邊界還是有邊界。

好了,感覺我們已經(jīng)徹底學(xué)習(xí)完了二分搜索并且完全理解了,可以關(guān)上電腦好好休息了。但是——不!Hold On!我還有一個(gè)問題:如果在有序數(shù)組里面不存在這個(gè)target,我們想把它插入到這個(gè)數(shù)組里面應(yīng)該怎么做?上面的代碼應(yīng)該怎么修改。

進(jìn)階——搜索插入位置

這是leetcdoe第35題搜索插入位置

給定一個(gè)排序數(shù)組和一個(gè)目標(biāo)值,在數(shù)組中找到目標(biāo)值,并返回其索引。如果目標(biāo)值不存在于數(shù)組中,返回它將會(huì)被按順序插入的位置。

你可以假設(shè)數(shù)組中無重復(fù)元素。

示例1:

輸入: [1,3,5,6], 5
輸出: 2

示例2:

輸入: [1,3,5,6], 2
輸出: 1

看到這道題,感覺和案例二特別類似,我們找到元素的左邊界不就可以了,但是我們不知道當(dāng)元素不存在的時(shí)候,返回的位置是不是正確的位置,所以我們修改一下上面的searchRangeLeft方法如下:

public int searchInsert(int[] nums, int target) {
    int left = 0 , right = nums.length-1, mid;
    while (left < right) {
        mid = (left+right) / 2;
        if (target <= nums[mid])
            right = mid;
        else
            left = mid+1;
    }

    return left;    // 這里修改返回的結(jié)果
}

然后運(yùn)行一下代碼測(cè)試一下示例1示例2,發(fā)現(xiàn)返回的結(jié)果果然是正確的,分別是21,接下來不管我們?cè)趺葱薷?code>target的值只要是target<=6都能夠得到正確的結(jié)果,當(dāng)target==8的時(shí)候,返回了3,是錯(cuò)誤的結(jié)果。所以直接照搬代碼肯定不行,那我們要怎么修改代碼呢?記住我們現(xiàn)在要尋找的坐標(biāo)是第一個(gè)大于等于target的元素的坐標(biāo)

還記得案例二里面的分析方法嗎,我們?cè)俜秩N條件來分析一下:

  1. 如果target==nums[mid],找到了等于target的元素,保留下來mid位置,設(shè)置right=mid
  2. 如果target<nums[mid],通過以下兩個(gè)結(jié)論可以得到right=mid
    • 如果target存在,一定在mid左邊
    • 如果target不存在,第一個(gè)大于等于target的元素可以能是nums[mid],也有可能在mid的左邊
  3. 如果target>nums[mid],通過以下兩個(gè)結(jié)論可以設(shè)置left=mid+1
    • 如果target存在,一定在mid右邊
    • 如果target不存在,第一個(gè)大于等于target的元素也一定在mid的右邊

這里發(fā)現(xiàn)邏輯和原來的尋找左邊界的時(shí)候情況一模一樣,但是對(duì)于返回的結(jié)果是否正確還不確定,還要繼續(xù)討論一下。

對(duì)于target的值我們分三種情況討論:

1 當(dāng)target<nums[0]也就是當(dāng)target比數(shù)組里面的最小值還要小的時(shí)候,可知在循環(huán)中每次都進(jìn)入第二個(gè)判斷條件,最后得到的left0,符合預(yù)期。

2 當(dāng)target>nums[nums.length-1]的時(shí)候,在循環(huán)中每次都進(jìn)入最后一個(gè)判斷主體,最后得到left=num.length-1,也就是right的初始值,這樣得到的答案是明顯不對(duì)的。所以在進(jìn)入這個(gè)循環(huán)之前,我們可以直接判斷一下target的值是否大于數(shù)組最右側(cè)的值,如果是的就直接返回nums.length;如果不是才進(jìn)入下面的循環(huán)。

3 如果target處于元素中間的位置,就進(jìn)入接下來的分析。

這里我們仔細(xì)看這個(gè)循環(huán)

while (left < right) {
    mid = (left+right) / 2;
    if (target <= nums[mid])
        right = mid;
    else
        left = mid+1;
}

在進(jìn)行最后一輪循環(huán)的時(shí)候,left==right-1,我們定義兩個(gè)元素num1=nums[left]num2=nums[right],并且一定滿足num1<=num2,在這個(gè)討論中我們可以忽視target存在于數(shù)組中的情況(我們?cè)谏厦嬉呀?jīng)討論過了這個(gè)計(jì)算結(jié)果的正確性),target和這兩個(gè)值之間只有可能有下面三種情況:

  1. target<num1<=num2 可得target<nums[mid]=nums[start]=num1,所以進(jìn)入第一個(gè)判斷條件,最后得到nums[left]=nums[right]=num1,也就是第一個(gè)大于target的元素
  2. num1<target<num2 可得target>nums[mid]=nums[start]=num1,所以進(jìn)入第二個(gè)判斷條件,最后得到nums[left]=nums[mid+1]=nums[right]=num2,這也是第一個(gè)大于target的元素
  3. num1<=num2<target 這種情況不可能出現(xiàn)在這個(gè)循環(huán)里面,因?yàn)楦鶕?jù)循環(huán)的條件決定了不可能出現(xiàn)right所在元素的值小于target

所以從上面就可以判斷出最后得到的left的值就是第一個(gè)大于等于target的元素的位置的索引。

歸納一下上面的代碼可以得到:

public int searchInsert(int[] nums, int target) {
    int left = 0 , right = nums.length-1, mid;
    if (target > nums[nums.length-1])   // 循環(huán)前置條件
        return nums.length;

    while (left < right) {
        mid = (left+right) / 2;
        if (target <= nums[mid])
            right = mid;
        else
            left = mid+1;
    }

    return left;
}

其實(shí)這道題還有一個(gè)小小的技巧,我們可以拋棄這個(gè)前置條件target<=nums[nums.lenght-1],然后初始化right的時(shí)候設(shè)置成nums.length,這里其實(shí)我們是自己偷偷的把數(shù)組的最后面增加了一個(gè)值為無窮大的元素,target一定小于無窮大,最遠(yuǎn)也只能插入到這個(gè)位置,不會(huì)越界。而且也不用擔(dān)心指針會(huì)溢出,因?yàn)?code>mid值在計(jì)算的過程中永遠(yuǎn)不可能等于nums.length

總結(jié)

二分搜索的核心就是循環(huán)結(jié)束條件左右邊界迭代規(guī)則,明白了如何確定這兩點(diǎn),二分搜索就能輕松為你所用。

二分搜索本身并不是特別難的一個(gè)知識(shí)點(diǎn),但是是非常重要的一個(gè)概念,如果題目提到了或者暗示要用O(logN)(或者更普遍的說類似O(NlogN)甚至O(N^2logN))的時(shí)間復(fù)雜度,首先應(yīng)該想到二分,如果不能二分搜索,二分搜索的相關(guān)思想分治法也應(yīng)該從腦袋里面出現(xiàn)。

這篇文章是二分搜索基礎(chǔ)知識(shí)以及應(yīng)用,在LeetCode上面還有其他的二分搜索精彩應(yīng)用的題目,希望大家能夠?qū)W習(xí)完這篇文章之后能夠熟練使用二分搜索解決下面的問題

更多內(nèi)容請(qǐng)看我的個(gè)人博客

參考

二分查找算法細(xì)節(jié)詳解
Binary search algorithm
Fractional cascading
Clean iterative solution with two binary searches (with explanation)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評(píng)論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,740評(píng)論 3 420
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評(píng)論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,931評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,321評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評(píng)論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,533評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,082評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,891評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,067評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,319評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評(píng)論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評(píng)論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,794評(píng)論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,076評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容