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());
        }
    }
}
img_33179f17d25bdb5b0c80121539fddc13.jpe

傲慢与偏见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");

    }
}

运行结果:

img_38b1deabd5536916f5c751e6632f46c4.jpe

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

集合的时间复杂度分析

集合不涉及改。

  • 本来链表添加只需要O(1)时间复杂度,但是我们为了保证不重复,先调用了一遍contains,因此变成了O(n)
  • contains操作,我们必须要从头到尾扫一遍链表,复杂度O(n)
  • remove操作,要先找到待删除元素前一个元素,时间复杂度O(n)

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

img_3beea53cb28ce304d299e47ce5584f92.jpe

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

img_c41de9be61390d9a423800021b072f59.jpe

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

img_7b1a0c496fac6805370af4583550770e.jpe

h层一共有多少个节点?

img_a7566e1dccf30e30f2c009935380da05.jpe

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

img_bc4f7d36e5a76468cbc83578c313f234.jpe

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

logn和n的差距
img_4ee33f0957693a26259d71ab0df51785.jpe

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

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

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

1,2,3,4,5,6

img_0752ce1dee94727567e95ca7c320768c.jpe

它也可以对应一个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();
    }
}

运行结果:

img_de567d1fa63aa98c846eeaaf7c52fb07.jpe
有序集合和无序集合

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

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

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

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

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

img_33194835ff794df26baca60d94bd94e7.jpe
多重集合

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

映射Map

函数一一映射。

img_8ac2377b5857fa9d27cf02b460a8d75d.jpe

映射更形象的说法应该是字典。字典中从单词到释义: python中dict,c++ java中Map

img_a36df902bfa1c301be319af21bb0ab84.jpe

名册: 身份证号 对应到 人; 车牌号 对应 车; id 对应 信息;词频统计 单词 对应 频率。

Key 和 Value相对应的,存储(键,值)数据对的数据结构(Key, Value);根据键(Key) ,寻找值(Value)

看起来要存储两个数据,但是依然可以非常容易的使用链表或者二分搜索树进行实现。将原本存储的e,变成存储两个key 与 Value

class Node{
  K key;
  V value;
  Node left;
  Node right;
}
class Node{
  K key;
  V Value;
  Node next;
}

定义我们的映射接口:

Map<K,V>
void add(K,V)
V remove(K)
boolean contains(K)
V get(K)
void set(K,V)
int getSize( )
boolean isEmpty()

remove时只指定key就可以了。contains不去管可以对应的信息。

package cn.mtianyan.map;

public interface Map<K, V> {
    void add(K key, V value);

    V remove(K key);

    boolean contains(K key);

    V get(K key);

    void set(K key, V value);

    int getSize();

    boolean isEmpty();
}

上面就是我们定义好的映射类的接口。

package cn.mtianyan.map;

import cn.mtianyan.FileOperation;

import java.util.ArrayList;

public class LinkedListMap<K, V> implements Map<K, V> {
    /**
     * Node 节点类(private 不被用户感知) For Map
     */
    private class Node {
        public K key;
        public V value;
        public Node next; // c++实现时是指针

        /**
         * 传入key和value以及next节点的构造函数
         *
         * @param key
         * @param value
         * @param next
         */
        public Node(K key, V value, Node next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }

        /**
         * 只传入key,next置空。构造函数
         *
         * @param key
         */
        public Node(K key) {
            this.key = key;
            this.next = null;
        }


        /**
         * 默认构造函数,节点元素,next均为空。
         */
        public Node() {
            this(null, null, null);
        }

        /**
         * 打印节点元素信息。
         *
         * @return
         */
        @Override
        public String toString() {
            return key.toString() + ":" + value.toString();
        }
    }

    private Node dummyHead; // 虚拟头结点
    private int size; // 链表元素个数

    /**
     * LinkedListMap构造函数
     */
    public LinkedListMap() {
        dummyHead = new Node();
        size = 0;
    }

    /**
     * 获取链表中元素个数
     *
     * @return
     */
    public int getSize() {
        return size;
    }

    /**
     * 返回链表是否为空
     *
     * @return
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 辅助函数,给key,返回该节点
     *
     * @param key
     * @return
     */
    private Node getNode(K key) {
        Node cur = dummyHead.next;
        while (cur != null) {
            if (cur.key.equals(key))
                return cur;
            cur = cur.next;
        }
        return null;
    }


    /**
     * 传入key 返回是否包含该节点
     *
     * @param key
     * @return
     */
    @Override
    public boolean contains(K key) {
        return getNode(key) != null;
    }

    /**
     * 传入key,返回value。如果key不存在返回null
     *
     * @param key
     * @return
     */
    @Override
    public V get(K key) {
        Node node = getNode(key);
        return node == null ? null : node.value;
    }

    /**
     * 添加key与value,已存在的key,更新value
     *
     * @param key
     * @param value
     */
    @Override
    public void add(K key, V value) {
        // 去重
        Node node = getNode(key);
        if (node == null) {
            dummyHead.next = new Node(key, value, dummyHead.next);
            size++;
        } else {
            // 设计上如果传入已存在的key,更新value
            node.value = value;
        }
    }

    /**
     * 更新key对应的value值
     *
     * @param key
     * @param newValue
     */
    @Override
    public void set(K key, V newValue) {
        Node node = getNode(key);
        if (node == null) {
            throw new IllegalArgumentException(key + " doesn't exist");
        }
        node.value = newValue;
    }

    /**
     * 删除key对应的节点
     *
     * @param key
     * @return
     */
    @Override
    public V remove(K key) {

        Node prev = dummyHead;
        while (prev.next != null) {
            if (prev.next.key.equals(key))
                break;
            prev = prev.next;
        }

        if (prev.next != null) {
            Node delNode = prev.next;
            prev.next = delNode.next;
            delNode.next = null;
            size--;
            return delNode.value;
        }
        return null;
    }

    public static void main(String[] args) {

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

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

            LinkedListMap<String, Integer> map = new LinkedListMap<>();
            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"));
        }

        System.out.println();
    }
}
img_400023657845bc387b5cdfe3f1bb1af7.jpe

因为是由链表来实现的,而且在main中做了contains,set,add等一系列操作,所以有丢丢慢是正常的。

基于二分搜索树的映射实现

public class BSTMap<K extends Comparable<K>,V> implements Map<K,V> {
    
}

这里因为底层的数据结构,二分搜索树它要求传入的值,必须是可比较的。

package cn.mtianyan.map;

import cn.mtianyan.FileOperation;

import java.util.ArrayList;

public class BSTMap<K extends Comparable<K>, V> implements Map<K, V> {
    /**
     * 节点类对用户屏蔽
     */
    private class Node {
        public K key; // 节点key
        public V value;
        public Node left, right; // 左子树,右子树引用

        /**
         * 默认的节点构造函数
         *
         * @param key
         * @param value
         */
        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            left = null;
            right = null;
        }
    }

    private Node root;  // 根节点
    private int size;   // 树中元素的个数

    /**
     * 默认的二分搜索树构造函数
     */
    public BSTMap() {
        root = null;
        size = 0;
    }

    /**
     * 获取搜索树中节点元素个数
     *
     * @return
     */
    @Override
    public int getSize() {
        return size;
    }

    /**
     * 二分搜索树是否为空
     *
     * @return
     */
    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 向二分搜索树找那个添加新的键值对
     *
     * @param key
     * @param value
     */
    @Override
    public void add(K key, V value) {
        root = add(root, key, value);
    }

    /**
     * 返回插入新的键值对后二分搜索树的根
     *
     * @param node
     * @param key
     * @param value
     * @return
     */
    private Node add(Node node, K key, V value) {
        if (node == null) {
            size++;
            return new Node(key, value);
        }
        // 上面条件不满足,说明还得继续往下找左右子树为null可以挂载上的节点
        if (key.compareTo(node.key) < 0)
            // 如果e小于node.e,那么继续往它的左子树添加该节点,这里插入结果可能根发生了变化。
            node.left = add(node.left, key, value); // 新节点赋值给node.left,改变了二叉树
        else if (key.compareTo(node.key) > 0)
            // 大于,往右子树添加。
            node.right = add(node.right, key, value);
            // 如果相等
        else
            node.value = value;
        return node;
    }

    /**
     * 辅助方法: 返回以node为根节点到的二分搜索树中,key所在的节点。
     *
     * @param node
     * @param key
     * @return
     */
    private Node getNode(Node node, K key) {
        if (node == null)
            return null;
        if (key.compareTo(node.key) == 0)
            return node;
        else if (key.compareTo(node.key) < 0)
            return getNode(node.left, key);
        else // >0
            return getNode(node.right, key);
    }

    /**
     * 是否包含传入key所对应的键值对
     *
     * @param key
     * @return
     */
    @Override
    public boolean contains(K key) {
        return getNode(root, key) != null;
    }

    /**
     * 通过key获取对应的value
     *
     * @param key
     * @return
     */
    @Override
    public V get(K key) {
        Node node = getNode(root, key);
        return node == null ? null : node.value;
    }

    /**
     * 更新指定key的value值
     *
     * @param key
     * @param newValue
     */
    @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;
    }

    /**
     * 通过key删除对应的键值对
     *
     * @param key
     * @return
     */
    @Override
    public V remove(K key) {
        Node node = getNode(root, key);
        if (node != null) {
            root = remove(root, key);
            return node.value;
        }
        return null;
    }

    /**
     * 删除掉以node为根的二分搜索树中键为key的节点, 递归算法 返回删除节点后新的二分搜索树的根
     *
     * @param node
     * @param key
     * @return
     */
    private Node remove(Node node, K key) {

        if (node == null)
            return null;

        if (key.compareTo(node.key) < 0) {
            node.left = remove(node.left, key);
            return node;
        } else if (key.compareTo(node.key) > 0) {
            node.right = remove(node.right, key);
            return node;
        } else {   // e.compareTo(node.e) == 0

            // 待删除节点左子树为空的情况
            if (node.left == null) {
                Node rightNode = node.right;
                node.right = null;
                size--;
                return rightNode;
            }

            // 待删除节点右子树为空的情况
            if (node.right == null) {
                Node leftNode = node.left;
                node.left = null;
                size--;
                return leftNode;
            }

            // 待删除节点左右子树均不为空的情况

            // 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
            // 用这个节点顶替待删除节点的位置
            Node successor = minimum(node.right);
            successor.right = removeMin(node.right);
            successor.left = node.left;

            node.left = node.right = null;

            return successor;
        }
    }

    /**
     * 寻找二分搜索树的最小key(面向用户)
     *
     * @return
     */
    public K minimum() {
        if (size == 0)
            throw new IllegalArgumentException("BST is empty");

        Node minNode = minimum(root);
        return minNode.key;
    }

    /**
     * 返回以node为根的二分搜索树的最小key值所在的节点
     *
     * @param node
     * @return
     */
    private Node minimum(Node node) {
        if (node.left == null)
            return node;

        return minimum(node.left);
    }

    /**
     * 从二分搜索树中删除最小key值所在节点, 返回最小值
     *
     * @return
     */
    public K removeMin() {
        K ret = minimum();
        root = removeMin(root);
        return ret;
    }

    /**
     * 删除掉以node为根的二分搜索树中的最小key值节点 返回删除节点后新的二分搜索树的根
     *
     * @param node
     * @return
     */
    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;
    }

    public static void main(String[] args) {

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

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

            BSTMap<String, Integer> map = new BSTMap<>();
            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"));
        }

        System.out.println();
    }

}

运行结果:

img_556df8b31e02c719b55bfcf5d36d02e9.jpe

这里可以感受到BST的map相比链表实现的快了很多。

映射的复杂度分析和更多映射问题

package cn.mtianyan;

import cn.mtianyan.map.BSTMap;
import cn.mtianyan.map.LinkedListMap;
import cn.mtianyan.map.Map;

import java.util.ArrayList;

public class CompareTwoBstLInkedListMap {
    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) / 1e9;
    }

    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");

        System.out.println();

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

    }
}

运行结果:

img_2ec70e31ebeee9e93b4add84b91a8c98.jpe

可以看到,得到的数据是一致的,但是运行的时间上,链表实现的要比二分搜索树慢的多。

img_53a06e8892e6260cc9a94f796827062a.jpe
有序映射和无序映射

有序映射中的键具有顺序性,基于搜索树的实现;无序映射中的键没有顺序性,基于哈希表的实现。

多重映射

多重映射中的键可以重复。

img_9bfd7bb9fdbd1a937a1ad31b0de83f3f.jpe

这两种数据结构都既可以使用链表实现,又可以使用二分搜索树实现,实现时有很多相通之处,理解成映射也是一个集合,是一个键值对的集合,而且每一个键还携带了一个value。因为之前我们的集合只能存储一个元素,相应map重写了很多方法。

基于集合的实现,实现映射。 或基于映射的实现,实现集合。重定义集合中元素是一个键值对,并定义比较是根据key来比较的规则。

更常见基于映射,包装成集合。只考虑键,所有value为null。(因为映射方法多)。也可以从树结构开始就进行泛型。

LeetCode上更多集合和映射的问题

https://leetcode-cn.com/problems/intersection-of-two-arrays/description/

两个数组的交集: 输出结果中的每个元素一定是唯一的,借助集合。

package cn.mtianyan.leetcode_349;

import java.util.ArrayList;
import java.util.TreeSet;

class Solution {
    public int[] intersection(int[] nums1, int[] nums2) {
        TreeSet<Integer> set = new TreeSet<>();
        // 对于nums1进行去重存入set
        for (int num : nums1) {
            set.add(num);
        }
        ArrayList<Integer> list = new ArrayList<>();
        // 对于nums2进行遍历
        for (int num: nums2){
            // 如果是set中已经有的元素,那么就加入结果ArrayList中
            if (set.contains(num)){
                list.add(num);
                // 将set中该元素去除,那么num2中重复的不会再被匹配到,因为set是删一个少一个。
                set.remove(num);
            }
        }
        int[] res = new int[list.size()];
        for (int i = 0; i < list.size(); i++) {
            res[i] = list.get(i);
        }
        return res;
    }
}
img_d78088ceedea7f58b3d599b6d0bd2246.jpe

350 https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/description/

考虑重复元素的计数,使用映射实现。

package cn.mtianyan.leetcode_350;

import java.util.ArrayList;
import java.util.TreeMap;

class Solution {
    public int[] intersect(int[] nums1, int[] nums2) {
        TreeMap<Integer,Integer> map = new TreeMap<>();
        // 遍历nums1
        for (int num : nums1) {
            // 如果map中不包含nums1的数字。
            if (!map.containsKey(num))
                map.put(num,1); // map中put进该num,并为其计次1
            else
                map.put(num,map.get(num)+1); // 如果map中已经有该键值,num+1
        }
        // 此时map <1,2> <2,2>
        ArrayList<Integer> list = new ArrayList<>();
        // 遍历nums2
        for (int num : nums2) {
            // 如果map中已经包含该键值,说明是公共键
            if (map.containsKey(num)){
                list.add(num); // 公共键添加进值列表中 2 2
                map.put(num,map.get(num)-1); // <1,2> <2,1> | <2,0>
                if (map.get(num) == 0)
                    map.remove(num);
            }
        }

        int[] res = new int[list.size()];
        for (int i = 0; i < list.size(); i++) {
            res[i] = list.get(i);
        }
        return res;
    }
}

运行结果:

img_4a78b743bc7cf0f95e11c66ad0c8692f.jpe

标签: 哈希表的大多数问题也可以使用TreeSet 和Treemap;也可以使用HashSet HashMap。我们提供了两种底层实现,但是接口相同。Treeset TreeMap 底层是平衡二叉树(红黑树),而HashMap HashSet 底层是哈希表。底层不需要管,只需要实现相应的功能,不同实现时间复杂度不同。

TreeSet和TreeMap实现 时间上没有问题 logn也是很小的了。集合和映射是算法面试的重灾区。

下一章对于堆进行学习,堆的学习会让我们对于队列也会更深认识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值