散列表的原理與實(shí)現(xiàn)

本文主要介紹散列表(Hash Table)這一常見數(shù)據(jù)結(jié)構(gòu)的原理與實(shí)現(xiàn)。由于個(gè)人水平有限,文章中難免存在不準(zhǔn)確或是不清晰的地方六水,希望大家可以指正:)

概述

符號(hào)表是一種用于存儲(chǔ)鍵值對(duì)(key-value pair)的數(shù)據(jù)結(jié)構(gòu)增炭,我們平常經(jīng)常使用的數(shù)組也可以看做是一個(gè)特殊的符號(hào)表,數(shù)組中的“鍵”即為數(shù)組索引搬俊,值為相應(yīng)的數(shù)組元素紊扬。也就是說(shuō),當(dāng)符號(hào)表中所有的鍵都是較小的整數(shù)時(shí)唉擂,我們可以使用數(shù)組來(lái)實(shí)現(xiàn)符號(hào)表餐屎,將數(shù)組的索引作為鍵,而索引處的數(shù)組元素即為鍵對(duì)應(yīng)的值玩祟,但是這一表示僅限于所有的鍵都是比較小的整數(shù)時(shí)腹缩,否則可能會(huì)使用一個(gè)非常大的數(shù)組。散列表是對(duì)以上策略的一種“升級(jí)”,但是它可以支持任意的鍵而并沒(méi)有對(duì)它們做過(guò)多的限定藏鹊。對(duì)于基于散列表實(shí)現(xiàn)的符號(hào)表润讥,若我們要在其中查找一個(gè)鍵,需要進(jìn)行以下步驟:

  • 首先我們使用散列函數(shù)將給定鍵轉(zhuǎn)化為一個(gè)“數(shù)組的索引”伙判,理想情況下象对,不同的key會(huì)被轉(zhuǎn)為不同的索引,但在實(shí)際應(yīng)用中我們會(huì)遇到不同的鍵轉(zhuǎn)為相同的索引的情況宴抚,這種情況叫做碰撞勒魔。解決碰撞的方法我們后面會(huì)具體介紹。
  • 得到了索引后菇曲,我們就可以像訪問(wèn)數(shù)組一樣冠绢,通過(guò)這個(gè)索引訪問(wèn)到相應(yīng)的鍵值對(duì)。

以上就是散列表的核心思想常潮,散列表是時(shí)空權(quán)衡的經(jīng)典例子弟胀。當(dāng)我們的空間無(wú)限大時(shí),我們可以直接使用一個(gè)很大的數(shù)組來(lái)保存鍵值對(duì)喊式,并用key作為數(shù)組索引孵户,因?yàn)榭臻g不受限,所以我們的鍵的取值可以無(wú)窮大岔留,因此查找任何鍵都只需進(jìn)行一次普通的數(shù)組訪問(wèn)夏哭。反過(guò)來(lái),若對(duì)查找操作沒(méi)有任何時(shí)間限制献联,我們就可以直接使用鏈表來(lái)保存所有鍵值對(duì)竖配,這樣把空間的使用降到了最低,但查找時(shí)只能順序查找里逆。在實(shí)際的應(yīng)用中进胯,我們的時(shí)間和空間都是有限的,所以我們必須在兩者之間做出權(quán)衡原押,散列表就在時(shí)間和空間的使用上找到了一個(gè)很好的平衡點(diǎn)胁镐。散列表的一個(gè)優(yōu)勢(shì)在于我們只需調(diào)整散列算法的相應(yīng)參數(shù)而無(wú)需對(duì)其他部分的代碼做任何修改就能夠在時(shí)間和空間的權(quán)衡上做出策略調(diào)整。

散列函數(shù)

介紹散列函數(shù)前班眯,我們先來(lái)介紹幾個(gè)散列表的基本概念希停。在散列表內(nèi)部,我們使用桶(bucket)來(lái)保存鍵值對(duì)署隘,我們前面所說(shuō)的數(shù)組索引即為桶號(hào)宠能,決定了給定的鍵存于散列表的哪個(gè)桶中。散列表所擁有的桶數(shù)被稱為散列表的**容量(capacity)磁餐。

現(xiàn)在假設(shè)我們的散列表中有M個(gè)桶违崇,桶號(hào)為0到M-1阿弃。我們的散列函數(shù)的功能就是把任意給定的key轉(zhuǎn)為[0, M-1]上的整數(shù)。我們對(duì)散列函數(shù)有兩個(gè)基本要求:一是計(jì)算時(shí)間要短羞延,二是盡可能把鍵分布在不同的桶中渣淳。對(duì)于不同類型的鍵,我們需要使用不同的散列函數(shù)伴箩,這樣才能保證有比較好的散列效果入愧。
我們使用的散列函數(shù)應(yīng)該盡可能滿足均勻散列假設(shè),以下對(duì)均勻散列假設(shè)的定義來(lái)自于Sedgewick的《算法》一書:

(均勻散列假設(shè))我們使用的散列函數(shù)能夠均勻并獨(dú)立地將所有的鍵散布于0到M – 1之間嗤谚。

以上定義中有兩個(gè)關(guān)鍵字棺蛛,第一個(gè)是均勻,意思是我們對(duì)每個(gè)鍵計(jì)算而得的桶號(hào)有M個(gè)“候選值”巩步,而均勻性要求這M個(gè)值被選中的概率是均等的旁赊;第二個(gè)關(guān)鍵字是獨(dú)立,它的意思是椅野,每個(gè)桶號(hào)被選中與否是相互獨(dú)立的终畅,與其他桶號(hào)是否被選中無(wú)關(guān)。這樣一來(lái)竟闪,滿足均勻性與獨(dú)立性能夠保證鍵值對(duì)在散列表的分布盡可能的均勻离福,不會(huì)出現(xiàn)“許多鍵值對(duì)被散列到同一個(gè)桶,而同時(shí)許多桶為空”的情況炼蛤。
顯然术徊,設(shè)計(jì)一個(gè)較好的滿足均勻散列假設(shè)的散列函數(shù)是不容易的,好消息是通常我們無(wú)需設(shè)計(jì)它鲸湃,因?yàn)槲覀兛梢灾苯邮褂靡恍┗诟怕式y(tǒng)計(jì)的高效的實(shí)現(xiàn),比如Java中許多常用的類都重寫了hashCode方法(Object類的hashCode方法默認(rèn)返回對(duì)象的內(nèi)存地址)子寓,用于為該類型對(duì)象返回一個(gè)hashCode暗挑,通常我們用這個(gè)hashCode除以桶數(shù)M的余數(shù)就可以獲取一個(gè)桶號(hào)。下面我們以Java中的一些類為例斜友,來(lái)介紹一下針對(duì)不同數(shù)據(jù)類型的散列函數(shù)的實(shí)現(xiàn)炸裆。

String類的hashCode方法

String類的hashCode方法如下所示:

public int hashCode() { 
  int h = hash; 
  if (h == 0 && value.length > 0) { 
    char val[] = value; 
    for (int i = 0; i < value.length; i++) { 
      h = 31 * h + val[i]; 
    } 
    hash = h; 
  } 
  return h;
}

hashCode方法中的value是一個(gè)char[]數(shù)組,存儲(chǔ)中字符串的的每字符鲜屏。我們可以看到在方法的最開始我們會(huì)把hash賦給h烹看,這個(gè)hash就表示之前計(jì)算的hashCode,這樣以來(lái)若之前已經(jīng)計(jì)算過(guò)這個(gè)字符串對(duì)象的hashCode洛史,這次我們就無(wú)需再計(jì)算了惯殊,直接返回之前計(jì)算過(guò)得即可。這種把hashCode緩存的策略只對(duì)不可變對(duì)象有效也殖,因?yàn)椴豢勺儗?duì)象的hashCode是不會(huì)變的土思。
根據(jù)上面的代碼我們可以知道,若h為null,意味著我們是第一次計(jì)算hashCode己儒,if語(yǔ)句體中就是hashCode的具體計(jì)算方法崎岂。假設(shè)我們的字符串對(duì)象str包含4個(gè)字符,ck表示的是字符串中的第k個(gè)字符(從0開始計(jì)數(shù))闪湾,那么str的hashCode就等于:31 * (31 * (31 * c0 + c1) + c2) +c3冲甘。

數(shù)值類型的hashCode方法

這里我們以Integer和Double為例,介紹一下數(shù)值類型的hashCode方法的一般實(shí)現(xiàn)途样。
Integer類的hashCode方法如下:

public int hashCode() { 
  return Integer.hashCode(value);
}
public static int hashCode(int value) { 
  return value;
}

其中value表示Integer對(duì)象所包裝的整型值江醇,所以Integer類的hashCode方法僅僅是簡(jiǎn)單的返回了自身的值。

我們?cè)賮?lái)看一下Double類的hashCode方法:

@Override
public int hashCode() { 
  return Double.hashCode(value);
}
public static int hashCode(double value) { 
  long bits = doubleToLongBits(value); 
  return (int)(bits ^ (bits >>> 32));
}

我們可以看到Double類的hashCode方法首先會(huì)將它的值轉(zhuǎn)為long類型娘纷,然后返回低32位和高32位的異或的結(jié)果作為hashCode嫁审。

Date類的hashCode方法

前面我們介紹的數(shù)據(jù)類型都可以看做一種數(shù)值型(String可以看做一個(gè)整型數(shù)組),那么對(duì)于非數(shù)值類型對(duì)象的hashCode要怎么計(jì)算呢赖晶,這里我們以Date類為例簡(jiǎn)單的介紹一下律适。Date類的hashCode方法如下:

public int hashCode() { 
  long ht = this.getTime(); 
  return (int) ht ^ (int) (ht >> 32);
}

我們可以看到,它的hashCode方法的實(shí)現(xiàn)非常簡(jiǎn)單遏插,只是返回了Date對(duì)象所封裝的時(shí)間的低32位和高32位的異或結(jié)果捂贿。從Date類的hashCode的實(shí)現(xiàn)我們可以了解到,對(duì)于非數(shù)值類型的hashCode的計(jì)算胳嘲,我們需要選取一些能區(qū)分各個(gè)類實(shí)例的實(shí)例域來(lái)作為計(jì)算的因子厂僧。比如對(duì)于Date類來(lái)說(shuō),通常具有相同的時(shí)間的Date對(duì)象我們認(rèn)為它們相等了牛,因此也就具有相同的hashCode颜屠。這里我們需要說(shuō)明一下,對(duì)于等價(jià)的兩個(gè)對(duì)象(也就是調(diào)用equals方法返回true)鹰祸,它們的hashCode必須相同甫窟,而反之則不然。

由hashCode獲取桶號(hào)

前面我們介紹了計(jì)算對(duì)象hashCode的一些方法蛙婴,那么我們獲取了hashCode之后粗井,如何進(jìn)一步得到桶號(hào)呢?一個(gè)直接的辦法就是直接拿得到的hashCode除以capacity(桶的數(shù)量)街图,然后用所得的余數(shù)作為桶號(hào)浇衬。不過(guò)在Java中,hashCode是int型的餐济,而Java中的int型均為有符號(hào)耘擂,所以我們要是直接使用返回的hashCode的話可能會(huì)得到一個(gè)負(fù)數(shù),顯然桶號(hào)是不能為負(fù)的颤介。所以我們先將返回的hashCode轉(zhuǎn)變?yōu)橐粋€(gè)非負(fù)整數(shù)梳星,再用它除以capacity取余數(shù)赞赖,作為key的對(duì)應(yīng)桶號(hào),具體代碼如下:

private int hash(K key) { return (x.hashCode() & 0x7fffffff) % M;} 

現(xiàn)在我們已經(jīng)知道了如何通過(guò)一個(gè)鍵獲取桶號(hào)冤灾,那么接下來(lái)我們來(lái)介紹使用散列表查找的第二步——處理碰撞前域。

使用拉鏈法處理碰撞

使用不同的碰撞處理方式,我們便得到了散列表的不同實(shí)現(xiàn)韵吨。首先我們要介紹的是使用拉鏈法來(lái)處理碰撞的散列表的實(shí)現(xiàn)匿垄。以這種方式實(shí)現(xiàn)的散列表,每個(gè)桶里都存放了一個(gè)鏈表归粉。初始時(shí)所有鏈表均為空椿疗,當(dāng)一個(gè)鍵被散列到一個(gè)桶時(shí),這個(gè)鍵就成為相應(yīng)桶中鏈表的首結(jié)點(diǎn)糠悼,之后若再有一個(gè)鍵被散列到這個(gè)桶(即發(fā)生碰撞)届榄,第二個(gè)鍵就會(huì)成為鏈表的第二個(gè)結(jié)點(diǎn),以此類推倔喂。這樣一來(lái)铝条,當(dāng)桶數(shù)為M,散列表中存儲(chǔ)的鍵值對(duì)數(shù)目為N時(shí)席噩,平均每個(gè)桶中的鏈表包含的結(jié)點(diǎn)數(shù)為N / M班缰。因此,當(dāng)我們查找一個(gè)鍵時(shí)悼枢,首先通過(guò)散列函數(shù)確定它所在的桶埠忘,這一步所需時(shí)間為O(1);然后我們依次比較桶中結(jié)點(diǎn)的鍵與給定鍵馒索,若相等則找到了指定鍵值對(duì)莹妒,這一步所需時(shí)間為O(N / M)。所以查找操作所需的時(shí)間為O(N / M)绰上,而通常我們都能夠保證N是M的常數(shù)倍动羽,所以散列表的查找操作的時(shí)間復(fù)雜度為O(1),同理我們也可以得到插入操作的復(fù)雜度也為O(1)渔期。

理解了以上的描述,實(shí)現(xiàn)基于拉鏈法的散列表也就很容易了渴邦,這里簡(jiǎn)單起見疯趟,我們直接使用前面的SeqSearchList作為桶中的鏈表,參考代碼如下:

public class ChainingHashMap<K, V> { 
  private int num; //當(dāng)前散列表中的鍵值對(duì)總數(shù) 
  private int capacity; //桶數(shù) 
  private SeqSearchST<K, V>[] st; //鏈表對(duì)象數(shù)組 
  
  public ChainingHashMap(int initialCapacity) { 
    capacity = initialCapacity; 
    st = (SeqSearchST<K, V>[]) new Object[capacity]; 
    for (int i = 0; i < capacity; i++) { 
      st[i] = new SeqSearchST<>(); 
    } 
  } 
  
  private int hash(K key) { 
    return (key.hashCode() & 0x7fffffff) % capacity; 
  } 
  
  public V get(K key) { 
      return st[hash(key)].get(key); 
  }

  public void put(K key, V value) { 
    st[hash(key)].put(key, value); 
  }
} 

在上面的實(shí)現(xiàn)中谋梭,我們固定了散列表的桶數(shù)信峻,當(dāng)我們明確知道我們要插入的鍵值對(duì)數(shù)目最多只能到達(dá)桶數(shù)的常數(shù)倍時(shí),固定桶數(shù)是完全可行的瓮床。但是若鍵值對(duì)數(shù)目會(huì)增長(zhǎng)到遠(yuǎn)遠(yuǎn)大于桶數(shù)盹舞,我們就需要?jiǎng)討B(tài)調(diào)整桶數(shù)的能力产镐。實(shí)際上,散列表中的鍵值對(duì)數(shù)與桶數(shù)的比值叫做負(fù)載因子(load factor)踢步。通常負(fù)載因子越小癣亚,我們進(jìn)行查找所需時(shí)間就越短,而空間的使用就越大获印;若負(fù)載因子較大述雾,則查找時(shí)間會(huì)變長(zhǎng),但是空間使用會(huì)減小兼丰。比如玻孟,Java標(biāo)準(zhǔn)庫(kù)中的HashMap就是基于拉鏈法實(shí)現(xiàn)的散列表,它的默認(rèn)負(fù)載因子為0.75鳍征。HashMap實(shí)現(xiàn)動(dòng)態(tài)調(diào)整桶數(shù)的方式是基于公式loadFactor = maxSize / capacity黍翎,其中maxSize為支持存儲(chǔ)的最大鍵值對(duì)數(shù),而loadFactor和capacity(桶數(shù))都會(huì)在初始化時(shí)由用戶指定或是由系統(tǒng)賦予默認(rèn)值艳丛。當(dāng)HashMap中的鍵值對(duì)的數(shù)目達(dá)到了maxSize時(shí)匣掸,就會(huì)增大散列表中的桶數(shù)。
以上代碼中還用到了SeqSearchST质礼,實(shí)際上這就是一個(gè)基于鏈表的符號(hào)表實(shí)現(xiàn)旺聚,支持向其中添加key-value pair,查找指定鍵時(shí)使用的是順序查找眶蕉,它的代碼如下:

public class SeqSearchST<K, V> { 
  private Node first; 
  
  private class Node { 
    K key; 
    V val; 
    Node next; 
    public Node(K key, V val, Node next) { 
      this.key = key; 
      this.val = val; 
      this.next = next; 
    } 
  } 

  public V get(K key) { 
    for (Node node = first; node != null; node = node.next) { 
      if (key.equals(node.key)) { 
        return node.val; 
      } 
    } 
    return null; 
  } 

  public void put(K key, V val) { 
    //先查找表中是否已存在相應(yīng)key 
    Node node; 
    for (node = first; node != null; node = node.next) { 
      if (key.equals(node.key)) { 
        node.val = val; 
        return; 
      } 
    } 
    //表中不存在相應(yīng)key 
    first = new Node(key, val, first); 
  }
}

使用線性探測(cè)法處理碰撞

基本原理與實(shí)現(xiàn)

線性探測(cè)法是另一種散列表的實(shí)現(xiàn)策略的具體方法砰粹,這種策略叫做開放定址法。開放定址法的主要思想是:用大小為M的數(shù)組保存N個(gè)鍵值對(duì)造挽,其中M > N碱璃,數(shù)組中的空位用于解決碰撞問(wèn)題。

線性探測(cè)法的主要思想是:當(dāng)發(fā)生碰撞時(shí)(一個(gè)鍵被散列到一個(gè)已經(jīng)有鍵值對(duì)的數(shù)組位置)饭入,我們會(huì)檢查數(shù)組的下一個(gè)位置嵌器,這個(gè)過(guò)程被稱作線性探測(cè)。線性探測(cè)可能會(huì)產(chǎn)生三種結(jié)果:

  • 命中:該位置的鍵與要查找的鍵相同谐丢;
  • 未命中:該位置為空爽航;
  • 該位置的鍵和被查找的鍵不同。

當(dāng)我們查找某個(gè)鍵時(shí)乾忱,首先通過(guò)散列函數(shù)得到一個(gè)數(shù)組索引后讥珍,之后我們就開始檢查相應(yīng)位置的鍵是否與給定鍵相同,若不同則繼續(xù)查找(若到數(shù)組末尾也沒(méi)找到就折回?cái)?shù)組開頭)窄瘟,直到找到該鍵或遇到一個(gè)空位置衷佃。由線性探測(cè)的過(guò)程我們可以知道,若數(shù)組已滿的時(shí)候我們?cè)傧蚱渲胁迦胄骆I蹄葱,會(huì)陷入無(wú)限循環(huán)之中氏义。

理解了以上原理锄列,要實(shí)現(xiàn)基于線性探測(cè)法的散列表也就不難了。這里我們使用數(shù)組keys保存散列表中的鍵惯悠,數(shù)組values保存散列表中的值邻邮,兩個(gè)數(shù)組同一位置上的元素共同確定一個(gè)散列表中的鍵值對(duì)。具體代碼如下:

public class LinearProbingHashMap<K, V> { 
  private int num; //散列表中的鍵值對(duì)數(shù)目 
  private int capacity; 
  private K[] keys; 
  private V[] values; 

  public LinearProbingHashMap(int capacity) { 
    keys = (K[]) new Object[capacity]; 
    values = (V[]) new Object[capacity]; 
    this.capacity = capacity; 
  } 

  private int hash(K key) { 
    return (key.hashCode() & 0x7fffffff) % capacity; 
  } 
  
  public V get(K key) { 
    int index = hash(key); 
    while (keys[index] != null && !key.equals(keys[index])) { 
      index = (index + 1) % capacity; 
    } 
    return values[index]; //若給定key在散列表中存在會(huì)返回相應(yīng)value吮螺,否則這里返回的是null 
  }
  
 public void put(K key, V value) { 
    int index = hash(key); 
    while (keys[index] != null && !key.equals(keys[index])) { 
      index = (index + 1) % capacity; 
    } 
    if (keys[index] == null) { 
      keys[index] = key; 
      values[index] = value; return; 
    } 
    values[index] = value; num++; 
  }
}

動(dòng)態(tài)調(diào)整數(shù)組大小

在我們上面的實(shí)現(xiàn)中饶囚,數(shù)組的大小為桶數(shù)的2倍,不支持動(dòng)態(tài)調(diào)整數(shù)組大小鸠补。而在實(shí)際應(yīng)用中萝风,當(dāng)負(fù)載因子(鍵值對(duì)數(shù)與數(shù)組大小的比值)接近1時(shí),查找操作的時(shí)間復(fù)雜度會(huì)接近O(n)紫岩,而當(dāng)負(fù)載因子為1時(shí)规惰,根據(jù)我們上面的實(shí)現(xiàn),while循環(huán)會(huì)變?yōu)橐粋€(gè)無(wú)限循環(huán)泉蝌。顯然我們不想讓查找操作的復(fù)雜度退化至O(n)歇万,更不想陷入無(wú)限循環(huán)。所以有必要實(shí)現(xiàn)動(dòng)態(tài)增長(zhǎng)數(shù)組來(lái)保持查找操作的常數(shù)時(shí)間復(fù)雜度勋陪。當(dāng)鍵值對(duì)總數(shù)很小時(shí)贪磺,若空間比較緊張,可以動(dòng)態(tài)縮小數(shù)組诅愚,這取決于實(shí)際情況寒锚。

要實(shí)現(xiàn)動(dòng)態(tài)改變數(shù)組大小,只需要在上面的put方法最開始加上一個(gè)如下的判斷:

if (num == capacity / 2) { 
  resize(2 * capacity); 
}

resize方法的邏輯也很簡(jiǎn)單:

private void resize(int newCapacity) { 
  LinearProbingHashMap<K, V> hashmap = new LinearProbingHashMap<>(newCapacity); 
  for (int i = 0; i < capacity; i++) { 
    if (keys[i] != null) { 
      hashmap.put(keys[i], values[i]); 
    } 
  } 
  keys = hashmap.keys; 
  values = hashmap.values; 
  capacity = hashmap.capacity; 
}

關(guān)于負(fù)載因子與查找操作的性能的關(guān)系违孝,這里貼出《算法》(Sedgewick等)中的一個(gè)結(jié)論:

在一張大小為M并含有N = a*M(a為負(fù)載因子)個(gè)鍵的基于線性探測(cè)的散列表中刹前,若散列函數(shù)滿足均勻散列假設(shè),命中和未命中的查找所需的探測(cè)次數(shù)分別為:~ 1/2 * (1 + 1/(1-a))和~1/2*(1 + 1/(1-a)^2)

關(guān)于以上結(jié)論雌桑,我們只需要知道當(dāng)a約為1/2時(shí)喇喉,查找命中和未命中所需的探測(cè)次數(shù)分別為1.5次和2.5次。還有一點(diǎn)就是當(dāng)a趨近于1時(shí)校坑,以上結(jié)論中的估計(jì)值的精度會(huì)下降拣技,不過(guò)我們?cè)趯?shí)際應(yīng)用中不會(huì)讓負(fù)載因子接近1,為了保持良好的性能耍目,在上面的實(shí)現(xiàn)中我們應(yīng)保持a不超過(guò)1/2过咬。

參考資料

《算法(第四版)》(Sedgewick等)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市制妄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泵三,老刑警劉巖耕捞,帶你破解...
    沈念sama閱讀 222,590評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衔掸,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡俺抽,警方通過(guò)查閱死者的電腦和手機(jī)敞映,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)磷斧,“玉大人振愿,你說(shuō)我怎么就攤上這事〕诜梗” “怎么了冕末?”我有些...
    開封第一講書人閱讀 169,301評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)侣颂。 經(jīng)常有香客問(wèn)我档桃,道長(zhǎng),這世上最難降的妖魔是什么憔晒? 我笑而不...
    開封第一講書人閱讀 60,078評(píng)論 1 300
  • 正文 為了忘掉前任藻肄,我火速辦了婚禮,結(jié)果婚禮上拒担,老公的妹妹穿的比我還像新娘嘹屯。我一直安慰自己,他們只是感情好从撼,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評(píng)論 6 398
  • 文/花漫 我一把揭開白布州弟。 她就那樣靜靜地躺著,像睡著了一般谋逻。 火紅的嫁衣襯著肌膚如雪呆馁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,682評(píng)論 1 312
  • 那天毁兆,我揣著相機(jī)與錄音浙滤,去河邊找鬼。 笑死气堕,一個(gè)胖子當(dāng)著我的面吹牛纺腊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茎芭,決...
    沈念sama閱讀 41,155評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼揖膜,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了梅桩?” 一聲冷哼從身側(cè)響起壹粟,我...
    開封第一講書人閱讀 40,098評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后趁仙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體洪添,經(jīng)...
    沈念sama閱讀 46,638評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評(píng)論 3 342
  • 正文 我和宋清朗相戀三年雀费,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了干奢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,852評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡盏袄,死狀恐怖忿峻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辕羽,我是刑警寧澤逛尚,帶...
    沈念sama閱讀 36,520評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站逛漫,受9級(jí)特大地震影響黑低,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜酌毡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評(píng)論 3 335
  • 文/蒙蒙 一克握、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧枷踏,春花似錦菩暗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至掏熬,卻和暖如春佑稠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背旗芬。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工舌胶, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疮丛。 一個(gè)月前我還...
    沈念sama閱讀 49,279評(píng)論 3 379
  • 正文 我出身青樓幔嫂,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親誊薄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子履恩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評(píng)論 2 361

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