链表之单链表原理和常用方法

链表是离散存储线性结构,物理地址上不要求连续。

  • 链表优点
    物理地址不需要连续,插入删除元素比较快
  • 链表缺点
    查询速度慢
    现在用java来实现一个简单的链表,其中涉及到的算法有:添加,删除,查找,获取结点个数,查找倒数第k个结点,两个链表的公共结点。
  1. 链表结构

单向链表中主要由结点来体现的,而结点只包含两个域,数据域data+指向下一个结点的引用。


链表.png

如下所示:

/*********************************************************************结点定义*********************************************************/
    //构成链表的结点(此处是单链表,每个结点都保存了指向下一个结点的引用)
    class Node{
        public int data;
        public Node next;
        
        public Node(int data,Node next){
            this.data = data;
            this.next = next;
        }
    }

而链表只需要知道头结点即可:

package com.wang.suan;
/**
 * 链表
 * @author wxe
 * @since 0.0.1
 */
public class LinkedList {
    
    private Node head;//头结点
/*********************************************************************结点定义*********************************************************/
    //构成链表的结点(此处是单链表,每个结点都保存了指向下一个结点的引用)
    class Node{
        public int data;
        public Node next;
        
        public Node(int data,Node next){
            this.data = data;
            this.next = next;
        }
    }
}

链表中只定义了一个头结点,主要是为了方便其他比如遍历,插入等方法。

  1. 插入结点

  • 插入到尾结点
    这里我们先来介绍一下,插入到尾结点的算法。如果要插入到尾结点,那么势必需要先通过遍历链表获取到尾结点,时间复杂度为O(n)。比如我们要将value为data0数据插入到尾结点中:
    图片.png

    接下来我们先遍历找到尾结点,current当前指定的结点就是尾结点:
    图片.png

    执行插入时,新的结点就成了尾结点,其next指向null:
    图片.png

    执行插入操作,遍历到的尾结点的next执行新节点即可:
    图片.png

    算法如下:
    /**
     * 插入结点(插入到尾结点)
     * @param value
     * @return
     */
    public void insertTail(int value){
        Node current = head;
        Node newNode = new Node(value, null);
        if (head == null) {
            //1.首次插入,为头结点
            head = newNode;
        } else {
            //2.非首次插入,那么必须找到尾结点
            while (current.next != null) {
                current = current.next;
            }
            current.next = newNode;//将新节点添加到尾结点
        }
    }
  • 插入到头结点
    有时候链表为了节省时间,就插在头结点处。由于不用遍历整个链表寻找尾结点,所以时间复杂度为O(n),比如我们要将value为data0数据插入到头结点中。
    图片.png

    执行插入操作,分两步如下所示:
    图片.png

    图片.png

    整理一下就是:
    图片.png

    算法如下:
/**
     * 插入头结点,时间复杂度为O(1)
     * @param value
     */
    public void insertHead(int value){
        Node newNode = new Node(value, null);
        if (head != null) {
            newNode.next = head.next;
        }
        
        head = newNode;
    }
  • 插入指定位置
    插入指定位置,那么我们首先要找到这个位置上的结点,然后再执行插入操作,比如将data0数据项插入到index为2的位置:


    图片.png

    先找到index=2位置处的结点:


    图片.png

    然后执行插入操作:
    图片.png

    图片.png

    整理一下就是:


    图片.png

    实现代码如下:
/**
     * 指定位置添加结点,先遍历链表找到index-1位置上的结点和index上的结点
     * @param value
     * @param index
     */
    public void add(int value,int index){
        Node newNode = new Node(value, null);
        //1.检验插入位置是否合法
        if (index <1 || index > size()) {
            System.out.println("输入位置不合法!");
            return;
        }
        
        Node current = head;
        int currentPos = 0;
        while(current.next != null){
            currentPos ++;
            if (index == currentPos ) {
                newNode.next = current.next;
                current.next = newNode;
                return;
            }
            
            current = current.next;
        }
    }
  1. 删除结点

  • 时间复杂度为O(n)
    删除结点,我们首先要知道删除的那个结点位置,还要知道删除结点的前一个结点和后一个结点。然后直接将要删除结点的前一个结点的next指向后一个结点即可。但是我们不需要注意到这个结点是否为空,是否为头结点,是否为尾结点等边界条件,由于需要遍历需要这个结点,因此其时间复杂度为O(n).比如我们要删除数据项为data2的结点。
    图片.png

    首先我们要找到删除的结点delNode和前一个结点preNode:
    图片.png

    然后直接将preNode的next指向delNode的next,然后清空delNode就可以了。
    图片.png

    实现代码如下:
/**
     * 删除指定结点,首先要找到删除的结点的位置,还必须找到该节点的前置,后置结点。O(n)时间复杂度
     * @param value
     * @return
     */
    public void delete(int value){
        Node current = head;
        Node preNode = head;
        Node delNode = null;
        
        while (current.next != null) {
            if (current.data == value) {
                delNode = current;
                break;
            }
            
            preNode = current;
            current = current.next;
        }
        
        if (delNode == null) {
            return ;
        }
        
        if (delNode == head) {
            //如果删除的是头结点
            head = delNode.next;
        } else if (delNode.next == null) {
            //如果删除的是尾结点
            preNode.next = null;
        } else {
            preNode.next = delNode.next;
        }
        delNode = null;
    }
  • 时间复杂度为O(1)
    剑指offer中有一道关于删除链表的算法题,给定头结点,和一个结点指针,要求时间复杂度为O(1)时间删除该结点。首先我们需要明白给定的这个结点指针,不仅有值,而且还有指向下一个结点的引用。那么就是说,如果我们将下一个结点的值复制到该结点中,再将下一个结点的next指向复制给该节点的next。好像就实现了删除这个结点的操作。但是这是建立在这个结点必须存在于链表中,而且该节点不能为尾结点,因为如果为尾结点的话,还是需要遍历整个链表的。这样一来时间复杂度为 O(n)。我们计算一下平均复杂度为:[(n-1)*O(n) + O(n) ] / n ,时间复杂度其实还是O(1)是符合要求的。比如要删除delNode = new Node(data2,nextNode):


    图片.png

    首先将nextNode中的数据项复制到要删除的node中


    图片.png

    然后再将该结点的next指向nextNode的next,如图所示:
    图片.png

    如果正好只有一个结点的话,head结点直接为null了。

    或者删除的是尾结点,那么必须遍历了,参考上面O(n)算法删除操作。
    实现代码如下:

/**
     * 删除结点(O(1)时间复杂度),可以用被删除结点的下一个结点P1覆盖被删除的结点P2,然后再删除P1结点即可。
     * @param value
     */
    public void deleteNode(Node delNode){
        if (head == null) {
            return;
        }
        //如果删除的不是尾结点
        if (delNode.next != null) {
            //1.找到下一个结点
            Node nextNode = delNode.next;
            //2.复制下一个结点给要删除的结点
            delNode.data  = nextNode.data;
            delNode.next = nextNode.next;
            nextNode = null;//方便回收
        }
        //如果正好只有一个结点,头结点就是尾结点
        else if (head == delNode) {
            head = null;
            delNode = null;
        }
        //链表中有多个结点,删除尾结点
        else {
            Node current = head;
            while (current.next != delNode) {
                current = current.next;
            }
            current.next = null;
            delNode = null;
        }
    }
  1. 查找倒数第k个结点

要查找倒数第k个结点,假设链表有n个结点,那么第K个结点就是从前到后遍历到的第n-k+1个结点。这个时候我们有一种思路就是遍历一遍链表知道这个链表的结点数n,然后再遍历一遍链表找到第n-k+1结点,就是所谓的第K个结点。但是这样一来,相当于我们遍历了两次链表。我们还可以使用一些技巧只遍历一次链表就可以了。我们可以定义两个指针,一个指针preKNode先向前走k-1个位置,然后另一个指针current和preKNode同时遍历链表,直到preKNode遍历到尾结点,那么current指向的就是倒数第k个结点。比如我们要找到倒数第2个结点,也就是数据项为data2的结点。分析如下:


图片.png

现在先让preKNode向前走k-1步,也就是1步:


图片.png

然后两个指针同时向前走,直到preKNode到达尾结点:
图片.png

图片.png

那么current指针指向的就是倒数第K个结点了。
实现代码如下:

/**
     * 查找倒数第k个结点
     * @param value
     * @param k
     * @return
     */
    public Node findKNode(int k){
        Node current = head;
        Node preKNode = head;
        //preKNode为第k-1个结点
        for (int i = 0; i < k-1; i++) {
            preKNode = preKNode.next; 
        }
        
        //当第k-1的结点遍历到了尾结点,current就是第k个结点
        while (preKNode.next != null) {
            preKNode = preKNode.next;
            current = current.next;
        }
        
        return current;
    }
  1. 反转链表

比如我们通过遍历链表的方式来反转如下链表:


图片.png

首先,我们定义两个指针,分别指向相邻的两个结点一个是current,一个是preNode,我们只需要将current的next指向上一个结点preNode,就相当于做了一次反转,可是我们知道current.next指向preNode时,相当于data1和data2两个结点断了关系,没有办法继续遍历下去了。此时,我们在断开data1和data2之前就需要一个临时结点保存tempNode来保存data1的next,也就是tempNode = current.next;这样才能继续保持遍历链表。
第一次反转,头结点相当于尾结点了。


第一次反转.png

继续向下遍历
图片.png

继续向下遍历


图片.png

继续向下走:


图片.png

此时反转完成,而preNode成为了新的头结点,直接返回即可。
实现代码如下:

/**
     * 反转链表,并返回头结点
     * 
     * @return
     */
    public Node reverse() {
        //空链表
        if (head == null) {
            return head;
        }
        //只有一个结点
        if (head.next == null) {
            return head;
        }
        Node preNode = head;// 上一结点
        Node current = head.next;// 当前结点
        Node tempNode = null;
        while (current != null) {
            tempNode = current.next;
            if (preNode == head) {
                preNode.next = null;
            }

            current.next = preNode;//指针方向反转
            preNode = current;//preNode向前移动
            current = tempNode;//current向前移动
        }

        return preNode;
    }
  1. 合并两个排序的链表

比如对这两个链表进行排序


图片.png

其实,这里就是通过遍历两个链表,通过比较两个链表的结点然后找到合适的合并后的结点。如下所示:


图片.png

我们来进行第一次比较,也就是找到第一个合适的合并后的结点mergeNode。由于p1.data < p2.data,因此,此时mergeNode应该指向p1,如下所示:
图片.png

找到了头结点,我们来找一下mergeNode的下一个结点,开始第二次比较:


图片.png

继续比较,寻找mergeNode的下一个结点,开始第三次比较:
图片.png

依次类推下去,我们知道每次找到了mergeNode其实我们都用了相同的比较方法来寻找其下一个结点,这种就可以用递归的方法来表达了。知道最后完成比较并排序,结果如下所示:
图片.png

实现代码如下:
/**
     * 用递归方式合并两个有序链表,并保持合并后的链表有序
     * @param head1
     * @param head2
     * @return
     */
    public Node mergeList(Node head1,Node head2){
        Node mergedNode = null; //合并后的结点
        if (head1 == null) {
            return head2;
        }
        
        if (head2 == null) {
            return head1;
        }
        
        if (head1.data < head2.data) {
            mergedNode = head1;
            mergedNode.next = mergeList(head1.next, head2);
        } else {
            mergedNode = head2;
            mergedNode.next = mergeList(head1, head2.next);
        }
        
        return mergedNode;
    }
  1. 两个链表的第一个公共结点

题目是:输入两个链表,找到他们的第一个公共结点。
先来理解一下这个题目哈,找到第一个公共节点,那说明这个公共结点之后的结点都是一样的。所以,从第一个公共节点开始之后的结点都是重合的。看起来像了一个Y字。比如,下面两个链表:


图片.png

那么其实这个时候,我们肯定不能通过遍历一个链表(m个结点),然后再另个一个链表(n个结点)中找到相同数据项的结点,这样子的算法相当于是O(mn)的时间效率。
可以这么考虑,如果我们保持两个链表长度相同,同时遍历链表然后进行比较。这样提高了时间效率。但是给出的链表长度是未知的,不一定是相同的,这个时候,我们需要以长度短的链表为主来寻找第一个公共结点。也就是说,链表比较长的,我们先要走一定的步数,让其保持和另一个链表长度一致,然后再同时遍历比较。
实现代码如下:

/**
     * 查找第一个公共祖先
     * @param head1
     * @param head2
     * @return
     */
    public Node findFirstCommonNode(Node head1,Node head2){
              if (head1 == null || head2 == null) {
            return null;
        }
        int size1 = size(head1);
        int size2 = size(head2);
        int diff = size1 - size2;
        Node longNode = head1;
        Node shortNode = head2;
        if (diff < 0) {
            diff = size2 - size1;
            longNode = head2;
            shortNode = head1;
        }
        
        //让长的链表先走diff步
        for (int i = 0; i < diff; i++) {
            longNode = longNode.next;
        }
        
        //现在长度一致同时遍历
        while (longNode != null && shortNode != null && longNode.data != shortNode.data) {
            longNode = longNode.next;
            shortNode = shortNode.next;
        }
        
        return longNode;
    }


  public int size(Node head) {
        int size = 0;
        if (head == null) {
            return size;
        }

        while (head.next != null) {
            head = head.next;
            size++;
        }

        return size + 1;// 加上最后一个尾结点个数
    }
  1. 寻找中间结点

设置一个快慢指针,快指针P2每次走两步,慢指针P1每次走一步,由于快指针P2走的快,因此要注意结束条件以P2为主。比如我们要寻找下面这个链表的中间结点:


图片.png

P1走一步,P2走两步,如下所示:


图片.png

由上面我们可以看到P2只能走一步,没有办法再走两步了,因此这里已经达到结束时间了。P1指向的就是中间结点。
或者我们看看链表长度为奇数时:
图片.png

P1走一步,P2走两步,如下所示:


图片.png

P2此时没法走两步,达到结束条件。P1指向的就是中间结点。
代码实现如下:
/**
     * 查找中间结点
     * @return
     */
    public Node findMiddleNode(Node head){
        Node p1 = head;
        Node p2 = head;
        if (head == null) {
            //空链表
            return null;
        } else if (head.next == null) {
            //只有一个结点
            return head;
        } else {
            while (p2 != null && p2.next != null && p2.next.next != null) {
                p1 = p1.next;
                p2 = p2.next.next;
            }
        }
        return p1;
    }

最后附上所有的源代码

package com.wang.suan;

/**
 * 链表
 * 
 * @author wxe
 * @since 0.0.1
 */
public class LinkedList {

    private Node head;// 头结点

    /******************************************************* 链表的增删改查操作 *********************************************************************/
    /**
     * 插入结点(插入到尾结点)
     * 
     * @param value
     * @return
     */
    public void insertTail(int value) {
        Node current = head;
        Node newNode = new Node(value, null);
        if (head == null) {
            // 1.首次插入,为头结点
            head = newNode;
        } else {
            // 2.非首次插入,那么必须找到尾结点
            while (current.next != null) {
                current = current.next;
            }
            current.next = newNode;// 将新节点添加到尾结点
        }
    }

    /**
     * 插入头结点,时间复杂度为O(1)
     * 
     * @param value
     */
    public void insertHead(int value) {
        Node newNode = new Node(value, null);
        if (head != null) {
            newNode.next = head.next;
        }

        head = newNode;
    }

    /**
     * 查找结点
     * 
     * @param value
     * @return
     */
    public Node find(int value) {
        Node current = head;
        while (current.next != null) {
            if (current.data == value) {
                return current;
            }

            current = current.next;
        }

        return null;
    }

    /**
     * 删除指定结点,首先要找到删除的结点的位置,还必须找到该节点的前置,后置结点。O(n)时间复杂度
     * 
     * @param value
     * @return
     */
    public void delete(int value) {
        Node current = head;
        Node preNode = head;
        Node delNode = null;

        while (current.next != null) {
            if (current.data == value) {
                delNode = current;
                break;
            }

            preNode = current;
            current = current.next;
        }

        if (delNode == null) {
            return;
        }

        if (delNode == head) {
            // 如果删除的是头结点
            head = delNode.next;
        } else if (delNode.next == null) {
            // 如果删除的是尾结点
            preNode.next = null;
        } else {
            preNode.next = delNode.next;
        }
        delNode = null;
    }

    /**
     * 删除结点(O(1)时间复杂度),可以用被删除结点的下一个结点P1覆盖被删除的结点P2,然后再删除P1结点即可。
     * 
     * @param value
     */
    public void deleteNode(Node delNode) {
        if (head == null) {
            return;
        }
        // 如果删除的不是尾结点
        if (delNode.next != null) {
            // 1.找到下一个结点
            Node nextNode = delNode.next;
            // 2.复制下一个结点给要删除的结点
            delNode.data = nextNode.data;
            delNode.next = nextNode.next;
            nextNode = null;// 方便回收
        }
        // 如果正好只有一个
        else if (head == delNode) {
            head = null;
            delNode = null;
        }
        // 链表中有多个结点,删除尾结点
        else {
            Node current = head;
            while (current.next != delNode) {
                current = current.next;
            }
            current.next = null;
            delNode = null;
        }
    }

    /**
     * 指定位置添加结点,先遍历链表找到index-1位置上的结点和index上的结点
     * 
     * @param value
     * @param index
     */
    public void add(int value, int index) {
        Node newNode = new Node(value, null);
        // 1.检验插入位置是否合法
        if (index < 1 || index > size()) {
            System.out.println("输入位置不合法!");
            return;
        }

        Node current = head;
        int currentPos = 0;
        while (current.next != null) {
            currentPos++;
            if (index == currentPos) {
                newNode.next = current.next;
                current.next = newNode;
                return;
            }

            current = current.next;
        }
    }

    /**
     * 获取结点个数
     * 
     * @return
     */
    public int size() {
        int size = 0;
        if (head == null) {
            return 0;
        }

        Node current = head;
        while (current.next != null) {
            current = current.next;
            size++;
        }

        return size + 1;// 加上最后一个尾结点个数
    }
    
    public int size(Node head) {
        int size = 0;
        if (head == null) {
            return size;
        }

        while (head.next != null) {
            head = head.next;
            size++;
        }

        return size + 1;// 加上最后一个尾结点个数
    }

    /**
     * 查找倒数第k个结点
     * 
     * @param value
     * @param k
     * @return
     */
    public Node findKNode(int k) {
        Node current = head;
        Node preKNode = head;
        // preKNode为第k-1个结点
        for (int i = 0; i < k - 1; i++) {
            preKNode = preKNode.next;
        }

        // 当第k-1的结点遍历到了尾结点,current就是第k个结点
        while (preKNode.next != null) {
            preKNode = preKNode.next;
            current = current.next;
        }

        return current;
    }

    /**
     * 反转链表,并返回头结点
     * 
     * @return
     */
    public Node reverse() {
        //空链表
        if (head == null) {
            return head;
        }
        //只有一个结点
        if (head.next == null) {
            return head;
        }
        Node preNode = head;// 上一结点
        Node current = head.next;// 当前结点
        Node tempNode = null;
        while (current != null) {
            tempNode = current.next;
            if (preNode == head) {
                preNode.next = null;
            }

            current.next = preNode;//指针方向反转
            preNode = current;//preNode向前移动
            current = tempNode;//current向前移动
        }

        return preNode;
    }
    /**
     * 用递归方式合并两个有序链表,并保持合并后的链表有序
     * @param head1
     * @param head2
     * @return
     */
    public Node mergeList(Node head1,Node head2){
        Node mergedNode = null; //合并后的结点
        if (head1 == null) {
            return head2;
        }
        
        if (head2 == null) {
            return head1;
        }
        
        if (head1.data < head2.data) {
            mergedNode = head1;
            mergedNode.next = mergeList(head1.next, head2);
        } else {
            mergedNode = head2;
            mergedNode.next = mergeList(head1, head2.next);
        }
        
        System.out.println(mergedNode.data);
        return mergedNode;
    }
    /**
     * 查找第一个公共祖先
     * @param head1
     * @param head2
     * @return
     */
    public Node findFirstCommonNode(Node head1,Node head2){
        if (head1 == null || head2 == null) {
            return null;
        }
        int size1 = size(head1);
        int size2 = size(head2);
        int diff = size1 - size2;
        Node longNode = head1;
        Node shortNode = head2;
        if (diff < 0) {
            diff = size2 - size1;
            longNode = head2;
            shortNode = head1;
        }
        
        //让长的链表先走diff步
        for (int i = 0; i < diff; i++) {
            longNode = longNode.next;
        }
        
        //现在长度一致同时遍历
        while (longNode != null && shortNode != null && longNode.data != shortNode.data) {
            longNode = longNode.next;
            shortNode = shortNode.next;
        }
        
        return longNode;
    }
    /**
     * 查找中间结点
     * @return
     */
    public Node findMiddleNode(Node head){
        Node p1 = head;
        Node p2 = head;
        if (head == null) {
            //空链表
            return null;
        } else if (head.next == null) {
            //只有一个结点
            return head;
        } else {
            while (p2 != null && p2.next != null && p2.next.next != null) {
                p1 = p1.next;
                p2 = p2.next.next;
            }
        }
        return p1;
    }
    
    
    public void display() {
        if (head != null) {
            while (head.next != null) {
                System.out.println(head.data);
            }
        }
    }

    /********************************************************************* 结点定义 *********************************************************/
    // 构成链表的结点(此处是单链表,每个结点都保存了指向下一个结点的引用)
    class Node {
        public int data;
        public Node next;

        public Node(int data, Node next) {
            this.data = data;
            this.next = next;
        }
    }

    /**************************************************** *********测试链表 ******************************************************************/
    public static void main(String[] args) {
//      LinkedList list = new LinkedList();
//      list.insertTail(1);
//      list.insertTail(2);
//      list.insertTail(3);
//      list.insertTail(4);
//      
//      
//
//      System.out.println(list.head.data);
//      // System.out.println(list.find(6));
//      // System.out.println(list.size());
//       System.out.println(list.findKNode(2).data);
////        list.reverse();
//      System.out.println(list.reverse().data);
//      System.out.println(list.findKNode(2).data);
        
        LinkedList list = new LinkedList();
        list.insertTail(1);
        list.insertTail(2);
        list.insertTail(5);
        list.insertTail(7);
        list.insertTail(9);
        
        LinkedList list2= new LinkedList();
        list2.insertTail(2);
        list2.insertTail(4);
        list2.insertTail(8);
        
//      System.out.println(list.mergeList(list.head, list2.head).data);
//      System.out.println(list.findFirstCommonNode(list.head, list2.head).data);
        System.out.println(list.findMiddleNode(list.head).data);
    }
}

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

推荐阅读更多精彩内容