找工作知識(shí)儲(chǔ)備(2)---數(shù)組字符串那些經(jīng)典算法:最大子序列和,最長(zhǎng)遞增子序列声邦,最長(zhǎng)公共子串乏奥,最長(zhǎng)公共子序列,字符串編輯距離亥曹,最長(zhǎng)不重復(fù)子串邓了,最長(zhǎng)回文子串

作者:寒小陽 時(shí)間:2013年9月。
出處:http://blog.csdn.net/han_xiaoyang/article/details/11596001媳瞪。
聲明:版權(quán)所有骗炉,轉(zhuǎn)載請(qǐng)注明出處,謝謝蛇受。

0句葵、前言

這一部分的內(nèi)容原本是打算在之后的字符串或者數(shù)組專題里面寫的,但看著目前火熱進(jìn)行的各家互聯(lián)網(wǎng)公司筆試面試中兢仰,出現(xiàn)了其中的一兩個(gè)內(nèi)容乍丈,就隨即將這些經(jīng)典問題整理整理,單寫一篇發(fā)上來了把将。這里爭(zhēng)取覆蓋面廣一些轻专,列舉了7個(gè)最經(jīng)典的問題,也會(huì)是之后大家筆試面試常見到的問題察蹲,而每個(gè)問題下都列舉了幾種思路请垛,掌握這些經(jīng)典問題的解題思路和算法相信對(duì)同類型問題的解答都能有幫助。

這里總結(jié)的幾個(gè)問題分別是最大子序列和递览,最長(zhǎng)遞增子序列叼屠,最長(zhǎng)公共子串,最長(zhǎng)公共子序列绞铃,字符串編輯距離镜雨,最長(zhǎng)不重復(fù)子串,最長(zhǎng)回文子串儿捧。其中前兩個(gè)問題是針對(duì)數(shù)組求解的荚坞,后五個(gè)問題是針對(duì)字符串求解的。多數(shù)問題都有動(dòng)態(tài)規(guī)劃的解法(博主不堪地表示菲盾,自己動(dòng)態(tài)規(guī)劃也較弱颓影,只能想到一些基本的思路),這些解法需要細(xì)細(xì)琢磨懒鉴,可發(fā)散式地使用在很多其他的題目上诡挂。

一碎浇、最大子序列和

這里把最大子序列和放在第一個(gè)位置,它并不是字符串相關(guān)的問題璃俗,事實(shí)上它的目的是要找出由數(shù)組成的一維數(shù)組中和最大的連續(xù)子序列奴璃。比如[0,-2城豁,3苟穆,5,-1唱星,2]應(yīng)返回9雳旅,[-9,-2间聊,-3攒盈,-5,-3]應(yīng)返回-2甸饱。

1沦童、動(dòng)態(tài)規(guī)劃法
你也許從這兩個(gè)例子中已經(jīng)可以看出仑濒,使用動(dòng)態(tài)規(guī)劃的方法很容易完成這個(gè)任務(wù)叹话,\color{red}{只要前i項(xiàng)的和還沒有小于0那么子序列就一直向后擴(kuò)展,否則丟棄之前的子序列開始新的子序列}墩瞳,\color{red}{同時(shí)我們要記下各個(gè)子序列的和驼壶,最后找到和最大的子序列。}但是你可能需要謹(jǐn)慎一些喉酌,在整個(gè)數(shù)組都為負(fù)的情況下热凹,所以初始的和最大值賦值不當(dāng)?shù)脑捒赡軙?huì)出問題。

根據(jù)以上的思路我們可以有以下的代碼:

/**********************************************************************
動(dòng)態(tài)規(guī)劃求最大子序列和
**********************************************************************/
int Maxsum(int * arr, int size)
{
    int maxSum = -INF; //很重要泪电,初始值賦值為負(fù)無窮大
    int sum = 0;
    for(int i = 0; i < size; ++i)
{
//小于0則舍棄
        if(sum < 0)
        {
            sum = arr[i];
        }else
        {
            sum += arr[i];
        }
//比現(xiàn)有最大值大般妙,則替換
        if(sum > maxSum)
        {
            maxSum = sum;
        }
    }
    return maxSum;
}


/*************************************************************************
如果想獲得最大子序列和的初始和結(jié)束位置怎么辦呢?我們知道相速,每當(dāng)當(dāng)前子數(shù)組和的小于0時(shí)碟渺,
便是新一輪子數(shù)組的開始,每當(dāng)更新最大和時(shí)突诬,便對(duì)應(yīng)可能的結(jié)束下標(biāo)苫拍,這個(gè)時(shí)候,
只要順便用本輪的起始和結(jié)束位置更新始末位置就可以旺隙,程序結(jié)束绒极,最大子數(shù)組和以及其始末位置便一起被記錄下來了
*****************************************************************************/
void Maxsum_location(int * arr, int size, int & start, int & end)
{
    int maxSum = -INF;
    int sum = 0;
    int curstart = start = 0;  /* curstart記錄每次當(dāng)前起始位置 */
    for(int i = 0; i < size; ++i)
    {
        if(sum < 0)
        {
            sum = arr[i];
            curstart = i;     /* 記錄當(dāng)前的起始位置 */
        }else
        {
            sum += arr[i];
        }
        if(sum > maxSum)
        {
            maxSum = sum;
            start = curstart; /* 記錄并更新最大子數(shù)組起始位置 */
            end = i;
        }
    }
}

2、分治法
其實(shí)數(shù)組的問題蔬捷,最好留點(diǎn)心垄提,\color{red}{有一大部分題目是可以用分治的辦法完成的},比如說這道題里面:最大子序列和可能出現(xiàn)在三個(gè)地方,1)整個(gè)出現(xiàn)在輸入數(shù)據(jù)的左半部分铡俐,2)整個(gè)出現(xiàn)在輸入數(shù)據(jù)的右半部分摘昌,3)或者跨越輸入數(shù)據(jù)的中部從而占據(jù)左右兩個(gè)半部分「叻洌可以有以下代碼:

/**************************************************************
分治法求解最大子序列和
***************************************************************/
int MaxSumRec( const vector<int> & a, int left, int right )
{
    if( left == right )  // Base case
        if( a[ left ] > 0 )
            return a[ left ];
        else
            return 0;
    int center = ( left + right ) / 2;
    int maxLeftSum  = maxSumRec( a, left, center );
    int maxRightSum = maxSumRec( a, center + 1, right );
    int maxLeftBorderSum = 0, leftBorderSum = 0;
    for( int i = center; i >= left; i-- )
    {
        leftBorderSum += a[ i ];
        if( leftBorderSum > maxLeftBorderSum )
            maxLeftBorderSum = leftBorderSum;
    }
    int maxRightBorderSum = 0, rightBorderSum = 0;
    for( int j = center + 1; j <= right; j++ )
    {
        rightBorderSum += a[ j ];
        if( rightBorderSum > maxRightBorderSum )
            maxRightBorderSum = rightBorderSum;
    }
    return max3( maxLeftSum, maxRightSum, maxLeftBorderSum + maxRightBorderSum );
}

二聪黎、最長(zhǎng)遞增子序列

和上一問題一樣,這是數(shù)組序列中的問題备恤,比如arr={1,5,8,2,3,4}的最長(zhǎng)遞增子序列是1,2,3,4

1稿饰、動(dòng)態(tài)規(guī)劃法
結(jié)合上一題的思路,在數(shù)組的這類問題里面使用動(dòng)態(tài)規(guī)劃還是很常見的露泊,從后向前分析喉镰,很容易想到,\color{red}{第i個(gè)元素之前的最長(zhǎng)遞增子序列的長(zhǎng)度要么是1(比如說遞減的數(shù)列)惭笑,要么就是第i-1個(gè)元素之前的最長(zhǎng)遞增子序列加1}侣姆,我們可以得到以下關(guān)系:

\color{red}{LIS[i] = max\{1,LIS[k]+1\}},其中沉噩,對(duì)于任意的\color{red}{k<=i-1捺宗,arr[i] > arr[k]},這樣arr[i]才能在arr[k]的基礎(chǔ)上構(gòu)成一個(gè)新的遞增子序列川蒙。這種方法代碼如下:

#include <iostream>
using namespace std;
 
//動(dòng)態(tài)規(guī)劃法求最長(zhǎng)遞增子序列 LIS
 
int dp[101]; /* 設(shè)數(shù)組長(zhǎng)度不超過100蚜厉,dp[i]記錄到[0,i]數(shù)組的LIS */
int lis;    /* LIS 長(zhǎng)度 */
 
int LIS(int * arr, int size)
{
    for(int i = 0; i < size; ++i)
    {
        dp[i] = 1;
        for(int j = 0; j < i; ++j)
        {
            if(arr[i] > arr[j] && dp[i] < dp[j] + 1)
            {
                dp[i] = dp[j] + 1;
                if(dp[i] > lis)
                {
                    lis = dp[i];
                }
            }
        }
    }
    return lis;
}
 
/* 輸出LIS */
void outputLIS(int * arr, int index)
{
    bool isLIS = 0;
    if(index < 0 || lis == 0)
    {
        return;
    }
    if(dp[index] == lis)
    {
        --lis;
        isLIS = 1;
    }
 
    outputLIS(arr,--index);
 
    if(isLIS)
    {
        printf("%d ",arr[index+1]);
    }
}
 
void main()
{
    int arr[] = {1,-1,2,-3,4,-5,6,-7};
 
    /* 輸出LIS長(zhǎng)度; sizeof 計(jì)算數(shù)組長(zhǎng)度 */
    printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));
 
    /* 輸出LIS */
    outputLIS(arr,sizeof(arr)/sizeof(int) - 1);
    printf("\n");
}

2畜眨、數(shù)組排序后昼牛,與原數(shù)組求最長(zhǎng)公共子序列
這個(gè)方法還是非常巧妙的,因?yàn)?img class="math-inline" src="https://math.jianshu.com/math?formula=%5Ccolor%7Bred%7D%7BLIS%E6%98%AF%E5%8D%95%E8%B0%83%E9%80%92%E5%A2%9E%E7%9A%84%E6%80%A7%E8%B4%A8%EF%BC%8C%E6%89%80%E4%BB%A5%E4%BB%BB%E6%84%8F%E4%B8%80%E4%B8%AALIS%E4%B8%80%E5%AE%9A%E8%B7%9F%E6%8E%92%E5%BA%8F%E5%90%8E%E7%9A%84%E5%BA%8F%E5%88%97%E6%9C%89%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%AD%90%E5%BA%8F%E5%88%97%EF%BC%8C%E5%B9%B6%E4%B8%94%E5%B0%B1%E6%98%AFLIS%E6%9C%AC%E8%BA%AB%7D" alt="\color{red}{LIS是單調(diào)遞增的性質(zhì)康聂,所以任意一個(gè)LIS一定跟排序后的序列有最長(zhǎng)公共子序列贰健,并且就是LIS本身}" mathimg="1">。不過這里還沒有提到最長(zhǎng)公共子序列恬汁,可以先移步下一節(jié)伶椿,看完后再回來看這個(gè)方法的代碼,代碼如下:

#include <iostream>
using namespace std;
 
/* 最長(zhǎng)遞增子序列 LIS
 * 設(shè)數(shù)組長(zhǎng)度不超過 100
 * quicksort + LCS
*/
 
void swap(int * arr, int i, int j)
{
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}
 
void qsort(int * arr, int left, int right)
{
    if(left >= right)    return ;
    int index = left;
    for(int i = left+1; i <= right; ++i)
    {
        if(arr[i] < arr[left])
        {
            swap(arr,++index,i);
        }
    }
    swap(arr,index,left);
    qsort(arr,left,index-1);
    qsort(arr,index+1,right);
}
 
int dp[101][101];
 
int LCS(int * arr, int * arrcopy, int len)
{
    for(int i = 1; i <= len; ++i)
    {
        for(int j = 1; j <= len; ++j)
        {
            if(arr[i-1] == arrcopy[j-1])
            {
                dp[i][j] = dp[i-1][j-1] + 1;
            }else if(dp[i-1][j] > dp[i][j-1])
            {
                dp[i][j] = dp[i-1][j];
            }else
            {
                dp[i][j] = dp[i][j-1];
            }
        }
    }
    return dp[len][len];
}
 
void main()
{
    int arr[] = {1,-1,2,-3,4,-5,6,-7};
    int arrcopy [sizeof(arr)/sizeof(int)];
 
    memcpy(arrcopy,arr,sizeof(arr));
    qsort(arrcopy,0,sizeof(arr)/sizeof(int)-1);
 
    /* 計(jì)算LCS蕊连,即LIS長(zhǎng)度 */
    int len = sizeof(arr)/sizeof(int);
    printf("%d\n",LCS(arr,arrcopy,len));
}

3悬垃、動(dòng)態(tài)規(guī)劃和二分查找結(jié)合
我們期望在前i個(gè)元素中的所有長(zhǎng)度為len的遞增子序列中找到這樣一個(gè)序列,它的最大元素比arr[i+1]小甘苍,而且長(zhǎng)度要盡量的長(zhǎng)尝蠕,如此,\color{red}{我們只需記錄len長(zhǎng)度的遞增子序列中最大元素的最小值就能使得將來的遞增子序列盡量地長(zhǎng)载庭。}

在這里我們維護(hù)一個(gè)數(shù)組MaxV[i]看彼,記錄長(zhǎng)度為i的遞增子序列中最大元素的最小值廊佩,并對(duì)于數(shù)組中的每個(gè)元素考察其是哪個(gè)子序列的最大元素,二分更新MaxV數(shù)組靖榕,最終i的值便是最長(zhǎng)遞增子序列的長(zhǎng)度标锄。這個(gè)方法真是太巧妙了,妙不可言茁计。

具體代碼如下:

#include <iostream>
using namespace std;
 
/* 最長(zhǎng)遞增子序列 LIS
 * 設(shè)數(shù)組長(zhǎng)度不超過 30
 * DP + BinarySearch
*/
 
int MaxV[30]; /* 存儲(chǔ)長(zhǎng)度i+1(len)的子序列最大元素的最小值 */
int len;      /* 存儲(chǔ)子序列的最大長(zhǎng)度 即MaxV當(dāng)前的下標(biāo)*/
 
/* 返回MaxV[i]中剛剛大于x的那個(gè)元素的下標(biāo) */
int BinSearch(int * MaxV, int size, int x)
{
    int left = 0, right = size-1;
    while(left <= right)
    {
        int mid = (left + right) / 2;
        if(MaxV[mid] <= x)
        {
            left = mid + 1;
        }else
        {
            right = mid - 1;
        }
    }
    return left;
}
 
int LIS(int * arr, int size)
{
    MaxV[0] = arr[0]; /* 初始化 */
    len = 1;
    for(int i = 1; i < size; ++i) /* 尋找arr[i]屬于哪個(gè)長(zhǎng)度LIS的最大元素 */
    {
        if(arr[i] > MaxV[len-1]) /* 大于最大的自然無需查找料皇,否則二分查其位置 */
        {
            MaxV[len++] = arr[i];
        }else
        {
            int pos = BinSearch(MaxV,len,arr[i]);
            MaxV[pos] = arr[i];
        }
    }
    return len;
}
 
void main()
{
    int arr[] = {1,-1,2,-3,4,-5,6,-7};
 
    /* 計(jì)算LIS長(zhǎng)度 */
    printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));
}

三、最長(zhǎng)公共子串(LCS)

回到最常見的字符串問題了星压,這里的找兩個(gè)字符串的最長(zhǎng)公共子串践剂,要求在原字符串中是連續(xù)的。其實(shí)和上面兩個(gè)問題一樣娜膘,這里依舊可以用動(dòng)態(tài)規(guī)劃來求解逊脯,其實(shí)博主自己也不大擅長(zhǎng)動(dòng)態(tài)規(guī)劃,但是可以仿照上面的思路來操作竣贪。\color{red}{我們采用一個(gè)二維矩陣來記錄中間的結(jié)果}军洼。這個(gè)二維矩陣怎么構(gòu)造呢?直接舉個(gè)例子吧:"bab"和"caba"演怎,則數(shù)組如下:

b a b
c 0 0 0
a 0 \color{red}{1} 0
b \color{red}{1} 0 \color{red}{1}
a 0 \color{red}{1} 0

我們看\color{red}{矩陣的斜對(duì)角線最長(zhǎng)的那個(gè)就是我們找的最長(zhǎng)公共子串}匕争。

那怎么求最長(zhǎng)的由1組成的斜對(duì)角線呢?可以做這樣的操作:當(dāng)要在\color{red}{矩陣是填1時(shí)讓它等于其左上角元素加1}颤枪。

b a b
c 0 0 0
a 0 \color{red}{1} 0
b \color{red}{1} 0 \color{red}{2}
a 0 \color{red}{2} 0

這樣矩陣中的最大元素就是 最長(zhǎng)公共子串的長(zhǎng)度汗捡。

在構(gòu)造這個(gè)二維矩陣的過程中由于得出矩陣的某一行后其上一行就沒用了淑际,所以\color{red}{實(shí)際上在程序中可以用一維數(shù)組來代替這個(gè)矩陣}(這樣空間復(fù)雜度就降低了哈)畏纲。

代碼如下:

#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
//str1為橫向,str2這縱向
const string LCS(const string& str1,const string& str2){
    int xlen=str1.size();       //橫向長(zhǎng)度
    vector<int> tmp(xlen);        //保存矩陣的上一行
    vector<int> arr(tmp);     //當(dāng)前行
    int ylen=str2.size();       //縱向長(zhǎng)度
    int maxele=0;               //矩陣元素中的最大值
    int pos=0;                  //矩陣元素最大值出現(xiàn)在第幾列
    for(int i=0;i<ylen;i++){
        string s=str2.substr(i,1);
        arr.assign(xlen,0);     //數(shù)組清0
        for(int j=0;j<xlen;j++){
            if(str1.compare(j,1,s)==0){
                if(j==0)
                    arr[j]=1;
                else
                    arr[j]=tmp[j-1]+1;
                if(arr[j]>maxele){
                    maxele=arr[j];
                    pos=j;
                }
            }       
        }
        tmp.assign(arr.begin(),arr.end());
    }
    string res=str1.substr(pos-maxele+1,maxele);
    return res;
}
int main(){
    string str1("21232523311324");
    string str2("312123223445");
    string lcs=LCS(str1,str2);
    cout<<lcs<<endl;
    return 0;
}

四春缕、最長(zhǎng)公共子序列

這才是筆試面試中出現(xiàn)頻度最高的問題盗胀,前面提到了一個(gè)最長(zhǎng)公共子串,這里的\color{red}{最長(zhǎng)公共子序列與它的區(qū)別在于最長(zhǎng)公共子序列不要求在原字符串中是連續(xù)的}锄贼,比如ADEFG和ABCDEG的最長(zhǎng)公共子序列是ADEG票灰。

1)遞歸方法求解
這個(gè)地方可能最容易想到的方法就是遞歸處理了,設(shè)有字符串a(chǎn)[0...n]宅荤,b[0...m]屑迂,\color{red}{則易知當(dāng)數(shù)組a的i位置上和b的j位置上對(duì)應(yīng)位相同時(shí)}\color{red}{則直接求解兩個(gè)串從下一個(gè)位置開始的剩下部分的最長(zhǎng)公共子序列即可}冯键;\color{red}{當(dāng)不同時(shí)惹盼,則求a[i+1...n]、b[j...m]和a[i...n]惫确、b[j+1...m]兩種情況中的較大數(shù)值即可}手报,用公式表示如下:


代碼如下:

#include<stdio.h>
#include<string.h>
char a[100],b[100];
int lena,lenb;
int LCS(int,int);///兩個(gè)參數(shù)分別表示數(shù)組a的下標(biāo)和數(shù)組b的下標(biāo)
int main()
{
    strcpy(a,"ABCBDAB");
    strcpy(b,"BDCABA");
    lena=strlen(a);
    lenb=strlen(b);
    printf("%d\n",LCS(0,0));
    return 0;
}
int LCS(int i,int j)
{
    if(i>=lena || j>=lenb)
        return 0;
    if(a[i]==b[j])
        return 1+LCS(i+1,j+1);
    else
        return LCS(i+1,j)>LCS(i,j+1)? LCS(i+1,j):LCS(i,j+1);
}

這種處理方法優(yōu)點(diǎn)是\color{red}{編程簡(jiǎn)單}蚯舱,\color{red}{非常容易理解}。缺點(diǎn)是效率太低了掩蛤,有\color{red}{大量的重復(fù)執(zhí)行遞歸調(diào)用}枉昏,一般情況下面試官是不會(huì)滿意的。另一個(gè)致命的缺點(diǎn)是只能求出最大公共子序列的長(zhǎng)度揍鸟,求不出具體的最大公共子序列兄裂,而在大部分筆試或者面試時(shí)會(huì)要求我們求出具體的最大公共子序列。

2)動(dòng)態(tài)規(guī)劃
這里依舊可以采用動(dòng)態(tài)規(guī)劃的方法來解決這個(gè)問題阳藻,可以\color{red}{借助一個(gè)二維數(shù)組來標(biāo)識(shí)中間計(jì)算結(jié)果懦窘,避免重復(fù)的計(jì)算來提高效率},可能需要消耗一部分空間稚配,但是時(shí)間復(fù)雜度大大降低畅涂。

如下圖所示的兩個(gè)串,求解最長(zhǎng)公共子序列的過程很明了:


設(shè)有字符串a(chǎn)[0...n]道川,b[0...m]午衰,字符串a(chǎn)對(duì)應(yīng)的是二維數(shù)組num的行,字符串b對(duì)應(yīng)的是二維數(shù)組num的列冒萄。我們有以下的遞推公式:

我們?cè)诔绦蛑校?div id="gs4cg4s" class="image-package"> \color{red}{可以使用二維數(shù)組flag來記錄下標(biāo)i和j的走向臊岸。數(shù)字"1"表示,斜向下尊流;}
\color{red}{數(shù)字"2"表示帅戒,水平向右;數(shù)字"3"表示崖技,豎直向下逻住。這樣我們可以求解出行進(jìn)的路徑}
,從而得到最長(zhǎng)公共子序列迎献。代碼如下:

#include<stdio.h>
#include<string.h>
char a[500],b[500];
char num[501][501]; ///記錄中間結(jié)果的數(shù)組
char flag[501][501];    ///標(biāo)記數(shù)組瞎访,用于標(biāo)識(shí)下標(biāo)的走向,構(gòu)造出公共子序列
void LCS(); ///動(dòng)態(tài)規(guī)劃求解
void getLCS();    ///采用倒推方式求最長(zhǎng)公共子序列
int main()
{
    int i;
    strcpy(a,"ABCBDAB");
    strcpy(b,"BDCABA");
    memset(num,0,sizeof(num));
    memset(flag,0,sizeof(flag));
    LCS();
    printf("%d\n",num[strlen(a)][strlen(b)]);
    getLCS();
    return 0;
}
void LCS()
{
    int i,j;
    for(i=1;i<=strlen(a);i++)
    {
        for(j=1;j<=strlen(b);j++)
        {
            if(a[i-1]==b[j-1])   ///注意這里的下標(biāo)是i-1與j-1
            {
                num[i][j]=num[i-1][j-1]+1;
                flag[i][j]=1;  ///斜向下標(biāo)記
            }
            else if(num[i][j-1]>num[i-1][j])
            {
                num[i][j]=num[i][j-1];
                flag[i][j]=2;  ///向右標(biāo)記
            }
            else
            {
                num[i][j]=num[i-1][j];
                flag[i][j]=3;  ///向下標(biāo)記
            }
        }
    }
}
void getLCS()
{
    char res[500];
    int i=strlen(a);
    int j=strlen(b);
    int k=0;    ///用于保存結(jié)果的數(shù)組標(biāo)志位
    while(i>0 && j>0)
    {
        if(flag[i][j]==1)   ///如果是斜向下標(biāo)記
        {
            res[k]=a[i-1];
            k++;
            i--;
            j--;
        }
        else if(flag[i][j]==2)  ///如果是斜向右標(biāo)記
            j--;
        else if(flag[i][j]==3)  ///如果是斜向下標(biāo)記
            i--;
    }
    for(i=k-1;i>=0;i--)
        printf("%c",res[i]);
}

五吁恍、字符串編輯距離

給定一個(gè)源字符串和目標(biāo)字符串扒秸,能夠?qū)υ创M(jìn)行如下操作:

   1.在給定位置上插入一個(gè)字符

   2.替換任意字符

   3.刪除任意字符

求通過以上操作使得源字符串和目標(biāo)字符串一致的最小操作步數(shù)。

簡(jiǎn)單描述一下解該題的思想冀瓦,源字符串和目標(biāo)字符串分別為str_a伴奥、str_b,二者的長(zhǎng)度分別為la翼闽、lb仅讽,定義f[i,j]為子串str_a[0...i]和str_b[0...j]的最小編輯距離中捆,簡(jiǎn)單分析可知求得的str_a[0...i]和str_b[0...j]的最小編輯距離有一下三種可能:

\color{red}{(1)去掉str_a[0...i]的最后一個(gè)字符跟str_b[0...j]匹配,則f[i, j]的值等于f[i-1, j]+1蹦掐;}

\color{red}{(2)去掉str_b[0...j]的最后一個(gè)字符跟str_a[0...i]匹配,則f[i, j]的值等于f[i, j-1]+1;}

\color{red}{(3)去掉str_a[0...i]和str_b[0...j]的最后一個(gè)字符,讓二者匹配求得f[i-1, j-1],} \color{red}{計(jì)算f[i, j]時(shí)要考慮當(dāng)前字符是否相等古徒,如果str_a[i]==str_b[j]說明該字符不用編輯,} \color{red}{所以f[i, j]的值等于f[i-1, j-1]读恃,如果str_a[i]!=str_b[j]說明該字符需要編輯一次} \color{red}{(任意修改str_a[i]或者str_b[j]即可)隧膘,所以f[i, j]的值等于f[i-1, j-1]+1。}

因?yàn)轭}目要求的是最小的編輯距離寺惫,所以去上面上中情況中的最小值即可疹吃,因此可以得到遞推公式:

\color{red}{f[i, j] = Min ( f[i-1, j]+1, f[i, j-1]+1, f[i-1, j-1]+(str_a[i]==str_b[j] ? 0 : 1) )}

維基百科中的描述如下:


1)遞歸方法(用到動(dòng)態(tài)規(guī)劃)
由上述的遞歸公式可以有以下代碼:

//求兩個(gè)字符串的編輯距離問題
//遞歸版本,備忘錄C[i,j]表示strA[i]...strA[size_A-1]與strB[j]...strB[size_B-1]的編輯距離
int editDistance_mem(char *strA,int size_A,char *strB,int size_B){
 int **C=new int*[size_A+1];
 for(int i=0;i<=size_A;i++){
  C[i]=new int[size_B+1]();
 }
 //初始化
 for(int i=0;i<=size_A;i++){
  for(int j=0;j<=size_B;j++)
   C[i][j]=INT_MAX;
 }
 int res=EDM(C,strA,0,size_A-1,strB,0,size_B-1);
 //free mem
 for(int i=0;i<=size_A;i++){
  delete [] C[i];
 }
 delete [] C;
 return res;
}
int EDM(int **C,char *strA,int i,int A_end,char *strB,int j,int B_end){
 if(C[i][j]<INT_MAX)//做備忘
  return C[i][j];
 if(i>A_end){
  if(j>B_end)
   C[i][j]=0;
  else
   C[i][j]=B_end-j+1;
 }else if(j>B_end){
  if(i>A_end)
   C[i][j]=0;
  else
   C[i][j]=A_end-i+1;
 }
 else if(strA[i]==strB[j])
  C[i][j]=EDM(C,strA,i+1,A_end,strB,j+1,B_end);
 else{
  int a=EDM(C,strA,i+1,A_end,strB,j+1,B_end);
  int b=EDM(C,strA,i,A_end,strB,j+1,B_end);
  int c=EDM(C,strA,i+1,A_end,strB,j,B_end);
  C[i][j]=min(a,b,c)+1;
 }
 return C[i][j];
}

2)矩陣標(biāo)記法
遞推方法(也可稱為矩陣標(biāo)記法)西雀,通過分析可知\color{red}{可以將f[i, j]的計(jì)算在一個(gè)二維矩陣中進(jìn)行}萨驶,上面的遞推式實(shí)際上可以看做是矩陣單元的計(jì)算遞推式,只要把矩陣填滿了艇肴,f[la-1, lb-1]的值就是要求得最小編輯距離腔呜。代碼如下:

//求兩個(gè)字符串的編輯距離問題
//遞推版本 C[i,j]表示strA[i]...strA[size_A-1]與strB[j]...strB[size_B-1]的編輯距離
int editDistance_iter(char *strA,int size_A,char *strB,int size_B){
 int **C=new int*[size_A+1];
 for(int i=0;i<=size_A;i++){
  C[i]=new int[size_B+1]();
 }
 for(int i=size_A;i>=0;i--){
  for(int j=size_B;j>=0;j--){
   if(i>size_A-1){
    if(j>size_B-1)
     C[i][j]=0;
    else
     C[i][j]=size_B-j;
   }else if(j>size_B-1){
    if(i>size_A-1)
     C[i][j]=0;
    else
     C[i][j]=size_A-i;
   }else if(strA[i]==strB[j])
    C[i][j]=C[i+1][j+1];
   else
    C[i][j]=min(C[i+1][j+1],C[i+1][j],C[i][j+1])+1;
  }
 }
 int res=C[0][0];
 //free mem
 for(int i=0;i<=size_A;i++){
  delete [] C[i];
 }
 delete [] C;
 return res;
}

六、最長(zhǎng)不重復(fù)子串

很好理解再悼,即求一個(gè)串內(nèi)最長(zhǎng)的不重復(fù)子串核畴。

1)使用Hash
要求子串中的字符不能重復(fù),判重問題首先想到的就是hash冲九,尋找滿足要求的子串谤草,最直接的方法就是遍歷每個(gè)字符起始的子串,輔助hash莺奸,尋求最長(zhǎng)的不重復(fù)子串丑孩,由于要遍歷每個(gè)子串故復(fù)雜度為O(n^2),n為字符串的長(zhǎng)度憾筏,輔助的空間為常數(shù)hash[256]嚎杨。代碼如下:

/* 最長(zhǎng)不重復(fù)子串 我們記為 LNRS */
int maxlen;
int maxindex;
void output(char * arr);
/* LNRS 基本算法 hash */
char visit[256];
void LNRS_hash(char * arr, int size)
{
    for(int i = 0; i < size; ++i)
    {
        memset(visit,0,sizeof(visit));
        visit[arr[i]] = 1;
        for(int j = i+1; j < size; ++j)
        {
            if(visit[arr[j]] == 0)
            {
                visit[arr[j]] = 1;
            }
else
            {
                if(j-i > maxlen)
                {
                    maxlen = j - i;
                    maxindex = i;
                }
                break;
            }
        }
    }
    output(arr);
}

2)動(dòng)態(tài)規(guī)劃法
字符串的問題,很多都可以用動(dòng)態(tài)規(guī)劃處理氧腰,比如這里求解最長(zhǎng)不重復(fù)子串,和前面討論過的最長(zhǎng)遞增子序列問題就有些類似刨肃,在LIS(最長(zhǎng)遞增子序列)問題中古拴,對(duì)于當(dāng)前的元素,要么是與前面的LIS構(gòu)成新的最長(zhǎng)遞增子序列真友,要么就是與前面稍短的子序列構(gòu)成新的子序列或單獨(dú)構(gòu)成新子序列黄痪。

這里我們采用類似的思路:某個(gè)當(dāng)前的字符,\color{red}{如果它與前面的最長(zhǎng)不重復(fù)子串中的字符沒有重復(fù)盔然,那么就可以以它為結(jié)尾構(gòu)成新的最長(zhǎng)子串}桅打;\color{red}{如果有重復(fù)是嗜,那么就與某個(gè)稍短的子串構(gòu)成新的子串或者單獨(dú)成一個(gè)新子串}

我們來看看下面兩個(gè)例子:

1)字符串“abcdeab”挺尾,第二個(gè)a之前的最長(zhǎng)不重復(fù)子串是“abcde”鹅搪,a與最長(zhǎng)子串中的字符有重復(fù),
但是它與稍短的“bcde”串沒有重復(fù)遭铺,于是它可以與其構(gòu)成一個(gè)新的子串丽柿,之前的最長(zhǎng)不重復(fù)子串“abcde”結(jié)束;

2)字符串“abcb”魂挂,跟前面類似甫题,最長(zhǎng)串“abc”結(jié)束,第二個(gè)字符b與稍短的串“c”構(gòu)成新的串涂召;

我們貌似可以總結(jié)出一些東西:\color{red}{當(dāng)一個(gè)最長(zhǎng)子串結(jié)束時(shí)(即遇到重復(fù)的字符)坠非,新的子串的長(zhǎng)度是與(第一個(gè)重復(fù)的字符)的下標(biāo)有關(guān)的}

于是類似LIS果正,\color{red}{對(duì)于每個(gè)當(dāng)前的元素麻顶,我們“回頭”去查詢是否有與之重復(fù)的,} \color{red}{如沒有舱卡,則最長(zhǎng)不重復(fù)子串長(zhǎng)度+1辅肾,如有,} \color{red}{則是與第一個(gè)重復(fù)的字符之后的串構(gòu)成新的最長(zhǎng)不重復(fù)子串轮锥,新串的長(zhǎng)度便是當(dāng)前元素下標(biāo)與重復(fù)元素下標(biāo)之差矫钓。}

可以看出這里的動(dòng)態(tài)規(guī)劃方法時(shí)間復(fù)雜度為O(N^2),我們可以與最長(zhǎng)遞增子序列的動(dòng)態(tài)規(guī)劃方案進(jìn)行對(duì)比舍杜,是一個(gè)道理的新娜。代碼如下:

/* LNRS 動(dòng)態(tài)規(guī)劃求解 */
int dp[100];
void LNRS_dp(char * arr, int size)
{
    int i, j;
    maxlen = maxindex = 0;
    dp[0] = 1;
    for(i = 1; i < size; ++i)
    {
        for(j = i-1; j >= 0; --j)
        {
            if(arr[j] == arr[i])
            {
                dp[i] = i - j;
                break;
            }
        }
        if(j == -1)
        {
            dp[i] = dp[i-1] + 1;
        }
        if(dp[i] > maxlen)
        {
            maxlen = dp[i];
            maxindex = i + 1 - maxlen;
        }
    }
    output(arr);
}

3)動(dòng)態(tài)規(guī)劃和hash結(jié)合
我們發(fā)現(xiàn)在動(dòng)態(tài)規(guī)劃方法中,每次都要“回頭”去尋找重復(fù)元素的位置既绩,所以時(shí)間復(fù)雜度徒增到O(n^2)概龄,結(jié)合方法1)中的Hash思路,我們可以用hash記錄元素是否出現(xiàn)過饲握,我們當(dāng)然\color{red}{也可以用hash記錄元素出現(xiàn)過的下標(biāo)私杜,,這樣就不必“回頭”了}救欧,而時(shí)間復(fù)雜度必然降為O(N)衰粹,只不過需要一個(gè)輔助的常數(shù)空間visit[256],這也是之前我另外一篇文章找工作筆試面試那些事兒(15)---互聯(lián)網(wǎng)公司面試的零零種種和多家經(jīng)驗(yàn)提到的的空間換時(shí)間思路笆怠,不過一般我們的面試?yán)锩鎯?yōu)先考慮時(shí)間復(fù)雜度铝耻,所以這是可取的方法。

/* LNRS 動(dòng)態(tài)規(guī)劃 + hash 記錄下標(biāo) */
void LNRS_dp_hash(char * arr, int size)
{
    memset(visit, -1, sizeof visit); //visit數(shù)組是-1的時(shí)候代表這個(gè)字符沒有在集合中
    memset(dp, 0, sizeof dp);
    maxlen = maxindex = 0;
    dp[0] = 1;
    visit[arr[0]] = 0;
    for(int i = 1; i < size; ++i)
    {
        if(visit[arr[i]] == -1) //表示arr[i]這個(gè)字符以前不存在
        {
            dp[i] = dp[i-1] + 1;
            visit[arr[i]] = i; /* 記錄字符下標(biāo) */
        }else
        {
            dp[i] = i - visit[arr[i]];
            visit[arr[i]] = i; /* 更新字符下標(biāo) */
        }
        if(dp[i] > maxlen)
        {
            maxlen = dp[i];
            maxindex = i + 1 - maxlen;
        }
    }
    output(arr);
}

4)空間再優(yōu)化
上面的方法3)已經(jīng)將時(shí)間復(fù)雜度降到了O(n)蹬刷,可是這時(shí)面試官又發(fā)言了瓢捉,說你用的輔助空間多了频丘,還有優(yōu)化方法嗎,我們仔細(xì)觀察動(dòng)態(tài)規(guī)劃最優(yōu)子問題解的更新方程:

dp[i] = dp[i-1] + 1;

dp[i-1]不就是更新dp[i]當(dāng)前的最優(yōu)解么泡态?這又與之前提到的最大子數(shù)組和問題的優(yōu)化幾乎同出一轍搂漠,我們不需要O(n)的輔助空間去存儲(chǔ)子問題的最優(yōu)解,而只需O(1)的空間就可以了兽赁,至此状答,我們找到了時(shí)間復(fù)雜度O(N),輔助空間為O(1)(一個(gè)額外變量與256大小的散列表)的算法刀崖,代碼如下:

/* LNRS 動(dòng)態(tài)規(guī)劃+hash惊科,時(shí)間復(fù)雜度O(n) 空間復(fù)雜度O(1)算法*/
void LNRS_dp_hash_ultimate(char * arr, int size)
{
    memset(visit, -1, sizeof visit);
    maxlen = maxindex = 0;
    visit[arr[0]] = 0;
int curlen = 1;
    for(int i = 1; i < size; ++i)
    {
        if(visit[arr[i]] == -1)
        {
            ++curlen;
            visit[arr[i]] = i; /* 記錄字符下標(biāo) */
        }
else
        {
            curlen = i - visit[arr[i]];
            visit[arr[i]] = i; /* 更新字符下標(biāo) */
        }
        if(curlen > maxlen)
        {
            maxlen = curlen;
            maxindex = i + 1 - maxlen;
        }
    }
    output(arr);
}

七、最長(zhǎng)回文子串

給出一個(gè)字符串S亮钦,找到一個(gè)最長(zhǎng)的連續(xù)回文串馆截。例如串 babcbabcbaccba 最長(zhǎng)回文是:abcbabcba

1)自中心向兩端尋找
回文是一種特殊的字符串,\color{red}{我們可以以源字符串的每個(gè)字符為中心蜂莉,依次尋找出最長(zhǎng)回文子串P0, P1,...,Pn蜡娶。} \color{red}{這些最長(zhǎng)回文子串中的最長(zhǎng)串Pi = max(P1, P2,...,Pn)即為所求}
核心代碼如下:

string find_lps_method1(const string &str)
{
    int center = 0, max_len = 0;
    for(int i = 1; i < str.length()-1; ++i)
    {
        int j = 1;
        //以str[i]為中心映穗,依次向兩邊擴(kuò)展窖张,尋找最長(zhǎng)回文Pi
        while(i+j < str.length() && i-j >= 0 && str[i+j] == str[i-j])
            ++j;
        --j;
        if(j > 1 && j > max_len)
        {
            center = i;
            max_len = j;
        }
    }
    return str.substr(center-max_len, (max_len << 1) + 1);
}

2)利用最長(zhǎng)公共字串的方法
這里用到了一個(gè)我們觀察出來的結(jié)論:\color{red}{對(duì)于串S, 假設(shè)它反轉(zhuǎn)后得到的串是S'}, 那么\color{red}{S的最長(zhǎng)回文串是S和S'的最長(zhǎng)公共字串}

例如 S = babcbabcbaccba, S' = abccabcbabcbab蚁滋,S和S'的最長(zhǎng)公共字串是 abcbabcba也是S的最長(zhǎng)回文字串宿接。

代碼這個(gè)地方就不寫了,用首指針++辕录,尾指針--很容易實(shí)現(xiàn)串的翻轉(zhuǎn)睦霎,再結(jié)合前面寫過的最長(zhǎng)公共子串代碼可得到最后結(jié)果。

3)利用棧的性質(zhì)
這是一個(gè)不成熟的想法走诞,博主只是覺得比較好的想法需要拿出來分享一下副女,對(duì)于長(zhǎng)度為偶數(shù)的最長(zhǎng)回文,可以采用這樣一種思路求得:

將字符串中的字符從左至右逐個(gè)入棧蚣旱,
出現(xiàn)情況:
\color{red}{1)若棧頂字符和要入棧的字符相同碑幅,則該字符不入棧且棧pop出棧頂字符,回文長(zhǎng)度加一姻锁。}
\color{red}{2)若棧頂字符與要入棧的字符不相同枕赵,直接入棧。則依次入棧出棧位隶,求最長(zhǎng)連續(xù)出棧序列即可。}

因?yàn)閷?duì)于奇數(shù)長(zhǎng)度的字符串开皿,博主沒有想到時(shí)間復(fù)雜度低的類似處理方法涧黄,所以這里就不寫代碼了篮昧,大家有好的解法或者思路歡迎留言。

4)著名的Manacher’s Algorithm算法
算法首先將輸入字符串S笋妥, 轉(zhuǎn)換成一個(gè)特殊字符串T懊昨,轉(zhuǎn)換的原則就是將S的開頭結(jié)尾以及每?jī)蓚€(gè)相鄰的字符之間加入一個(gè)特殊的字符,例如#

例如: S = “abaaba”, T = “#a#b#a#a#b#a#”.

為了找到最長(zhǎng)的回文字串春宣,例如我們當(dāng)前考慮以Ti為回文串中間的元素酵颁,如果要找到最長(zhǎng)回文字串,我們要從當(dāng)前的Ti擴(kuò)展使得 Ti-d … Ti+d 組成最長(zhǎng)回文字串. 這里d其實(shí)和 以Ti為中心的回文串長(zhǎng)度是一樣的. 進(jìn)一步解釋就是說月帝,\color{red}{因?yàn)槲覀冞@里插入了 \# 符號(hào)躏惋,對(duì)于一個(gè)長(zhǎng)度為偶數(shù)的回文串,他應(yīng)該是以\#做為中心的嚷辅,} \color{red}{然后向兩邊擴(kuò)簿姨,對(duì)于長(zhǎng)度是奇數(shù)的回文串,它應(yīng)該是以一個(gè)普通字符作為中心的簸搞。} \color{red}{通過使用\#扁位,我們將無論是奇數(shù)還是偶數(shù)的回文串,都變成了一個(gè)以Ti為中心趁俊,} \color{red}{d為半徑兩個(gè)方向擴(kuò)展的問題域仇。并且d就是回文串的長(zhǎng)度。}

例如 #a#b#a#, P = 0103010, 對(duì)于b而言P的值是3寺擂,是最左邊的#暇务,也是延伸的最左邊。這個(gè)值和當(dāng)前的回文串是一致的沽讹。

如果我們求出所有的P值般卑,那么顯然我們要的回文串,就是以最大P值為中心的回文串爽雄。

T = # a # b # a # a # b # a #

P = 0 1 0 3 0 1 6 1 0 3 0 1 0

例如上面的例子蝠检,最長(zhǎng)回文是 “abaaba”, P6 = 6.

根據(jù)觀察發(fā)現(xiàn),如果我們?cè)谝粋€(gè)位置例如 abaaba的中間位置挚瘟,用一個(gè)豎線分開叹谁,兩側(cè)的P值是對(duì)稱的。當(dāng)然這個(gè)性質(zhì)不是在任何時(shí)候都會(huì)成立乘盖,接下來就是分析如何利用這個(gè)性質(zhì)焰檩,使得我們可以少算很多P的值。

下面的例子 S = “babcbabcbaccba” 存在更多的折疊回文字串订框。


C表示當(dāng)前的回文中心析苫,L和R處的線表示以C為中心可以到達(dá)的最左和最右位置,如果知道這些,我們?nèi)绾慰梢愿玫挠?jì)算C后面的P[i].

假設(shè)我們當(dāng)前計(jì)算的是 i = 13, 根據(jù)對(duì)稱性衩侥,我們知道對(duì)稱的那個(gè)下標(biāo) i' = 9.



根據(jù)C對(duì)稱的原則国旷,我們很容易得到如下數(shù)據(jù) P[ 12 ] = P[ 10 ] = 0, P[ 13 ] = P[ 9 ] = 1, P[ 14 ] = P[ 8 ] = 0).



當(dāng)時(shí)當(dāng)i = 15的時(shí)候,卻只能得到回文 “a#b#c#b#a”, 長(zhǎng)度是5茫死, 而對(duì)稱 i ' = 7 的長(zhǎng)度是7.

上圖所示跪但,如果以 i, i' 為中心,畫出對(duì)稱的區(qū)域如圖峦萎,其中以i‘ = 7 對(duì)稱的區(qū)域是 實(shí)心綠色 + 虛綠色 和 左側(cè)屡久,虛綠色表示當(dāng)前的對(duì)稱長(zhǎng)度已經(jīng)超過之前的對(duì)稱中心C。而之前的P對(duì)稱性質(zhì)成立的原因是 i 右側(cè)剩余的長(zhǎng)度 R - i 正好比 以 i‘ 為中心的回文小爱榔。

這個(gè)性質(zhì)可以這樣歸納被环,\color{red}{對(duì)于 i 而言,因?yàn)楦鶕?jù)C對(duì)稱的最右是R搓蚪,所以i的右側(cè)有 R - i 個(gè)元素是保證是 i' 左側(cè)是對(duì)稱的}蛤售。\color{red}{ 而對(duì)于 i' 而言他的P值,也就是回文串的長(zhǎng)度妒潭,可能會(huì)比 R-i 要大}悴能。 \color{red}{如果大于 R - i, 對(duì)于i而言,我們只能暫時(shí)的先填寫 P[i] = R - i, 然后依據(jù)回文的屬性來擴(kuò)充P[i] 的值}雳灾; \color{red}{如果P[i '] 小于R-i漠酿,那么說明在對(duì)稱區(qū)間C內(nèi),i的回文串長(zhǎng)度和i' 是一樣長(zhǎng)的}谎亩。例如我們的例子中 i = 15, 因?yàn)镽 = 20炒嘲,所以i右側(cè) 在對(duì)稱區(qū)間剩余的是 R - 15 = 5, 而 i’ = 7 的長(zhǎng)度是7. 說明 i' 的回文長(zhǎng)度已經(jīng)超出對(duì)稱區(qū)間。我們只能使得P[i] 賦值為5匈庭, 然后嘗試擴(kuò)充P[i].

if P[ i' ] ≤ R – i,
then P[ i ] ← P[ i' ]
else P[ i ] ≥R – i. (這里下一步操作是擴(kuò)充 P[ i ].

擴(kuò)充P[i] 之后夫凸,我們還要做一件事情是更新 R 和 C, 如果當(dāng)前對(duì)稱中心的最右延伸大于R阱持,我們就更新C和R夭拌。在迭代的過程中,我們?cè)囂絠的時(shí)候衷咽,如果P[i'] <= R - i, 那么只要做一件事情鸽扁。 如果不成立我們對(duì)當(dāng)前P[i] 做擴(kuò)展,因?yàn)樽畲箝L(zhǎng)度是n镶骗,擴(kuò)展最多就做n次桶现,所以最多做2*n。 所以最后算法復(fù)雜度是 O(n)

具體實(shí)現(xiàn)的代碼如下:

// 轉(zhuǎn)換S 到 T.
// 例如, S = "abba", T = "^#a#b#b#a#$".
// ^ 和 $ 作為哨兵標(biāo)記加到兩端以避免邊界檢查
string preProcess(string s) {
  int n = s.length();
  if (n == 0) return "^$";
  string ret = "^";
  for (int i = 0; i < n; i++)
    ret += "#" + s.substr(i, 1);
 
  ret += "#$";
  return ret;
}
 
string longestPalindrome(string s) {
  string T = preProcess(s);
  int n = T.length();
  int *P = new int[n];
  int C = 0, R = 0;
  for (int i = 1; i < n-1; i++) {
    int i_mirror = 2*C-i; // equals to i' = C - (i-C)
 
    P[i] = (R > i) ? min(R-i, P[i_mirror]) : 0;
 
    // Attempt to expand palindrome centered at i
    while (T[i + 1 + P[i]] == T[i - 1 - P[i]])
      P[i]++;
 
    // If palindrome centered at i expand past R,
    // adjust center based on expanded palindrome.
    if (i + P[i] > R) {
      C = i;
      R = i + P[i];
    }
  }
 
  // Find the maximum element in P.
  int maxLen = 0;
  int centerIndex = 0;
  for (int i = 1; i < n-1; i++) {
    if (P[i] > maxLen) {
      maxLen = P[i];
      centerIndex = i;
    }
  }
  delete[] P;
 
  return s.substr((centerIndex - 1 - maxLen)/2, maxLen);
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鼎姊,一起剝皮案震驚了整個(gè)濱河市骡和,隨后出現(xiàn)的幾起案子相赁,更是在濱河造成了極大的恐慌,老刑警劉巖即横,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件噪生,死亡現(xiàn)場(chǎng)離奇詭異裆赵,居然都是意外死亡东囚,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門战授,熙熙樓的掌柜王于貴愁眉苦臉地迎上來页藻,“玉大人,你說我怎么就攤上這事植兰》菡剩” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵楣导,是天一觀的道長(zhǎng)废境。 經(jīng)常有香客問我,道長(zhǎng)筒繁,這世上最難降的妖魔是什么噩凹? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮毡咏,結(jié)果婚禮上驮宴,老公的妹妹穿的比我還像新娘。我一直安慰自己呕缭,他們只是感情好堵泽,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著恢总,像睡著了一般迎罗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上片仿,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天纹安,我揣著相機(jī)與錄音,去河邊找鬼滋戳。 笑死钻蔑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的奸鸯。 我是一名探鬼主播咪笑,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼娄涩!你這毒婦竟也來了窗怒?” 一聲冷哼從身側(cè)響起映跟,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤扬虚,失蹤者是張志新(化名)和其女友劉穎努隙,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年坎匿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盾剩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡替蔬,死狀恐怖告私,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情进栽,我是刑警寧澤德挣,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站快毛,受9級(jí)特大地震影響格嗅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜唠帝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一屯掖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧襟衰,春花似錦贴铜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至苔悦,卻和暖如春轩褐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背玖详。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國打工把介, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留勤讽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓拗踢,卻偏偏與公主長(zhǎng)得像脚牍,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子巢墅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

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