08_经验总结算法(Java、Kotlin描述)

    一直以来,对算法既感到赞叹,又感到痛苦。赞叹的地方在于算法的美感,将问题抽象出来,以极为精炼的方式加以解决;有些算法还特别的巧妙,性能之高,让人叹为观止。痛苦的地方在于很多算法不好理解,记住则更难。所以,这里做一些算法记录,以供学习和查阅。部分问题可能会有多个算法实现,对此会列出它们。高效的也有,低效的也在,从对比中加深印象。
    本系列文章在文集《算法》里,分为这几篇:由经验总结的算法、算法相关的基本概念、经典排序查找算法、经典趣题算法、经典数学问题算法和业界经典算法。它们来源于一些算法手册、书籍、源码和自己的归纳总结。
    本篇文章是第一篇,主要介绍一些由经验总结的算法和结构。它们通常并不能抽象出来作为一般、常用的算法,但在实际开发中,底层经常出现它们的身影。

(1)任意范围的对数求解算法

    在Java中,基本整型都有一定的范围,比如long类型,它的最大值是pow(2 , 64) -1 , 如果超出此范围,则数据会溢出,结果是不确定的。pow(x , y)表示x的y次方。
    而且对于计算的中间值,也会有此限制。比如一个计算的结果是在long的范围内的,但某个中间变量超出了范围,那么计算结果也是未知的。典型的比如两个big long相乘再除以另外一个big long ,期待的结果是在long范围内的,尤其是以人的视角来看。但两个big long相乘超出了long的范围,该中间值就是错误的,再除以另外一个big long,所得结果也是错误的。
    在实际场景中,有这么一个需求:对任意给定long型值x,想知道它对应的字符个数。这在将long型数值转换成对应的字符表示时就要用到。例如Java中的Long类有一个方法stringSize(),里面使用了一个边界值19(来自Java 8的源码,android 33源码中也一样)。对此没有任何的注释或说明。一开始看到这里的时候非常疑惑,为什么是19?下面就来解答这个问题。
    在数学中,这其实是对数求解问题,即:以10为底,x的对数是多少?当然,在本需求中,需要一点变动,字符个数等于x以10为底的对数取整并加1。
    一个关键的问题是如何确定上边界 N ,使得pow(10 , N) 大于Long.MAX_VALUE , 而pow(10 , N - 1)小于或等于Long.MAX_VALUE 。因为中间结果pow(10 , N) 超出了Long的范围,所以不能用long型来存储它。因此,BigInteger派上了用场。
    BigInteger表示的整数,是没有范围限制的。用它来存储整数,可以获得可靠的结果。
    Java版本算法:

//require n>0 , 返回x = 以10为底n的对数取整加1,即使得10的x次方大于n,10的(x-1)次方小于等于n
    int getLg(long n){
        BigInteger bigN = BigInteger.valueOf(n);
        int pow = 0;
        while(powerOf(pow).compareTo(bigN)<=0){
            pow++;
        }
        return pow;
    }

    //require x >= 0 , 返回10的x次方的结果,用BigInteger表示
    BigInteger powerOf(int x){
        BigInteger ten = BigInteger.valueOf(10l);
        BigInteger result = BigInteger.valueOf(1l);
        for(int i =0;i<x;i++){
            result = result.multiply(ten);
        }
        return result;
    }

    方法getLg(long n)参数n的类型可以很方便的改为BigInteger,以突破long的限制,扩展至任意范围的整数。这里只是为了直观和方便。

    Kotlin版本:

    //require n>0 , 返回x = 以10为底n的对数取整加1,即使得10的x次方大于n,10的(x-1)次方小于等于n
    fun getLg(n: Long): Int {
        val bigN = BigInteger.valueOf(n)
        var pow = 0
        while (powerOf(pow).compareTo(bigN) <= 0) {
            pow++
        }
        return pow
    }

    //require x >= 0 , 返回10的x次方的结果,用BigInteger表示
    fun powerOf(x: Int): BigInteger {
        val ten = BigInteger.valueOf(10L)
        var result = BigInteger.valueOf(1L)
        for (i in 0 until x) {
            result = result.multiply(ten)
        }
        return result
    }

    调用getLg(Long.MAX_VALUE),获得的结果是19。也就是说,任意long的值,它对应的字符个数不会超过19 。
    将这个算法扩展一下,不限制以10为底。扩展后的Java版本:

//require n>0 , 返回x = 以value为底n的对数取整加1,即使得value的x次方大于n,value的(x-1)次方小于等于n
    int getLgOfV(long n,int value){
        BigInteger bigN = BigInteger.valueOf(n);
        int pow = 0;
        while(powerOf(pow,value).compareTo(bigN)<=0){
            pow++;
        }
        return pow;
    }

    //require x >=0 , n > 0 ,返回n的x次方的结果,用BigInteger表示
    BigInteger powerOf(int x,int n){
        BigInteger nBig = BigInteger.valueOf(n);
        BigInteger result = BigInteger.valueOf(1l);
        for(int i =0;i<x;i++){
            result = result.multiply(nBig);
        }
        return result;
    }

    Kotlin版本:

//require n>0 , 返回x = 以value为底n的对数取整加1,即使得value的x次方大于n,value的(x-1)次方小于等于n
    fun getLgOfV(n: Long, value: Int): Int {
        val bigN = BigInteger.valueOf(n)
        var pow = 0
        while (powerOf(pow, value).compareTo(bigN) <= 0) {
            pow++
        }
        return pow
    }

    //require x >=0 , n > 0 ,返回n的x次方的结果,用BigInteger表示
    fun powerOf(x: Int, n: Int): BigInteger {
        val nBig = BigInteger.valueOf(n.toLong())
        var result = BigInteger.valueOf(1L)
        for (i in 0 until x) {
            result = result.multiply(nBig)
        }
        return result
    }

(2)整数转字符对应的size算法

    如果想将整数输出到屏幕,那么先要将它们转为对应的字符。在Java中,字符用char表示。将不同类型的整数(int 、long等),转成对应的字符char数组,所需数组的大小,就是本次要描述的算法。本小节是基于Java源码(Long类和Integer类)做的一些总结。
    用一个示例来更清晰地描述问题,比如 int i = 32 ; 将它转换成字符数组的结果是: char[] result = { ‘3’ , ‘2’ } ; 所需的size为2,创建char数组时,数组的大小size被赋值为2 。
    先来说明一下数值和对应字符的不同。在Java中,char是采用Unicode编码的。数值3,对应的字符是‘3’,它的Unicode编码(也叫代码点,code point)是51," char c = 51 "和" char c = ‘3’ " 这两种方式是完全等值的。从人的视角看,我们想打印数值3 ;从计算机视角看,它处理的却是51。
    言归正传,该如果来求解这个size呢?先来看看int型的处理,Java版本:

    /**
     * int型的计算,require n >= 0 。 如果n<0,则 1+ stringSize(-n) 。加1是因为减号
     */
    int stringSize(int n){
        //int最大值是21亿多,这个数组的倒数第二个约9.9亿,再加个9,就是约99.9亿,超出了int最大值
        final int[] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999,99999999, 999999999, Integer.MAX_VALUE };
        for(int i = 0;;i++){
            if(n <= sizeTable[i]){
                return i+1;
            }
        }
    }

    Kotlin版本:

    fun stringSize(n: Int): Int {
        //int最大值是21亿多,这个数组的倒数第二个约9.9亿,再加个9,就是约99.9亿,超出了int最大值
        val sizeTable =
            intArrayOf(9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, Int.MAX_VALUE)
        var i = 0
        while (true) {
            if (n <= sizeTable[i]) {
                return i + 1
            }
            i++
        }
    }

    short 、byte 的类型,是将它们转换成int后,再调用上面的算法。
    针对long型,情况有点不同,它的Java版本算法如下:

    /**
     * long型的计算,require n >= 0 
     */
    int stringSize(long n){
        long p = 10;
        for (int i=1; i<19; i++) {
            if (n < p)
                return i;
            p = 10*p;
        }
        return 19;
    }

    Kotlin版本如下:

    /**
     * long型的计算,require n >= 0
     */
    fun stringSize(n: Long): Int {
        var p: Long = 10
        for (i in 1..18) {
            if (n < p) return i
            p = 10 * p
        }
        return 19
    }

    这里用到了值19,关于这个值的计算,可以看上面的第(5)条。
    扩展一下,int型是否也可以用这种边界值的算法呢?答案是可以的,它的边界范围是10,通过自然观察和用上面第(5)条的算法求解都可以得到结果。Java版本如下:

    /**
     * int型的计算,采用long型的类似算法
     */
    int stringSizeLg(int n){
        long p = 10;
        for (int i=1; i<10; i++) {
            if (n < p)
                return i;
            p = 10*p;
        }
        return 10;
    }

    Kotlin版本如下:

   /**
    * int型的计算,采用long型的类似算法
    */
   fun stringSizeLg(n: Int): Int {
       var p: Long = 10
       for (i in 1..9) {
           if (n < p) return i
           p = 10 * p
       }
       return 10
   }

(3)乘10、乘100的高性能算法

    如果想将一个值扩大10倍,再简单不过,乘以10就完事了。对计算机来说,乘以10却有不同的实现方式:乘法和移位(Shift) 。它们最底层的支持来自于电路,乘法需要乘法器电路,它比Shift方式更复杂,性能更低。
    乘法方式:

int q , r ;
r = q * 10  ;

    Shift方式:

int q , r ;
r = (q << 3) + (q << 1) ;

    在个人的mac电脑上做了一个实验,将上述两种方式分别放到一个100万次的循环中,计算它们的耗时,结果如下(以纳秒为单位):

multiply time : 13714938
shift time :     1668211 

    可以看出,性能不在一个量级。对于计算机来说,100万次的计算量算是少的,现在流行的CPU每秒能执行大约10亿次计算(数据来源于《C Primer Plus》第6版 中文版1.4节)。可以想象一下,当计算量非常大时,性能差距会有多大。
    乘100的Shift方式:

int q , r ;
r = (q << 6) + (q << 5) + (q << 2))

(4)求商和余数的高性能算法

    通常,求商、求余的做法是这样的:q = i / 10 ; r = i % 10;
    但这种方式并未在Java源码中采用。如Integer类的getChars()方法。尤其是求余数时,没有采用求模运算符,而采用移位运算符。这其中的原因是如(7)条中所说的,Shift方式更高效。采用Shift方式的算法如下:

int i ;
if (i < 65536) {

    int q = (i * 52429) >>> (16+3); //求商

    int r = i - ((q << 3) + (q << 1)); //求余数,i - 10 * q 

}

    这里对整数进行了区分,上面是小整数(小于65536)的求解算法。大整数的算法如下:

int i ;
if (i >= 65536) {
    int q = i / 100; //求商

    int r = i - ((q << 6) + (q << 5) + (q << 2)); //求余数 
}

(5)整数转字符算法

    数据准备:

final static char[] digits = {
    '0' , '1' , '2' , '3' , '4' , '5' ,
    '6' , '7' , '8' , '9' 
};

final static char [] DigitTens = {
    '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
    '1', '1', '1', '1', '1', '1', '1', '1', '1', '1',
    '2', '2', '2', '2', '2', '2', '2', '2', '2', '2',
    '3', '3', '3', '3', '3', '3', '3', '3', '3', '3',
    '4', '4', '4', '4', '4', '4', '4', '4', '4', '4',
    '5', '5', '5', '5', '5', '5', '5', '5', '5', '5',
    '6', '6', '6', '6', '6', '6', '6', '6', '6', '6',
    '7', '7', '7', '7', '7', '7', '7', '7', '7', '7',
    '8', '8', '8', '8', '8', '8', '8', '8', '8', '8',
    '9', '9', '9', '9', '9', '9', '9', '9', '9', '9',
    } ;

final static char [] DigitOnes = {
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    } ;

    先来看int型:
        1)如果大于65536 ,则除数设置为100,所得余数小于100,占2个字符,分别存储个位和十位到适当位置。
        2)如果小于等于65536 ,则除数设置为10 ,余数占1个字符,然后将它存储到适当位置。
    如果是1),则使用数据DigitTens 和 DigitOnes ,前者方便获取十位上的字符,后者方便获取个位字符,例如,余数37 ,将它作为下标,从DigitTens中获取的值刚好是3,而从DigitOnes中获取的值刚好是7。算法如下:

    /**
     * @param i: 是需要转字符的int值,如34
     * @param index:对应的字符个数,如34对应的字符个数为2,index 的值为2 ;如果是负数,则转为相反数后加1
     * @param buf: char[]数组,存储对应的字符
     */
static void getChars(int i, int index, char[] buf) {
    int q, r;
    int charPos = index;
    char sign = 0;

    if (i < 0) {
        sign = '-';
        i = -i;
    }

    // Generate two digits per iteration
    while (i >= 65536) {
        q = i / 100;
    // really: r = i - (q * 100);
        r = i - ((q << 6) + (q << 5) + (q << 2));
        i = q;
        buf [--charPos] = DigitOnes[r];
        buf [--charPos] = DigitTens[r];
    }

    // Fall thru to fast mode for smaller numbers
    // assert(i <= 65536, i);
    for (;;) {
        q = (i * 52429) >>> (16+3);
        r = i - ((q << 3) + (q << 1));  // r = i-(q*10) ...
        buf [--charPos] = digits [r];
        i = q;
        if (i == 0) break;
    }
    if (sign != 0) {
        buf [--charPos] = sign;
    }
}

    这里用到了上面第(8)条的算法。
    对于long型的数据,基本思想是一样的。只是在while (i >= 65536)前再加上:

while (i > Integer.MAX_VALUE) {
    q = i / 100;
    // really: r = i - (q * 100);
    r = (int)(i - ((q << 6) + (q << 5) + (q << 2)));
    i = q;
    buf[--charPos] = Integer.DigitOnes[r];
    buf[--charPos] = Integer.DigitTens[r];
}

(6)字符数组倒序算法

    主要思想:从中间开始,向高位、低位依次取值,并交换它们。Java版如下:

public char[] reverse(char[] value , int count) {
   int n = count - 1;
   for (int j = (n-1) >> 1; j >= 0; j--) {
       int k = n - j;
       char cj = value[j];
       char ck = value[k];
       value[j] = ck;
       value[k] = cj;
   }
   return value;
}

    Kotlin版本如下:

    fun reverse(value: CharArray, count: Int): CharArray {
        val n = count - 1
        for (j in n - 1 shr 1 downTo 0) {
            val k = n - j
            val cj = value[j]
            val ck = value[k]
            value[j] = ck
            value[k] = cj
        }
        return value
    }

(7)整数转二进制字符串

    介绍三种方式,第一种递归算法,Java版本:

    /**
     * 递归:将int型转为二进制的String
     */
    public static String intToBinary(int n) {
        if (n == 0) {
            return "0";
        } else if (n == 1) {
            return "1";
        } else {
            int remain = n % 2;
            String other = intToBinary(n / 2);
            return other + remain;
        }
    }

    Kotlin版本:

    /**
     * 递归:将int型转为二进制的String
     */
    fun intToBinary(n: Int): String {
        return if (n == 0) {
            "0"
        } else if (n == 1) {
            "1"
        } else {
            val remain = n % 2
            val other = intToBinary(n / 2)
            other + remain
        }
    }

    第二种,shift循环实现,Java版本:

    /**
     * shift 循环实现,
     */
    public static String intToBinaryShift(int n){
        char[] intChars = new char[32];//Java中int型为32位
        for(int i=31;i>=0;i--,n>>=1){
            intChars[i] = (0x1 & n) == 1 ? '1':'0';
        }
        return new String(intChars);
    }

    这种实现开头会包含多余的零,需要注意。
    Kotlin版本:

    /**
     * shift 循环实现,
     */
    fun intToBinaryShift(n: Int): String {
        var n = n
        val intChars = CharArray(32) //Java中int型为32位
        var i = 31
        while (i >= 0) {
            intChars[i] = if (0x1 and n == 1) '1' else '0'
            i--
            n = n shr 1
        }
        return String(intChars)
    }

    第三种,借助Java API实现,Java版如下:

    /**
     * api 实现
     */
    public static String intToBinaryW(int n) {
        StringBuilder builder = new StringBuilder();
        while (n > 0) {
            int remain = n % 2;
            builder.append(remain);
            n = n >> 1;
        }
        return builder.reverse().toString();
    }

    Kotlin版:

    /**
     * api 实现
     */
    fun intToBinaryW(n: Int): String {
        var n = n
        val builder = StringBuilder()
        while (n > 0) {
            val remain = n % 2
            builder.append(remain)
            n = n shr 1
        }
        return builder.reverse().toString()
    }

(8)字符转int

     将char字符转为对应的int值,例如字符'3'转为数值3。Java版如下:

    /**
     * char转int值
     */
    public static int charToInt(char c) {
        int codePoint = (int) c;//c的代码点,unicode码
        if (codePoint >= '0' && codePoint <= '9') {
            return codePoint - '0';
        }
        return -1;
    }

    Kotlin版:

    /**
     * char转int值
     */
    fun charToInt(c: Char): Int {
        val codePoint = c.code //c的代码点,unicode码
        return if (codePoint >= '0'.code && codePoint <= '9'.code) {
            codePoint - '0'.code
        } else -1
    }

(9)文章评论结构

    由看文章、帖子及关联的评论,临时起意,设计一种结构来表示它们。
    特点:一篇文章有作者、标题、内容、发布时间等属性,一条评论有评论者、评论内容、评论时间等属性;一篇文章可以有多个评论,每条评论还可以有多个子评论;每条子评论都有父评论;评论的数量是不限制的,随时可能新增。
    设计:采用树结构,将文章作为根节点,一级评论作为它的直接后继,二级评论作为一级评论的孩子节点,以此类推;文章是一级评论的父节点,一级评论是二级评论的父节点,以此类推;文章没有父节点。
    Java版本:

import java.util.ArrayList;
import java.util.List;

public class ArticleCommentStruct {
    String id; //编号
    String authorName; //作者名称
    String iconUrl;//图标地址
    long publishTime; //发布时间
    int type; // 0表示文章,1表示评论
    List<ArticleCommentStruct> childList; //子评论
    ArticleCommentStruct father; //父评论,如果是文章,则为空;一级评论的father是文章
    String content; //文章内容
    String title; //文章标题,只有type=0时才有值
    int hotNum;//热度值,可以用来排序

    public ArticleCommentStruct(){
        this(0);
    }

    public ArticleCommentStruct(int type){
        assert (type == 0 || type == 1);
        this.type = type;
    }

    /**
     * 如果是一篇文章,返回true
     * @return
     */
    public boolean isArticle(){
        return this.type == 0;
    }

    /**
     * 如果是一个评论,返回true
     * @return
     */
    public boolean isComment(){
        return this.type == 1;
    }

    /**
     * 如果是一级评论,返回true
     * @return
     */
    public boolean isFirstClassComment(){
        if (isComment() && father != null && father.isArticle()){
            return true;
        }
        return false;
    }

    /**
     * 添加一个子评论
     * @param child
     */
    public void addChild(ArticleCommentStruct child){
        if (childList == null){
            childList = new ArrayList<>();
        }
        childList.add(child);
    }

    /**
     * 创建一个类型为文章的ArticleCommentStruct对象
     * @param authorName 作者名称
     * @param content 文章内容
     * @return
     */
    public static ArticleCommentStruct makeArticle(String authorName,String content){
        ArticleCommentStruct tree = new ArticleCommentStruct();
        tree.authorName = authorName;
        tree.content = content;
        return tree;
    }

    /**
     * 创建一个类型为评论的ArticleCommentStruct对象
     * @param authorName 作者名称
     * @param content 评论内容
     * @return
     */
    public static ArticleCommentStruct makeComment(String authorName,String content){
        ArticleCommentStruct comment = new ArticleCommentStruct(1);
        comment.authorName = authorName;
        comment.content = content;
        return comment;
    }

    其实还可以做的更多,比如根据hotNum排序。一些将高点赞、高热度的评论置顶的做法就要用到它。这里就不再多说了。
    Kotlin版本:

import kotlin.jvm.JvmOverloads
import java.util.ArrayList

class ArticleCommentStruct @JvmOverloads constructor(type: Int = 0) {
    var id //编号
            : String? = null
    var authorName //作者名称
            : String? = null
    var iconUrl //图标地址
            : String? = null
    var publishTime //发布时间
            : Long = 0
    var type // 0表示文章,1表示评论
            : Int
    var childList //子评论
            : MutableList<ArticleCommentStruct>? = null
    var father //父评论,如果是文章,则为空;一级评论的father是文章
            : ArticleCommentStruct? = null
    var content //文章内容
            : String? = null
    var title //文章标题,只有type=0时才有值
            : String? = null
    var hotNum //热度值,可以用来排序
            = 0

    init {
        assert(type == 0 || type == 1)
        this.type = type
    }

    /**
     * 如果是一篇文章,返回true
     * @return
     */
    val isArticle: Boolean
        get() = type == 0

    /**
     * 如果是一个评论,返回true
     * @return
     */
    val isComment: Boolean
        get() = type == 1

    /**
     * 如果是一级评论,返回true
     * @return
     */
    val isFirstClassComment: Boolean
        get() = if (isComment && father != null && father!!.isArticle) {
            true
        } else false

    /**
     * 添加一个子评论
     * @param child
     */
    fun addChild(child: ArticleCommentStruct) {
        if (childList == null) {
            childList = ArrayList()
        }
        childList!!.add(child)
    }

    companion object {
        /**
         * 创建一个类型为文章的ArticleCommentStruct对象
         * @param authorName 作者名称
         * @param content 文章内容
         * @return
         */
        fun makeArticle(authorName: String?, content: String?): ArticleCommentStruct {
            val tree = ArticleCommentStruct()
            tree.authorName = authorName
            tree.content = content
            return tree
        }

        /**
         * 创建一个类型为评论的ArticleCommentStruct对象
         * @param authorName 作者名称
         * @param content 评论内容
         * @return
         */
        fun makeComment(authorName: String?, content: String?): ArticleCommentStruct {
            val comment = ArticleCommentStruct(1)
            comment.authorName = authorName
            comment.content = content
            return comment
        }
    }
}

     Over !

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

推荐阅读更多精彩内容