为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
数据结构

上一章我们详细的介绍了二分搜索树的底层实现。这章我们介绍两个高层的数据结构,集合和映射。这种高层的数据结构,更像是我们定义好了使用接口规则,但是具体的底层实现可以是多种多样的。

集合

承载元素的容器,元素的去重操作。回忆我们上一小节实现的二分搜索树是不能盛放重复元素的,非常好的实现“集合”的底层数据结构。

Set<E>  void add(E)
  void remove(E)
  boolean contains(E)
  int getSize()
  boolean isEmpty()

添加元素,删除元素,是否包含,大小,是否为空。连续add两次,只保留一份。典型应用: 客户统计。典型应用:词汇量统计。

BST依然是上一章实现的BST

package cn.mtianyan;public class BSTSet<E extends Comparable<E>> implements Set<E> {    private BST<E> bst;    public BSTSet() {
        bst = new BST<>();
    }    @Override
    public void add(E e) {
        bst.add(e); // 本身就可以对于重复不理会
    }    @Override
    public void remove(E e) {
        bst.remove(e);
    }    @Override
    public boolean contanins(E e) {        return bst.contains(e);
    }    @Override
    public int getSize() {        return bst.getSize();
    }    @Override
    public boolean isEmpty() {        return bst.isEmpty();
    }
}
package cn.mtianyan;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中的内容,并将其中包含的所有词语放进ArrayList words中
     *
     * @param filename
     * @param words
     * @return
     */
    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的位置开始的第一个字母字符的位置
     * 
     * @param s
     * @param start
     * @return
     */
    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();
    }
}

这里是一个文件读取单词后存入一个ArrayList words中的操作。

传入文件名,将每一个单词扔进数组中。使用英文文本,简单的分词,一行一行读取,把每一个词分出来,文本中所有的单词。

NLP中对于一个动词的不同形式等可以分为一个单词,我们这里只是最简单的就是字母不同的单词。

傲慢与偏见,双城记。年代久远,版权可以使用。

package cn.mtianyan;import java.util.ArrayList;public class Main {    public static void main(String[] args) {

        System.out.println("Pride and Prejudice");

        ArrayList<String> words1 = new ArrayList<>();        if (FileOperation.readFile("pride-and-prejudice.txt", words1)) {
            System.out.println("Total words: " + words1.size());

            BSTSet<String> set1 = new BSTSet<>();            for (String word : words1)
                set1.add(word);
            System.out.println("Total different words: " + set1.getSize());
        }

        System.out.println();


        System.out.println("A Tale of Two Cities");

        ArrayList<String> words2 = new ArrayList<>();        if (FileOperation.readFile("a-tale-of-two-cities.txt", words2)) {
            System.out.println("Total words: " + words2.size());

            BSTSet<String> set2 = new BSTSet<>();            for (String word : words2)
                set2.add(word);
            System.out.println("Total different words: " + set2.getSize());
        }
    }
}

5c127a190001331703540179.jpg

傲慢与偏见12万词,去重后只有六千多词。读原版书,读感兴趣的书是学英语的好方式。

双城记 14万词,去重后有九千多次。单词的多少是阅读这本书难度指标的一项。

基于链表的集合实现

为什么我们要再进行一下基于链表的实现呢?因为二分搜索树和LinkedList都属于动态数据结构,它们的数据都存储在一个一个的node中。

class Node{
  E e;
  Node left;
  Node right;
}

二分搜索树中指向左右子树。

class Node{
  E e;
  Node next ;
}

链表中指向下一个节点。两种不同实现之后我们可以比较性能。

public class LinkedListSet<E> implements Set<E>{    private LinkedList<E> list;    public LinkedListSet() {
        list = new LinkedList<>();
    }
}

基于链表的实现中,并不要求传入的类型具有可比性,这是线性数据结构的特点。

package cn.mtianyan.set;import cn.mtianyan.linked.LinkedList;public class LinkedListSet<E> implements Set<E>{    private LinkedList<E> list;    public LinkedListSet() {
        list = new LinkedList<>();
    }    @Override
    public boolean contanins(E e) {        return list.contains(e);
    }    @Override
    public int getSize() {        return list.getSize();
    }    @Override
    public boolean isEmpty() {        return list.isEmpty();
    }    @Override
    public void add(E e) {        // 保证不能重复e
        if (!list.contains(e))
            list.addFirst(e);
    }    @Override
    public void remove(E e) {
        list.removeElement(e);
    }
}
    public static void main(String[] args) {

        System.out.println("Pride and Prejudice");

        ArrayList<String> words1 = new ArrayList<>();        if (FileOperation.readFile("pride-and-prejudice.txt", words1)) {
            System.out.println("Total words: " + words1.size());

            LinkedListSet<String> set1 = new LinkedListSet<>();            for (String word : words1)
                set1.add(word);
            System.out.println("Total different words: " + set1.getSize());
        }

        System.out.println();


        System.out.println("A Tale of Two Cities");

        ArrayList<String> words2 = new ArrayList<>();        if (FileOperation.readFile("a-tale-of-two-cities.txt", words2)) {
            System.out.println("Total words: " + words2.size());

            LinkedListSet<String> set2 = new LinkedListSet<>();            for (String word : words2)
                set2.add(word);
            System.out.println("Total different words: " + set2.getSize());
        }
    }

运行结果一模一样,但是明显感觉到时间变长。

集合类的复杂度分析

前面我们已经实现了两种底层数据结构不同的Set实现。一个是基于二分搜索树的一个是基于链表的。

之前的测试中,基于链表的是明显的要慢的。

package cn.mtianyan;import cn.mtianyan.set.BSTSet;import cn.mtianyan.set.LinkedListSet;import cn.mtianyan.set.Set;import java.util.ArrayList;public class CompareTwoBstLinkedListSet {    private static double testSet(Set<String> set, 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)
                set.add(word);
            System.out.println("Total different words: " + set.getSize());
        }        long endTime = System.nanoTime();        return (endTime - startTime) / 1e9;
    }    public static void main(String[] args) {

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

        BSTSet<String> bstSet = new BSTSet<>();        double time1 = testSet(bstSet, filename);
        System.out.println("BST Set: " + time1 + " s");

        System.out.println();

        LinkedListSet<String> linkedListSet = new LinkedListSet<>();        double time2 = testSet(linkedListSet, filename);
        System.out.println("Linked List Set: " + time2 + " s");

    }
}

运行结果:

5c127a19000138b103630223.jpg

可以看到二分搜索树是要性能优于链表的。

集合的时间复杂度分析

集合不涉及改。

  • 本来链表添加只需要O(1)时间复杂度,但是我们为了保证不重复,先调用了一遍contains,因此变成了O(n)

  • contains操作,我们必须要从头到尾扫一遍链表,复杂度O(n)

  • remove操作,要先找到待删除元素前一个元素,时间复杂度O(n)

如果有n个单词,每个单词都不同,是o(n^2)级别的。

5c127a1a00013bee09130511.jpg

添加一个元素,走左子树,就不会去右子树,节省了很大一部分的寻找开销,最多能经历的节点数是树的高度。添加元素,删除元素,查找元素都是这样的,对于它来说,时间复杂度为O(h),h为二分搜索树的高度。

5c127a1a0001c41d07590446.jpg

下面我们来谈一谈高度n和h之间的关系,极端: 满二叉树中,第h-1层,有2^(h-1)个节点。

5c127a1a00017b1a02310471.jpg

h层一共有多少个节点?

5c127a1a0001a75606490260.jpg

根据等比数列的求和公式,可以得到h层一共有2^h-1 = n

5c127a1b0001bc4f03310184.jpg

通常我们不会计较这里的底,无论是以2位底,还是10,它们都是logN级别的。

logn和n的差距

5c127a1b00014ee309850435.jpg

相差了五万倍,一秒跑完,14个小时。一天跑完,137年跑完。

logn是一个非常快的时间复杂度,很多排序算法是nlogn的,比n^2快了很多很多(前面都乘以n)。但是这里我们的logn必须要注明是平均的,因为是在满二叉树下计算出来的。

同样的数据,可以对应不同的二分搜索树

1,2,3,4,5,6

5c127a1b0001075204710404.jpg

它也可以对应一个1到6排列的类似链表的,从根节点到下面每个节点都只有右子树的一个二分搜索树。

对于这种等同于链表的二分搜索树,它的高度等于节点的个数,它将退化成与链表一模一样的一棵树。这种最坏的顺序只要我们是按照123456的顺序创建就可以形成。

二分搜索树会退化为链表,虽然平均来讲是O(logn)级别的,但是当退化成链表的最差情况,会变成O(n)级别的。

解决这个问题的方法就是要来创建平衡二叉树,在课程比较靠后的位置会给大家讲解。最准确的二分搜索树的时间复杂度是O(h)

LeetCode中的集合相关问题以及更多集合相关问题。

804号问题:

https://leetcode-cn.com/problems/unique-morse-code-words/description/

将单词扔进集合中,然后看集合自动去重之后,剩下的集合大小。我们使用java的TreeSet进行实现,它的底层是一颗平衡二叉树(红黑树实现,不会出现最差情况,定义了更多操作)。

package cn.mtianyan.leetcode_804;import java.util.TreeSet;class Solution {    public int uniqueMorseRepresentations(String[] words) {
        String[] codes = {".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."};
        TreeSet set = new TreeSet();        for (String word : words) {
            StringBuffer res = new StringBuffer();            for (int i = 0; i < word.length(); i++) {
                res.append(codes[word.charAt(i) - 'a']); //a充当一个初始的偏移,a-a=0 b-a=1
            }
            set.add(res.toString());
        }        return set.size();
    }
}

运行结果:

5c127a1b0001de5605220162.jpg

有序集合和无序集合

我们之前的基于二分搜索树实现的集合,java中红黑树实现的集合本质都是有序的集合。

有序集合中的元素具有顺序性;可以从小到大遍历,可以查看前驱后继。

而链表实现的集合,实际上是一个无序的集合。我们只是根据元素插入的顺序决定了它在集合中的顺序,不能轻易的进行从小到大等操作。

就像我们刚做的那个LeetCode的题,就用不到集合的有序性,因此我们可以采用无序集合。基于哈希表的实现比搜索树更快。

搜索树保持了有序性,此时它付出的代价就是时间复杂度上稍微差于哈希表。

20180813182703_97qjcx_Screenshot.jpeg

多重集合

集合容纳重复元素,集合中的元素可以重复,在允许重复元素的二分搜索树基础上实现。

映射Map

函数一一映射。

20180813182848_ZW1WnY_Screenshot.jpeg



作者:天涯明月笙
链接:https://www.jianshu.com/p/52e224271f93


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消