聊聊java中的哪些Map:(九)TreeMap源码分析

[toc]

java中还有一个有序的Map是TreeMap,虽然相对于HashMap,其出镜率并不高,但是我们在某些场景下也会用到,另外这个TreeMap完全是基于红黑树的一种实现,分析其源码有利于对红黑树概念的理解。

1.类结构及其成员变量

1.1 类结构

TreeMap的类继承结构如下图:


image.png

可以看到,TreeMap继承了AbstractMap,此外实现了NavigableMap、Cloneable和Serializable接口。而NavigableMap接口又继承了SortedMap。这与ConcurrentSkipListMap的情况类似,因而也是有序的。

/**
 * A Red-Black tree based {@link NavigableMap} implementation.
 * The map is sorted according to the {@linkplain Comparable natural
 * ordering} of its keys, or by a {@link Comparator} provided at map
 * creation time, depending on which constructor is used.
 *
 * <p>This implementation provides guaranteed log(n) time cost for the
 * {@code containsKey}, {@code get}, {@code put} and {@code remove}
 * operations.  Algorithms are adaptations of those in Cormen, Leiserson, and
 * Rivest's <em>Introduction to Algorithms</em>.
 *
 * <p>Note that the ordering maintained by a tree map, like any sorted map, and
 * whether or not an explicit comparator is provided, must be <em>consistent
 * with {@code equals}</em> if this sorted map is to correctly implement the
 * {@code Map} interface.  (See {@code Comparable} or {@code Comparator} for a
 * precise definition of <em>consistent with equals</em>.)  This is so because
 * the {@code Map} interface is defined in terms of the {@code equals}
 * operation, but a sorted map performs all key comparisons using its {@code
 * compareTo} (or {@code compare}) method, so two keys that are deemed equal by
 * this method are, from the standpoint of the sorted map, equal.  The behavior
 * of a sorted map <em>is</em> well-defined even if its ordering is
 * inconsistent with {@code equals}; it just fails to obey the general contract
 * of the {@code Map} interface.
 *
 * <p><strong>Note that this implementation is not synchronized.</strong>
 * If multiple threads access a map concurrently, and at least one of the
 * threads modifies the map structurally, it <em>must</em> be synchronized
 * externally.  (A structural modification is any operation that adds or
 * deletes one or more mappings; merely changing the value associated
 * with an existing key is not a structural modification.)  This is
 * typically accomplished by synchronizing on some object that naturally
 * encapsulates the map.
 * If no such object exists, the map should be "wrapped" using the
 * {@link Collections#synchronizedSortedMap Collections.synchronizedSortedMap}
 * method.  This is best done at creation time, to prevent accidental
 * unsynchronized access to the map: <pre>
 *   SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));</pre>
 *
 * <p>The iterators returned by the {@code iterator} method of the collections
 * returned by all of this class's "collection view methods" are
 * <em>fail-fast</em>: if the map is structurally modified at any time after
 * the iterator is created, in any way except through the iterator's own
 * {@code remove} method, the iterator will throw a {@link
 * ConcurrentModificationException}.  Thus, in the face of concurrent
 * modification, the iterator fails quickly and cleanly, rather than risking
 * arbitrary, non-deterministic behavior at an undetermined time in the future.
 *
 * <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
 * as it is, generally speaking, impossible to make any hard guarantees in the
 * presence of unsynchronized concurrent modification.  Fail-fast iterators
 * throw {@code ConcurrentModificationException} on a best-effort basis.
 * Therefore, it would be wrong to write a program that depended on this
 * exception for its correctness:   <em>the fail-fast behavior of iterators
 * should be used only to detect bugs.</em>
 *
 * <p>All {@code Map.Entry} pairs returned by methods in this class
 * and its views represent snapshots of mappings at the time they were
 * produced. They do <strong>not</strong> support the {@code Entry.setValue}
 * method. (Note however that it is possible to change mappings in the
 * associated map using {@code put}.)
 *
 * <p>This class is a member of the
 * <a href="{@docRoot}/../technotes/guides/collections/index.html">
 * Java Collections Framework</a>.
 *
 * @param <K> the type of keys maintained by this map
 * @param <V> the type of mapped values
 *
 * @author  Josh Bloch and Doug Lea
 * @see Map
 * @see HashMap
 * @see Hashtable
 * @see Comparable
 * @see Comparator
 * @see Collection
 * @since 1.2
 */

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
}

其注释大意为:
TreeMap是一个基于红黑树的NavigableMap实现,可以根据Comparable实现自然排序,或者Comparator排序,至于根据哪个排序取决于构造函数。
这个实现对containsKey、get、put、remove提供了恒定的时间复杂度log(n)。这个算法是对Cormen、Leiserson和Rivest算法的改进。
请注意,TreeMap与任何排序的map一样,维护的顺序,以及是否提供显示比较必须与equals方法一致,以便排序的map能够正确实现。之所以这样,是因为map接口是根据equals的操作定义的,但是排序之后的map使用compareTo或者compare方法执行比较,因此从排序map的角度来看,此方法认为相等的两个key是相等的。排序后的map的行为是明确定义的,即使其排序与equals不一致,它也只是无法遵守map的一般约定。
需要注意的是,这个实现是非同步的。如果多线程并发访问,最后一个线程修改了这个map的结构,它必须在外部进行同步。结构的修改是指添加或者删除一个或者多个映射。使用key进行更新不是属于结构修改。这是通常通过封装TreeMap对象来实现。
如果这个对象不存在,那么应该使用Collections.synchronizedSortedMap方法。最好是在创建的时候完成,以防止不同步的访问。如下:

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

这个类的所有集合视图方法返回的集合的iterator的迭代器都是fail-fast的。如果在迭代器被映射之后的任何时间对结构进行修改,除非通过迭代器自己的remove方法,否则将抛出ConcurrentModificationException,因此,面对并发修改,迭代器将快速失败,而不是冒着未来不确定的时间产生任意不确定的风险。
注意,迭代器的fail-fast并不是一个担保的行为,因为通常来说,存在不同步修改的并发修改的情况下,不可能做出任何严格的保证,快速失败的迭代器将会最大努力的抛出ConcurrentModificationException。因此,编写任何依赖于此异常的程序代码以确保其正确性都是错误的。迭代器的快速失败机制仅仅用于检测错误。
此类中的方法返回的所有Entry对其视图均表示生成map的快照,他们不支持Entry.setValue方法,不过请注意,可以使用put更改关联map中的映射。
另外,TreeMap还是java集合框架的成员。

1.2 成员变量

由于TreeMap并不涉及线程安全和扩容,因此成员变量非常简单:

/**
 * The comparator used to maintain order in this tree map, or
 * null if it uses the natural ordering of its keys.
 *
 * @serial
 */
private final Comparator<? super K> comparator;

private transient Entry<K,V> root;

/**
 * The number of entries in the tree
 */
private transient int size = 0;

/**
 * The number of structural modifications to the tree.
 */
private transient int modCount = 0;

comparator 是用于进行比较的比较器。root指向根节点,由于红黑树是一个树形结构,因此不再需要数组之类的结构来存储。size用于存放tree中元素的长度。modCount则是在fail-fast机制所依赖的。

2. 核心内部类Entry

TreeMap是基于红黑树的数据结构,TreeMap实现了Map.Entry接口。具有key、value、left、right、parent等属性。

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;

    /**
     * Make a new cell with given key, value, and parent, and with
     * {@code null} child links, and BLACK color.
     */
    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }

    /**
     * Returns the key.
     *
     * @return the key
     */
    public K getKey() {
        return key;
    }

    /**
     * Returns the value associated with the key.
     *
     * @return the value associated with the key
     */
    public V getValue() {
        return value;
    }

    /**
     * Replaces the value currently associated with the key with the given
     * value.
     *
     * @return the value associated with the key before this method was
     *         called
     */
    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;

        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }

    public int hashCode() {
        int keyHash = (key==null ? 0 : key.hashCode());
        int valueHash = (value==null ? 0 : value.hashCode());
        return keyHash ^ valueHash;
    }

    public String toString() {
        return key + "=" + value;
    }
}

这个类的结构也非常简单,需要注意的是,其hashCode方法,实际上是keyHash ^ valueHash。虽然key和value都不支持为空,但是还做了特殊处理。

3.基本原理之红黑树

在要搞懂红黑树之前,我们需要先弄清楚,二叉搜索树(Binary Search Tree)和平衡二叉树(AVL树)是什么,之后再来理解红黑树(RB树)。

3.1 二叉搜索树(Binary Search Tree)

定义:它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
二叉搜索树的概念看起来复杂,实际上非常简单,要求就一个,任意节点,其值大于左节点,小于右节点。
如下就是一个二叉搜索树:


二叉搜索树

如果这个二叉搜索树比较平衡的话,其查找的时间复杂度为(log n)。
但是需要注意的是,二叉搜索树如果使用不当非常容易退化为链表,导致时间复杂度退化为n。为了解决这个问题,就出现了平衡二叉树(AVL树)。

3.2 平衡二叉树(AVL Tree)

平衡二叉树实际上是二叉搜索树的一种特殊情况。但是其带有平衡条件。要求每个节点的左右子树的高度绝对值最多为1(平衡因子)。
平衡二叉树通过一系列平衡操作,左旋、右旋等,来保证了平衡二叉树的平衡性。


平衡二叉树

这样就将查询的时间复杂度恒定在(log n)。但是代价就是在插入和删除的时候会带来非常高的时间消耗。将插入和删除的时间复杂度都变成了(log n)。

3.3 红黑树

红黑树是平衡二叉树的一个变种,其也是在二叉搜索树上进化而来,但是其转置是根据红黑标记而来。而不是像平衡二叉树那样做统一的要求。其平衡性相对平衡二叉树要低。颜色规则为:

  • 1.任意节点的颜色要么黑色,要么红色。
  • 2.根节点是黑色。
  • 3.如果一个节点为红色,其子节点一定是黑色。(不能允许有两个连续向连的红色节点)
  • 4.任意节点到其下面的空节点,(空接点为黑色)。的路径上,黑色节点的数量相同。
    通过从根节点到空节点的任意路径进行颜色约束,红黑树可以确保没用一条路径比其他路径的长度多出2倍。因此红黑树是一个近似平衡的树。(节点的平衡因子不能大于1)


    红黑树

红黑树是一个近似平衡的二叉树。也是采用旋转的方式来实现。但是由于其的近似特性,不用旋转到AVL树那种完全平衡的二叉树。AVL树一定是保证左边节点都是满的,如果存在不满的情况一定会出现在右侧。因此AVL树要保证平衡性,旋转的过程会更加复杂,耗时会更长。
实际上这也是大多数集合框架底层采用红黑树的原因。其插入和删除的效率高于AVL数,但是检索效率不会低多少。

4.构造函数

4.1 TreeMap()

支持空的构造函数:

public TreeMap() {
    comparator = null;
}

4.2 TreeMap(Comparator<? super K> comparator)

public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

此方法只是传入了一个比较器。

4.3 TreeMap(Map<? extends K, ? extends V> m)

从其他map中put

public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
}

内部通过putAll方法实现。

public void putAll(Map<? extends K, ? extends V> map) {
    int mapSize = map.size();
    if (size==0 && mapSize!=0 && map instanceof SortedMap) {
        Comparator<?> c = ((SortedMap<?,?>)map).comparator();
        if (c == comparator || (c != null && c.equals(comparator))) {
            ++modCount;
            try {
                buildFromSorted(mapSize, map.entrySet().iterator(),
                                null, null);
            } catch (java.io.IOException cannotHappen) {
            } catch (ClassNotFoundException cannotHappen) {
            }
            return;
        }
    }
    super.putAll(map);
}

如果为SortedMap的子类,则通过Sorted的方法实现。buildFromSorted。反之则通过父类的方法,遍历之后再插入。

4.4 TreeMap(SortedMap<K, ? extends V> m)

public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

通过buildFromSorted 实现sorted的插入。

5.重要方法

下面对TreeMap的一些重要方法的源码进行分析:

5.1 get

get方法:

public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}

实际上调用的是getEntry方法 :

final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    //如果比较器为空
    if (comparator != null)
        return getEntryUsingComparator(key);
    //如果key为空 则返回NPE异常 key不支持为空
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
       //key是可比较的
        Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    //如果不为空则根据比较结果左右遍历
    while (p != null) {
        int cmp = k.compareTo(p.key);
        //小于0则左树遍历
        if (cmp < 0)
            p = p.left;
        //大于0则右树遍历
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}

5.2 put

put源码:

public V put(K key, V value) {
    Entry<K,V> t = root;
    //t为root节点,如果t为空
    if (t == null) {
        //可比较性校验
        compare(key, key); // type (and possibly null) check
        //直接在root节点创建
        root = new Entry<>(key, value, null);
        //增加计数器和modCount
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    //比较器
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
       //如果比较器不为空则循环遍历
        do {
            parent = t;
            //比较
            cmp = cpr.compare(key, t.key);
            //小于0左树遍历
            if (cmp < 0)
                t = t.left;
            //大于0右树遍历
            else if (cmp > 0)
                t = t.right;
            //反之则直接修改值
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {
    //如果key为空 则NPE异常
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        //反之循环遍历
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            //左树遍历
            if (cmp < 0)
                t = t.left;
            //右树遍历
            else if (cmp > 0)
                t = t.right;
            //反之直接设置值
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    //之后进行插入之后的平衡操作
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

上述即是put方法。

5.3 remove

reomve方法源码如下:

public V remove(Object key) {
   //通过get方法找到p
    Entry<K,V> p = getEntry(key);
    //如果p为null则没找到 直接返回null
    if (p == null)
        return null;
    //将oldValue存储并返回,然后通过deleteEntry删除节点
    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}

操作逻辑在deleteEntry中:

private void deleteEntry(Entry<K,V> p) {
    //增加modCount次数和减少size
    modCount++;
    size--;

    // If strictly internal, copy successor's element to p and then make p
    // point to successor.
    //如果左右都不为空 
    if (p.left != null && p.right != null) {
       //后继节点 左右和父节点三个节点进行判断 按这个顺序取 之后将p的key和value改为后继节点
        Entry<K,V> s = successor(p);
        p.key = s.key;
        p.value = s.value;
        //将p和后继节点合并
        p = s;
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    //替换节点 p的左 右 只要不为空 按这个次序
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);
    //如果还有替换节点
    if (replacement != null) {
        // Link replacement to parent
        //将p的parent指向这个节点
        replacement.parent = p.parent;
        //如果parent为null 则将root指向它
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left  = replacement;
        else
            p.parent.right = replacement;

        // Null out links so they are OK to use by fixAfterDeletion.
        //将p的指征都清空 以便GC回收
        p.left = p.right = p.parent = null;

        //颜色检测
        // Fix replacement
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // return if we are the only node.
        root = null;
    } else { //  No children. Use self as phantom replacement and unlink.
        if (p.color == BLACK)
        //平衡操作
            fixAfterDeletion(p);

        if (p.parent != null) {
            if (p == p.parent.left)
                p.parent.left = null;
            else if (p == p.parent.right)
                p.parent.right = null;
            p.parent = null;
        }
    }
}

5.4 fixAfterInsertion

这是在insert之后,进行红黑树颜色校验:

 /** From CLR */
private void fixAfterInsertion(Entry<K,V> x) {
    //先将x的颜色改为红色
    x.color = RED;
    //之后遍历循环
    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    root.color = BLACK;
}

根据红黑树的规则,对颜色进行check,并修改为对应的颜色。

5.5 fixAfterDeletion

fixAfterDeletion是在删除元素之后再进行平衡的方法:

 /** From CLR */
private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) {
            Entry<K,V> sib = rightOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }

            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else { // symmetric
            Entry<K,V> sib = leftOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateRight(parentOf(x));
                sib = leftOf(parentOf(x));
            }

            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateLeft(sib);
                    sib = leftOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(leftOf(sib), BLACK);
                rotateRight(parentOf(x));
                x = root;
            }
        }
    }

    setColor(x, BLACK);
}

通过上述代码根据红黑树的规则进行检查。

6.总结

本文对TreeMap的源码进行了分析,实际上,再分析了前的HashMap和ConcurrentHashMap的源码之后,TreeMap的源码要相对简单得多。其核心就是红黑树,然后左右旋转。关于红黑树的具体操作,本文并没有进行描述。
红黑树是一种比平衡二叉树在插入和删除上效率更好,检索的时候效率与平衡二叉树基本一致的数据结构,相比平衡二叉树,在插入和删除从操作上可以节约时间。这也是为什么HashMap底层会引入红黑树的原因之一。红黑树会大量应用于操作系统底层的各种数据结构。
**需要注意的是,TreeMap不支持key为null的情况,但是支持value为null。这介于HashMap和concurrentHashMap之间。

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