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

2019-10-14  本文已影响0人  xkzhai

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

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 时间复杂度分析

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

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


image.png

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

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

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
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

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

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
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

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
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
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

3.3 使用二分搜索树实现Map
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)。集合与映射在实际生活中的应用非常广泛,可以用来实现很多复杂任务。

上一篇 下一篇

猜你喜欢

热点阅读