Java编程笔记番外1:浅谈散列

Java编程笔记番外1:浅谈散列

5c9c3b3b392ac581.jpg

图源:PHP中文网

散列作为一种数据结构,是为了解决一类特定问题:如何以尽可能小的时间复杂度保存和读取元素。

接下来我会用一系列示例来一步步说明散列如何实现以及为什么要使用散列。

首先来看一个最简单的在一段连续空间中保存元素的示例:

package ex1.hash;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Random;

import ch15.test2.Generator;
import ch15.test2.RandomGenerator;
import util.Fmt;

class Container<E> {
    private E[] contents;
    private Class<E> type;
    private int index = 0;

    @SuppressWarnings("unchecked")
    public Container(int size, Class<E> type) {
        contents = (E[]) Array.newInstance(type, size);
        this.type = type;
    }

    @SuppressWarnings("unchecked")
    public void add(E item) {
        if (index >= contents.length) {
            // 容量不够,扩容
            E[] oldContents = contents;
            contents = (E[]) Array.newInstance(type, oldContents.length * 2);
            System.arraycopy(oldContents, 0, contents, 0, oldContents.length);
            add(item);
            return;
        }
        contents[index] = item;
        index++;
    }

    @Override
    public String toString() {
        return Arrays.toString(contents);
    }

    public int size() {
        return contents.length;
    }

    public E get(int index) {
        return contents[index];
    }

    public int find(E target) {
        for (int i = 0; i < contents.length; i++) {
            if (target.equals(contents[i])) {
                return i;
            }
        }
        return -1;
    }
}

public class Main {
    private static Random rand = new Random();

    public static void main(String[] args) {
        Container<Integer> container = new Container<>(5, Integer.class);
        test(container, new RandomGenerator.IntGenerator());
    }

    private static <E> void test(Container<E> container, Generator<E> gen) {
        for (int i = 0; i < 10; i++) {
            container.add(gen.next());
        }
        System.out.println(container);
        E find = container.get(rand.nextInt(container.size()));
        int index = container.find(find);
        System.out.println(Fmt.sprintf("find %s in index %d", find, index));
    }
}
// [58, 58, 70, 37, 80, 90, 80, 13, 64, 8]
// find 80 in index 4

这个版本的Container底层是一个固定容量的数组,按照add调用的顺序依次添加元素,并且在容量不够的时候将容量扩大一倍。

这样的容器存在两个问题:

  1. 扩容的时候需要申请新的内存,并拷贝数据,这很浪费性能。
  2. 查找元素的时候必须遍历整个数组,性能低下。

对第一个问题,可以改善扩容的算法,在减少扩容次数和空间浪费方面取一个平衡点,在这里并不是问题关键。而第二个问题,可以用排序来解决,我们知道排序后的数组可以用二分查找等进行快速查找。

下面是基于上面的想法修改后的Container

package ex1.hash2;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collections;
import java.util.Random;

import ch15.test2.Generator;
import ch15.test2.RandomGenerator;
import util.Fmt;

class Container<E> {
    private E[] contents;
    private Class<E> type;
    private int index = 0;

    @SuppressWarnings("unchecked")
    public Container(int size, Class<E> type) {
        contents = (E[]) Array.newInstance(type, size);
        this.type = type;
    }

    @SuppressWarnings("unchecked")
    public void add(E item) {
        if (index >= contents.length) {
            // 容量不够,扩容
            E[] oldContents = contents;
            contents = (E[]) Array.newInstance(type, oldContents.length * 2);
            System.arraycopy(oldContents, 0, contents, 0, oldContents.length);
            add(item);
            return;
        }
        contents[index] = item;
        index++;
        // 对数组进行排序
        Arrays.sort(contents, 0, index);
    }

    @Override
    public String toString() {
        return Arrays.toString(contents);
    }

    public int size() {
        return index;
    }

    public E get(int index) {
        return contents[index];
    }

    public int find(E target) {
        // 使用二分查找
        return Arrays.binarySearch(contents, 0, index, target);
    }
}

public class Main {
    private static Random rand = new Random();

    public static void main(String[] args) {
        Container<Integer> container = new Container<>(5, Integer.class);
        test(container, new RandomGenerator.IntGenerator());
    }

    private static <E> void test(Container<E> container, Generator<E> gen) {
        for (int i = 0; i < 13; i++) {
            container.add(gen.next());
        }
        System.out.println(container);
        E find = container.get(rand.nextInt(container.size()));
        int index = container.find(find);
        System.out.println(Fmt.sprintf("find %s in index %d", find, index));
    }
}
// [7, 10, 25, 28, 36, 55, 55, 60, 65, 72, 78, 86, 99, null, null, null, null,
// null, null, null]
// find 78 in index 10

因为查找元素的时候是对已排序的数组使用二分查找来寻找,所以效率是可以保证的,但问题在于每次添加元素都不得不通过Arrays.sort对数组进行排序,这依然比较浪费性能。

不过这里可以进行一个小改动,因为现在我们在每次插入数据前都是一个有序数组,所以实际上可以通过二分查找来获取新元素“应当”插入的位置,然后我们直接在某个位置插入,并将尾部的剩余元素向后移动即可。

   ...
   @SuppressWarnings("unchecked")
    public void add(E item) {
        if (index >= contents.length) {
            // 容量不够,扩容
            E[] oldContents = contents;
            contents = (E[]) Array.newInstance(type, oldContents.length * 2);
            System.arraycopy(oldContents, 0, contents, 0, oldContents.length);
            add(item);
            return;
        }
        if (index == 0) {
            // 空数组,直接插入
            contents[index] = item;
            // index++;
        } else {
            // 通过二分查找获取新元素应当插入的位置
            int insertIndex = Arrays.binarySearch(contents, 0, index, item);
            if (insertIndex < 0) {
                // 没有重复元素,转换插入点
                insertIndex = Math.abs(insertIndex) - 1;
            }
            if (insertIndex >= index) {
                // 插入位置是数组尾部,直接插入
                contents[index] = item;
            } else {
                // 插入点之后的元素后移
                for (int i = index; i > insertIndex; i--) {
                    contents[i] = contents[i - 1];
                }
                contents[insertIndex] = item;
            }
        }
        index++;
    }
	...

但是每次插入数据都有很大概率需要移动尾部数据,这样依然有很大的性能开销。

用空间换时间

事实上如果你学习过《算法导论》,应该知道计算机算法实际上分为两类:

  • 用时间换空间。
  • 用空间换时间。

实际上对于Integer对象,我们完全可以申请一个超大容量的数组,可以装载所有你可能要填入的Integer对象的那种,然后直接按数组下标进行存储,这样做读写性能都很快。

我们看修改后的版本:

package ex1.hash4;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

import ch15.test2.Generator;
import ch15.test2.RandomGenerator;
import util.Fmt;

class Container<E extends Integer> {
    private List<E>[] contents;
    private Class<E> type;
    // private int index = 0;

    @SuppressWarnings("unchecked")
    public Container(int size, Class<E> type) {
        contents = (List<E>[]) Array.newInstance(List.class, size);
        this.type = type;
    }

    public void add(E item) {
        if (item.intValue() >= contents.length) {
            throw new IndexOutOfBoundsException();
        }
        if (contents[item] == null) {
            contents[item] = new LinkedList<>();
        }
        contents[item].add(item);
    }

    @Override
    public String toString() {
        return Arrays.toString(contents);
    }

    public int size() {
        return contents.length;
    }

    public E get(int index) {
        if (contents[index] == null) {
            return null;
        }
        return contents[index].get(0);
    }

    public int find(E target) {
        return target.intValue();
    }
}

public class Main {
    private static Random rand = new Random();

    public static void main(String[] args) {
        final int MAX = 10;
        Container<Integer> container = new Container<>(MAX, Integer.class);
        test(container, new RandomGenerator.IntGenerator(MAX));
    }

    private static <E extends Integer> void test(Container<E> container, Generator<E> gen) {
        for (int i = 0; i < 10; i++) {
            container.add(gen.next());
        }
        System.out.println(container);
        E find = null;
        do {
            find = container.get(rand.nextInt(container.size()));
            if (find != null) {
                break;
            }
        } while (true);
        int index = container.find(find);
        System.out.println(Fmt.sprintf("find %s in index %d", find, index));
    }
}
// [[0], [1], [2, 2], null, [4], null, null, [7], [8, 8, 8], [9]]
// find 7 in index 7

为了不对代码进行大范围修改,这里采取了将类型参数E限定为Integer子类的方式,实际上这么做会产生一个Warning,因为Integerfinal类,无法被继承。

考虑到会有元素重复添加,所以数组的每一个元素都是List,添加的元素都保存在List中。

最终的效果就是结果中打印的那样,Integer对象都按照相应的值保存到数组相应下标的List中。

这样做读写性能都很快,可以看做是数组的读写性能,即一个常数级时间复杂度O(1),但问题在于如果我们需要插入的数字规模增大,变为0~1000,那么我们就需要申请一个长度为1000的数组,而如果只是数字的范围增加,但个数其实很少,比如我们要插入随机0~1000的数字5个,那么数组中仅有5个元素是被使用了的,也就是说995个元素都被浪费掉了。

这就是用空间换时间的后果,时间复杂度的确降低了,但空间被浪费了,如果这种浪费在可接受的范围内,那就是成功的优化策略,但显然目前这种方式是不可能被接受的。

散列

散列就是为了解决上边的问题,它的思想是,既然我们是要将一个“大范围”但样本量不多的数据存放到数组中,申请一个和范围等同长度的数组不可接受,那么可不可以将样本数据进行某种“压缩”或“转换”,让它们不是按照数据范围1对1的对应到数组下标,而是经过转换后以某个值来对应一个缩小后的数组的下标。

我们看用这种思路改进后的版本:

package ex1.hash5;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

import ch15.test2.Generator;
import ch15.test2.RandomGenerator;
import util.Fmt;

class Container<E extends Integer> {
    private List<E>[] contents;
    private Class<E> type;
    // private int index = 0;

    @SuppressWarnings("unchecked")
    public Container(int size, Class<E> type) {
        contents = (List<E>[]) Array.newInstance(List.class, size);
        this.type = type;
    }

    public void add(E item) {
        int hash = item.intValue() % contents.length;
        if (contents[hash] == null) {
            contents[hash] = new LinkedList<>();
        }
        contents[hash].add(item);
    }

    @Override
    public String toString() {
        return Arrays.toString(contents);
    }

    public int size() {
        return contents.length;
    }

    public E get(int index) {
        if (contents[index] == null) {
            return null;
        }
        return contents[index].get(0);
    }

    public int find(E target) {
        int hash = target.intValue() % contents.length;
        return hash;
    }
}

public class Main {
    private static Random rand = new Random();

    public static void main(String[] args) {
        Container<Integer> container = new Container<>(10, Integer.class);
        test(container, new RandomGenerator.IntGenerator(1000));
    }

    private static <E extends Integer> void test(Container<E> container, Generator<E> gen) {
        for (int i = 0; i < 10; i++) {
            container.add(gen.next());
        }
        System.out.println(container);
        E find = null;
        do {
            find = container.get(rand.nextInt(container.size()));
            if (find != null) {
                break;
            }
        } while (true);
        int index = container.find(find);
        System.out.println(Fmt.sprintf("find %s in index %d", find, index));
    }
}
// [null, [891, 731, 501], [982, 902, 352], [563], null, [805], null, [747], null, [569]]
// find 747 in index 7

这里的填入数据的范围是0~1000,但我们用于保存输入的数组长度仅为10,为了将0~1000的数据“映射”到0~9的下标,这里采用了一个简单的取余过程,即int hash = target.intValue() % contents.length

最终的效果是像[891, 731, 501], [982, 902, 352]这样,个位数相同的数保存在同一个List中。而查找元素时,同样的可以通过取余来确定要查找的数组下标,然后通过遍历List就可以查找到目标对象。

在这个示例中,数组实际上就是一个散列散列表,所谓的散列实际上就是不连续保存数据的“列表”,就像示例中那样,为了一个理想的读写数据的性能,我们不得不浪费一些空间,但同时我们利用散列这种机制来让浪费的空间可以接受。

这个示例中的取余做法可以看做是散列算法,用于将要保存的数据映射到散列中。而散列算法产生的值,通常被称作散列码。

散列通常也被称作哈希(hash),散列算法也被称作哈希算法,散列码被称作哈希值(hash code)。

映射

散列最广泛的用途是映射(也称作字典),或者说关系数组。这种数据结构广泛存在于各种编程语言中,比如PHP就很极端,只有关系数组而没有普通数组。而Go、Python、Java都支持map或者类似的数据结构。

之所以映射采用散列实现,是因为映射本身由键值对(key-value)构成,并且需要对数据的插入和读取有一个良好的性能,散列恰恰可以满足这一点。

下面用一个示例说明,这个示例同样使用了数组+List的方式实现,只不过其中的元素是“键值对”,且通过键的散列值决定存储到数组的哪个元素的List中,查询也依靠散列值确定位置,以此来实现一个不错的读写性能。

package ex1.map;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import ch15.test2.Generator;
import ch15.test2.RandomGenerator;
import util.Fmt;

import java.util.Set;

class MyHashMap<K, V> implements Map<K, V> {
    private static class MapEntry<K, V> implements Entry<K, V> {
        private K key;
        private V value;

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

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }

        @Override
        public V setValue(V value) {
            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        @Override
        public String toString() {
            return Fmt.sprintf("%s=%s", key, value);
        }

    }

    private List<Entry<K, V>>[] contents;

    @SuppressWarnings("unchecked")
    public MyHashMap(int cap) {
        if (cap < 1) {
            cap = 1;
        }
        contents = new List[cap];
    }

    @Override
    public int size() {
        int size = 0;
        for (List<Entry<K, V>> list : contents) {
            if (list != null) {
                size += list.size();
            }
        }
        return size;
    }

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

    @Override
    public boolean containsKey(Object key) {
        Entry<K, V> entry = getEntry(key);
        return entry != null;
    }

    @Override
    public boolean containsValue(Object value) {
        for (List<Entry<K, V>> list : contents) {
            if (list == null) {
                continue;
            }
            for (Entry<K, V> entry : list) {
                if (entry.getValue().equals(value)) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public V get(Object key) {
        Entry<K, V> entry = getEntry(key);
        if (entry != null) {
            return entry.getValue();
        }
        return null;
    }

    @Override
    public V put(K key, V value) {
        V oldValue = null;
        Entry<K, V> entry = getEntry(key);
        if (entry != null) {
            oldValue = entry.getValue();
            entry.setValue(value);
            return oldValue;
        }
        addEntry(key, value);
        return oldValue;
    }

    @Override
    public V remove(Object key) {
        Entry<K, V> entry = getEntry(key);
        if (entry == null) {
            return null;
        }
        V oldValue = entry.getValue();
        int index = getContentsIndex(key);
        contents[index].remove(entry);
        return oldValue;
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> m) {
        for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
            put(entry.getKey(), entry.getValue());
        }
    }

    @Override
    public void clear() {
        Arrays.fill(contents, null);
    }

    @Override
    public Set<K> keySet() {
        Set<K> keys = new HashSet<>();
        for (Entry<K, V> entry : entrySet()) {
            keys.add(entry.getKey());
        }
        return keys;
    }

    @Override
    public Collection<V> values() {
        Set<V> values = new HashSet<>();
        for (Entry<K, V> entry : entrySet()) {
            values.add(entry.getValue());
        }
        return values;
    }

    @Override
    public Set<Entry<K, V>> entrySet() {
        Set<Entry<K, V>> entries = new HashSet<>();
        for (List<Entry<K, V>> list : contents) {
            if (list != null) {
                entries.addAll(list);
            }
        }
        return entries;
    }

    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append("[");
        for (Entry<K, V> entry : entrySet()) {
            sb.append(entry.toString());
            sb.append(",");
        }
        sb.delete(sb.length() - 1, sb.length());
        sb.append("]");
        return sb.toString();
    }

    private int getContentsIndex(Object object) {
        int hashCode = object.hashCode();
        int index = Math.abs(hashCode) % contents.length;
        return index;
    }

    private Entry<K, V> getEntry(Object key) {
        int index = getContentsIndex(key);
        if (contents[index] == null) {
            return null;
        }
        for (Entry<K, V> entry : contents[index]) {
            if (entry.getKey().equals(key)) {
                return entry;
            }
        }
        return null;
    }

    private void addEntry(K key, V value) {
        int index = getContentsIndex(key);
        if (contents[index] == null) {
            contents[index] = new LinkedList<>();
        }
        contents[index].add(new MapEntry<>(key, value));
    }
}

public class Main {
    public static void main(String[] args) {
        test(new MyHashMap<Integer, String>(10), new RandomGenerator.IntGenerator(),
                new RandomGenerator.StringGenerator());
    }

    private static void test(Map<Integer, String> map, Generator<Integer> kGen, Generator<String> vGen) {
        for (int i = 0; i < 10; i++) {
            map.put(kGen.next(), vGen.next());
        }
        System.out.println(map);
        map.clear();
        map.put(1, "hello");
        map.put(1, "hello");
        System.out.println(map);
        map.put(1, "world");
        System.out.println(map);
        System.out.println(map.containsKey(1));
        System.out.println(map.containsValue("world"));
    }
}
// [14=gcsmq,47=cltks,39=flvxy,95=abqfh,65=fponx,30=cizjh,45=wamrt,37=wlymv,36=dgpmh,63=nmmka]
// [1=hello]
// [1=world]
// true
// true

关于散列就说到这了,希望对你有所帮助,谢谢阅读。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值