JS实现广度优先搜索与深度优先搜索

图在数学及技术上的概念:
一个图G = (V, E),由以下元素组成。
 V: 一组顶点
 E: 一组边,连接V中的顶点

  在着手实现算法之前,让我们先了解一下图的一些术语。

  由一条边连接在一起的顶点称为相邻顶点。比如,A和B是相邻的,A和D是相邻的,A和C是相邻的,A和E不是相邻的。

   一个顶点的度是其相邻顶点的数量。比如,A和其他三个顶点相连接,因此,A的度为3; E和其他两个顶点相连,因此,E的度为2。

  路径是顶点v1, v2, ..., vk的一个连续序列,其中vi和vi+1是相邻的。以上一示意图中的图为例,其中包含路径A B E I和A C D G。

  简单路径要求不包含重复的顶点。举个例子,A D G是一条简单路径。除去最后一个顶点(因为它和第一个顶点是同一个顶点),环也是一个简单路径,比如A D C A(最后一个顶点重新回到A)。

  如果图中不存在环,则称该图是无环的。如果图中每两个顶点间都存在路径,则该图是联通的。

  我们可以使用图来解决计算机科学世界中的很多问题,比如搜索图中的一个特定顶点或搜索一条特定边,寻找图中的一条路径(从一个顶点到另一个顶点),寻找两个顶点之间的最短路径, 以及环检测。

图的表示

邻接矩阵


邻接表


关联矩阵


  我们用JS来实现图:

function Dictionary() {
    let items = {}
    this.set = function (key, value) {
        items[key] = value
    }
    this.has = function (key) {
        return key in items
    }
    this.remove = function (key) {
        if (this.has(key)) {
            delete items[key]
            return true
        }
        return false
    }
    this.get = function (key) {
        return this.has(key) ? items[key] : undefined
    }
    this.values = function () {
        let values = []
        for (let k in items) {
            if (this.has(k)) {
                values.push(items[k])
            }
        }
        return values
    }
    this.clear = function () {
        items = []
    }
    this.size = function () {
        return Object.keys(items).length
    }
    this.keys = function () {
        return Object.keys(items)
    }
    this.getItems = function () {
        return items
    }
}

function Graph() {

    let vertices = [] // 存储图中所有顶点的名字
    let adjList = new Dictionary() // 字典储存邻接表,使用顶点的名字作为键,邻接顶点列表作为值

    // 向图中添加一个新的顶点
    this.addVertex = function (v) {
        vertices.push(v)
        adjList.set(v, [])
    }

    // 添加顶点之间的边
    this.addEdge = function (v, w) {
        adjList.get(v).push(w)
        adjList.get(w).push(v)
    }

    this.toString = function() {
        let s = ''
        for (let i = 0; i < vertices.length; i++){
            s += vertices[i] + ' -> '
            let neighbors = adjList.get(vertices[i])
            for (let j = 0; j < neighbors.length; j++){
                s += neighbors[j] + ' '
            }
            s += '\n'
        }
        return s;
    };
}

let graph = new Graph()
let myVertices = ['A','B','C','D','E','F','G','H','I']
for (let i = 0; i < myVertices.length; i++){
    graph.addVertex(myVertices[i])
}

graph.addEdge('A', 'B')
graph.addEdge('A', 'C')
graph.addEdge('A', 'D')
graph.addEdge('C', 'D')
graph.addEdge('C', 'G')
graph.addEdge('D', 'G')
graph.addEdge('D', 'H')
graph.addEdge('B', 'E')
graph.addEdge('B', 'F')
graph.addEdge('E', 'I')

console.log(graph.toString())
// A -> B C D
// B -> A E F
// C -> A D G
// D -> A C G H
// E -> B I
// F -> B
// G -> C D
// H -> D
// I -> E 

图的遍历
  和树数据结构类似,我们可以访问图的所有节点。有两种算法可以对图进行遍历:广度优先搜索(Breadth-First Search,BFS)和深度优先搜索(Depth-First Search,DFS)。
  图遍历可以用来寻找特定的顶点或寻找两个顶点之间的路径,检查图是否连通,检查图是否含有环等。
  图遍历算法的思想是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。对于两种图遍历算法,都需要明确指出第一个被访问的顶点。
  完全探索一个顶点要求我们查看该顶点的每一条边。对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中。
  为了保证算法的效率,务必访问每个顶点至多两次。连通图中每条边和顶点都会被访问到。
  广度优先搜索算法和深度优先搜索算法基本上是相同的,只有一点不同,那就是待访问顶点列表的数据结构。

广度优先搜索
  广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访问图的一层。换句话说,就是先宽后深地访问顶点,如下图所示:

当要标注已经访问过的顶点时,我们用三种颜色来反映它们的状态。
 白色: 表示该顶点还没有被访问。
 灰色: 表示该顶点被访问过,但并未被探索过。
 黑色: 表示该顶点被访问过且被完全探索过。
这就是之前提到的务必访问每个顶点最多两次的原因。

function Queue() {
    let items = []
    this.enqueue = function (element) {
        items.push(element)
    }
    this.dequeue = function () {
        return items.shift()
    }
    this.front = function () {
        return items[0]
    }
    this.isEmpty = function () {
        return items.length == 0
    }
    this.clear = function () {
        items = []
    }
    this.size = function () {
        return items.length
    }
    this.print = function () {
        console.log(items.toString())
    }
}

function Dictionary() {
    let items = {}
    this.set = function (key, value) {
        items[key] = value
    }
    this.has = function (key) {
        return key in items
    }
    this.remove = function (key) {
        if (this.has(key)) {
            delete items[key]
            return true
        }
        return false
    }
    this.get = function (key) {
        return this.has(key) ? items[key] : undefined
    }
    this.values = function () {
        let values = []
        for (let k in items) {
            if (this.has(k)) {
                values.push(items[k])
            }
        }
        return values
    }
    this.clear = function () {
        items = []
    }
    this.size = function () {
        return Object.keys(items).length
    }
    this.keys = function () {
        return Object.keys(items)
    }
    this.getItems = function () {
        return items
    }
}

function Graph() {

    let vertices = [] // 存储图中所有顶点的名字
    let adjList = new Dictionary() // 字典储存邻接表,使用顶点的名字作为键,邻接顶点列表作为值

    // 向图中添加一个新的顶点
    this.addVertex = function (v) {
        vertices.push(v)
        adjList.set(v, [])
    }

    // 添加顶点之间的边
    this.addEdge = function (v, w) {
        adjList.get(v).push(w)
        adjList.get(w).push(v)
    }

    // 在控制台输出图
    this.toString = function(){
        let s = ''
        for (let i = 0; i < vertices.length; i++){
            s += vertices[i] + ' -> '
            let neighbors = adjList.get(vertices[i])
            for (let j = 0; j < neighbors.length; j++){
                s += neighbors[j] + ' '
            }
            s += '\n'
        }
        return s;
    }

    // 广度优先搜索算法
    let initializeColor = function () {
        let color = []
        for (let i = 0; i < vertices.length; i++) {
            color[vertices[i]] = 'white' // 顶点还没有被访问过
        }
        return color
    }
    this.bfs = function (v, callback) {
        let color = initializeColor(),
            queue = new Queue()
        queue.enqueue(v)
        while (!queue.isEmpty()) {
            let u = queue.dequeue(),
                neighbors = adjList.get(u)
            color[u] = 'grey'
            for (let i = 0; i < neighbors.length; i++) {
                let w = neighbors[i]
                if (color[w] === 'white') {
                    color[w] = 'grey'
                    queue.enqueue(w)
                }
            }
            color[u] = 'black'
            if (callback) {
                callback(u)
            }
        }
    }

}
function printNode (value) {
    console.log('Visited vertex: ' + value)
}


let graph = new Graph()
let myVertices = ['A','B','C','D','E','F','G','H','I']
for (let i = 0; i < myVertices.length; i++){
    graph.addVertex(myVertices[i])
}

graph.addEdge('A', 'B')
graph.addEdge('A', 'C')
graph.addEdge('A', 'D')
graph.addEdge('C', 'D')
graph.addEdge('C', 'G')
graph.addEdge('D', 'G')
graph.addEdge('D', 'H')
graph.addEdge('B', 'E')
graph.addEdge('B', 'F')
graph.addEdge('E', 'I')

graph.bfs(myVertices[0], printNode)
// Visited vertex: A
// Visited vertex: B
// Visited vertex: C
// Visited vertex: D
// Visited vertex: E
// Visited vertex: F
// Visited vertex: G
// Visited vertex: H
// Visited vertex: I

使用BFS寻找最短路径
  给定一个图G和源顶点v,找出对每个顶点u,u和v之间最短路径的距离(以边的数量计)。
  对于给定顶点v,广度优先算法会访问所有与其距离为1的顶点,接着是距离为2的顶点, 以此类推。所以,可以用广度优先算法来解这个问题。
 从v到u的距离d[u];
 前溯点pred[u],用来推导出从v到其他每个顶点u的最短路径。

// 改进过的广度优先搜索算法
    this.BFS = function (v) {
        let color = initializeColor(),
            queue = new Queue(),
            d = [], // v到u的距离
            pred = [] // 前溯点p
        queue.enqueue(v)

        for (let i = 0; i < vertices.length; i++) {
            d[vertices[i]] = 0
            pred[vertices[i]] = null
        }

        while (!queue.isEmpty()) {
            let u = queue.dequeue(),
                neighbors = adjList.get(u)
            color[u] = 'grey'
            for (let i = 0; i < neighbors.length; i++) {
                let w = neighbors[i]
                if (color[w] === 'white') {
                    color[w] = 'grey'
                    d[w] = d[u] + 1
                    pred[w] = u
                    queue.enqueue(w)
                }
            }
            color[u] = 'black'
        }

        return {
            distances: d,
            predecessors: pred
        }
    }

  通过前溯点数组,我们可以用下面这段代码来构建从顶点A到其他顶点的路径:

// 对predecessors进行追溯直到A点,然后展示回溯路径
let fromVertex = myVertices[0]
for (let i = 1; i < myVertices.length; i++){
    let toVertex = myVertices[i],
        path = new Stack()
    for (let v = toVertex; v !== fromVertex; v = shortestPathA.predecessors[v]) {
        path.push(v)
    }
    path.push(fromVertex)
    let s = path.pop()
    while (!path.isEmpty()) {
        s += ' - ' + path.pop()
    }
    console.log(s)
}
// A - B
// A - C
// A - D
// A - B - E
// A - B - F
// A - C - G
// A - D - H
// A - B - E - I

深度优先搜索
  深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。

  深度优先搜索算法不需要一个源顶点。在深度优先搜索算法中,若图中顶点v未访问,则访问该顶点v。

要访问顶点v,照如下步骤做。
(1) 标注v为被发现的(灰色)。
(2) 对于v的所有未访问的邻点w,访问顶点w,标注v为已被探索的(黑色)。

 // 深度优先算法
    let dfsVisit = function(u, color, callback){
        color[u] = 'grey'
        if (callback) {
            callback(u)
        }
        let neighbors = adjList.get(u);
        for (let i = 0; i < neighbors.length; i++){
            let w = neighbors[i]
            if (color[w] === 'white'){
                dfsVisit(w, color, callback)
            }
        }
        color[u] = 'black'
    }
    this.dfs = function(callback){
        let color = initializeColor()
        for (let i = 0; i < vertices.length; i++){
            if (color[vertices[i]] === 'white'){
                dfsVisit(vertices[i], color, callback)
            }
        }
    }
console.log(graph.dfs(printNode))
// Visited vertex: A
// Visited vertex: B
// Visited vertex: E
// Visited vertex: I
// Visited vertex: F
// Visited vertex: C
// Visited vertex: D
// Visited vertex: G
// Visited vertex: H

完整代码:

function Stack() {
    let items = []
    this.push = function (element) {  // 添加一个(或几个)新元素到栈顶
        items.push(element)
    }
    this.pop = function (element) { // 移除栈顶的元素,同时返回被移除的元素
        return items.pop()
    }
    this.peek = function (element) { // 返回栈顶的元素,不对栈做任何修改
        return items[items.length - 1]
    }
    this.isEmpty = function (element) {// 如果栈里没有任何元素就返回true
        return items.length === 0
    }
    this.size = function () { // 返回栈里的元素个数
        return items.length
    }
    this.clear = function () { // 移除栈里的所有元素
        items = []
    }
    this.print = function () { // 字符串化
        console.log(items.toString())
    }
}

function Queue() {
    let items = []
    this.enqueue = function (element) {
        items.push(element)
    }
    this.dequeue = function () {
        return items.shift()
    }
    this.front = function () {
        return items[0]
    }
    this.isEmpty = function () {
        return items.length == 0
    }
    this.clear = function () {
        items = []
    }
    this.size = function () {
        return items.length
    }
    this.print = function () {
        console.log(items.toString())
    }
}

function Dictionary() {
    let items = {}
    this.set = function (key, value) {
        items[key] = value
    }
    this.has = function (key) {
        return key in items
    }
    this.remove = function (key) {
        if (this.has(key)) {
            delete items[key]
            return true
        }
        return false
    }
    this.get = function (key) {
        return this.has(key) ? items[key] : undefined
    }
    this.values = function () {
        let values = []
        for (let k in items) {
            if (this.has(k)) {
                values.push(items[k])
            }
        }
        return values
    }
    this.clear = function () {
        items = []
    }
    this.size = function () {
        return Object.keys(items).length
    }
    this.keys = function () {
        return Object.keys(items)
    }
    this.getItems = function () {
        return items
    }
}

function Graph() {

    let vertices = [] // 存储图中所有顶点的名字
    let adjList = new Dictionary() // 字典储存邻接表,使用顶点的名字作为键,邻接顶点列表作为值

    // 向图中添加一个新的顶点
    this.addVertex = function (v) {
        vertices.push(v)
        adjList.set(v, [])
    }

    // 添加顶点之间的边
    this.addEdge = function (v, w) {
        adjList.get(v).push(w)
        adjList.get(w).push(v)
    }

    // 在控制台输出图
    this.toString = function(){
        let s = ''
        for (let i = 0; i < vertices.length; i++){
            s += vertices[i] + ' -> '
            let neighbors = adjList.get(vertices[i])
            for (let j = 0; j < neighbors.length; j++){
                s += neighbors[j] + ' '
            }
            s += '\n'
        }
        return s;
    }

    // 广度优先搜索算法
    let initializeColor = function () {
        let color = []
        for (let i = 0; i < vertices.length; i++) {
            color[vertices[i]] = 'white' // 顶点还没有被访问过
        }
        return color
    }
    this.bfs = function (v, callback) {
        let color = initializeColor(),
            queue = new Queue()
        queue.enqueue(v)
        while (!queue.isEmpty()) {
            let u = queue.dequeue(),
                neighbors = adjList.get(u)
            color[u] = 'grey'
            for (let i = 0; i < neighbors.length; i++) {
                let w = neighbors[i]
                if (color[w] === 'white') {
                    color[w] = 'grey'
                    queue.enqueue(w)
                }
            }
            color[u] = 'black'
            if (callback) {
                callback(u)
            }
        }
    }

    // 改进过的广度优先搜索算法
    this.BFS = function (v) {
        let color = initializeColor(),
            queue = new Queue(),
            d = [], // v到u的距离
            pred = [] // 前溯点p
        queue.enqueue(v)

        for (let i = 0; i < vertices.length; i++) {
            d[vertices[i]] = 0
            pred[vertices[i]] = null
        }

        while (!queue.isEmpty()) {
            let u = queue.dequeue(),
                neighbors = adjList.get(u)
            color[u] = 'grey'
            for (let i = 0; i < neighbors.length; i++) {
                let w = neighbors[i]
                if (color[w] === 'white') {
                    color[w] = 'grey'
                    d[w] = d[u] + 1
                    pred[w] = u
                    queue.enqueue(w)
                }
            }
            color[u] = 'black'
        }

        return {
            distances: d,
            predecessors: pred
        }
    }

    // 深度优先算法
    let dfsVisit = function(u, color, callback){
        color[u] = 'grey'
        if (callback) {
            callback(u)
        }
        let neighbors = adjList.get(u);
        for (let i = 0; i < neighbors.length; i++){
            let w = neighbors[i]
            if (color[w] === 'white'){
                dfsVisit(w, color, callback)
            }
        }
        color[u] = 'black'
    }
    this.dfs = function(callback){
        let color = initializeColor()
        for (let i = 0; i < vertices.length; i++){
            if (color[vertices[i]] === 'white'){
                dfsVisit(vertices[i], color, callback)
            }
        }
    }

    // 探索深度优先算法
    let time = 0;
    this.DFS = function(){
        let color = initializeColor(),
            d = [], // 顶点u的发现时间
            f = [], // 当顶点u被标注为黑色时,u的完成探索时间
            p = [] // 顶点u的前溯点
        time = 0;
        for (let i = 0; i < vertices.length; i++){
            f[vertices[i]] = 0;
            d[vertices[i]] = 0;
            p[vertices[i]] = null;
        }
        for (let i = 0; i < vertices.length; i++){
            if (color[vertices[i]] === 'white'){
                DFSVisit(vertices[i], color, d, f, p);
            }
        }
        return {
            discovery: d,
            finished: f,
            predecessors: p
        }
    }
    let DFSVisit = function(u, color, d, f, p){
        console.log('discovered ' + u)
        color[u] = 'grey'
        d[u] = ++time
        let neighbors = adjList.get(u)
        for (let i = 0; i < neighbors.length; i++){
            let w = neighbors[i]
            if (color[w] === 'white'){
                p[w] = u
                DFSVisit(w,color, d, f, p)
            }
        }
        color[u] = 'black'
        f[u] = ++time
        console.log('explored ' + u)
    }
}

function printNode (value) {
    console.log('Visited vertex: ' + value)
}

let graph = new Graph()
let myVertices = ['A','B','C','D','E','F','G','H','I']
for (let i = 0; i < myVertices.length; i++){
    graph.addVertex(myVertices[i])
}

graph.addEdge('A', 'B')
graph.addEdge('A', 'C')
graph.addEdge('A', 'D')
graph.addEdge('C', 'D')
graph.addEdge('C', 'G')
graph.addEdge('D', 'G')
graph.addEdge('D', 'H')
graph.addEdge('B', 'E')
graph.addEdge('B', 'F')
graph.addEdge('E', 'I')

graph.bfs(myVertices[0], printNode)
// Visited vertex: A
// Visited vertex: B
// Visited vertex: C
// Visited vertex: D
// Visited vertex: E
// Visited vertex: F
// Visited vertex: G
// Visited vertex: H
// Visited vertex: I

console.log(graph.BFS(myVertices[0]))
// { distances: [ A: 0, B: 1, C: 1, D: 1, E: 2, F: 2, G: 2, H: 2, I: 3 ],
//     predecessors:
//         [ A: null,
//     B: 'A',
//     C: 'A',
//     D: 'A',
//     E: 'B',
//     F: 'B',
//     G: 'C',
//     H: 'D',
//     I: 'E' ] }

// 对predecessors进行追溯直到A点,然后展示回溯路径
let shortestPathA = graph.BFS(myVertices[0])
let fromVertex = myVertices[0]
for (let i = 1; i < myVertices.length; i++){
    let toVertex = myVertices[i],
        path = new Stack()
    for (let v = toVertex; v !== fromVertex; v = shortestPathA.predecessors[v]) {
        path.push(v)
    }
    path.push(fromVertex)
    let s = path.pop()
    while (!path.isEmpty()) {
        s += ' - ' + path.pop()
    }
    console.log(s)
}
// A - B
// A - C
// A - D
// A - B - E
// A - B - F
// A - C - G
// A - D - H
// A - B - E - I


console.log(graph.dfs(printNode))
// Visited vertex: A
// Visited vertex: B
// Visited vertex: E
// Visited vertex: I
// Visited vertex: F
// Visited vertex: C
// Visited vertex: D
// Visited vertex: G
// Visited vertex: H

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

推荐阅读更多精彩内容