玩转数据结构6-集合与映射

上节课学习了二分搜索树这样一种有序数据结构 ,本节课将借助二分搜索树来实现更高级的数据结构--集合与映射。

1. 集合

1.1 基于二分搜索树的集合实现

集合的主要特点是不能盛放重复元素,在实际生活中,应用也很广泛:

  • 公司客户统计
  • 书籍词汇量统计

上一节实现的二分搜索树不能添加相同的元素,是非常好的实现集合的底层数据结构。要实现集合,首先需要定义如下通用型集合接口:

    public interface Set<E> {
        // 1. 添加元素
        public void add(E e);
        
        // 2. 删除元素
        public void remove(E e);
        
        // 3. 判断是否为空
        public boolean isEmpty();
        
        // 4. 查看集合内元素个数
        public int getSize();
        
        // 5. 判断是否包含某元素
        public boolean contains(E e);
    }

然后使用上一节创建的二分搜索树来实现该接口:

    import java.util.Comparator;

    public class BSTSet<E extends Comparable<E>> implements Set<E> {
        private BST<E> data;

        public BSTSet() {
            data = new BST<>();
        }

        @Override
        public int getSize() {
            return data.getSize();
        }
        
        @Override
        public boolean isEmpty() {
            return data.isEmpty();
        }
        
        @Override
        public boolean contains(E e) {
            return data.contains(e);
        }
        
        @Override
        public void add(E e) {
            data.addElement(e);
        }
        
        @Override
        public void remove(E e) {
            data.removeElement(e);
        }
    }

下面测试上述实现,编写测试函数,计算《傲慢与偏见》的词汇量,这里需要借助文件读取,首先实现文字的切割:

    import java.io.FileInputStream;
    import java.util.ArrayList;
    import java.util.Scanner;
    import java.util.Locale;
    import java.io.File;
    import java.io.BufferedInputStream;
    import java.io.IOException;

    // 文件相关操作
    public class FileOperation {

        // 读取文件名称为filename中的内容,并将其中包含的所有词语放进words中
        public static boolean readFile(String filename, ArrayList<String> words){

            if (filename == null || words == null){
                System.out.println("filename is null or words is null");
                return false;
            }

            // 文件读取
            Scanner scanner;

            try {
                File file = new File(filename);
                if(file.exists()){
                    FileInputStream fis = new FileInputStream(file);
                    scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
                    scanner.useLocale(Locale.ENGLISH);
                }
                else
                    return false;
            }
            catch(IOException ioe){
                System.out.println("Cannot open " + filename);
                return false;
            }

            // 简单分词
            // 这个分词方式相对简陋, 没有考虑很多文本处理中的特殊问题
            // 在这里只做demo展示用
            if (scanner.hasNextLine()) {

                String contents = scanner.useDelimiter("\\A").next();

                int start = firstCharacterIndex(contents, 0);
                for (int i = start + 1; i <= contents.length(); )
                    if (i == contents.length() || !Character.isLetter(contents.charAt(i))) {
                        String word = contents.substring(start, i).toLowerCase();
                        words.add(word);
                        start = firstCharacterIndex(contents, i);
                        i = start + 1;
                    } else
                        i++;
            }

            return true;
        }

        // 寻找字符串s中,从start的位置开始的第一个字母字符的位置
        private static int firstCharacterIndex(String s, int start){

            for( int i = start ; i < s.length() ; i ++ )
                if( Character.isLetter(s.charAt(i)) )
                    return i;
            return s.length();
        }
    }

测试函数:

    public static void main(String[] args) {
        ArrayList<String> words = new ArrayList<>();
        if(FileOperation.readFile("pride-and-prejudice.txt", words)) {
            
            System.out.println("Total words:"+words.size()); // 125901
            
            BSTSet<String> bst =  new BSTSet<>();
            for(String word:words) {
                bst.add(word);
            }
            
            System.out.println("Total different words: "+bst.getSize()); // 6530
        }
        else {
            throw new IllegalArgumentException("Read failed");
        }
    }

可以看到,完成二分搜索树的增删逻辑之后,借助二分搜索树实现集合是非常简单的,下面考虑用链表实现集合。

1.2 基于链表的集合实现

基于之前构造的链表结构,也可以实现集合功能,只不过在添加元素时,需要先判断链表中是否已包含该元素,另外,链表集合中的泛型无需实现比较器:

    import java.util.ArrayList;

    public class LinkedListSet<E> implements Set<E> {
        private LinkedList<E> list;
        
        public LinkedListSet() {
            list = new LinkedList<>();
        }
        
        @Override
        public int getSize() {
            return list.getSize();
        }
        
        @Override
        public boolean isEmpty() {
            return list.isEmpty();
        }
        
        @Override
        public boolean contains(E e) {
            return list.contains(e);
        }
        
        @Override
        public void add(E e) {
            // 加一句判断,若列表中已包含元素,则不添加元素
            if(!list.contains(e))
                list.addFirst(e);
        }
        
        @Override
        public void remove(E e) {
            list.removeElement(e);
        }
    }

可以采用相同的测试函数来验证上述实现的有效性:

    public static void main(String[] args) {
        ArrayList<String> words = new ArrayList<>();
        if(FileOperation.readFile("pride-and-prejudice.txt", words)) {
            
            System.out.println("Total words:"+words.size()); // 125901
            
            LinkedListSet<String> list =  new LinkedListSet<>();
            for(String word:words) {
                list.add(word);
            }
            
            System.out.println("Total different words: "+list.getSize()); // 6530
        }
        else {
            throw new IllegalArgumentException("Read failed");
        }
    }

运行时可以很明显感觉到将链表作为底层结构,耗时要比二分搜索树长很多。

1.3 时间复杂度分析

对于底层结构是链表的集合,其时间复杂度与链表的时间复杂度相关:

  • 增(add):链表本身添加操作的时间复杂度为O(1),然而为实现集合元素不能重复的特性,添加元素时需要先遍历一遍链表,判断是否包含待添加元素,因此时间复杂度为O(n)
  • 删(remove):链表的删除操作需要找到待删除元素的前一节点,因此时间复杂度为O(n)
  • 查(contains): 需要遍历链表所有元素,复杂度为O(n)

下面分析二分搜索树的时间复杂度,二分搜索树本质上是一种顺序结构,增删查操作与二分搜索树的深度h有关:


image.png

考虑二分搜索树中节点个数n与深度h的关系:

  • 对于一个满二叉树,除了叶子节点,每个节点都有两个孩子
    image.png

    可以看到,第0层有1个节点,第1层有2个节点,第2层有4个节点,...,第i层有2^i个节点,因此对于满二叉树,n = 2^0+2^1+...+2^{h-1}=2^h-1 增删查的时间复杂度为O(h)=O(logn)
  • 上面满二叉树是一种平均情况,对于极端的情况,二分搜索树有可能退化成链表,此时时间复杂度为O(n)
    image.png

下面的测试函数用来简单测试链表集合类与二分搜索树集合类的效率:

public static double  testSet(Set<String> set,String filename) {
    long startTime = System.nanoTime();
    ArrayList<String> words = new ArrayList<>();
    if(FileOperation.readFile(filename, words)) {
        
        System.out.println("Total words:"+words.size());
        
        for(String word:words) {
            set.add(word);
        }
        
        System.out.println("Total different words: "+set.getSize()); 
    }
    
    long endTime = System.nanoTime();
    
    return (endTime - startTime)/ 1000000000.0;
}

public static void main(String[] args) {
      String filename = "pride-and-prejudice.txt";
      BSTSet<String> bst = new BSTSet<String>();
      double time1 = testSet(bst,filename);
      System.out.println("BST set:"+time1+"s"); // 0.3
      
      System.out.println();
      
      LinkedListSet<String> linkedListSet = new LinkedListSet<>();
      double time2 = testSet(linkedListSet, filename);
      System.out.println("LinkedList set:"+time2+"s"); // 4.0355  
}

2. leetcode中与集合有关的问题

804

国际摩尔斯密码定义一种标准编码方式,将每个字母对应于一个由一系列点和短线组成的字符串, 比如: "a" 对应 ".-", "b" 对应 "-...", "c" 对应 "-.-.", 等等。
所有26个英文字母对应的摩尔斯密码表如下:
[".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."]
给定一个单词列表,每个单词可以写成每个字母对应摩尔斯密码的组合。例如,"cab" 可以写成 "-.-..--...",(即 "-.-." + "-..." + ".-"字符串的结合)。我们将这样一个连接过程称作单词翻译。不同的单词有可能被翻译为相同的摩尔斯密码,要求返回单词列表中不同摩尔斯密码的数量。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/unique-morse-code-words
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

使用集合很容易解答该题,遍历单词列表,将每个单词的翻译结果放入一个集合中,由于集合不能存放相同的元素,因此,遍历完单词列表后,集合中元素的数量即为不同摩尔斯密码的数量。

  • 应用Java集成的TreeSet类

     import java.util.TreeSet;
    
     // leetcode 804
     public class Solution {
          public int uniqueMorseRepresentations(String[] words) {
              String[] codes = {".-","-...","-.-.","-..",".","..-.",
                      "--.","....","..",".---","-.-",".-..","--","-.",
                      "---",".--.","--.-",".-.","...","-","..-","...-",
                      ".--","-..-","-.--","--.."};
              TreeSet<String> treeSet = new TreeSet<>();
              for(String word:words) {
                  StringBuilder res = new StringBuilder();
                  for(int i=0;i<word.length();i++) {
                      // 将单词索引为i位置的字母翻译成摩尔斯密码,追加到翻译结果中
                      res.append(codes[word.charAt(i)-'a']);
                  }
                  treeSet.add(res.toString());
              }
              return treeSet.size();
          }
     }
    
  • 使用上一节中自己创建的Set类

      import java.util.Comparator;
      import java.util.LinkedList;
      import java.util.Queue;
      import java.util.Stack;
    
      public class Solution {
              public int uniqueMorseRepresentations(String[] words) {
               String[] codes = {".-","-...","-.-.","-..",".","..-.",
                       "--.","....","..",".---","-.-",".-..","--","-.",
                       "---",".--.","--.-",".-.","...","-","..-","...-",
                       ".--","-..-","-.--","--.."};
               BSTSet<String> set = new BSTSet<>();
               for(String word:words) {
                   StringBuilder res = new StringBuilder();
                   for(int i=0;i<word.length();i++) {
                       // 将单词索引为i位置的字母翻译成摩尔斯密码,追加到翻译结果中
                       res.append(codes[word.charAt(i)-'a']);
                   }
                   set.add(res.toString());
               }
               return set.getSize();
           }
      }
    
349

给定两个数组,编写一个函数来计算它们的交集。

示例 1:

输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2]
示例 2:

输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [9,4]
说明:

  • 输出结果中的每个元素一定是唯一的。
  • 我们可以不考虑输出结果的顺序。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/intersection-of-two-arrays
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  • 使用基于二分搜索树构造的集合类解决该问题:

      class Solution {
    
          public int[] intersection(int[] nums1, int[] nums2) {
              BSTSet<Integer> bstSet = new BSTSet<>();
              // 先将第一个数组中的元素添加到集合中
              for(int num:nums1) {
                  bstSet.add(num);
              }
              ArrayList<Integer> arr = new ArrayList<>();
              for(int num:nums2) {
                  // 遍历第二个数组中的所有元素,如果存在于集合中,则将该元素添加到数组列表中
                  // 同时从集合中移除该元素
                  if(bstSet.contains(num)) {
                      arr.add(num);
                      bstSet.remove(num);
                  }
              }
              int[] res = new int[arr.size()];
              for(int i = 0;i<arr.size();i++) {
                  res[i] = arr.get(i);
              }
              return res;
          }
      }
    

3. 映射

映射(Map)是一种存储(键,值)数据对的数据结构(key,value),键在映射中是不可重复的,通过键(key)来寻找值(value)。映射在实际生活中同样具有广泛的应用:

  • 字典中一个单词对应一个释义
  • 名册中一个身份证号对应一个人
  • 车辆管理中一个车牌对应一辆车

回顾之前介绍的链表和二分搜索树的结构:

    // 链表中的节点结构            // 二分搜索树中的节点结构
    class Node{                  class Node{
        E e;                         E e;
        Node next;                   Node left;
    }                                Node right;
                                 }

很容易用这样的结构来实现映射机制,只需在节点中存储一对数据即可:

    // 存储键值对的链表                  // 存储键值对的二分搜索树
    class Node{                        class Node{
        K key;                            K key;
        V value;                          V value;
        Node next;                        Node left;
    }                                     Node right;
                                        }

一个Map应该包含如下基本接口:

    public interface Map<K,V> {
        // 1. 获取map中键值对个数
        public int getSize();
        // 2. 判断map是否为空
        public boolean isEmpty();
        // 3. 添加键值对
        public void add(K key,V value);
        // 4. 删除key对应的键值对,返回value
        public V remove(K key);
        // 5. 更新key对应的value
        public void set(K key,V value);
        // 6. 查询key对应的值
        public V get(K key);
        // 7. 判断是否包含指定的key
        public boolean contains(K key);
    }
3.1 使用链表实现Map
  • 首先使用链表来实现Map,对链表节点结构进行修改,注意增加泛型支持:

      public class LinkedListMap<K,V> implements Map<K,V> {
          // 私有结点类,存储键值对
          private class Node{
              private K key;
              private V value;
              private Node next;
              
              public Node(K key, V value, Node next) {
                  this.key = key;
                  this.value = value;
                  this.next = next;
              }
              
              public Node(K key,V value){
                  this(key,value,null);
              }
              
              public Node() {
                  this(null,null,null);
              }
              
              @Override
              public String toString() {
                  return "Key: "+key.toString()+"Value:"+value.toString();
              }
          }
          
          private Node dummyHead;
          private int size;
      }
    
  • 简单函数的实现

          // 1. 获取map中键值对个数
          public int getSize(){
              return size;
          }
          
          // 2. 判断map是否为空
          public boolean isEmpty() {
              return size == 0;
          }
          // 私有成员函数,根据key找到所在节点
          private Node getNode(K key) {
              Node cur = dummyHead.next;
              while(cur!=null) {
                  if(cur.key.equals(key)) {
                      return cur;
                  }
                  cur = cur.next;
              }
              return null;
          }
    
    • 根据键查询

      // 6. 判断是否包含指定的key
      @Override
      public boolean contains(K key) {
          Node node = getNode(key);
          return node != null;
      }
      
      // 7. 查询key对应的值
      @Override
      public V get(K key) {
          Node node = getNode(key);
          return node.value;
      }
      
  • 更新/修改

      // 5. 更新key对应的value
      @Override
      public void set(K key, V value) {
          Node node = getNode(key);
          
          if(node!=null) {
              node.value = value;
              Node preNode = dummyHead.next;
              while(preNode.next!=null) {
                  // 找到待更新结点的前一结点
                  if(preNode.next.key.equals(key)) {
                      node.next = preNode.next.next;
                      preNode.next = node;
                      return;
                  }
                  preNode = preNode.next;
              }
          }
          // node.value = value; // 需事先判断Map中是否存在该键
      }
    
  • 添加元素

      // 3. 添加新的键值对
      @Override
      public void add(K key, V value) {
          
          if(contains(key)) { // 如果已包含该键,更新该键对应的值
              set(key,value);
          }
          else { // 如果不包含该键,添加到链表头
              Node node = new Node(key,value);
              node.next = dummyHead.next;
              dummyHead.next = node;
              size++;
          }
      }
    
  • 删除元素

      // 4. 删除key对应的键值对,返回value
      @Override
      public V remove(K key) {
          Node pre = dummyHead;
          Node delNode = new Node();
          // 从虚拟头节点出发,找到待删除元素的前一节点
          while(pre.next!=null) {
              if(pre.next.key.equals(key))
                  break;
              pre = pre.next;
          }
          // 如果找到的前一节点不是最后一个元素,则执行删除操作
          if(pre.next!=null) {
              delNode = pre.next;
              pre.next = delNode.next;
              delNode.next = null;
              size --;
          }
          return delNode.value;
      }
    
3.2 leetcode中与Map相关的问题

给定两个数组,编写一个函数来计算它们的交集。

示例 1:

输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2,2]
示例 2:

输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [4,9]
说明:

输出结果中每个元素出现的次数,应与元素在两个数组中出现的次数一致。
我们可以不考虑输出结果的顺序。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/intersection-of-two-arrays-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

  • 利用基于链表构造的映射类来解决问题

        public class Solution {
          public static int[] intersect(int[] nums1, int[] nums2) {
              LinkedListMap<Integer,Integer> linkedListMap = new LinkedListMap<Integer,Integer>();
              // 遍历第一个数组中的元素,将元素及其对应出现的次数存入映射中
              for(int num:nums1) {
                  // 如果是之前添加过的元素,将元素出现的次数+1
                  if(linkedListMap.contains(num)) {
                      linkedListMap.set(num, linkedListMap.get(num)+1);
                  }
                  // 如果之前没有添加过,以出现次数为1添加进映射中
                  else
                      linkedListMap.add(num, 1);
              }
              
              ArrayList<Integer> arr = new ArrayList<>();
              for(int num:nums2) {
                  // 遍历第二个数组中的元素,如果包含在映射中,加入数组 列表中
                  if(linkedListMap.contains(num)) {
                      // 如果该元素的次数大于1,加入数组列表,同时将出现次数减1
                      if(linkedListMap.get(num)>=1) {
                          arr.add(num);
                          linkedListMap.set(num, linkedListMap.get(num)-1);
                      }
                      // 如果该元素次数是0,则将其从映射中移除
                      else
                          linkedListMap.remove(num);
                  }
              }
              int[] res = new int[arr.size()];
              for(int i = 0;i<arr.size();i++) {
                  res[i] = arr.get(i);
              }
              return res;
          }
      }
    
3.3 使用二分搜索树实现Map
  • 支持泛型的二分搜索树基本结构,注意键的可比较性

      public class BSTMap<K extends Comparable<K>,V> implements Map<K,V> {
          // 私有结点类
          private class Node{
              private K key;
              private V value;
              private Node left,right;
              public Node(K key, V value) {
                  this.key = key;
                  this.value = value;
                  left = null;
                  right = null;
              }
              
              @Override
              public String toString() {
                  return "Key: "+key.toString()+"Value:"+value.toString();
              }
          }
          private Node root;
          private int size;
          
          public BSTMap() {
              root = null;
              size = 0;
          }
          
          @Override
          public int getSize() {
              return size;
          }
          
          @Override
          public boolean isEmpty() {
              return size == 0;
          }
      }
    
  • 添加元素

      // 向二分搜索树中添加新的元素(key,value)
      @Override
      public void add(K key, V value) {
          root = add(root,key,value);
      }
      
      // 私有添加方法,递归向以node为根结点的二分搜索树中添加键值对
      // 返回插入新结点后二分搜索树的根
      private Node add(Node node,K key,V value) {
          if(node==null) {
              node = new Node(key,value);
              size++;
              return node;
          }
          if(node.key.compareTo(key)>0) {
              node.left = add(node.left,key,value);
          }
          else if(node.key.compareTo(key)<0) {
              node.right = add(node.right,key,value);
          }
          else { // node.key.compareTo(key)==0
              // 如果已存在该元素,做更新操作
              node.value = value;
          }
          return node;
      }
    
  • 查询操作

      // 返回以node为根节点的二分搜索树中,key所在的节点
      private Node getNode(Node node,K key) {
          if(node==null) {
              return null;
          }
          if(node.key.compareTo(key)>0) {
              return getNode(node.left,key);
          }
          else if(node.key.compareTo(key)<0) {
              return getNode(node.right,key);
          }
          else { // node.key.compareTo(key)==0
              return node;
          }
      }
      
      @Override
      public boolean contains(K key) {
          return getNode(root,key)!=null;
      }
      
      @Override
      public V get(K key) {
          Node node = getNode(root,key);
          return (node == null)?null:node.value;
      }
      // 返回以node为根的二分搜索树的最小值所在的节点
      private Node minimize(Node node) {
          if(node.left==null) {
              return node;
          }
          return minimize(node.left);
      }
    
  • 修改/更新

      @Override
      public void set(K key, V newValue) {
          Node node = getNode(root,key);
          if(node==null)
              throw new IllegalArgumentException(key+" doesn't exist!");
          node.value = newValue;
      }
    
  • 删除

      // 删除掉以node为根的二分搜索树中的最小节点
      // 返回删除节点后新的二分搜索树的根
      private Node removeMin(Node node){
          if(node.left == null) {
              Node rightNode = node.right;
              node.right = null;
              size --;
              return rightNode;
          }
          
          node.left = removeMin(node.left);
          return node;
      }
      // 从二分搜索树中删除键为key的节点
      @Override
      public V remove(K key) {
          Node node = getNode(root,key);
          if(node!=null) {
              root = remove(root,key);
          }
          return node.value;
      }
    
      // 删除掉以node为根的二分搜索树中键为key的结点
      // 返回删除节点后新的二分搜索树的根
      private Node remove(Node node,K key) {
          if(node==null) {
              return null;
          }
          if(node.key.compareTo(key)>0) {
              node.left = remove(node.left,key);
              return node;
          }
          else if(node.key.compareTo(key)<0) {
              node.right = remove(node.right,key);
              return node;
          }
          else { // node.key.compareTo(key)==0
              // 待删除结点右子树为空的情况
              if(node.right==null) {
                  Node leftNode = node.left;
                  node.left = null;
                  size--;
                  return leftNode;
              }
              // 待删除结点左子树为空的情况
              if(node.left==null) {
                  Node rightNode = node.right;
                  node.right = null;
                  size--;
                  return rightNode;
              }
              // 待删除节点左右子树均不为空的情况
    
              // 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
              // 用这个节点顶替待删除节点的位置
              Node successor = minimize(node.right);
              successor.right = removeMin(node.right);
              successor.left = node.left;
              
              node.left = node.right = null;
              return successor;
          }
      }
    
3.4 两种底层实现的比较
    import java.util.ArrayList;

    public class Main {

        private static double testMap(Map<String, Integer> map, String filename){

            long startTime = System.nanoTime();

            System.out.println(filename);
            ArrayList<String> words = new ArrayList<>();
            if(FileOperation.readFile(filename, words)) {
                System.out.println("Total words: " + words.size());

                for (String word : words){
                    if(map.contains(word))
                        map.set(word, map.get(word) + 1);
                    else
                        map.add(word, 1);
                }

                System.out.println("Total different words: " + map.getSize());
                System.out.println("Frequency of PRIDE: " + map.get("pride"));
                System.out.println("Frequency of PREJUDICE: " + map.get("prejudice"));
            }

            long endTime = System.nanoTime();

            return (endTime - startTime) / 1000000000.0;
        }

        public static void main(String[] args) {

            String filename = "pride-and-prejudice.txt";

            BSTMap<String, Integer> bstMap = new BSTMap<>();
            double time1 = testMap(bstMap, filename);
            System.out.println("BST Map: " + time1 + " s"); // 0.1520 s

            System.out.println();

            LinkedListMap<String, Integer> linkedListMap = new LinkedListMap<>();
            double time2 = testMap(linkedListMap, filename);
            System.out.println("Linked List Map: " + time2 + " s"); // 9.7194 s

        }
    }

4. 总结

本节课主要学习了集合与映射两种高级数据结构,分别以链表和二分搜索树为底层结构实现了这两种数据结构,并对存取效率进行了比较,存取效率与底层实现结构相关,以二分搜索树为底层结构的数据存取效率更高,这是因为二分搜索树的存取时间复杂度为O(logn),远小于链表的时间复杂度O(n)。集合与映射在实际生活中的应用非常广泛,可以用来实现很多复杂任务。

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

推荐阅读更多精彩内容

  • 1.HashMap是一个数组+链表/红黑树的结构,数组的下标在HashMap中称为Bucket值,每个数组项对应的...
    谁在烽烟彼岸阅读 1,009评论 2 2
  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 4,253评论 0 16
  • 本系列出于AWeiLoveAndroid的分享,在此感谢,再结合自身经验查漏补缺,完善答案。以成系统。 Java基...
    济公大将阅读 1,523评论 1 6
  • Mc安杰(1999年1月18日)网络歌手,出生于美丽江苏省泰州市,现居泰州。从小就很喜欢音乐,经常在网络发布原创歌...
    MC安杰阅读 391评论 0 0
  • 随风潜夜惊幽梦,拂晓堪闻雨噤声。 缥缈荷塘花落处,轻纱抚叶阐蛙鸣。 (图片来自网络)
    大老鲍阅读 1,206评论 14 49