《数据结构与算法之美》笔记009

43. | 拓扑排序:如何确定代码源文件的编译依赖关系 - 开始

  1. 编译器如何通过源文件两两之间的局部依赖关系,确定一个全局的编译顺序:
    使用 '图' 这种数据结构的 '拓扑排序算法' 解决.
  2. 什么是拓扑排序
  • 多个元素,部分或全部元素的两两依赖关系已经确定,如何安排一个序列,能够满足上述元素的两两依赖关系.
  • 很多时候,拓扑排序的序列不是唯一的,因为可能不是所有元素之间都具有依赖关系.


    c26d0f472d9a607c0c4eb688c01959bd[1].jpg
  1. 拓扑排序的原理很简单,重要的是拓扑排序的实现.
  2. 如何确定代码源文件的编译依赖关系:
  • 把源文件和源文件之间的依赖关系,抽象成1个有向图;
  • 每个源文件对应图中的一个顶点;
  • 源文件之间的依赖关系就是顶点之间的边;
  • 如果A先于B执行,或者说B依赖于A,就在A和B之间,构建一条从A指向B的的边;
  • 注意这个图必须是一个有向无环图. 因为图中一旦成环,拓扑排序就无法工作;
  • 拓扑排序本身就是基于有向无环图的一个算法.
public class Graph {
  //定义顶点的个数
  private int v;
  //每个顶点对应一个邻接表, 多个顶点构邻接表数组
  private LinkeList<Integer>[] adj;
  //构造函数
  public Graph(int v){
    this.v = v;
    adj = new LinkedList<Integer>[v];
    for(int i=0; i<v; i++){
      adj[i] = new LinkedList<Integer>();
    }
  }
  
  //s优先于t / t依赖于s
  //从顶点s出发,构建一条指向t的边
  public void addEdge(int s, int t){
    adj[s].add(t);
  }
}
  1. 如何在Graph这个有向无环图上,实现拓扑排序,有2种算法: Kahn算法 和 DFS深度优先搜索算法.
  2. 看Kahn算法 和 DFS深度优先搜索算法 之前,首先回顾下之前的'图'相关知识.

43前置知识: 图

  • 图是一种非线性表数据结构.
  • 图中的每个元素叫做顶点.
  • 图中的1个顶点可以与其他任意顶点进行连接,连接叫做边.
  • 有向图:
    每条边都是有方向的.A->B:从A到B. B->A:从B到A.
  • 无向图:
    每条边都是没有方向的.单纯代表A-B两个顶点建立连接.
  • 每个顶点有多少条边,叫做顶点的度.
  • 有向图中,每个顶点有 入度 和 出度.
    • 入度:有多少条边指向该顶点;
    • 出度:从该顶点发出多少条边;


  1. 带权图
  • 每条边都有1个权重/weight.


    带权图
  1. 图的存储方法: 邻接矩阵 和 邻接表.
  2. 使用 邻接矩阵 存储图
  • 邻接矩阵底层是1个二维数组.
  • 对于有向图: 顶点i向顶点j发出一条边 , 则 A[i][j] = 1;
  • 对于无向图: 顶点i和顶点j之间有边,则 A[i][j] 和 A[j][i] 都是1;
  • 对于带权图: 数组元素存储连接的2个顶点之间的权重;


    使用邻接矩阵存储图

    练一下markdown中怎么画矩阵.

$$\begin{matrix}
A & B & C\\
D & E & F\\
G & H & I\\
\end{matrix}$$

无向图:
\begin{matrix} 0 & 1 & 1 & 0\\ 1 & 0 & 1 & 1\\ 1 & 1 & 0 & 1\\ 0 & 1 & 1 & 0\\ \end{matrix}
有向图:
\begin{matrix} 0 & 1 & 0 & 0\\ 0 & 0 & 1 & 0\\ 1 & 1 & 0 & 0\\ 0 & 1 & 1 & 0\\ \end{matrix}
带权图:
\begin{matrix} 0 & 5 & 3 & 0\\ 5 & 0 & 2 & 6\\ 3 & 2 & 0 & 1\\ 0 & 6 & 1 & 0\\ \end{matrix}

  • 邻接矩阵的缺点: 浪费存储空间.
    • 对于无向图,顶点i,j之间有边,需要A[i][j]和A[j][i]都存储为1,实际只存1个就够了.浪费了一半空间;
    • 对于稀疏图.即顶点很多,但每个顶点的边不多,使用邻接矩阵就更加浪费存储空间.
      • 比如微信十亿用户,但每个用户最多2000个好友.使用邻接矩阵会浪费绝大部分存储空间.
  • 邻接矩阵的有点:
    • 邻接矩阵基于数组,获取2个顶点的关系非常快;
    • 临街矩阵存储图可以将很多图的运算,转换为矩阵之间的运算,效率很高.
  1. 使用 邻接表 存储图


    使用邻接表存储图
  • 邻接表存储图很像散列表.
  • 每个顶点对应1条链表,存储与该顶点相连的其他顶点.
  • 邻接表存储图 和 邻接矩阵存储图 比较:
    • 相对于邻接数组节省空间,尤其是稀疏图场景.只有相连的顶点的边才会存储在对应顶点的链表中.
    • 是用时间换空间,比如查询顶点A和顶点B是否相连,就要在A顶点对应的链表中遍历查询.
    • 链表的存储对缓存不友好,所以链表中查询两个顶点之间的关系效率低.
    • 可以将邻接表中的链表替换为支持快速查找的数据结构: 红黑树、跳表、有序动态数组,散列表
  1. 使用图,存储微博的用户之间关系
    针对微博用户关系,假设我们需要支持下面这样几个操作:判断用户 A 是否关注了用户 B;判断用户 A 是否是用户 B 的粉丝;用户 A 关注用户 B;用户 A 取消关注用户 B;根据用户名称的首字母排序,分页获取用户的粉丝列表;根据用户名称的首字母排序,分页获取用户的关注列表。
public class Graph {
  //微博系统中用户量
  private int v;
  //BestType用于指代特定的数据结构,用于替换链表,提升链表的查询效率
  //followers用于存储每个用户关注的人
  private BestType<Integer>[] followers;
  //fans用户存储每个用户被哪些人关注
  private BestType<Integer>[] fans;
  public Graph(int v){
    this.v = v;
    followers = new BestType<Integer>[v];
    fans= new BestType<Integer>[v];
    for(int i=0; i<v; i++){
      followers[i] = new BestType<Integer>();
      fans[i] = new BestType<Integer>();
    }
  }
  //用户a关注用户b
  public void follow(User a, User b){
    Integer aIndex = hash(a);
    Integer bIndex = hash(b);
    Integer aId = a.userId;
    Integer bId = b.userId;
    //将用户b的id添加到用户a对应的 关注的人 中
    followers[aIndex].add(bId);
    //将用户a的id添加到用户b对应的 粉丝 中
    fans[bIndex].add(aId);
  }
  //用户a不再关注用户b
  public void unfollow(User a,User b){
    Integer aIndex = hash(a);
    Integer bIndex = hash(b);
    Integer aId = a.userId;
    Integer bId = b.userId;
    //将用户b的id从用户a对应的 关注的人 中删除
    followers[aIndex].remove(bId);
    //将用户a的id从用户b对应的 粉丝 中删除
    fans[bIndex].remove(aId);
  }
  
  //BestType具体选择哪一种支持快速查询的数据结构: 跳表 .
  //跳表插入、删除、查找都非常高效,时间复杂度是 O(logn),空间复杂度上稍高,是 O(n)。
  //最重要的一点,跳表中存储的数据本来就是有序的了,分页获取粉丝列表或关注列表,就非常高效
}
  1. 深度优先搜索算法 和 广度优先搜索算法 , 都是基于 '图' 这种数据结构.
  2. 广度优先搜索/BFS/Breadth-First-Search
public class Graph { // 无向图
  private int v; // 顶点的个数
  private LinkedList<Integer> adj[]; // 邻接表

  public Graph(int v) {
    this.v = v;
    adj = new LinkedList[v];
    for (int i=0; i<v; ++i) {
      adj[i] = new LinkedList<>();
    }
  }

  public void addEdge(int s, int t) { // 无向图一条边存两次
    adj[s].add(t);
    adj[t].add(s);
  }
}
  • 广度优先搜索,就是先查找离起始顶点最近的,然后是次近的,一层层往外搜索.
  • 从顶点s出发,搜索一条从s到t的最短路径.
private void bfs(int s, int t){
  if(s == t){
    //同一顶点不用继续搜索
    return;
  }
  //记录指定顶点是否访问过
  boolean[] visited = new boolean[v];
  visited[s] = true;
  //保存每个访问过的顶点的前一顶点
  int[] prev = new int[v];
  for(int i=0;i<v;i++){
    prev[i] = -1;
  }
  //保存待比较其后续顶点的顶点
  Queue<Integer> queue = new LinkedList<Integer>();
  queue.add(s);
  while(queue.size() > 0){
    //获取当前顶点
    int curr = queue.poll();
    //遍历比较当前顶点的所有子顶点
    for(int i = 0; i<adj[curr].size(); i++){
      int c = adj[curr].get(i);
      if(!visited[c]){
        //标记当前子顶点 已被访问过
        visited[c] = true;
        //标记当前子顶点 的上一结点就是curr
        prev[c] = curr;
        //如果当前子顶点 就是 要寻找的最终顶点
        if(c == t){
          //打印整个搜索路径, 终止循环
          printRouter(prev, s, t);
          return;
        }
        queue.add(c);
      }
    }
  }
}
private void printRouter(int[] prev, int start, int target){
  if(prev[target] != -1 && target != start){
    //继续打印 start 到 target之前1个元素的路径
    printRouter(prev, start, prev[target]);
  }
  //打印当前元素
  System.out.println("当前元素:" + target);
}
  • 广度优先搜索算法的 时间复杂度,空间复杂度:
    • 时间复杂度是O(V+E)
      • V表示顶点的个数,E表示边的个数
      • 对于1个连通图.即一个图中所有的顶点都是连通的图,E肯定大于V
      • 所以广度优先搜索算法的时间复杂度简写为O(E)
    • 空间复杂度的O(V)
      • visited 数组,prev 数组,queue队列,这三个存储空间大小都不会超过顶点个数V.
      • 所以其空间复杂度是O(V).
  1. 深度优先搜索 / DFS / Depth-First-Search
  • 深度优先搜索算法,本质上就是回溯算法
  • 每一步,都要n种走法,每一种走法都走一遍
  • 回溯算法的套路
1: 整体问题最终值 存放在回溯方法体外;
2: 回溯方法中有判断终止条件 及 整体问题最终值修改逻辑;
3: 回溯方法中:
每一步都有n种走法;
都走一遍;

套路:
int bestValue = -1;
private void recur(int param, int bound){
  if(shouldReturn(param, bound)){
    if(shouldModifBest(param,bound)){
      bestValue = **;
    }
    return;
  }
  //n种走法
  recur(p1,bound);
  ***
  recur(pn,bound);
}

调用方式:
int bestValue = -1;
recur(p1.bound);
sys: 最优解: + bestValue.
  • 使用深度优先搜索算法寻找顶点s到顶点t的路径
private boolean found = false;
private void recurDfs(int curr, int target, boolean[] visited, int[] prev){
  if(found){
    //之前已经找到路径,避免重复执行
    //终止条件
    return;
  }
  visited[curr] = true;
  if(curr == target){
    //终止条件
    //修改方法体外的变量
    found = true;
    return;
  }
  //获取n条可能走的路径
  for(int i=0; i<adj[curr].size(); i++){
    int c = adj[curr].get(i);
    if(!visited[c]){
      //之前没有遍历过该子顶点
      prev[c] = curr;
      //每一条可以走的路径,都走一次
      recurDfs(c, target, visited,prev);
    }
  }
}
private testDFS(int s, int t){
  found = false;
  boolean[] visited = new boolean[v];
  int[] prev = new int[v];
  for(int i=0;i<v;i+){
    prev[i] = -1;
  }
  //回溯算法
  recurDfs(s, t, visited, prev)
  //打印路径
  printRouter(prev, s, t);
}
  • 深度优先搜索算法/回溯算法 的时间复杂度 和 空间复杂度
    • 遍历 + 回溯, 每条边最多访问2次: parent -> n个child; 每个child -> 包括parent在内的n个顶点.
    • 时间复杂度是O(E). E是边数量
    • 空间复杂度是O(V). 因为额外的visited ,prev 数量不超过V.

43. | 拓扑排序:如何确定代码源文件的编译依赖关系 - 继续

public class Graph{
  private int v;
  private LinkedList<Integer>[] adj;
  public Graph(int v){
    this.v = v;
    this.adj = new LinkedList<Integer>[v];
    for(int i=0;i<v;i++){
      adj[i] = new LinkedList<Integer>();
    }
  }
  //添加从顶点s到顶点t的一条边
  //因为是依赖关系,所以是有向图
  public void addEdge(int s, int t){
    adj[s].add(t);
  }
}

如何在有向无环图上,实现拓扑排序.
拓扑排序有2种实现方法: Kahn算法 及 DFS深度优先搜索算法.

  1. Kahn算法
  • Kahn算法实际用的是贪心算法思想
  • 顶点的入度为0,说明该顶点不依赖任何其他顶点,可以立即执行.
  • Kahn实现步骤:
    • 定义1个数组,统计图中所有顶点的入度,存储于该数组
    • 定义1个队列,存储图中所有入度为0的顶点
    • 从队列头部不断取入度为0的顶点,执行/打印.
      • 将该顶点对应的所有子顶点的入度-1
      • 若某个子顶点的入度变为0,则将该子顶点也加入队列
  • Kahn代码实现
public void topoSortByKahn(){
  int[] inDegree = new int[v];
  for(int i = 0; i<v; i++){
    for(int j=0;j<adj[i].size();j++){
      int curr = adj[i].get(j);
      inDegree[curr]++;
    }
  }
  Queue<Integer> q = new LinkedList<Integer>();
  for(int i=0;i<v;i++){
    if(inDegree[i] == 0){
      q.add(i);
    }
  }
  int curr;
  int child;
  while(!q.isEmpty()){
    curr = q.poll();
    System.out.println("当前编译:" + curr);
    for(int i=0;i<adj[curr].size();i++){
      //遍历当前顶点的所有子顶点
      child = adj[curr].get(i);
      //每个子顶点的入度-1
      inDegree[child ]--;
      //若子顶点入度变为0
      if(inDegree[child] == 0){
        q.add(child);
      }
    }
  }
}
  1. DFS算法
  • DFS算法本质上是回溯算法
  • 原始数据是 顶点->多个子顶点
  • 要答应整个依赖关系,需要知道每个顶点都依赖哪些父顶点, 根据原始的 curr->children 构建逆邻接表 curr->parents
  • 每次打印1个顶点,都要先保证其 parents 都打印完毕,再打印自身.
  • DFS代码实现
public void dfsDeps(){
  boolean[] visited = new boolean[v];
  LinkedList<Integer>[] parents = new LinkedList<Integer>[v];
  for(int i =0;i<v;i++){
    parents[i] = new LinkedList<Integer>();
  }
  int child;
  for(int i=0;i<adj.length;i++){
    for(int j=0;j<adj[i].size();j++){
      child = adj[i].get(j);
      parents[child].add(i);
    }
  }
  //遍历parents,将每个顶点都打印一遍
  for(int i=0;i<v;i++){
    if(!visited[i]){
      printCurr(i,visited,parents);
    }
  }
}
private void printCurr(int curr,boolean[] visited,LinkedList<Integer>[] parents){
  if(visited[curr]){
    return;
  }
  //标记当前顶点已访问过
  visited[curr] = true;
  int parent;
  //获取当前顶点所有父顶点
  for(int i =0; i<parents[curr].size(); i++){
    parent = parents[curr].get(i);
    //打印父顶点
    printCurr(parent,visited,parents);
  }
  //所有父顶点打印完毕,再打印自身
  sysout: 当前编译项为: + curr;
}
  1. kahn算法的时间复杂度O(E+V)/每个顶点被访问1次,每条边也被访问1次, Kahn的空间复杂度O(V);
  2. DFS算法的空间复杂度是O(V),时间复杂度是O(E+V)/每个顶点被访问2次,每条边被访问1次.
  • 每个顶点被访问2次: curr -> parents; parent -> parents;
  1. 拓扑排序的应用场景: 凡是需要通过局部顺序来推导全局顺序的,一般都能用拓扑排序来解决.
  2. 检测图中是否存在环:
    6.1: Kahn算法,如果最终打印的顶点个数数量少于顶点总量,说明图中成环,部分顶点始终无法达到入度为0
    6.2: 如果是A推荐B,B推荐C,C推荐D,D推荐E,E又推荐A这样的环状结构,如何检测.
    只要从A开始向下递推,每个访问过的人都记录下来,如果成环,同1个人会被访问2次.
    A->B->C->D->E->A.
    只要记录中出现1个人被访问2次,说明成环了.
private HaseSet<Integer> visited = new HashSet<Integer>();
private int findFinalRef(int userId){
  if(visited.contains(userId)){
    //说明同一用户被访问第2次,说明出现了环状结构
    return;
  }
  int refId = gainCurrUserRef(userId);
  if(refId == -1){
    //说明当前userId没有下一个推荐人,则返回自身
    return userId;
  }
  return findFinalRef(refId);
}

44. | 最短路径:地图软件是如何计算出最优出行路径的

  1. Dijkstra算法相关文章
  1. 计算地图上两个地点的最优路径,可以将地图抽象成'图'这种结构.每个地点是图的1个顶点.两个地点之间的路是一条边,单向路就是A->B,双向路就是 A->B + B->A.
    路的长度,就是每条边的权重.
    地图就被抽象成1个有向有权图.
  2. 地图中两个地点的最短路径,变成 有向有权图中2个顶点间的最短距离.
  3. 将地图抽象成图的代码
public class Graph{
  //邻接表
  private LinkedList<Edge>[] adj;
  //顶点个数
  private int v;
  public Graph(int v){
    this.v = v;
    this.adj = new LinkedList<Edge>[v];
    for(int i =0;i<v;i++){
      adj[i] = new LinkedList<Edget>();
    }
  }
  //添加一条边. 从s到t.
  public void addEdge(int s,int t, int w){
    this.adj[s].add(new Edge(s, t, w));
  }
  private class Edge{
    public int sid; //边的起始顶点编号
    public int tid; //边的终止顶点编号
    public int w; //边的权重
    public Edge(int sid, int tid, int w){
      this.sid = sid;
      this.tid = tid;
      this.w = w;
    }
  }
  //这个类专门为dijkstra算法实现的
  private class Vertex{
    //顶点id
    public int id;
    //从起始顶点 到这个顶点的 最短距离
    public int dist;
    public Vertex(int id, int dist){
      this.id = id;
      this.dist = dist;
    }
  }
}

一个顶点到另一个顶点的最短路径算法,最著名的是Dijkstra算法.

  1. Dijkstra算法实现
//Java提供的优先级队列,没有提供更新单个元素的接口,所以需要自行实现1个
private class PriorityQueue{
  //需要根据Vertex.dist构建小顶堆
  private Vertex[] nodes;
  private int count;
  private PriorityQueue(int v){
    this.nodes = new Vertex[v + 1];
    this.count = v;
  }
  //待实现
  public Vertex poll(){}
  //待实现
  public void add(Vertex vertex){
  }
  //待实现: 更新结点的值,并且从下往上堆化, 使之重新符合小顶堆的定义
  public void update(Vertex vertex){
  }
  //待实现
  public boolean isEmpty(){
  }
}

//dijkstra算法实现
public void dijkstra(int s,int t){
  int[] pre = new int[v];
  Vertex[] vertexes = new Vertex[v];
  for(int i=0;i<v;i++){
    vertexes[i] = new Vertex(i,Integer.MAX_VALUE);
  }
  PriorityQueue queue = new PriorityQueue(v);
  boolean[] inqueue = new boolean[v];
  queue.add(vertexes[s]);
  inqueue[s] = true;
  while(!queue.isEmpty()){
    Vertex minVertex = queue.poll();
    if(minVertex.id == t){
      break;
    }
    for(int i=0;i<adj[minVertex.id].size();i++){
      Edge e = adj[minVertex.id].get(i);
      Vertex nextVertex = vertexes[e.tid];
      if(minVertex.dist + e.w < nextVertex.dist){
        nextVertex.dist = minVertex.dist + e.w;
        pre[nextVertex.id] = minVertex.id;
        if(inqueue[nextVertex.id]){
          queue.update(nextVertex);
        }else{
          queue.add(nextVertex);
          inqueue[nextVertex.id] = true;
        }
      }
    }
  }
  System.out.println("从s到t最短路径长度:" + vertexes[t].dist);
  //依次答应最短路径长度对应的每个顶点
  print(s, t, pre);
}
private void print(int s, int t, int[] pre){
  if(s == t){
    System.out.println("当前元素:" + t);
    return;
  }
  print(s, pre[t], pre);
  //打印t
  System.out.println("当前元素:" + t);
}
  1. Dijkstra的时间复杂度
  • while循环次数: 最多是V次, V代表顶点的个数.
  • while循环内,每次for循环次数不同,V次总体for循环次数 <= E. E是图中边的数量.
  • 优先级队列是1个小顶堆,每次poll,add或update,时间复杂度是O(logV)
  • 整个Dijkstra的时间复杂度是 O(E*logV)
  1. 地图软件用的更多的是类似 A* 的启发式搜索算法,不过也是在 Dijkstra 算法上的优化.

45 | 位图:如何实现网页爬虫中的URL去重功能?

前置知识

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