九、集合Set和映射Map

233 阅读5分钟

关注公众号:EZ大数据(ID:EZ_DATA)。每天进步一点点,感觉很爽!

昨天说完二叉搜索树,今天来个简单点的:集合Set和映射Map。然后我们再使用链表和二叉树来初步实现Set和Map的功能,并研究下相关操作的时间复杂度。

OK,正餐开始(我是来对比树和链表的,别搞错)!

说起集合Set,其里面的元素不会重复,所以,经常使用集合来做去重操作。而日常工作中,典型的应用有:客户统计、文本词汇量统计等。

首先我们使用之前的链表来实现集合Set。

void add(E)

void remove(E)

boolean contains(E)

int getSize()

boolean isEmpty()

话不多少,直接上代码:

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

接下来我们基于昨天的二分搜索树,来实现Set的功能。具体如下:

public class BSTSet<E extends Comparable<E>> implements Set<E> {

    private BST<E> bst;
    public BSTSet(){
        bst = new BST<>();
    }

    @Override
    public int getSize(){
        return bst.size();
    }

    @Override
    public boolean isEmpty(){
        return bst.isEmpty();
    }

    @Override
    public void add(E e){
        bst.add(e);
    }

    @Override
    public boolean contains(E e){
        return bst.contains(e);
    }

    @Override
    public void remove(E e){
        bst.remove(e);
    }
}

接下来我们分析下二者在时间复杂度上的区别。

首先对于链表来说,其添加元素add时,需要判断是否包含contains,所以其时间复杂度为O(n),删除操作也为O(n)。

那么对于二分搜索树来说,假如h为二分搜索树的高度,无论添加、删除、查找操作,时间复杂度都为O(h)。

接下来我们就研究下高度h和节点数n的关系,对于二分搜索树来说,其h层的元素个数最多为2^(h-1)个,那么一个h层的满二叉树其元素个数即为:

n = 2^0+2^1+2^2+...+2^(h-1)=2^h-1,用对数来表示就是:h=log(n+1)。那么用大O表示法就是O(h)=O(logn)。

对于二叉树来说,其最坏的情况就是退化为链表的形式,其最坏时间复杂度为O(n)。小伙伴们有兴趣的话,可以计算下,当n逐渐变大时,O(logn)和O(n)的区别。

当然,我写了个小脚本,来测验二者的区别:

    public 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) / 1000000000.0;
    }

计算结果如下:

Total different words: 6530
BST Set: 0.1222529s

Total different words: 6530
Linked List Set: 2.1274651s

由此可以看出,用二叉树的效率比链表要快很多很多。同时,当时间复杂度涉及到log时,基本都是与树有关。

现在我们来看看映射Map,对于Map来说,它是一种存储(键, 值)数据对的数据结构(key, value)。比如x=f(n)这样的映射关系。日常使用中,我们会根据键(key),来快速查询值(value)。

同样,我们先用链表来实现映射Map,具体功能如下:

void add(k, v)

V remove(k)

boolean contains(k)

V get(k)

void set(k)

int getSize()

boolean isEmpty()

限于篇幅,我就把重要的功能实现代码展示一波:

    private Node getNode(K key) {
        Node cur = dummyHead.next;
        while (cur != null) {
            if (cur.key.equals(key)) {
                return cur;
            }
            cur = cur.next;
        }
        return null;
    }

    @Override
    public boolean contains(K key) {
        return getNode(key) != null;
    }

    @Override
    public V get(K key) {
        Node node = getNode(key);
        return node == null ? null : node.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 {
            node.value = value;
        }
    }

    @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;
    }

    @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;
    }

整体基于链表来实现,设定虚拟头结点dummyHead,也算是又复习了一波链表。

下面来看基于二分搜索树实现的Map。

    // 向二分搜索树中添加新的元素(key, value)
    @Override
    public void add(K key, V value){
        root = add(root, key, value);
    }

    // 向以node为根的二分搜索树中插入元素(key, value),递归算法
    // 返回插入新节点后二分搜索树的根
    private Node add(Node node, K key, V value){

        if(node == null){
            size ++;
            return new Node(key, value);
        }

        if(key.compareTo(node.key) < 0) {
            node.left = add(node.left, key, value);
        } else if(key.compareTo(node.key) > 0) {
            node.right = add(node.right, key, value);
        } else // key.compareTo(node.key) == 0
        {
            node.value = value;
        }

        return node;
    }

    // 删除元素
    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{   // key.compareTo(node.key) == 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, value)形式的数据,然后我们比较key值的大小,来实现左右子树的相关逻辑。

同样,我们最后来对比二者的时间复杂度,基于对链表的熟悉,我们知道无论是添加add、删除remove、修改set、查询contains等操作,其时间复杂度均为O(n)。而根据上述对于树结构的时间复杂度来看,其相关操作均为O(logn)。

OK,今天关于集合Set和映射Map的总结到这里,其实呢,主要是对比了链表和二叉树的时间复杂度……

那么,涉及到时间复杂度包含log的,基本都使用到树结构。另外,对于Set和Map,更多的东西,后续我会更新哈希表相关的知识点。

好了,今天就到这里。

拜了个拜~