MySQL左右值无限分类预排序遍历树算法

引言

大多数用户都曾在数据库中处理过分层数据(hierarchical data),认为分层数据的管理不是关系数据库的目的。之所以这么认为,是因为关系数据库中的表没有层次关系,只是简单的平面化的列表;而分层数据具有父-子关系,显然关系数据库中的表不能自然地表现出其分层的特性。

我们认为,分层数据是每项只有一个父项和零个或多个子项(根项除外,根项没有父项)的数据集合。分层数据存在于许多基于数据库的应用程序中,包括论坛和邮件列表中的分类、商业组织图表、内容管理系统的分类、产品分类。我们打算使用下面一个虚构的电子商店的产品分类:


这些分类层次与上面提到的一些例子中的分类层次是相类似的。在本文中我们将从传统的邻接表(adjacency list)模型出发,阐述2种在MySQL中处理分层数据的模型。

邻接表模型

上述例子的分类数据将被存储在下面的数据表中(我给出了全部的数据表创建、数据插入的代码,你可以跟着做):

CREATETABLEcategory( category_idINTAUTO_INCREMENT PRIMARYKEY,nameVARCHAR(20)NOTNULL,parentINTDEFAULTNULL);INSERTINTOcategoryVALUES(1,'ELECTRONICS',NULL),(2,'TELEVISIONS',1),(3,'TUBE',2), (4,'LCD',2),(5,'PLASMA',2),(6,'PORTABLE ELECTRONICS',1), (7,'MP3 PLAYERS',6),(8,'FLASH',7), (9,'CD PLAYERS',6),(10,'2 WAY RADIOS',6);SELECT*FROMcategoryORDERBYcategory_id; +-------------+----------------------+--------+ | category_id | name | parent | +-------------+----------------------+--------+ | 1 | ELECTRONICS | NULL | | 2 | TELEVISIONS | 1 | | 3 | TUBE | 2 | | 4 | LCD | 2 | | 5 | PLASMA | 2 | | 6 | PORTABLE ELECTRONICS | 1 | | 7 | MP3 PLAYERS | 6 | | 8 | FLASH | 7 | | 9 | CD PLAYERS | 6 | | 10 | 2 WAY RADIOS | 6 | +-------------+----------------------+--------+ 10 rows in set (0.00 sec)

在邻接表模型中,数据表中的每项包含了指向其父项的指示器。在此例中,最上层项的父项为空值(NULL)。邻接表模型的优势在于它很简单,可以很容易地看出FLASH是MP3 PLAYERS的子项,哪个是portable electronics的子项,哪个是electronics的子项。虽然,在客户端编码中邻接表模型处理起来也相当的简单,但是如果是纯SQL编码的话,该模型会有很多问题。

检索整树

通常在处理分层数据时首要的任务是,以某种缩进形式来呈现一棵完整的树。为此,在纯SQL编码中通常的做法是使用自连接(self-join):

SELECTt1.nameASlev1, t2.nameaslev2, t3.nameaslev3, t4.nameaslev4FROMcategoryASt1LEFTJOINcategoryASt2ONt2.parent = t1.category_idLEFTJOINcategoryASt3ONt3.parent = t2.category_idLEFTJOINcategoryASt4ONt4.parent = t3.category_idWHEREt1.name ='ELECTRONICS'; +-------------+----------------------+--------------+-------+ | lev1 | lev2 | lev3 | lev4 | +-------------+----------------------+--------------+-------+ | ELECTRONICS | TELEVISIONS | TUBE | NULL | | ELECTRONICS | TELEVISIONS | LCD | NULL | | ELECTRONICS | TELEVISIONS | PLASMA | NULL | | ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH | | ELECTRONICS | PORTABLE ELECTRONICS | CD PLAYERS | NULL | | ELECTRONICS | PORTABLE ELECTRONICS | 2 WAY RADIOS | NULL | +-------------+----------------------+--------------+-------+ 6 rows in set (0.00 sec)

检索所有叶子节点

我们可以用左连接(LEFT JOIN)来检索出树中所有叶子节点(没有孩子节点的节点):

SELECTt1.nameFROMcategoryASt1LEFTJOINcategoryast2ONt1.category_id = t2.parentWHEREt2.category_idISNULL; +--------------+ | name | +--------------+ | TUBE | | LCD | | PLASMA | | FLASH | | CD PLAYERS | | 2 WAY RADIOS | +--------------+

检索单一路径

通过自连接,我们也可以检索出单一路径:

SELECTt1.nameASlev1, t2.nameaslev2, t3.nameaslev3, t4.nameaslev4FROMcategoryASt1LEFTJOINcategoryASt2ONt2.parent = t1.category_idLEFTJOINcategoryASt3ONt3.parent = t2.category_idLEFTJOINcategoryASt4ONt4.parent = t3.category_idWHEREt1.name ='ELECTRONICS'ANDt4.name ='FLASH'; +-------------+----------------------+-------------+-------+ | lev1 | lev2 | lev3 | lev4 | +-------------+----------------------+-------------+-------+ | ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH | +-------------+----------------------+-------------+-------+ 1 row in set (0.01 sec)

这种方法的主要局限是你需要为每层数据添加一个自连接,随着层次的增加,自连接变得越来越复杂,检索的性能自然而然的也就下降了。

邻接表模型的局限性

用纯SQL编码实现邻接表模型有一定的难度。在我们检索某分类的路径之前,我们需要知道该分类所在的层次。另外,我们在删除节点的时候要特别小心,因为潜在的可能会孤立一棵子树(当删除portable electronics分类时,所有他的子分类都成了孤儿)。部分局限性可以通过使用客户端代码或者存储过程来解决,我们可以从树的底部开始向上迭代来获得一颗树或者单一路径,我们也可以在删除节点的时候使其子节点指向一个新的父节点,来防止孤立子树的产生。

嵌套集合(Nested Set)模型

我想在这篇文章中重点阐述一种不同的方法,俗称为嵌套集合模型。在嵌套集合模型中,我们将以一种新的方式来看待我们的分层数据,不再是线与点了,而是嵌套容器。我试着以嵌套容器的方式画出了electronics分类图:


从上图可以看出我们依旧保持了数据的层次,父分类包围了其子分类。在数据表中,我们通过使用表示节点的嵌套关系的左值(left value)和右值(right value)来表现嵌套集合模型中数据的分层特性:

CREATE TABLE nested_category ( category_id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(20) NOT NULL, lft INT NOT NULL, rgt INT NOT NULL ); INSERT INTO nested_category VALUES(1,'ELECTRONICS',1,20),(2,'TELEVISIONS',2,9),(3,'TUBE',3,4), (4,'LCD',5,6),(5,'PLASMA',7,8),(6,'PORTABLE ELECTRONICS',10,19), (7,'MP3 PLAYERS',11,14),(8,'FLASH',12,13), (9,'CD PLAYERS',15,16),(10,'2 WAY RADIOS',17,18); SELECT * FROM nested_category ORDER BY category_id; +-------------+----------------------+-----+-----+| category_id |name| lft |rgt| +-------------+----------------------+-----+-----+ |1| ELECTRONICS |1| 20 || 2 |TELEVISIONS| 2 |9| |3| TUBE |3| 4 || 4 |LCD| 5 |6| |5| PLASMA |7| 8 || 6 |PORTABLE ELECTRONICS| 10 |19| |7| MP3 PLAYERS |11| 14 || 8 |FLASH| 12 |13| |9| CD PLAYERS |15| 16 || 10 |2WAY RADIOS| 17 |18| +-------------+----------------------+-----+-----+

我们使用了lftrgt来代替left和right,是因为在MySQL中left和right是保留字。http://dev.mysql.com/doc/mysql/en/reserved-words.html,有一份详细的MySQL保留字清单。

那么,我们怎样决定左值和右值呢?我们从外层节点的最左侧开始,从左到右编号:


这样的编号方式也同样适用于典型的树状结构:


当我们为树状的结构编号时,我们从左到右,一次一层,为节点赋右值前先从左到右遍历其子节点给其子节点赋左右值。这种方法被称作改进的先序遍历算法

检索整树

我们可以通过自连接把父节点连接到子节点上来检索整树,是因为子节点的lft值总是在其父节点的lft值和rgt值之间:

SELECTnode.nameFROMnested_categoryASnode, nested_categoryASparentWHEREnode.lftBETWEENparent.lftANDparent.rgtANDparent.name ='ELECTRONICS'ORDERBYnode.lft; +----------------------+ | name | +----------------------+ | ELECTRONICS | | TELEVISIONS | | TUBE | | LCD | | PLASMA | | PORTABLE ELECTRONICS | | MP3 PLAYERS | | FLASH | | CD PLAYERS | | 2 WAY RADIOS | +----------------------+

不像先前邻接表模型的例子,这个查询语句不管树的层次有多深都能很好的工作。在BETWEEN的子句中我们没有去关心node的rgt值,是因为使用node的rgt值得出的父节点总是和使用lft值得出的是相同的。

检索所有叶子节点

检索出所有的叶子节点,使用嵌套集合模型的方法比邻接表模型的LEFT JOIN方法简单多了。如果你仔细得看了nested_category表,你可能已经注意到叶子节点的左右值是连续的。要检索出叶子节点,我们只要查找满足rgt=lft+1的节点:

SELECT name FROM nested_category WHERE rgt = lft +1; +--------------+| name |+--------------+| TUBE || LCD || PLASMA || FLASH || CD PLAYERS || 2 WAY RADIOS |+--------------+

检索单一路径

在嵌套集合模型中,我们可以不用多个自连接就可以检索出单一路径:

SELECTparent.nameFROMnested_categoryASnode, nested_categoryASparentWHEREnode.lftBETWEENparent.lftANDparent.rgtANDnode.name ='FLASH'ORDERBYparent.lft; +----------------------+ | name | +----------------------+ | ELECTRONICS | | PORTABLE ELECTRONICS | | MP3 PLAYERS | | FLASH | +----------------------+

检索节点的深度

我们已经知道怎样去呈现一棵整树,但是为了更好的标识出节点在树中所处层次,我们怎样才能检索出节点在树中的深度呢?我们可以在先前的查询语句上增加COUNT函数和GROUP BY子句来实现:

SELECTnode.name, (COUNT(parent.name) -1)ASdepthFROMnested_categoryASnode, nested_categoryASparentWHEREnode.lftBETWEENparent.lftANDparent.rgtGROUPBYnode.nameORDERBYnode.lft; +----------------------+-------+ | name | depth | +----------------------+-------+ | ELECTRONICS | 0 | | TELEVISIONS | 1 | | TUBE | 2 | | LCD | 2 | | PLASMA | 2 | | PORTABLE ELECTRONICS | 1 | | MP3 PLAYERS | 2 | | FLASH | 3 | | CD PLAYERS | 2 | | 2 WAY RADIOS | 2 | +----------------------+-------+

我们可以根据depth值来缩进分类名字,使用CONCAT和REPEAT字符串函数:

SELECTCONCAT(REPEAT(' ',COUNT(parent.name) -1), node.name)ASnameFROMnested_categoryASnode, nested_categoryASparentWHEREnode.lftBETWEENparent.lftANDparent.rgtGROUPBYnode.nameORDERBYnode.lft; +-----------------------+ | name | +-----------------------+ | ELECTRONICS | | TELEVISIONS | | TUBE | | LCD | | PLASMA | | PORTABLE ELECTRONICS | | MP3 PLAYERS | | FLASH | | CD PLAYERS | | 2 WAY RADIOS | +-----------------------+

当然,在客户端应用程序中你可能会用depth值来直接展示数据的层次。Web开发者会遍历该树,随着depth值的增加和减少来添加

    • 标签。

      检索子树的深度

      当我们需要子树的深度信息时,我们不能限制自连接中的node或parent,因为这么做会打乱数据集的顺序。因此,我们添加了第三个自连接作为子查询,来得出子树新起点的深度值:

      SELECTnode.name, (COUNT(parent.name) - (sub_tree.depth +1))ASdepthFROMnested_categoryASnode, nested_categoryASparent, nested_categoryASsub_parent, (SELECTnode.name, (COUNT(parent.name) -1)ASdepthFROMnested_categoryASnode, nested_categoryASparentWHEREnode.lftBETWEENparent.lftANDparent.rgtANDnode.name ='PORTABLE ELECTRONICS'GROUPBYnode.nameORDERBYnode.lft )ASsub_treeWHEREnode.lftBETWEENparent.lftANDparent.rgtANDnode.lftBETWEENsub_parent.lftANDsub_parent.rgtANDsub_parent.name = sub_tree.nameGROUPBYnode.nameORDERBYnode.lft; +----------------------+-------+ | name | depth | +----------------------+-------+ | PORTABLE ELECTRONICS | 0 | | MP3 PLAYERS | 1 | | FLASH | 2 | | CD PLAYERS | 1 | | 2 WAY RADIOS | 1 | +----------------------+-------+

      这个查询语句可以检索出任一节点子树的深度值,包括根节点。这里的深度值跟你指定的节点有关。

      检索节点的直接子节点

      可以想象一下,你在零售网站上呈现电子产品的分类。当用户点击分类后,你将要呈现该分类下的产品,同时也需列出该分类下的直接子分类,而不是该分类下的全部分类。为此,我们只呈现该节点及其直接子节点,不再呈现更深层次的节点。例如,当呈现PORTABLEELECTRONICS分类时,我们同时只呈现MP3 PLAYERS、CD PLAYERS和2 WAY RADIOS分类,而不呈现FLASH分类。

      要实现它非常的简单,在先前的查询语句上添加HAVING子句:

      SELECTnode.name, (COUNT(parent.name) - (sub_tree.depth +1))ASdepthFROMnested_categoryASnode, nested_categoryASparent, nested_categoryASsub_parent, (SELECTnode.name, (COUNT(parent.name) -1)ASdepthFROMnested_categoryASnode, nested_categoryASparentWHEREnode.lftBETWEENparent.lftANDparent.rgtANDnode.name ='PORTABLE ELECTRONICS'GROUPBYnode.nameORDERBYnode.lft )ASsub_treeWHEREnode.lftBETWEENparent.lftANDparent.rgtANDnode.lftBETWEENsub_parent.lftANDsub_parent.rgtANDsub_parent.name = sub_tree.nameGROUPBYnode.nameHAVINGdepth<=1ORDERBYnode.lft; +----------------------+-------+ | name | depth | +----------------------+-------+ | PORTABLE ELECTRONICS | 0 | | MP3 PLAYERS | 1 | | CD PLAYERS | 1 | | 2 WAY RADIOS | 1 | +----------------------+-------+

      如果你不希望呈现父节点,你可以更改HAVING depth <= 1HAVING depth = 1

      嵌套集合模型中集合函数的应用

      让我们添加一个产品表,我们可以使用它来示例集合函数的应用:

      CREATETABLEproduct( product_idINTAUTO_INCREMENT PRIMARYKEY,nameVARCHAR(40), category_idINTNOTNULL);INSERTINTOproduct(name, category_id)VALUES('20" TV',3),('36" TV',3), ('Super-LCD 42"',4),('Ultra-Plasma 62"',5),('Value Plasma 38"',5), ('Power-MP3 5gb',7),('Super-Player 1gb',8),('Porta CD',9),('CD To go!',9), ('Family Talk 360',10);SELECT*FROMproduct; +------------+-------------------+-------------+ | product_id | name | category_id | +------------+-------------------+-------------+ | 1 | 20" TV | 3 | | 2 | 36" TV | 3 | | 3 | Super-LCD 42" | 4 | | 4 | Ultra-Plasma 62" | 5 | | 5 | Value Plasma 38" | 5 | | 6 | Power-MP3 128mb | 7 | | 7 | Super-Shuffle 1gb | 8 | | 8 | Porta CD | 9 | | 9 | CD To go! | 9 | | 10 | Family Talk 360 | 10 | +------------+-------------------+-------------+

      现在,让我们写一个查询语句,在检索分类树的同时,计算出各分类下的产品数量:

      SELECT parent.name, COUNT(product.name) FROM nested_category AS node , nested_category AS parent, product WHERE node.lft BETWEEN parent.lft AND parent.rgt AND node.category_id = product.category_id GROUP BY parent.name ORDER BY node.lft; +----------------------+---------------------+| name |COUNT(product.name)| +----------------------+---------------------+ |ELECTRONICS| 10 || TELEVISIONS |5| |TUBE| 2 || LCD |1| |PLASMA| 2 || PORTABLE ELECTRONICS |5| |MP3 PLAYERS| 2 || FLASH |1| |CD PLAYERS| 2 || 2 WAY RADIOS |1| +----------------------+---------------------+

      这条查询语句在检索整树的查询语句上增加了COUNT和GROUP BY子句,同时在WHERE子句中引用了product表和一个自连接。

      新增节点

      到现在,我们已经知道了如何去查询我们的树,是时候去关注一下如何增加一个新节点来更新我们的树了。让我们再一次观察一下我们的嵌套集合图:


      当我们想要在TELEVISIONS和PORTABLE ELECTRONICS节点之间新增一个节点,新节点的lft和rgt 的 值为10和11,所有该节点的右边节点的lft和rgt值都将加2,之后我们再添加新节点并赋相应的lft和rgt值。在MySQL 5中可以使用存储过程来完成,我假设当前大部分读者使用的是MySQL 4.1版本,因为这是最新的稳定版本。所以,我使用了锁表(LOCK TABLES)语句来隔离查询:

      LOCKTABLEnested_category WRITE;SELECT@myRight := rgtFROMnested_categoryWHEREname='TELEVISIONS';UPDATEnested_categorySETrgt = rgt +2WHERErgt > @myRight;UPDATEnested_categorySETlft = lft +2WHERElft > @myRight;INSERTINTOnested_category(name, lft, rgt)VALUES('GAME CONSOLES', @myRight +1, @myRight +2);UNLOCKTABLES; 我们可以检验一下新节点插入的正确性:SELECTCONCAT(REPEAT(' ', (COUNT(parent.name) -1) ), node.name)ASnameFROMnested_categoryASnode, nested_categoryASparentWHEREnode.lftBETWEENparent.lftANDparent.rgtGROUPBYnode.nameORDERBYnode.lft; +-----------------------+ | name | +-----------------------+ | ELECTRONICS | | TELEVISIONS | | TUBE | | LCD | | PLASMA | | GAME CONSOLES | | PORTABLE ELECTRONICS | | MP3 PLAYERS | | FLASH | | CD PLAYERS | | 2 WAY RADIOS | +-----------------------+

      如果我们想要在叶子节点下增加节点,我们得稍微修改一下查询语句。让我们在2 WAYRADIOS叶子节点下添加FRS节点吧:

      LOCKTABLEnested_category WRITE;SELECT@myLeft := lftFROMnested_categoryWHEREname='2 WAY RADIOS';UPDATEnested_categorySETrgt = rgt +2WHERErgt > @myLeft;UPDATEnested_categorySETlft = lft +2WHERElft > @myLeft;INSERTINTOnested_category(name, lft, rgt)VALUES('FRS', @myLeft +1, @myLeft +2);UNLOCKTABLES;

      在这个例子中,我们扩大了新产生的父节点(2 WAY RADIOS节点)的右值及其所有它的右边节点的左右值,之后置新增节点于新父节点之下。正如你所看到的,我们新增的节点已经完全融入了嵌套集合中:

      SELECTCONCAT(REPEAT(' ', (COUNT(parent.name) -1) ), node.name)ASnameFROMnested_categoryASnode, nested_categoryASparentWHEREnode.lftBETWEENparent.lftANDparent.rgtGROUPBYnode.nameORDERBYnode.lft; +-----------------------+ | name | +-----------------------+ | ELECTRONICS | | TELEVISIONS | | TUBE | | LCD | | PLASMA | | GAME CONSOLES | | PORTABLE ELECTRONICS | | MP3 PLAYERS | | FLASH | | CD PLAYERS | | 2 WAY RADIOS | | FRS | +-----------------------+

      删除节点

      最后还有个基础任务,删除节点。删除节点的处理过程跟节点在分层数据中所处的位置有关,删除一个叶子节点比删除一个子节点要简单得多,因为删除子节点的时候,我们需要去处理孤立节点。

      删除一个叶子节点的过程正好是新增一个叶子节点的逆过程,我们在删除节点的同时该节点右边所有节点的左右值和该父节点的右值都会减去该节点的宽度值:

      LOCKTABLEnested_category WRITE;SELECT@myLeft := lft, @myRight := rgt, @myWidth := rgt - lft +1FROMnested_categoryWHEREname='GAME CONSOLES';DELETEFROMnested_categoryWHERElftBETWEEN@myLeftAND@myRight;UPDATEnested_categorySETrgt = rgt - @myWidthWHERErgt > @myRight;UPDATEnested_categorySETlft = lft - @myWidthWHERElft > @myRight;UNLOCKTABLES;

      我们再一次检验一下节点已经成功删除,而且没有打乱数据的层次:

      SELECTCONCAT(REPEAT(' ', (COUNT(parent.name) -1) ), node.name)ASnameFROMnested_categoryASnode, nested_categoryASparentWHEREnode.lftBETWEENparent.lftANDparent.rgtGROUPBYnode.nameORDERBYnode.lft; +-----------------------+ | name | +-----------------------+ | ELECTRONICS | | TELEVISIONS | | TUBE | | LCD | | PLASMA | | PORTABLE ELECTRONICS | | MP3 PLAYERS | | FLASH | | CD PLAYERS | | 2 WAY RADIOS | | FRS | +-----------------------+

      这个方法可以完美地删除节点及其子节点:

      LOCKTABLEnested_category WRITE;SELECT@myLeft := lft, @myRight := rgt, @myWidth := rgt - lft +1FROMnested_categoryWHEREname='MP3 PLAYERS';DELETEFROMnested_categoryWHERElftBETWEEN@myLeftAND@myRight;UPDATEnested_categorySETrgt = rgt - @myWidthWHERErgt > @myRight;UPDATEnested_categorySETlft = lft - @myWidthWHERElft > @myRight;UNLOCKTABLES;

      再次验证我们已经成功的删除了一棵子树:

      SELECTCONCAT(REPEAT(' ', (COUNT(parent.name) -1) ), node.name)ASnameFROMnested_categoryASnode, nested_categoryASparentWHEREnode.lftBETWEENparent.lftANDparent.rgtGROUPBYnode.nameORDERBYnode.lft; +-----------------------+ | name | +-----------------------+ | ELECTRONICS | | TELEVISIONS | | TUBE | | LCD | | PLASMA | | PORTABLE ELECTRONICS | | CD PLAYERS | | 2 WAY RADIOS | | FRS | +-----------------------+

      有时,我们只删除该节点,而不删除该节点的子节点。在一些情况下,你希望改变其名字为占位符,直到替代名字的出现,比如你开除了一个主管(需要更换主管)。在另外一些情况下,你希望子节点挂到该删除节点的父节点下:

      LOCKTABLEnested_category WRITE;SELECT@myLeft := lft, @myRight := rgt, @myWidth := rgt - lft +1FROMnested_categoryWHEREname='PORTABLE ELECTRONICS';DELETEFROMnested_categoryWHERElft = @myLeft;UPDATEnested_categorySETrgt = rgt -1, lft = lft -1WHERElftBETWEEN@myLeftAND@myRight;UPDATEnested_categorySETrgt = rgt -2WHERErgt > @myRight;UPDATEnested_categorySETlft = lft -2WHERElft > @myRight;UNLOCKTABLES;

      在这个例子中,我们对该节点所有右边节点的左右值都减去了2(因为不考虑其子节点,该节点的宽度为2),对该节点的子节点的左右值都减去了1(弥补由于失去父节点的左值造成的裂缝)。我们再一次确认,那些节点是否都晋升了:

      SELECTCONCAT(REPEAT(' ', (COUNT(parent.name) -1) ), node.name)ASnameFROMnested_categoryASnode, nested_categoryASparentWHEREnode.lftBETWEENparent.lftANDparent.rgtGROUPBYnode.nameORDERBYnode.lft; +---------------+ | name | +---------------+ | ELECTRONICS | | TELEVISIONS | | TUBE | | LCD | | PLASMA | | CD PLAYERS | | 2 WAY RADIOS | | FRS | +---------------+

      有时,当删除节点的时候,把该节点的一个子节点挂载到该节点的父节点下,而其他节点挂到该节点父节点的兄弟节点下,考虑到篇幅这种情况不在这里解说了。

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

      推荐阅读更多精彩内容

      • 表结构 新增:通过我们刚才新增数据得到这个结构的操作,我们发现新增分两种情况。第一种如下图所示:1:变更所有受影响...
        李小贱AA阅读 555评论 0 2
      • 单例模式 适用场景:可能会在场景中使用到对象,但只有一个实例,加载时并不主动创建,需要时才创建 最常见的单例模式,...
        Obeing阅读 2,058评论 1 10
      • 前几天在项目开发中遇到了前辈们所设计的结构(用来实现商品分类),所设计的结构便是利用了预排序遍历树算法。故特...
        AduGEN阅读 4,867评论 6 13
      • 有个大家都很熟悉的成语叫“眼见为实”,小时候学习这个成语的时候,我是带着权威崇拜的心态学习的。因为它是成语词典上的...
        根号四等于二阅读 3,977评论 7 51
      • 向导是个老驴,他深知无人区的艰苦,清楚人在这样极端的条件下会是怎样的反应。自私也好,贪婪也罢,在我看来不过是本能的...
        alexischi阅读 246评论 4 2