在上一章的查找中, 即使是二分搜索, 也要一点一点的对比过来, 有没有一种办法, 可以根据要查找的内容, 直接定位到数组中的具体一个索引, 而不是反复操作索引和进行对比.

这个思想就促使了一种通常叫字典的数据结构的诞生, 就像一本字典一样, 每一个词对应着一个页码, 我无需一页一页去翻直到找到那个词语, 而是可以通过一种方式(查字典的索引), 直接得到那个页码, 再翻过去就行了.

这种数据结构本质上是一个映射关系, 即将一个对象A映射到一个对象B上, 如果映射的过程或者说操作足够简单, 那么这个数据结构用来查找的效率是很高的.

  1. 字典的接口
  2. 无序字典的实现 – 基础设施
  3. 无序字典的实现 – 核心方法
  4. 无序字典的实现 – 迭代器

字典的接口

在设计字典之前需要想一想, 尤其是字典是否允许null值和键. 在我们之前的ADT中, 都是线性的, 可以支持放入null.但问题是字典中, 如果允许null值, 可能会导致返回null语义不清楚.

按照道理, 字典中所有的键和值都要对应, 不同的键对应不同的值, 即使两个键可能对应的值相等, 这两个映射也是不同的. 如果一个值是null, 则难以区分到底是不存在对应的键, 还是键的值都是null.

所以一般是不允许键和值是null的. 这个可以在向字典添加键的时候就规定好, 然后就可以相应的规定好, 如果找不到, 就返回null, 说明字典中不存在对应的键值对(即同时不存在键和对应的值).

字典的接口我们可以来重新设计一下, 由于字典多出来一个键的特性, 因此需要两个泛型类型.

import java.util.Iterator;

public interface Dictionary<K, V> {

    void add(K key, V value);

    V remove(K key);

    V getValue(K key);

    boolean contains(K key);

    Iterator<K> getKeyIterator();

    Iterator<V> getValueIterator();

    boolean isEmpty();

    int getSize();

    void clear();

}

很显然, 这种数据结构, java也给我们提供了, 就是常见的Map类型, Map的名称也说明了这是一种基于映射的数据结构.

其接口如下:

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

    boolean isEmpty();

    boolean containsKey(Object var1);

    boolean containsValue(Object var1);

    V get(Object var1);

    V put(K var1, V var2);

    V remove(Object var1);

    void putAll(Map<? extends K, ? extends V> var1);

    void clear();

    Set<K> keySet();

    Collection<V> values();

    Set<Map.Entry<K, V>> entrySet();

}

和我们的很相似, 还有很多默认方法, 就不列出来了.

无序字典的实现

我们现在手中有的数据结构, 就是线性表及其更低层次的变种. 很容易想到, 由于键值总是成对出现的, 因此可以将其封装到一个对象中(此时我们脑海中一定会同构到java提供的Map中的内部类Entry, 集异璧这书真的是厉害), 因此我们也可以创建一个内部类来装键和值.

然后问题就简单了一些, 只要在线性表中保存Entry类型就行了. 但需要注意的是, 由于键是不能重复的, 因此插入操作必须要进行额外的动作才可以.

然后我们就可以在一个线性表的外层再套一个壳, 来创建一个字典类. 先来搭建一些基础设施:

public class LinearDictionary<K, V> implements Dictionary<K, V> {

    //实际承担字典数据结构的线性表, 里边每一个对象都是一个Entry对象
    private MyArrayList<Entry<K, V>> dictionary;

    private static int DEFAULT_CAPACITY = 16;

    //由于线性表的基础设施已经搭建好, 所以可以直接利用基础设施.
    public LinearDictionary() {
        dictionary = new MyArrayList<>(DEFAULT_CAPACITY);
    }

    public LinearDictionary(int size) {
        dictionary = new MyArrayList<>(size);
    }

    //内部Entry类, 非常关键
    private class Entry<K, V> {
        K key;
        V value;

        Entry(K key, V value) {
            this.key = key;
            this.value = value;
        }

        @Override
        public String toString() {
            return "Entry{" +
                    "key=" + key +
                    ", value=" + value +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Entry<?, ?> entry = (Entry<?, ?>) o;
            return key.equals(entry.key);
        }

        @Override
        public int hashCode() {
            return Objects.hash(key, value);
        }
    }

}

注意, 由于Entry类现在是承载着Key和Value的核心部件, 所以要对其编写equals方法, 以用来确定两个Entry是否相等, 否则后边的方法无法正确工作. 注意红色语句, 只要Key相等, 就是相等的.

无序字典的实现 – 核心方法

首先, 第一个方法void add(K key, V value)就需要好好思考, 由于键是唯一的, 因此在将键添加到字典中的时候, 就不能简单的添加, 而是必须先进行搜索.

如果搜索到了指定的键, 要做的就是更新其值, 而不是添加一个新的Entry对象, 如果没有搜索到, 再向其中添加Entry对象. 这里发现无序线性表没有编写一个查找位置的方法, 所以要给其添加一个:

public class MyArrayList<T> implements ListInterface<T>, Iterable<T> {

    ......

    //新添加的获取指定索引的方法.
    public int getPosition(T anEntry) {

        int result = -1;

        for (int i = 0; i < numberOfEntries; i++) {
            if (list[i].equals(anEntry)) {
                result = i;
                break;

            }
        }

        return result;
    }
}

之后再来实现字典的add方法:

@Override
public void add(K key, V value) {

    if (key == null || value == null) {
        throw new RuntimeException("不允许添加null.");
    }

    Entry<K, V> newEntry = new Entry<>(key, value);

    int index = dictionary.getPosition(newEntry);

    if (index == -1) {
        dictionary.add(newEntry);
    } else {
        dictionary.replace(index, newEntry);
    }

}

这个方法会先进行查找, 如果找不到, 就新增Entry, 如果找到了, 就替换指定索引上的Entry对象.

remove()方法需要先查找, 再删除指定位置的Entry对象:

@Override
@SuppressWarnings("unchecked")
public V remove(K key) {

    int index = dictionary.getPosition(new Entry<K,V>(key, (V) new Object()));
    if (index == -1) {
        return null;
    } else {
        return dictionary.remove(index).value;
    }
}

由于判断只需要Key相等,所以这里组装了一个对象用于进行搜索, 如果搜到就返回那个被删除的元素的值, 如果没删除成功, 说明键不存在于字典中,就返回null.

然后再来编写public V getValue(K key)方法, 很显然也需要先查找:

@Override
@SuppressWarnings("unchecked")
public V getValue(K key) {
    int index = dictionary.getPosition(new Entry<K, V>(key, (V) new Object()));
    if (index == -1) {
        return null;
    } else {
        return dictionary.getEntry(index).value;
    }
}

当然, 这时候会发现, 我们可以将按照Key查找这个过程抽到一个方法中来, 确实如此:

@SuppressWarnings("unchecked")
private int getIndex(K key) {
    return dictionary.getPosition(new Entry<K, V>(key, (V) new Object()));
}

上边两个方法remove和getValue就不用压制警告了, 修改成:

@Override
public V remove(K key) {

    int index = getIndex(key);
    if (index == -1) {
        return null;
    } else {
        return dictionary.remove(index).value;
    }
}

@Override
public V getValue(K key) {
    int index = getIndex(key);
    if (index == -1) {
        return null;
    } else {
        return dictionary.getEntry(index).value;
    }
}

然后还有几个辅助方法就简单多了:

@Override
public boolean contains(K key) {
    return getIndex(key) != -1;
}

@Override
public boolean isEmpty() {
    return dictionary.getLength() == 0;
}

@Override
public int getSize() {
    return dictionary.getLength();
}

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

这样除了键和值的迭代器, 实际上我们全部都实现了. 此时可以回头来看看这些方法, 不用花时间就可以发现, 和增删改查相关的方法, 由于需要通过查找来确定唯一性, 显然全部都是O(n)的方法.

实际上也很容易想明白, 内部都是线性表,我们的各个方法并没有在线性表的操作上边额外的添加新的复杂度, 只有add方法例外, 是需要遍历的, 所以基本上都是线性表的复杂度.

无序字典的实现 – 迭代器

下边就来实现迭代器, 要实现迭代器也不难, 核心思路就是需要一个个遍历过来, 而数组正好是可以根据索引遍历的. 因此在内部可以编写两个类, 先是键的迭代器:

@Override
public Iterator<K> getKeyIterator() {
    return new KeyIterator();
}

private class KeyIterator implements Iterator<K>{

    private int index;

    private int number;

    KeyIterator() {
        this.index = 0;
        this.number = getSize();
    }

    @Override
    public boolean hasNext() {
        return index != number;
    }

    @Override
    public K next() {
        K result = dictionary.getEntry(index).key;
        index++;
        return result;
    }
}

值的迭代器完全相同, 就是泛型不同:

@Override
public Iterator<V> getValueIterator() {
    return new ValueIterator();
}

private class ValueIterator implements Iterator<V>{

    private int index;

    private int number;

    ValueIterator() {
        this.index = 0;
        this.number = getSize();
    }

    @Override
    public boolean hasNext() {
        return index != number;
    }

    @Override
    public V next() {
        V result = dictionary.getEntry(index).value;
        index++;
        return result;
    }
}

到这里我们的实现就结束了. 实际上也可以将这个内部的线性表换成有序线性表, 不过需要注意的是, 有序线性表在插入的时候就本身就会进行O(n)的操作,倒是查找是可以进行一些优化. 这里就不再写了.

链式字典可以知道, 相比数组线性表没有本质的变化.