跳到正文
返回

数据结构之集合与映射

发表于 更新于
浏览量: --

封面画师:ツチヤ     封面ID:82630820

本文参考视频:小马哥教育(SEEMYGO) 2019 年 恋上数据结构与算法(第一季)

源码仓库:mofan212/data-structure-and-algorithm (github.com)

辅助学习网址:数据结构和算法动态可视化

1. 集合

1.1 简介

集合(Set),特点是 不存放重复的元素。根据这个特性,集合可用于以下场景:

集合接口:

public interface Set<E> {
    int size();

    boolean isEmpty();

    void clear();

    boolean contains(E element);

    void add(E element);

    void remove(E element);

    void traversal(Visitor<E> visitor);

    public static abstract class Visitor<E> {
        boolean stop;

        // 记得设置成公共的方法
        public abstract boolean visit(E element);
    }
}

思考:集合的内部实现能否直接利用学过的数据结构?

答案是可以的。可以使用 动态数组链表红黑树 来实现。

1.2 ListSet

编写接口 Set,其中包含集合的一些方法:

public interface Set<E> {
    int size();

    boolean isEmpty();

    void clear();

    boolean contains(E element);

    void add(E element);

    void remove(E element);

    /**
         * 遍历接口
         * 集合内元素是无序、互斥的
         * 动态数组、链表有索引,可以直接循环遍历
         * 集合没有,只能提供接口
         */
    void traversal(Visitor<E> visitor);

    public static abstract class Visitor<E> {
        boolean stop;

        public abstract boolean visit(E element);
    }
}

使用链表实现集合,编写 ListSet,链表使用以前编写的双向链表(也可以使用 JDK 的双向链表):

// 使用链表实现集合
public class ListSet<E> implements Set<E> {
    private List<E> list = new LinkedList<>();

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

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

    @Override
    public void clear() {
        list.clear();
    }

    @Override
    public boolean contains(E element) {
        return list.contains(element);
    }

    @Override
    public void add(E element) {
        // if (list.contains(element)) return;
        int index = list.indexOf(element);
        if (index != List.ELEMENT_NOT_FOUND) {
            // 存在相同元素时,新元素覆盖旧元素
            list.set(index, element);
        } else {
            // 不存在相同元素时,直接添加
            list.add(element);
        }
    }

    @Override
    public void remove(E element) {
        int index = list.indexOf(element);
        if (index != List.ELEMENT_NOT_FOUND) {
            list.remove(index);
        }
    }

    @Override
    public void traversal(Visitor<E> visitor) {
        if (visitor == null) return;
        int size = list.size();
        for (int i = 0; i < size; i++) {
            if (visitor.visit(list.get(i))) return;
        }
    }
}

编写测试代码进行测试:

public static void main(String[] args) {
    Set<Integer> listSet = new ListSet<>();
    listSet.add(10);
    listSet.add(11);
    listSet.add(11);
    listSet.add(12);
    listSet.add(10);
    listSet.traversal(new Set.Visitor<Integer>() {
        @Override
        public boolean visit(Integer element) {
            System.out.println(element);
            return false;
        }
    });
}

预测输出: 10 11 12

1.3 TreeSet

使用上述的集合接口 Set,然后使用二叉树来实现集合。编写 TreeSet,二叉树使用以前学习的红黑树,需要将以前的代码拷贝到当前 Project 或 Module 中。

如果使用以前红黑树的代码,需要修改一个地方。给表示二叉树的类 BinaryTree 中的抽象接口 Visitor 的抽象方法 visit(E element) 添加一个访问修饰符 public。最初这个抽象方法的访问修饰符是缺省的,如今表示红黑树的类和表示集合的类处于不同的包中,因此需要添加一个 public,否则无法访问。

public static abstract class Visitor<E> {
    // 遍历终止标志 true:终止 false:全遍历
    boolean stop;

    // 如果返回true,表示停止遍历
    public abstract boolean visit(E element);
}

编写 TreeSet 的代码:

public class TreeSet<E> implements Set<E> {
    private RBTree<E> tree = new RBTree<>();

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

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

    @Override
    public void clear() {
        tree.clear();
    }

    @Override
    public boolean contains(E element) {
        return tree.contains(element);
    }

    @Override
    public void add(E element) {
        // 使用红黑树时,默认去重
        tree.add(element);
    }

    @Override
    public void remove(E element) {
        tree.remove(element);
    }

    @Override
    public void traversal(Visitor<E> visitor) {
        tree.inorder(new BinaryTree.Visitor<E>() {
            @Override
            public boolean visit(E element) {
                return visitor.visit(element);
            }
        });
    }
}

测试代码:

public static void main(String[] args) {
    Set<Integer> treeSet = new TreeSet<>();
    treeSet.add(12);
    treeSet.add(10);
    treeSet.add(7);
    treeSet.add(11);
    treeSet.add(10);
    treeSet.add(11);
    treeSet.add(9);
    treeSet.traversal(new Set.Visitor<Integer>() {
        @Override
        public boolean visit(Integer element) {
            System.out.println(element);
            return false;
        }
    });
}

1.4 性能对比

从时间复杂度分析,使用双向链表实现的集合中:

而使用红黑树实现集合的三种功能的时间复杂度都为 O(logn)O(logn)

除此之外,我们可以添加大量的数据进集合中,比较这两种方式的性能。比如,统计 JDK 源码中有多少个不一样的单词,然后比较两种实现方式所消耗的时间,最后可以得出:

红黑树实现的集合性能远大于链表实现的集合性能

1.5 局限性

使用红黑树实现的集合虽然流弊,但是它有一个局限性。😲

红黑树是二叉搜索树的一种,因此红黑树的节点除了满足自身特定的要求外,默认满足二叉搜索树的特点。二叉搜索树有一个很重要的特点: 节点具有可比较性 。向使用红黑树实现的集合中添加元素时,这些元素必须具备可比较性,否则无法添加,而对于使用链表实现的集合就没有这样的限制。

由于这个特点,继续完善 TreeSet,添加两个构造方法,就像编写二叉搜索树的代码一样,让具有可比较性的元素才能添加至红黑树实现的集合中:

public class TreeSet<E> implements Set<E> {
    private RBTree<E> tree;

    public TreeSet() {
        this(null);
    }

    public TreeSet(Comparator<E> comparator) {
        tree = new RBTree<>(comparator);
    }

    // --snip--
}

如果有一个需求:「实现添加没有可比较性元素至集合,而性能还能达到红黑树那样高性能」,这样的需求能实现吗?

答案是肯定的,可以使用 哈希表,但本文暂不介绍。

2. 映射

2.1 简介

映射(Map),在有些编程语言中也被叫做字典(dictionary),比如 Python、 Objective-C、Swift 等。

那什么是映射呢?所谓映射、字典,就是可以通过一个键(Key)找到唯一的值(value)。键不能重复,一个键对应一个值;值可以重复,一个值可以被多个键指向。

映射接口:

public interface Map<K, V> {
    int size();

    boolean isEmpty();

    void clear();

    V put(K key, V value);

    V get(K key);

    V remove(K key);

    boolean containsKey(K key);

    boolean containsValue(V value);

    void traversal(Visitor<K, V> visitor);

    public static abstract class Visitor<K, V> {
        boolean stop;

        // 记得设置成公共的方法
        public abstract boolean visit(K key, V value);
    }
}

思考:映射的内部实现能否直接利用学过的数据结构?

答案是可以的。类似 Set,Map 可以直接利用之前学习的 链表BST(AVL 树、红黑树)等数据结构来实现。

前面设计集合时,已经知道红黑树的性能远好于链表,因此映射的底层也选用红黑树。

2.2 节点设计

如果使用红黑树实现映射,那么红黑树的一个节点需要存储两个值:一个表示键,一个表示值。

前面编写的红黑树中一个节点只能存储一个值,同时范型也只有一个 RBTree<E>,而这里需要两个范型。只写 K 不行,只写 V 也不行,可以额外定义一个类,这个类有两个范型 KV<K, V>,在这个类中定义映射的范型,最后在红黑树的范型中嵌套这个类:

public class TreeMap<K, V> implements Map<K, V> {
    private static class KV<K, V> {
        K key;
        V value;
    }

    private RBTree<KV<K, V>> rbTree = new RBTree<>();

    // 省略实现的方法
    // --snip--
}

上述方式虽然可以实现,但是使用嵌套不是很「优雅」🍷,因此需要换一种方式。直接将 TreeMap 看成一个红黑树的类,只不过这是个特殊的红黑树,不仅拥有红黑树的特点,还满足了映射的要求。

可以将红黑树中涉及的节点类 NodeBRNode 的部分代码拷贝到这个类中。最终结果如下:

public class TreeMap<K, V> implements Map<K, V> {
    private static final boolean RED = false;
    private static final boolean BLACK = true;

    // 省略实现的方法
    // --snip--

    private static class Node<K, V> {
        K key;
        V value;
        boolean color = RED;
        Node<K, V> left;   // 左节点
        Node<K, V> right;  // 右节点
        Node<K, V> parent; // 父节点

        public Node(K key, V value, Node<K, V> parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }

        public boolean isLeaf() {
            return left == null && right == null;
        }

        public boolean hasTwoChildren() {
            return left != null && right != null;
        }

        // 判断当前节点是否是其父节点的左子节点
        public boolean isLeftChild() {
            return parent != null && this == parent.left;
        }

        // 判断当前节点是否是其父节点的右子节点
        public boolean isRightChild() {
            return parent != null && this == parent.right;
        }

        // 获取兄弟节点
        public Node<K, V> sibling() {
            if (isLeftChild()) {
                return parent.right;
            }
            if (isRightChild()) {
                return parent.left;
            }
            return null;
        }
    }
}

2.3 接口实现

既然将 TreeMap<K, V> 看成一棵红黑树,那么就要在这个类中实现红黑树的功能。利用以前编写的红黑树的代码进行改造,改造的主要内容如下:

理一下需要哪些方法:

⚠️ 警告!以下涉及大量代码,请注意折叠! ⚠️

⚠️ 已删除部分代码注释,可前往 数据结构之红黑树 一文查看。 ⚠️

完整代码

public class TreeMap<K, V> implements Map<K, V> {

    private static final boolean RED = false;
    private static final boolean BLACK = true;
    private int size;   // 节点个数
    private Node<K, V> root; // 根节点
    private Comparator<K> comparator;

    public TreeMap() {
        this(null);
    }

    public TreeMap(Comparator<K> comparator) {
        this.comparator = comparator;
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public void clear() {
        root = null;
        size = 0;
    }

    @Override
    public V put(K key, V value) { // 使用Key进行比较
        keyNotNullCheck(key);
        // 添加第一个节点 (根节点)
        if (root == null) {
            root = new Node<>(key, value, null);
            size++;
            // 添加节点后的处理 传入参数类型为Node
            afterPut(root);
            return null;
        }
        // 添加的不是第一个节点
        // 找到插入节点的父节点
        Node<K, V> parent = root;
        Node<K, V> node = root;
        int cmp = 0;
        while (node != null) {
            cmp = compare(key, node.key);
            parent = node;  // 保存父节点
            if (cmp > 0) {
                node = node.right;
            } else if (cmp < 0) {
                node = node.left;
            } else { // 相等
                node.key = key; // 相等时覆盖
                V oldValue = node.value;
                node.value = value;
                return oldValue;
            }
        }
        // 看看插入到父节点的哪个位置
        Node<K, V> newNode = new Node<>(key, value, parent);
        if (cmp > 0) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
        size++;
        // 添加节点后的处理 传入参数类型为Node
        afterPut(newNode);
        return null;
    }

    @Override
    public V get(K key) {
        Node<K, V> node = node(key);
        return node != null ? node.value : null;
    }

    @Override
    public V remove(K key) {
        return remove(node(key));
    }

    @Override
    public boolean containsKey(K key) {
        return node(key) != null;
    }

    @Override
    public boolean containsValue(V value) {
        /** 由于 value 不具备可比较性,同时允许为 null
             *  因此,只能遍历红黑树节点,一个一个找
             *  在这里使用层序遍历
             * */
        if (root == null) {
            return false;
        }
        Queue<Node<K, V>> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            Node<K, V> node = queue.poll();
            if (valEquals(value, node.value)) return true;
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
        return false;
    }

    @Override
    public void traversal(Visitor<K, V> visitor) {
        if (visitor == null) return;
        traversal(root, visitor);
    }

    // 使用中序遍历
    private void traversal(Node<K, V> node, Visitor<K, V> visitor) {
        if (node == null || visitor.stop) return;
        traversal(node.left, visitor);
        if (visitor.stop) return;
        visitor.visit(node.key, node.value);
        traversal(node.right, visitor);
    }

    private boolean valEquals(V v1, V v2) {
        return v1 == null ? v2 == null : v1.equals(v2);
    }

    private V remove(Node<K, V> node) {
        if (node == null) return null;
        size--;
        V oldValue = node.value;
        if (node.hasTwoChildren()) { // 度为2的节点
            // 找到后继节点
            Node<K, V> s = successor(node);
            // 用后继节点的值覆盖被删除节点的值
            node.key = s.key;
            node.value = s.value;
            // 变量node指向其后继节点,等待后续删除
            node = s;
        }
        // 删除node节点(node的度必然为1或0)
        Node<K, V> replacement = node.left != null ? node.left : node.right;
        if (replacement != null) {   // node度为1
            // 更改parent
            replacement.parent = node.parent;
            // 更改node的parent的left、right的指向
            if (node.parent == null) { // node度为1,且为根节点
                root = replacement;
            } else if (node == node.parent.left) {
                node.parent.left = replacement;
            } else { // node == node.parent.right
                node.parent.right = replacement;
            }

            afterRemove(replacement);
        } else if (node.parent == null) {     // node度为0,是叶子节点,并且是根节点
            root = null;
            // 被删除的节点
            // 删除根节点后其实可以不用进行操作,即: afterRemove可以省略
            // afterRemove(node);
        } else { // node是叶子节点,但不是根节点
            if (node == node.parent.left) {
                node.parent.left = null;
            } else {
                node.parent.right = null;
            }
            // 被删除的节点
            afterRemove(node);
        }
        return oldValue;
    }

    private void afterRemove(Node<K, V> node) {
        if (isRed(node)) {
            black(node);
            return;
        }
        // 获取被删除节点的父节点
        Node<K, V> parent = node.parent;
        // 被删除节点是根节点
        if (parent == null) return;

        boolean left = parent.left == null || node.isLeftChild();
        Node<K, V> sibling = left ? parent.right : parent.left;

        if (left) { // 被删除节点在左边,兄弟节点在右边
            if (isRed(sibling)) { // 被删除节点兄弟节点是红色
                black(sibling);
                red(parent);
                rotateLeft(parent);
                // 更换兄弟
                sibling = parent.right;
            }
            // 此时兄弟节点必然是黑色
            if (isBlack(sibling.left) && isBlack(sibling.right)) {
                // 兄弟节点一个红色子节点都没,父节点向下跟兄弟节点合并
                boolean parentBlack = isBlack(parent);
                black(parent);
                red(sibling);
                if (parentBlack) {
                    // 父节点为黑色时,下溢,进行递归
                    afterRemove(parent);
                }
            } else { // 兄弟节点至少有一个红色子节点,向兄弟节点借元素
                // 兄弟节点左子节点是黑色,先对兄弟进行旋转
                if (isBlack(sibling.right)) {
                    rotateRight(sibling);
                    // 旋转后,重置兄弟节点位置
                    sibling = parent.right;
                }
                // 先染色兄弟节点,再染色父节点:兄弟节点跟随父节点染色
                color(sibling, colorOf(parent));
                black(sibling.right);
                black(parent);
                rotateLeft(parent);

            }
        } else { // 被删除节点在右边,兄弟节点在左边
            if (isRed(sibling)) { // 被删除节点兄弟节点是红色
                black(sibling);
                red(parent);
                rotateRight(parent);
                // 更换兄弟
                sibling = parent.left;
            }
            // 此时兄弟节点必然是黑色
            if (isBlack(sibling.left) && isBlack(sibling.right)) {
                // 兄弟节点一个红色子节点都没,父节点向下跟兄弟节点合并
                boolean parentBlack = isBlack(parent);
                black(parent);
                red(sibling);
                if (parentBlack) {
                    afterRemove(parent);
                }
            } else { // 兄弟节点至少有一个红色子节点,向兄弟节点借元素
                // 兄弟节点左子节点是黑色,先对兄弟进行旋转
                if (isBlack(sibling.left)) {
                    rotateLeft(sibling);
                    sibling = parent.left;
                }
                color(sibling, colorOf(parent));
                black(sibling.left);
                black(parent);
                rotateRight(parent);
            }
        }
    }

    private Node<K, V> predecessor(Node<K, V> node) {
        if (node == null) return null;
        // 前驱节点在左子树中
        Node<K, V> p = node.left;
        if (p != null) {
            while (p.right != null) {
                p = p.right;
            }
            return p;
        }
        // 从“祖宗节点”中寻找前驱节点
        while (node.parent != null && node == node.parent.left) {
            node = node.parent;
        }

        // node.parent == null || node == node.parent.right
        return node.parent;
    }

    private Node<K, V> successor(Node<K, V> node) {
        if (node == null) return null;
        // 后继节点在右子树中
        Node<K, V> p = node.right;
        if (p != null) {
            while (p.left != null) {
                p = p.left;
            }
            return p;
        }
        // 从“祖宗节点”中寻找后继节点
        while (node.parent != null && node == node.parent.right) {
            node = node.parent;
        }

        // node.parent == null || node == node.parent.left
        return node.parent;
    }

    // 根据元素找寻节点
    private Node<K, V> node(K key) {
        Node<K, V> node = root;
        while (node != null) {
            int cmp = compare(key, node.key);
            if (cmp == 0) return node;
            if (cmp > 0) {
                node = node.right;
            } else { // cmp < 0
                node = node.left;
            }
        }
        return null;
    }

    private void afterPut(Node<K, V> node) {
        Node<K, V> parent = node.parent;
        // 添加节点是根节点时 或 上溢到根节点
        if (parent == null) {
            black(node);
            return;
        }
        // 如果添加节点的父节点是黑色,直接返回
        if (isBlack(parent)) return;
        // 获取叔父节点
        Node<K, V> uncle = parent.sibling();
        // 获取祖父节点
        Node<K, V> grand = red(parent.parent);
        if (isRed(uncle)) { // 叔父节点是红色时[B树节点上溢]
            black(parent);
            black(uncle);
            // 把祖父节点当作新添加的节点
            afterPut(grand);
            return;
        }
        // 叔父节点不是红色时
        if (parent.isLeftChild()) { // L
            if (node.isLeftChild()) { // LL
                black(parent);
            } else { // LR
                black(node);
                rotateLeft(parent);
            }
            rotateRight(grand);
        } else { // R
            if (node.isLeftChild()) { // RL
                black(node);
                rotateRight(parent);
            } else { // RR
                black(parent);
            }
            rotateLeft(grand);
        }
    }

    // 左旋转
    private void rotateLeft(Node<K, V> grand) {
        Node<K, V> parent = grand.right;
        Node<K, V> child = parent.left;
        // 旋转
        grand.right = child;
        parent.left = grand;
        // 旋转后
        afterRotate(grand, parent, child);
    }

    // 右旋转
    private void rotateRight(Node<K, V> grand) {
        Node<K, V> parent = grand.left;
        Node<K, V> child = parent.right;
        // 旋转
        grand.left = child;
        parent.right = grand;
        // 旋转后
        afterRotate(grand, parent, child);
    }

    private void afterRotate(Node<K, V> grand, Node<K, V> parent, Node<K, V> child) {
        // 让 parent 成为根节点
        parent.parent = grand.parent;
        if (grand.isLeftChild()) {
            grand.parent.left = parent;
        } else if (grand.isRightChild()) {
            grand.parent.right = parent;
        } else { // 没有父节点, grand是根节点
            root = parent;
        }
        // 更新其他节点的父节点
        if (child != null) {
            child.parent = grand;
        }
        grand.parent = parent;
    }

    /****辅助方法****/
    // 节点染色
    private Node<K, V> color(Node<K, V> node, boolean color) {
        if (node == null) return node;
        node.color = color;
        return node;
    }

    // 节点染成红色
    private Node<K, V> red(Node<K, V> node) {
        return color(node, RED);
    }

    // 节点染成黑色
    private Node<K, V> black(Node<K, V> node) {
        return color(node, BLACK);
    }

    // 查看某一节点的颜色
    private boolean colorOf(Node<K, V> node) {
        return node == null ? BLACK : node.color;
    }

    // 判断节点颜色是否是黑色
    private boolean isBlack(Node<K, V> node) {
        return colorOf(node) == BLACK;
    }

    // 判断节点颜色是否是红色
    private boolean isRed(Node<K, V> node) {
        return colorOf(node) == RED;
    }


    private int compare(K k1, K k2) {
        if (comparator != null) {
            return comparator.compare(k1, k2);
        }
        // 未创建比较器时,强制节点拥有比较性
        return ((Comparable<K>) k1).compareTo(k2);
    }

    // Key 空值检查
    private void keyNotNullCheck(K key) {
        if (key == null) {
            throw new IllegalArgumentException("element must not be null");
        }
    }

    private static class Node<K, V> {
        K key;
        V value;
        boolean color = RED;
        Node<K, V> left;   // 左节点
        Node<K, V> right;  // 右节点
        Node<K, V> parent; // 父节点

        public Node(K key, V value, Node<K, V> parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }


        public boolean isLeaf() {
            return left == null && right == null;
        }

        public boolean hasTwoChildren() {
            return left != null && right != null;
        }

        // 判断当前节点是否是其父节点的左子节点
        public boolean isLeftChild() {
            return parent != null && this == parent.left;
        }

        // 判断当前节点是否是其父节点的右子节点
        public boolean isRightChild() {
            return parent != null && this == parent.right;
        }

        // 获取兄弟节点
        public Node<K, V> sibling() {
            if (isLeftChild()) {
                return parent.right;
            }
            if (isRightChild()) {
                return parent.left;
            }
            return null;
        }
    }
}

测试代码:

static void test1() {
    Map<String, Integer> map = new TreeMap<>();
    map.put("c", 2);
    map.put("a", 5);
    map.put("b", 6);
    map.put("a", 8);
    // 遍历出来结果是从小到大的
    map.traversal(new Map.Visitor<String, Integer>() {
        @Override
        public boolean visit(String key, Integer value) {
            System.out.println(key + "_" + value);
            return false;
        }
    });
}

根据阅读 JDK 源码得到的启发,删除红黑树的根节点时,其实可以不用进行任何操作,直接将根节点设置为 null 即可。同时,将 remove() 方法中删除根节点时调用的 afterRemove() 方法删除。但是不能够删除 afterRemove() 方法中判断节点的父节点为空时进行的操作,因为这个判断不仅在删除节点是根节点时要执行,下溢到根节点时也要执行。

根据上述操作,向红黑树中添加第一个节点(根节点)时,只需要将该节点染黑即可,不需要进行其他操作。即: 删除 put() 方法中添加根节点后调用的 afterPut() 方法,并在其前添加 black(root);。注意不能够删除 afterPut() 方法中判断节点的父节点为空时进行的操作,因为这个判断不仅在添加节点是根节点时要执行,上溢到根节点时也要执行。

2.4 Map 与 Set

在设计的红黑树中,如果添加了重复元素,默认新元素会覆盖旧元素,即: 默认去重。

前面的映射 Map 是用红黑树实现的,同时是将 Map 中的 Key 作为比较标准,即: Key 是唯一的。

在 Map 中,Key 和 Value 是一一对应的,如果只看 Key,那么 Key 是唯一的。这似乎与集合 Set 有些关系。

是的! 集合 Map 只看 Key 或去掉 Value(Map 的所有 Key 组合在一起),就可以看成集合 Set。因此, Set 可以间接利用 Map 来做内部实现。

具体实现

拷贝一份集合 Set 的接口到当前目录中,然后编写 TreeSet 类。

/**
 * @author 默烦
 * @date 2020/7/21
 */
public class TreeSet<E> implements Set<E> {
    Map<E, Object> map = new TreeMap<>();

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

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

    @Override
    public void clear() {
        map.clear();
    }

    @Override
    public boolean contains(E element) {
        return map.containsKey(element);
    }

    @Override
    public void add(E element) {
        map.put(element, null);
    }

    @Override
    public void remove(E element) {
        map.remove(element);
    }

    @Override
    public void traversal(Visitor<E> visitor) {
        map.traversal(new Map.Visitor<E, Object>() {
            @Override
            public boolean visit(E key, Object value) {
                return visitor.visit(key);
            }
        });
    }
}

测试方法:

static void test2() {
    Set<String> set = new TreeSet<>();
    set.add("a");
    set.add("c");
    set.add("b");
    set.add("c");
    set.add("c");
    set.traversal(new Set.Visitor<String>() {
        @Override
        public boolean visit(String element) {
            System.out.println(element);
            return false;
        }
    });
}

2.5 代码启发

在编写实现 TreeMap 的方法时,其中有一个接口是判断当前 Map 中是否存在某个 value,由于在设计时 value 不具备比较性,且 value 可以为空。因此在实现这个接口时,需要编写一个方法 valEquals(),用于判断传入的值与当前 Map 的 value 是否相等。这个方法是像下面这样的:

private boolean valEquals(V v1, V v2) {
    return v1 == null ? v2 == null : v1.equals(v2);
}

说明一下上述代码的含义: 先判断 v1 是否为 null,如果 v1 为 null,再判断 v2 是否为 null,v2 为 null 时,返回 true,不为 null 时,返回 false;如果最初判断 v1 不为 null,则直接使用 equals() 方法判断 v1 是否与 v2 相等,相等返回 true,不相等返回 false

根据上述这段代码,可以回想起在 实现动态数组或链表 时,有一个名为 indexOf() 的方法,这个方法用于判断某个元素在当前数组或链表中的位置。

在动态数组和链表中允许元素为 null,可以向其中插入 null。在实现 indexOf() 时,需要判断传递的值是否 null,如果不判断直接使用 equals() 进行比较,会出现空指针异常。

动态数组中的 indexOf() 方法如下:

public int indexOf(E element) {
    // 处理空值
    if (element == null) { // 1
        for (int i = 0; i < size; i++) {
            if (elements[i] == null) return i;
        }
    } else {
        for (int i = 0; i < size; i++) {
            if (element.equals(elements[i])) return i; // n
        }
    } // 一共 n + 1 次判断
    return ELEMENT_NOT_FOUND;
}

根据 valEquals() 方法的启发,可以将 indexOf() 方法改成成以下代码:

public int indexOf(E element) {
    for (int i = 0; i < size; i++) {
        // 一共 2n 次判断
        if (valEquals(element, elements[i])) return i; // 2n
    }
    return ELEMENT_NOT_FOUND;
}

private boolean valEquals(Object v1, Object v2) {
    // 判断两次
    return v1 == null ? v2 == null : v1.equals(v2);
}

这样改写可以减少了代码量,但是会存在另外一个问题: 判断的次数。

注意:这两种方式的时间复杂度是一样的。

对于第一种方式: 如果传递的值为 null 只需要判断一次;如果不为 null,最坏情况下需要判断 nn 次。总共需要判断 (n+1)(n + 1) 次。

对于第二种方式: 无论传递的值是否为 null,都需要判断 nn 次。总共需要判断 2n2n 次。

虽然第二种减少代码量,但是效率相对于第一种是要低一点的。因此,在 JDK 源码中,动态数组也是采用的第一种方式进行比较。

同样的还有在 TreeMap 中的添加节点时执行的代码,由于使用的是红黑树实现的,添加节点时一定会进行比较,然后满足二叉搜索树的性质时才成功添加。

下面是自行实现的比较方法:

自己编写的put方法节点比较

下面是 JDK 源码的比较方式:

JDK源码的put方法节点比较

经过比较,可以很简单地发现:JDK 源码的判断次数比自行实现的判断次数少,也说明了 JDK 源码的效率更高。

果然还是 JDK 流弊!

流弊

3. 额外补充

3.1 NavigableSet

NavigableSet 是 Java 集合框架中 SortedSet 接口的子接口,扩展了 SortedSet 的功能,提供了更强大的导航方法,允许开发者基于元素的排序规则快速查找、遍历或获取子集。

public interface NavigableSet<E> extends SortedSet<E> {
    // --snip--
}

在 JDK 中,TreeSetNavigableSet 最常用的实现类,基于红黑树实现,保证元素有序且操作时间复杂度为 O(logn)O(logn)

在日常刷题中,某些场景下可能需要用到 TreeSet,并且会使用到 NavigableSet 中的一些方法,因此在此简单介绍下。

除此之外,TreeSet 对应了 TreeMapNavigableSet 也对应的 NavigableMap,两个接口中的方法很类似,关于 NavigableMap 就不再赘述。

查找与目标元素最接近的方法

方法描述
E lower(E e)返回严格小于 e 的最大元素,没有返回 null
E floor(E e)返回小于或等于 e 的最大元素,没有返回 null
E ceiling(E e)返回大于或等于 e 的最小元素,没有返回 null
E higher(E e)返回严格大于 e 的最小元素,没有返回 null
NavigableSet<Integer> set = new TreeSet<>(Arrays.asList(2, 4, 6, 8));
set.lower(5);   // 返回 4(严格小于5的最大元素)
set.floor(5);   // 返回 4(小于等于5的最大元素)
set.ceiling(5); // 返回 6(大于等于5的最小元素)
set.higher(5);  // 返回 6(严格大于5的最小元素)

获取并移除元素

方法描述
E pollFirst()移除并返回第一个元素,集合为空时返回 null
E pollLast()移除并返回最后一个元素,集合为空时返回 null

继承 SortedSet 的方法

方法描述
E first()返回第一个元素(集合为空抛出异常)
E last()返回最后一个元素(集合为空抛出异常)
如果这篇文章对你有帮助,可以通过
支付宝
支付宝
微信
微信
请我喝杯 Coffee ☕


上一篇
数据结构之哈希表
下一篇
帅气地使用 IDEA