Data Structure_堆_二叉樹_并查集

堆這種數據結構的應用很廣泛,比較常用的就是優先隊列。普通的隊列就是先進先出,后進后出。優先隊列就不太一樣,出隊順序和入隊順序沒有關系,只和這個隊列的優先級相關,比如去醫院看病,你來的早不一定是先看你,因為病情嚴重的病人可能需要優先接受治療,這就和時間順序沒有必然聯系。優先隊列最頻繁的應用就是操作系統,操作系統的執行是劃分成一個一個的時間片的,每一次在時間片里面的執行的任務是選擇優先級最高的隊列,如果一開始這個優先級是固定的可能就很好選,但是在操作系統里面這個優先級是動態變化的,隨著執行變化的,所以每一次如果要變化,就可以使用優先隊列來維護,每一次進或者出都動態著在優先隊列里面變化。在游戲中也有使用到,比如攻擊對象,也是一個優先隊列。所以優先隊列比較適合處理一些動態變化的問題,當然對于靜態的問題也可以求解,比如求解1000個數字的前100位出來,最簡單的方法就是排序了,,但是這樣多此一舉,直接構造一個優先隊列,然后出的時候出一百次最大的元素即可。這個時候算法的復雜度就是NlogN,如果使用優先隊列可以降到NlogM的級別。

入隊 出隊
普通數組 O(1) O(n)
順序數組 O(n) O(1)
O(lgn) O(lgn)

現在就要用堆來實現一個優先隊列堆一般都是樹形結構:

二叉堆就和二叉樹有點相像,沒一個節點都有兩個子節點,而且任何一個字節點都不會大于它的父節點,當然這樣構造出來的就是一個最大堆了,也可以每一個子節點不小于它的父節點,這樣就是最小堆了。而且,這個二叉堆一定是一個完全二叉樹,非最后一層的節點一定要是滿的,最后一層的節點一定要集中在左邊:
這個性質,非常好,所以可以用給一個數組來表示堆。另外,層數越高并不意味著數值就越大或者越小,可以從右邊的圖看的出來。
可以看的出來,每一個節點子節點都是父節點的兩倍。對于一個子節點,想要找到它的父類就直接/2即可,這里用的是四舍五入的除法,所以直接就向下取整了。父節點找子節點,左孩子2i,右孩子2i+1即可。首先是建立一個堆,這個還是蠻簡單的:

template<typename item>
class MaxHeap {
private:
    item *data;
    int count = 0;
public:
    MaxHeap(int capacity){
        data = new item[capacity + 1];
        count = 0;
    }
    ~MaxHeap(){
        delete[] data;
    }
    int size(){
        return count;
    }
    bool isEmpty(){
        return count == 0;
    }
};

對于插入一個元素其實也很簡單,直接放到最后然后再一步一步的浮上來。新加入的那個元素和他的父親對比,如果是大于它父親那么就交換,只到是不大于或者是到了1。因為如果是小于父親節點那就本身是正確的,如果不終止,再往下比那就是比較其他的元素了,但是其他元素本來就是正確的,所以不需要比較直接結束好了。

    void shiftUp(int index) {
        while (index > 1 && data[index / 2] < data[index]) {
            swap(data[index / 2], data[index]);
            index /= 2;
        }
    }
    void insert(item number) {
        assert(count + 1 <= capacity);
        data[count + 1] = number;
        count++;
        shiftUp(count);
    }

這樣就完成了插入。對于彈出最大的元素就有點邊界問題要討論了。這是一個完全二叉樹,所以只有左節點沒有右節點,所以首先先要判斷是不是有做孩子,也就是越界的問題了,如果沒有就繼續判斷有沒有右孩子,有的話就左右孩子比較咯,哪個大就拿哪個出來,在把最大的拿出來和原節點比較即可。這里就需要一個額外的變量了。

    void shiftDown(int index) {
        while (2 * index <= count) {
            int change = 2 * index;
            if (change + 1 < count && data[change + 1] > data[change]) {
                change++;
            }
            if (data[change] <= data[index]) {
                break;
            }
            swap(data[change], data[index]);
            index = change;
        }
    }
    item pop() {
        assert(count > 0);
        item target = data[1];
        swap(data[1], data[count]);
        count--;
        shiftDown(1);
        return target;
    }

這樣就完成了彈出,彈出的就是最大的數字。事實上這樣是可以直接做排序操作的。
現在使用的堆數據結構,是通過不斷的交換元素來達到符合堆這個數據結構的目的,但是如果堆里面的元素不是數字,而是字符串或者是很長很長的其他復雜的數據結構,那么交換起來效率就很低,而且這樣的話索引起來也有難度。為了解決這些問題,于是就引入了索引堆來解決:


每一個元素加上一個索引值
交換的時候就只是需要交換索引值即可,第一個元素的索引是10,那么就是對應著索引下標的62。對于查找,也很簡單,直接索引下標即可。修改起來也很簡單,大致是和之前一樣,只不過是修改的時候就是修改索引即可。
首先是對于索引堆的創建,我們不僅僅是要一個存儲數據的數組,還要一個存儲索引的數組,因為最后改變的是使用索引,索引對應下的數組是不變的。:

template<typename item>
class IndexHeap {
private:
    item *indexes;
    item *data;
    int count;
    int capacity;

shiftDown和shiftUp操作其實就是變一下,把交換的對象換成索引即可:

    void shiftUp(int k) {
        while (k > 1 && data[indexes[k / 2]] < data[indexes[k]]) {
            swap(indexes[k / 2], indexes[k]);
            k /= 2;
        }
    }

    void shitDown(int k) {
        while (2 * k <= count) {
            int change = 2 * k;
            if (change + 1 <= count && data[indexes[change]] < data[indexes[change + 1]]) {
                change++;
            }
            if (data[indexes[change]] <= data[indexes[k]]) {
                break;
            }
            swap(indexes[change], indexes[k]);
            k = change;
        }
    }

彈出和插入都是一樣的:

    void insert(int i, item itemNumber) {
        assert(count + 1 <= capacity);
        assert(i + 1 >= 1 && i + 1 < capacity);
        i++;
        data[i] = itemNumber;
        indexes[++count] = i;
        shiftUp(count);
    }

    item extractMax(){
        item num = data[indexes[1]];
        swap(indexes[1], indexes[count]);
        count -- ;
        shitDown(1);
        return num;
    }

主要的變化就是對于改變對應索引下的一個元素。改變了之后,我們要知道這個元素的索引是在這個堆的第幾個位置才可以進行shift操作,之后還要進行shifDown和shiftUp操作,因為不知道它是可以上浮還是下沉。

    void change(int i, item itemNumber){
        i += 1;
        data[i] = itemNumber;
        for (int j = 1; j <= count; ++j) {
            if (indexes[j] == i){
                shiftUp(j);
                shitDown(j);
                return;
            }
        }
    }

但是這個種改變方法最差的情況下是O(nlogn),最后還可以使用一種反向查找的方法進行改進,可以將復雜度提升到O(1)。使用一個reverse數組存儲當前索引所在的位置,只有在修改了元素之后就可以直接用O(1)的復雜度從reverse中取出來了。所以有reverse[index[i]] = i,reverse[index]就可以找到當前的元素的位置了。修改代碼很簡單,reverse和index是綁定在一起的,所以只要index變的地方reverse也要變的。

template<typename Item>
class IndexMinHeap{

private:
    Item *data;
    int *indexes;
    int *reverse;

    int count;
    int capacity;

    void shiftUp( int k ){

        while( k > 1 && data[indexes[k/2]] > data[indexes[k]] ){
            swap( indexes[k/2] , indexes[k] );
            reverse[indexes[k/2]] = k/2;
            reverse[indexes[k]] = k;
            k /= 2;
        }
    }

    void shiftDown( int k ){

        while( 2*k <= count ){
            int j = 2*k;
            if( j + 1 <= count && data[indexes[j]] > data[indexes[j+1]] )
                j += 1;

            if( data[indexes[k]] <= data[indexes[j]] )
                break;

            swap( indexes[k] , indexes[j] );
            reverse[indexes[k]] = k;
            reverse[indexes[j]] = j;
            k = j;
        }
    }

public:
    IndexMinHeap(int capacity){

        data = new Item[capacity+1];
        indexes = new int[capacity+1];
        reverse = new int[capacity+1];

        for( int i = 0 ; i <= capacity ; i ++ )
            reverse[i] = 0;

        count = 0;
        this->capacity = capacity;
    }

    ~IndexMinHeap(){
        delete[] data;
        delete[] indexes;
        delete[] reverse;
    }

    int size(){
        return count;
    }

    bool isEmpty(){
        return count == 0;
    }

    void insert(int index, Item item){
        assert( count + 1 <= capacity );
        assert( index + 1 >= 1 && index + 1 <= capacity );

        index += 1;
        data[index] = item;
        indexes[count+1] = index;
        reverse[index] = count+1;
        count++;
        shiftUp(count);
    }

    Item extractMin(){
        assert( count > 0 );

        Item ret = data[indexes[1]];
        swap( indexes[1] , indexes[count] );
        reverse[indexes[count]] = 0;
        reverse[indexes[1]] = 1;
        count--;
        shiftDown(1);
        return ret;
    }

    int extractMinIndex(){
        assert( count > 0 );

        int ret = indexes[1] - 1;
        swap( indexes[1] , indexes[count] );
        reverse[indexes[count]] = 0;
        reverse[indexes[1]] = 1;
        count--;
        shiftDown(1);
        return ret;
    }

    Item getMin(){
        assert( count > 0 );
        return data[indexes[1]];
    }

    int getMinIndex(){
        assert( count > 0 );
        return indexes[1]-1;
    }

    bool contain( int index ){
        return reverse[index+1] != 0;
    }

    Item getItem( int index ){
        assert( contain(index) );
        return data[index+1];
    }

    void change( int index , Item newItem ){

        assert( contain(index) );
        index += 1;
        data[index] = newItem;

        shiftUp( reverse[index] );
        shiftDown( reverse[index] );
    }

};

堆可以解決的一些問題,首先,就是使用堆來實現優先隊列,實現一些選擇優先級最高的任務執行??梢宰鳛橐恍┑图壒ぞ邅韺崿F一些高級數據結構。

Tree

二叉樹

二叉樹比較常用的地方就是查找了,其實就是類似于二分查找法,把數據分成兩份,使用logn這樣的復雜度來進行查找搜索,但是這樣就要求這個數組是有序的。比較常用的實現就是查找表的實現。如果使用順序數組進行查找,使用的復雜度是logn,相對應的插入元素也是要o(n),因為它要遍歷所有的元素找到相對應的位置然后插入。但是二分搜索樹就更好一些,插入刪除查找都是O(logn)的復雜度。所以,二分搜索樹不僅可以查找數據,還可以高效的插入刪除等等,效率很高,適合動態維護數據。而且這種方便的數據結構也可以很好的回答數據關系之間的問題。
二分搜索樹首先是一顆二叉樹:

每個節點的建值大于左孩子小于右孩子,而以左右孩子為根的子樹仍然是二分搜索樹。對于堆來說一定是要完全二叉樹,但是對于一個二分搜索樹來說,是不一定的,所以二叉樹就不能用數組來存儲了。實現二叉樹的結構其實很簡單,按照查找表來實現,要有一個建和一個值。

template<typename Key, typename Value>
class BST{
private:
    struct Node{
        Key key;
        Value value;
        Node *left;
        Node *right;
        Node(Key key, Value value){
            this->key = key;
            this->value = value;
            this->left = this->right = NULL;
        }
    };
    Node *root;
    int count;
public:
    BST(){
        root = NULL;
        count = 0;
    }
    ~BST(){
        //TODO
    }
    int size(){
        return count;
    }
    bool isEmpty(){
        return count == 0;
    }
};

對于插入其實也很簡單。首先和當前根節點比較,如果小于就往左邊遞歸,大于就往右邊遞歸,當當前節點是NULL的時候就到達了遞歸終點,這個時候已經到頭了,new一個新的節點返回當前節點即可。

    void insert(Key key, Value value){
        root = insert(root, key, value);
    }

private:
    Node *insert(Node *node, Key key, Value value){
        if (node == NULL){
            count ++;
            return new Node(key, value);
        }
        if (node->key == key){
            node->value = value;
        } else if(key < node->key){
            node->left = insert(node->left, key, value);
        } else{
            node->right = insert(node->right, key, value);
        }
        return node;
    }

二叉樹的包含,查找其實都很類似,都是小于往左邊找,大于往右邊找,只是返回值不一樣,操作很常規,比較復雜的是刪除操作,要旋轉之類的。

    Value *search(Node *node, Key key){
        if (NULL == node){
            return NULL;
        } else if(node->key == key){
            return &(node->value);
        } else if(key < node->key){
            return search(node->left, key);
        } else{
            return search(node->right, key);
        }
    }

    bool contain(Node *node, Key key){
        if (NULL == node){
            return false;
        }
        if (node->key == key){
            return true;
        } else if(key < node->key){
            return contain(node->left, key);
        } else{
            return contain(node->right, key);
        }
    }
};

以上方法都是放在了私有方法里面的,要調用只需要在public里面加上調用即可:

    Value *search(Key key){
        return search(root, key);
    }

    bool contain(Key key){
        return contain(root, key);
    }

對于search的返回值,其實見仁見智,返回node,固定的一個值都可以。但是如果返回的是一個node,那么調用的時候就需要用戶知道程序的結構,比如這道直觀node節點是啥才能拿出來,這樣封裝性就不好了;如果返回的是一個值,那么如果為空的時候就回不來了,所以把它的指針作為返回值。
二叉樹的遍歷方式有三種,前序遍歷,中序遍歷和后續遍歷。前序遍歷先訪問當前節點,再訪問左右節點;中序遍歷先訪問左節點,再訪問自身,最后右節點;后序遍歷先訪問左右子樹最后才訪問自身節點。

每一個節點都會存在左右子樹,用三個點來表示訪問時輸出的順序。

如果是前序遍歷,那么輸出的位置就是第一個園點的位置。中序遍歷也是一樣:
中序遍歷就是在遍歷到中間那個點的時候就輸出。后序遍歷自然就是最后一個位置輸出:
后序遍歷有一個很好的應用,就是釋放內存的時候可以使用后序遍歷的操作。遞歸實現二叉樹的遍歷其實很簡單,就是調換幾個位置就好了,結合上面的圖理解一下就好。

    void preOrder(Node *node){
        if (node != NULL){
            cout << node->value;
            preOrder(node->left);
            preOrder(node->right);
        }
    }

    void inOrder(Node *node){
        if (node != NULL){
            inOrder(node->left);
            cout << node->value;
            inOrder(node->right);
        }
    }

    void postOrder(Node * node){
        if (node != NULL){
            postOrder(node->left);
            postOrder(node->right);
            cout << node->value;
        }
    }

沒有什么難度的。注意到后序遍歷是左右才到中間,所以我們可以使用這種方法來對對整棵樹進行釋放。

    void destory(Node *node){
        if (node != NULL){
            destory(node->left);
            destory(node->right);
            delete node;
            count --;
        }
    }

之前我們遍歷二叉樹都是往深處遍歷,都是遍歷完一顆子樹再遍歷其他子樹,所以又叫深度遍歷。對于遍歷方式還有另外一種,就是廣度優先遍歷,對應到二叉樹里面就是層序遍歷。先遍歷完樹的一層再遍歷下一層。這種遍歷方式沒有往深處遍歷到底,而是更關注廣度的遍歷。要實現這種遍歷就需要使用到隊列,事實上很多廣度優先遍歷都是用隊列來實現,圖的廣度也是這樣實現。

    void levelOrder(){
        queue<Node *> q;
        q.push(root);
        while (!q.empty()){
            Node *p = q.front();
            q.pop();
            cout << p->value;
            if (p->left){
                q.push(p->left);
            }
            if (p->right){
                q.push(p->right);
            }
        }
    }

這樣很容易就實現了。二叉樹的刪除操作應該是屬于二叉樹操作里面比較難的部分,難的不是因為·刪除本身,而是在于刪除之后要怎么保持樹的性質,所以是需要旋轉。首先從一個最簡單的問題開始,求二叉樹最大值和最小值。

因為左節點是比當前節點小的,右節點是比當前節點大的,所以找左節點就是不斷往左邊走,直到沒有左孩子為止:

    Key minimum() {
        assert(count != 0);
        Node *node = minimum(root);
        return node->key;
    }

    Key maximum(){
        assert(count != 0);
        Node *node = maximum(root);
        return node->key;
    }

最大值和最小值都是一樣。刪除最小值,刪除最小值有兩種情況,如果這個最小值剛剛好沒有任何節點,刪除就很簡單,如果是有的話那就需要一些操作了。


這種情況就很好刪了,直接去掉即可。但是如果是有右孩子
這種情況刪除最小的就需要做一些變化了,但也不是特別復雜,直接把右節點拉上來就好了,因為子樹也是一顆二叉樹。類似的思路刪除最大值
這種情況刪除最大值也是很簡單的,直接扔了就好,但是如果是有左孩子的,直接把左邊的子樹拉上來就好了。
    Node *removeMax(Node *node){
        if (node->right == NULL){
            Node *left = node->left;
            delete node;
            count --;
            return left;
        }
        node->right = removeMax(node->right);
        return node;
    }

    Node *removeMin(Node *node){
        if (node->left == NULL){
            Node *right = node->right;
            delete node;
            count --;
            return right;
        }
        node->left = removeMin(node->left);
        return node;
    }

其實差不多的。最大值只會有左孩子,最小值只有右孩子。回到刪除節點,對于只有右孩子,因為右孩子本來就大于當前節點,只有左孩子也是一樣的。最難的就是左右孩子都有的情況,這樣就尷尬了:

如果是這種情況,我們就需要尋找當前節點的右孩子的作結點,找到
s = min(d->right)
,如果59沒有那么就是刪除60了,很明顯,就是右子樹的最小值了。

    Node *remove(Node *node, Key key){
        if ( node == NULL){
            return NULL;
        } else if (key < node->key){
            node->left = remove(node->left, key);
        } else if (key > node->key){
            node->right = remove(node->right, key);
        } else{
            if (node->left == NULL){
                Node *right = node->right;
                delete node;
                count --;
                return right;
            } else if (node->right == NULL){
                Node *left = node->left;
                delete node;
                count --;
                return left;
            } else{
                Node *delNode = node;
                Node *successor = new Node(minimum(node->right));
                count ++;
                successor->right = removeMin(node->right);
                successor->left = node->left;
                delete delNode;
                count --;
                return successor; 
            }
        }
    }

首先判斷一下是不是只有左子樹和右子樹,如果只是有一邊,那么可以直接把那一邊拉上去,如果是兩邊都有,那就需要找到右邊的最小值,代替要刪除的節點。這里有一個陷阱,使用successor得到最小的節點之后,后面又刪除了,這樣就會得到一個空指針,我們只是要得到它并不是要刪除,這個時候我們應該要復制一份過來,不能直接就拉過來。
以上的操作都是把二叉樹當成一個查找表來實現的,但是二叉樹還有一個很好的性質,二叉樹是有很好的順序性,所以二叉樹不僅僅可以用來實現查找,還可以找最小值最大值,前驅后繼等等。但是二叉樹并不是一成不變的,各種元素的插入順序的不同是可以導致二叉樹的結構不一樣,如果數據是基本有序的,插入基本就是一個鏈表的形狀了,退化成了一個順序鏈表。改進方法后面會提到紅黑樹AVL樹這些更加高級的數據結構。

并查集

這個數據結構并不算高級的數據結構,但是在后面的圖會用到來判斷是否形成環,如果兩個節點的根是相同的,那么就可以判斷這兩個節點是同一組的了,也就是已經連在了一起。之前的堆,二叉樹都是樹形的結構,而這個并查集也算是一種樹形結構,但是和上述的兩種不太一樣。并查集可以解決的問題有很多,首先就是連接問題:

想知道相鄰兩個點是不是連在一起的,如果這兩個點是相鄰的,沿著線可以很清楚的看到,但如果是一個左上角,一個右下角,那就很難看出來了。這種結構如果用并查集容易多了。連接可以應用在互聯網之中好友之間的連接,一個人是否可以通過另外一個人認識另外一個人,這就是一種連接問題。
首先要知道一個并查集要支持什么操作,并查集主要支持兩個操作,union,find,連接和查找操作。
用元素的下標是不是相同來表示這兩個元素是不是連接的:
前面的一半就是一組,后面的一組又是一對。

class unionFind {
private:
    int *id;
    int count;
public:
    unionFind(int n) {
        count = n;
        id = new int(n);
        for (int i = 0; i < n; ++i) {
            id[i] = i;
        }
    }

    ~unionFind() {
        delete[] id;
    }

一開始大家都是各自為政,沒有組團,所以都是不一樣的,直接按照序列號即可。查找就是很簡單了,就是直接返回id[]即可。

    int find(int p) {
        assert(p >= 0 && p <= count);
        return id[p];
    }

判斷是不是連在一起的,直接找到當前屬于的下標再比較即可,

    void unionElements(int p, int q){
        int pID = find(p);
        int qID = find(q);
        if (pID == qID){
            return;
        }
        for (int i = 0; i < count; ++i) {
            if (id[i] == pID){
                id[i] = qID;
            }
        }
    }

連接其實就是把兩個堆連在一起,掃描下面的坐標相同的元素賦值即可,賦值那邊賦值都可以。這種并查集查找很快,也叫quickUnion。

    UF_version1::unionFind uF = UF_version1::unionFind(10);
    uF.unionElements(1, 2);
    uF.unionElements(5, 4);
    uF.unionElements(3, 1);
    cout << uF.isConnected(4, 5) << endl;

上面這一種實現方法雖然實現查找的時候很快,但是實現并操作的時候很慢,需要進行遍歷。并查集的另一種實現方式,而這種實現方式是后面實現并查集的一種常規實現方式。將每一個元素看成是一個節點,如果兩個元素是一起的,那么這兩個元素是一個指向另一個的。

這個時候2 3 1是指向了一起的,那么這三個就是一伙的,2是這個堆的根節點。
如果這兩個堆要連在一起,那么只需要把他們的根連在一起就好了。
如果一個元素指向另一個元素,那么他的下標就存那個被指向的元素。把6,3或者是7,3連接一起都是一樣的,因為他們的根是一樣的。要修改的其實就是find而已,其他都是差不多的。

        int find(int p){
            assert(p >= 0 && p <= count);
            while (parent[p] != p){
                p = parent[p];
            }
            return p;
        }

        bool isConnected(int p, int q){
            return find(p) == find(q);
        }

        void unionElements(int p, int q){
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot == qRoot){
                return;
            }
            parent[pRoot] = qRoot;
        }

不斷向上找到自己的根,自己等于自己的就是當前的根了,如果不是就向下尋找,直到找到為止。這個時候的合并效率就不低了。注意到代碼中的構造函數中有一個ecplicit,因為在c++中單個參數的構造函數是存在有隱式的轉換的,加上這個關鍵字就可以禁止了,只能顯示的構造,比如String = "Hello";就是一種隱式構造。
對于這種并查集,還是有優化空間的:


比如這兩個堆要和在一起,如果是9合到另外一個,
這樣做沒有問題,但是這樣查找效率不高。
效果雖然是一樣的,但是查找起來確比上面那種要快很多。比較之后哪一個小的就插入到大的那個上面去。

    private:
        int *parent;
        int *sz;
        int count;

增加一個sz的數組存儲當前這個集合的數量,用于后面插入的比較,哪個大就插哪個。只要改變一下unionElements就好了,其他的不用變。

        void unionElements(int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot == qRoot) {
                return;
            }
            if (sz[pRoot] < sz[pRoot]){
                parent[pRoot] = qRoot;
                sz[qRoot] += sz[pRoot];
            } else{
                parent[qRoot] = pRoot;
                sz[pRoot] += sz[qRoot];
            }
        }

其實基于數量的優化還不是最優的,如果一個堆過于分散那么合并起來的效率不是很高的,所以還有一種改進方法,就是對比兩者的層數插入即可。并查集的線路壓縮,以上的插入很多時候有些隨機的成分,如果對這個線路的結構進行調整會快很多。這種方法就叫路徑壓縮。

這種路徑的查找明顯是效率很低的,
直觀上進行調整,這樣才是比較合理的。代碼實現很簡單,就是一句話的事情。

        int find(int p) {
            assert(p >= 0 && p <= count);
            while (parent[p] != p) {
                parent[p] = parent[parent[p]];
                p = parent[p];
            }
            return p;
        }

沒了,路徑壓縮就這樣。當前節點指向他的根就好了,而find就是找到它的根。遞歸求解即可。在經過了路徑壓縮的優化之后查找的復雜度幾乎就是O(1)復雜度了。

最后附上github地址:https://github.com/GreenArrow2017/DataStructure/tree/master/DataStucture

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容