Java数据结构之散列表详解

目录
  • 介绍
  • 1 散列表概述
    • 1.1 散列表概述
    • 1.2 散列冲突(hash collision)
  • 2 散列函数的选择
    • 2.1 散列函数的要求
    • 2.2 散列函数构造方法
  • 3 散列冲突的解决
    • 3.1 分离链接法
    • 3.2 开放定址法
    • 3.3 再散列法
  • 4 散列表的简单实现
    • 4.1 测试

介绍

本文详细介绍了散列表的概念、散列函数的选择、散列冲突的解决办法,并且最后提供了一种散列表的Java代码实现。

数组的特点是寻址容易,插入和删除困难;而链表的特点是寻址困难,插入和删除容易。而对于tree结构,它们的查找都是先从根节点进行查找,从节点取出数据或索引与查找值进行比较,虽然查找和增删的综合效率较好,但是最终还是需要进行多次查找。为此引入了散列表来尝试进一步提升查找效率和增删的综合效率。

1 散列表概述

1.1 散列表概述

之前所掌握的查找算法,最简单的顺序表结构查找包括简单的顺序查找、二分查找、插值查找、斐波那契查找,以及后来的树结构查找包括二叉排序树、平衡二叉树、多路查找树、红黑树等。它们有一个功能特点就是,要查找的元素始终要与已经存在的元素进行多次比较,才能最终的出要该的元素是否存在或者不存在的结果。

我们知道,这些比较用于逐渐的定位某一个确切的位置,上面的大部分查找算法要求数据必须是有序存储的,算法就是通过比较两个数据的大小来缩小查找的范围,最终找到一个大小相等的数据,或说明该元素存在,或者最终也没有找到一个大小相等的数据,说明不存在。

为什么一定要“比较”?能否直接通过关键字key得到要查找的记录内存存储位置呢?当然有,这就是散列表。

事先在数据(这里可以是key-value键值对形式的数据,也可以是单个key形式的数据)的存储位置和它的关键字之间建立一个确定的对应函数关系f,使得每个关键字k对应一个存储位置f(key)。即:存储位置=f(key),该映射被称为散列函数,利用散列函数来存储数据的数据结构被称为散列表。通过f(key)计算出存储位置的过程被称为散列,所得的存储位置称散列地址。

散列表通常基于数组来实现。存放数据的时候,散列函数f(key)根据key计算出数据应该存储的位置-数组下标,从而将不同的数据分散在不同的存储位置,这也是“散列”的由来;查找的时候,通过散列函数f(key)对应的key可以直接确定查找值所在位置-数组下标,而不需要一个个比较。这样就“预先知道”key所在的位置,直接找到数据,提升效率。散列表存放元素的数组位置也被称为“槽(slot)”。

散列表与线性表、树、图等结构不同的是,后几种结构数据元素之间都存在某种逻辑关系,而使用散列技术的散列表的数据元素之间不存在什么逻辑关系,元素的位置只与关键字key和散列函数f(key)有关联。

对于查找来说,散列技术简化了比较过程,效率就会大大提高,但万事有利就有弊,由于数据元素之间并没有确切的关系,散列技术不具备很多常规数据结构的能力。相对于其他查找结构,它只支持部分操作(查找、增删……),另一部分操作不能实现(排序、索引操作、范围查找、顺序输出、查找最大/小值……)。因此,散列表主要是面向查找的存储结构。

散列表的英文名称为 hash table,因此散列表又被称为哈希表,散列函数又被称为哈希函数,函数的实现步骤被称为散列算法/哈希算法。

1.2 散列冲突(hash collision)

我们还能看出来,散列算法实际上是将任意长度的key变换成固定范围长度的值。这种转换是一种压缩映射,简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。也就是,散列值的范围大小通常远小于输入的key的范围。

例如,输入的key为5,28,19,15,20,33,12,17,10,共9个,此时肯定不能将哈希表(内部数组)的长度初始化为33,那样的话就太浪费空间了。理想的结果是,将这9个key通过某个散列函数f(key)将它们放入长度为10的哈希表(数组)中,并且然而由于散列算法是一种压缩映射算法,散列表的长度单元是有限的,关键字key是无限的,对于某个散列算法,如果key样本如果大,那么两个不同的key映射到相同的单元,即f(key)的值相等的情况几乎是一种必然,这种现象也被称为散列冲突/哈希冲突。

例如对上面的key个数采用的散列函数是f(key)=key mod 9,f(5)=5,所以数据5应该放在散列表的第5个槽里;f(28)=1,所以数据28应该放在散列表的第1个槽里;f(19)=1,也就是说,数据19也应该放在散列表表的第1个槽里——于是就造成了碰撞。

尽管我们可以通过精心设计的散列函数让冲突尽可能的少,但是不能完全避免。因此散列表必须具备处理散列冲突的能力。

2 散列函数的选择

2.1 散列函数的要求

从上面的散列表的概述可以看出来,要实现散列表,最关键的就是散列函数f(key)的选择和处理散列冲突的能力,我们先来看散列函数的选择。

良好的散列函数应该满足下面两个原则:

  • 计算简单:如果散列算法需要很复杂的计算,会耗费很多时间,这对于需要频繁地查找来说,就会大大降低查找的效率了。因此散列函数的计算时间不应该超过其他查找技术与关键字比较的时间。
  • 散列地址分布均匀:我们刚才也提到冲突带来的问题,虽然不能完全避免冲突,但是可能设计好的散列函数尽量让散列地址均匀地分布在存储空间中,这样可以保证存储空间的有效利用,并减少冲突的发生和为处理冲突而耗费的时间。 下面介绍几种常用的散列函数构造方法!

2.2 散列函数构造方法

直接定址法

取关键字或关键字的某个线性函数值为散列地址(这种散列函数也叫自身函数)。f(key)=a×key+b(a、b为常数)。

比如对0-100岁人口数统计,直接采用年龄作为关键字。

比如统计1980年忠厚每年出生的人口数目,我们可以对出生年份关键字减去1980来作为地址。

这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。由于这样的限制,在现实应用中,此方法虽然简单,但却并不常用。

数字分析法

假设关键字是以r为基的数,并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。

比如对于手机号码的key,用手机号码后四位参与计算。

数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。

折叠法

将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。 比如我们的关键字是9876543210,散列表表长为三位,我们将它分为四组,987|654|321|0,然后将它们叠加求和987+654+321+0=1962,再求后3位得到散列地址为962。

有时可能这还不能够保证分布均匀,不妨从一端向另一端来回折叠后对齐相加。比如我们将987和321反转,再与654和0相加,变成789+654+123+0=1566,此时散列地址为566。

折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。

平方取中法

取关键字平方后的中间几位为哈希地址。通过平方扩大差别,另外中间几位与乘数的每一位相关,由此产生的散列地址较为均匀。

假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用做散列地址。再比如关键字是4321,那么它的平方就是18671041,抽取中间的3位就可以是671,也可以是710,用做散列地址。

平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。

除留余数法

此方法为最常用的构造散列函数方法。对于散列表长为m的散列函数公式为:f(key)=key mod p(p≤m)

mod是取模(求余数)的意思。事实上,这方法不仅可以对关键字直接取模,也可在折叠、平方取中后再取模。很显然,本方法的关键就在于选择合适的p,p如果选得不好,就可能会容易产生冲突。HashMap就采用了这种方法(利用位运算代替取模运算,提高程序的计算效率)。需要注意的是,只有在特定情况下,位运算才可以转换成取模运算(当 b = 2^n 时,a % b = a & (b - 1) )。

因此根据前辈们的经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。

随机数法

选择一个随机数,取关键字的随机函数值为它的散列地址。也就是:f(key)=random(key)

这里random是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。

总之,现实中,应该视不同的情况采用不同的散列函数。我们只能给出一些考虑的因素来提供参考:

1.计算散列地址所需的时间。

2.关键字的长度。

3.散列表的大小。

4.关键字的分布情况。

5.记录查找的频率。

综合这些因素,才能决策选择哪种散列函数更合适。

3 散列冲突的解决

设计得再好的散列函数也不可能完全避免冲突。对无论以何种散列函数构建的散列表,还必须考虑如何处理散列冲突。常见方法有以下几种:

  • 使用辅助数据结构:分离链接法/链地址法/拉链法
  • 不使用辅助数据结构:开放定址法(线性探测、平方探测、双散列)
  • 再散列

3.1 分离链接法

在存储数据的过程中,如果发生冲突,可以利用单链表在已有数据的后面插入新数据,访问的数组下标元素作为链表的头部。这种解决冲突的方法被称为“分离链接法”,又被称为分离链接法、拉链法。可以想象,除了链表之外,其他辅助结构都能解决冲突现象:二叉树或者另一张散列表,如果采用链表来辅助解决散列冲突,并且散列函数设计的很好,那么链表应该是比较短的,此时其他复杂的辅助结构便不值得尝试了。

如下图,使用链地址法的散列表:

JDK1.8之前的HashMap就是使用的链表来处理散列冲突,为了降低链表过长造成的遍历性能损耗,在JDK1.8中采用链表+红黑树的方法来处理散列冲突,当链表长度大于8个时,转换为红黑树,红黑树的查找效率明显高于单链表的。而小于等于8个时,采用链表则完全可以接受,避免红黑树的复杂结构。

3.2 开放定址法

开放定址法的基本思路就是出现冲突时,通过另外的算法寻找其他空余的位置,因此不需要额外的辅助数据结构,只要散列表足够大,空的散列地址总能找到,并将记录存入。

开放定址法法又可以分为线性探测法、平方探测法、双散列法。

3.2.1 线性探测法(Linear Probing)

线性探测公式为:(H(key)+di)% m;其中H(key)为哈希函数,m 为表长-1,di为一个增量序列(di=0,1,2,3...,m-1)。

线性探测法的主要思想是:当发生碰撞时(一个键被散列到一个已经有键值对的数组位置),我们会检查数组的下一个位置,这个过程被称作线性探测。线性探测可能会产生三种结果:

  • 命中:该位置的键与要查找的键相同;
  • 未命中:该位置为空;
  • 该位置的键和被查找的键不同。

当我们查找某个键时,首先通过散列函数得到一个数组索引后,之后我们就开始检查相应位置的键是否与给定键相同,若不同则继续查找(若到数组末尾也没找到就折回数组开头),直到找到该键或遇到一个空位置。由线性探测的过程我们可以知道,若数组已满的时候我们再向其中插入新键,会陷入无限循环之中。

3.2.2 平方探测法

如果散列函数选的不是那么的好,可能导致冲突不断出现。如果先存入key1,能够找到某个空余位置存入,存入key2时与key1冲突,此时被存入到key1的下一个位置,后来key3又与它们发生散列冲突……这样就出现了关键字聚集在某一区域的情况,即出现了数据聚集的现象。

一种解决方法是,增大di的增量:(H(key)+di²)% m(di=0,1,2,3...,m/2)

增加平方运算的目的是为了不让关键字都聚集在某一块区域。我们称这种方法为平方探测法。

如果m—表长-1不为素数,那么备选单元的数量将会减少,造成散列冲突的可能性就会大大增加。

3.2.3 双散列法

准备两个散列函数。双散列一般公式为:F(i)= i * hash2(x),意思是用第二个散列函数算出x的散列值,然后在距离hash2(x),2hash2(x)的地方探测。

3.3 再散列法

前面的链地址法和开放定址法都是为了让散列表中的元素分布的更加合理,但是散列表空间总有用完的时候,甚至当它们的散列表填充元素过多时,都会增大发生散列冲突的概率。这里的再散列法就是计算出什么时候让散列表进行扩展以及在散列表扩展的时候,如何让原来的元素在新的空间中合理的分布。

一般方法是:当散列表的元素足够的多时,建立另外一个大约两倍大的表,再用一个新的散列函数,扫描整个原始表然后按照新的映射插入到新的表里,这就是再散列。其开销非常大,因为涉及到所有元素的移动。

再散列的触发条件通常有:只要表有一半满了就做、只有当插入失败时才做(这种比较极端)、当表到达某个扩容因子时再做。第三种是较好的方法,HashMap就是用的这种方法。

散列表的扩容因子: 所谓的扩容因子α=填入表中的记录个数/散列表长度,α标志着散列表的装满的程度。一般来说,当元素数量达到设定的扩容因子的数量时,就表示可以进行再散列扩容了,也叫装填因子。因此一个合理的扩容因子非常重要。α越大,产生冲突的可能性就越大;α越小,产生冲突的可能性就越小,但是造成了空间浪费。JDK1.8的HasmMap装填因子默认为0.75。

4 散列表的简单实现

JDK已经提供了现成的散列表实现,比如著名的HashMap。JDK1.8的HashMap是采用数组+链表+红黑树来实现的。散列函数采用基于hashcode()的去除留余法,并且采用分离链接法和再散列法的散列冲突解决办法。

这里提供另一种采用线性探测的散列表的Java简单实现,从下面的实现上可以看出来,实际上线性探测的散列表效率并不高,并且产生了数据聚集,但是JDK中也有使用线性探测实现的散列表类,比如ThreadLocal中的ThreadLocalMap,因为线性探测的实现比较简单。

/**
 * 基于线性探测法的散列表简单实现
 *
 * @param <K> key类型
 * @param <V> value类型
 */
public class LinearProbingHashMap<K, V> {

    /**
     * 存储节点数据的数组
     */
    transient Entry<K, V>[] table;

    /**
     * 存储的节点对象
     *
     * @param <K>
     * @param <V>
     */
    private static class Entry<K, V> implements Map.Entry<K, V> {
        /**
         * 保存hash值,避免重复计算
         */
        final int hash;
        /**
         * key值
         */
        final K key;
        /**
         * value值
         */
        V value;

        /**
         * 构造器
         *
         * @param hash
         * @param key
         * @param value
         */
        private Entry(int hash, K key, V value) {
            this.hash = hash;
            this.key = key;
            this.value = value;
        }

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

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

        /*@Override
        public final String toString() {

            return hash + "=" + key + "=" + value;
        }*/
        @Override
        public final String toString() {

            return key + "=" + value;
        }

        @Override
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        @Override
        public int hashCode() {
            return hash;
        }
    }

    /**
     * 散列表的容量,初始为16
     */
    private int capacity = 16;
    /**
     * 散列表节点数量
     */
    private int size;

    /**
     * 空构造器,并不会初始化内部数组
     */
    public LinearProbingHashMap() {
    }

    /**
     * 插入
     *
     * @param key   k
     * @param value v
     */
    public V put(K key, V value) {
        //初始化
        if (table == null) {
            table = new Entry[capacity];
        }
        //扩容,这里判断元素数量大于等于0.75*capacity
        if (size >= capacity * 0.75) {
            resize(2 * capacity);
        }
        int hash = hash(key);
        //计算应该插入的数组元素下标
        int position = hash & (capacity - 1);
        //插入逻辑,总会成功
        while (true) {
            if (table[position] == null) {
                table[position] = new Entry<>(hash, key, value);
                size++;
                break;
                //判断是否是重复的key,这里使用hash值和==判断
            } else if (hash == table[position].hashCode() && table[position].getKey() == key) {
                return table[position].setValue(value);
            }
            position = nextIndex(position, capacity);
        }
        return null;
    }

    /**
     * 查找
     *
     * @param key 要查找的key
     * @return 查找到的value
     */
    public V get(K key) {
        if (table == null) {
            return null;
        }
        //计算出key对应的数组位置
        int position = hash(key) & (capacity - 1);
        //如果该位置不为null,则开始查找连续的元素
        if (table[position] != null) {
            do {
                if (table[position].getKey() == key) {
                    return table[position].getValue();
                }
                position = nextIndex(position, capacity);
            } while (table[position] != null);
        }
        return null;
    }

    /**
     * 删除元素
     *
     * @param key 查找的元素
     * @return 被删除的元素value;null不代表不是被删除的value
     */
    public V delete(K key) {
        if (table == null) {
            return null;
        }
        //计算出key对应的数组位置
        int position = hash(key) & (capacity - 1);
        V value;
        //如果该位置不为null,则开始查找连续的元素
        if (table[position] != null) {
            do {
                if (table[position].getKey() == key) {
                    //删除元素
                    value = table[position].getValue();
                    table[position] = null;
                    size--;
                    //将后面的连续的元素全部重新插入
                    position = nextIndex(position, capacity);
                    Entry<K, V> positionEntry;
                    while ((positionEntry = table[position]) != null) {
                        table[position] = null;
                        size--;
                        put(positionEntry.getKey(), positionEntry.getValue());
                        position = nextIndex(position, capacity);
                    }
                    return value;
                }
                position = nextIndex(position, capacity);
            } while (table[position] != null);
        }
        return null;
    }

    public int size() {
        return size;
    }

    /**
     * 获取hash值,不是直接取hash值,而是借鉴了HashMap的扰动算法,这样可以让元素分布更加均匀
     *
     * @param key k
     * @return hash值
     */
    private int hash(K key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    /**
     * 扩容
     *
     * @param newCapacity 新容量
     */
    private void resize(int newCapacity) {
        if (newCapacity <= 0) {
            throw new StackOverflowError("容量超出最大容量");
        }
        this.capacity = newCapacity;
        Entry<K, V>[] oldTab = table;
        table = new Entry[capacity];
        for (Entry<K, V> e : oldTab) {
            if (e != null) {
                int position = e.hashCode() & (capacity - 1);
                while (table[position] != null) {
                    position = nextIndex(position, capacity);
                }
                table[position] = e;
            }
        }
    }

    /**
     * 下一个位置
     *
     * @param i   当前位置
     * @param len 数组长度
     * @return 下一个位置, 此处包含循环的逻辑循环
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    @Override
    public String toString() {
        return "LinearProbingHashST{" +
                "table=" + Arrays.toString(table) +
                '}';
    }
}

4.1 测试

public class LinearProbingHashMapTest {
    public static void main(String[] args) {
        System.out.println("==========>插入一批元素");
        LinearProbingHashMap<Object, Object> objectObjectLinearProbingHashMap = new LinearProbingHashMap<>();
        objectObjectLinearProbingHashMap.put(49, 16);
        objectObjectLinearProbingHashMap.put("Aa", 1);
        objectObjectLinearProbingHashMap.put(34, 78);
        objectObjectLinearProbingHashMap.put(17, 85);
        //Aa与BB的hash值是一样的
        objectObjectLinearProbingHashMap.put("BB", 2);
        objectObjectLinearProbingHashMap.put(36, 36);
        objectObjectLinearProbingHashMap.put(21, 37);
        objectObjectLinearProbingHashMap.put(9, 87);
        objectObjectLinearProbingHashMap.put("兓", 62);
        objectObjectLinearProbingHashMap.put(46, 43);
        objectObjectLinearProbingHashMap.put("呵呵", 3);
        objectObjectLinearProbingHashMap.put("嘻嘻", 4);
        System.out.println(objectObjectLinearProbingHashMap);
        System.out.println("size:" + objectObjectLinearProbingHashMap.size());
        System.out.println(objectObjectLinearProbingHashMap);

        System.out.println("==========>删除 BB");
        Object delete = objectObjectLinearProbingHashMap.delete("BB");
        System.out.println(delete);
        System.out.println("size:" + objectObjectLinearProbingHashMap.size());
        System.out.println(objectObjectLinearProbingHashMap);

        System.out.println("==========>插入(46,44) 重复添加46,将会替换value");
        Object put = objectObjectLinearProbingHashMap.put(46, 44);
        System.out.println(put);
        System.out.println("size:" + objectObjectLinearProbingHashMap.size());
        System.out.println(objectObjectLinearProbingHashMap);

        System.out.println("==========>插入null 将会尝试添加到第一个元素");
        Object putNull = objectObjectLinearProbingHashMap.put(null, "nullnull");
        System.out.println(putNull);
        System.out.println("size:" + objectObjectLinearProbingHashMap.size());
        System.out.println(objectObjectLinearProbingHashMap);

        System.out.println("==========>获取null 对应的value");
        Object o = objectObjectLinearProbingHashMap.get(null);
        System.out.println(o);

        System.out.println("==========>扩容");
        objectObjectLinearProbingHashMap.put("BB", 5);
        System.out.println("size:" + objectObjectLinearProbingHashMap.size());
        System.out.println(objectObjectLinearProbingHashMap);

        System.out.println("==========>获取BB 对应的value");
        Object bb = objectObjectLinearProbingHashMap.get("BB");
        System.out.println(bb);

        System.out.println("==========>删除 34");
        Object delete34 = objectObjectLinearProbingHashMap.delete(34);
        System.out.println(delete34);
        System.out.println("size:" + objectObjectLinearProbingHashMap.size());
        System.out.println(objectObjectLinearProbingHashMap);
    }
}

以上就是Java数据结构之散列表详解的详细内容,更多关于Java 散列表的资料请关注我们其它相关文章!

(0)

相关推荐

  • 散列表的原理与Java实现方法详解

    本文实例讲述了散列表的原理与Java实现方法.分享给大家供大家参考,具体如下: 概述 符号表是一种用于存储键值对(key-value pair)的数据结构,我们平常经常使用的数组也可以看做是一个特殊的符号表,数组中的"键"即为数组索引,值为相应的数组元素.也就是说,当符号表中所有的键都是较小的整数时,我们可以使用数组来实现符号表,将数组的索引作为键,而索引处的数组元素即为键对应的值,但是这一表示仅限于所有的键都是比较小的整数时,否则可能会使用一个非常大的数组.散列表是对以上策略的一种&

  • Java 哈希表详解(google 公司的上机题)

    1 哈希表(散列)-Google 上机题 1) 看一个实际需求,google 公司的一个上机题: 2) 有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,住址..),当输入该员工的 id 时,要求查 找到该员工的 所有信息. 3) 要求: 不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列) 2 哈希表的基本介绍 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构.也就是说,它通 过把关键码值映射到表中一个位置

  • Java实现哈希表的基本功能

    一.哈希表头插法放入元素 /** * user:ypc: * date:2021-05-20; * time: 11:05; */ public class HashBuck { class Node { public int key; int value; Node next; Node(int key, int value) { this.key = key; this.value = value; } } public int usedSize; public Node[] array;

  • Java数据结构之散列表(动力节点Java学院整理)

    基本概念 散列表(Hash table,也叫哈希表),是根据关键字(key value)而直接进行访问的数据结构. 说的具体点就是它通过吧key值映射到表中的一个位置来访问记录,从而加快查找的速度. 实现key值映射的函数就叫做散列函数 存放记录的数组就就叫做散列表 实现散列表的过程通常就称为散列(hashing),也就是常说的hash 散列 这里的散列的概念不仅限于数据结构了,在计算机科学领域中,散列-哈希是一种对信息的处理方法,通过某种特定的函数/算法(散列函数/hash()方法)将要检索的

  • 带你了解Java数据结构和算法之哈希表

    目录 1.哈希函数的引入 ①.把数字相加 ②.幂的连乘 2.冲突 3.开放地址法 ①.线性探测 ②.装填因子 ③.二次探测 ④.再哈希法 4.链地址法 5.桶 6.总结 1.哈希函数的引入 大家都用过字典,字典的优点是我们可以通过前面的目录快速定位到所要查找的单词.如果我们想把一本英文字典的每个单词,从 a 到 zyzzyva(这是牛津字典的最后一个单词),都写入计算机内存,以便快速读写,那么哈希表是个不错的选择. 这里我们将范围缩小点,比如想在内存中存储5000个英文单词.我们可能想到每个单词

  • Java数据结构之散列表详解

    目录 介绍 1 散列表概述 1.1 散列表概述 1.2 散列冲突(hash collision) 2 散列函数的选择 2.1 散列函数的要求 2.2 散列函数构造方法 3 散列冲突的解决 3.1 分离链接法 3.2 开放定址法 3.3 再散列法 4 散列表的简单实现 4.1 测试 介绍 本文详细介绍了散列表的概念.散列函数的选择.散列冲突的解决办法,并且最后提供了一种散列表的Java代码实现. 数组的特点是寻址容易,插入和删除困难:而链表的特点是寻址困难,插入和删除容易.而对于tree结构,它们

  • Java数据结构之单链表详解

    一.图示 二.链表的概念及结构 链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 . 实际中链表的结构非常多样,以下情况组合起来就有8种链表结构: 单向.双向 带头.不带头 循环.非循环 今天,我们实现的是一个 单向 无头 非循环的链表. 下面是此链表的结构组成. 三.单链表的实现 (1)定义一个节点类型 我们创建一个 ListNode 的类作为节点类型,那么我们如何定义成员属性呢? 通过上面的结构分析,我们需要定义两个成员变量 val --作为该节点的

  • Java数据结构顺序表用法详解

    目录 1.什么是顺序表 2.顺序表的基本功能和结构 3.顺序表基本功能的实现和解析 1.判断线性表是否为空 2.获取指定位置的元素 3.向线性表表添加元素 4.在位置i处插入元素 5.删除指定位置的元素,并返回该元素 6.查找t第一次出现的位置 7.手动扩容方法 1.什么是顺序表 在程序中,经常需要将一组(通常是同为某个类型的)数据元素作为整体管理和使用,需要创建这种元素组,用变量记录它们,传进传出函数等.一组数据中包含的元素个数可能发生变化(可以增加或删除元素). 对于这种需求,最简单的解决方

  • Java数据结构之线段树详解

    目录 介绍 代码实现 线段树构建 区间查询 更新 总结 介绍 线段树(又名区间树)也是一种二叉树,每个节点的值等于左右孩子节点值的和,线段树示例图如下 以求和为例,根节点表示区间0-5的和,左孩子表示区间0-2的和,右孩子表示区间3-5的和,依次类推. 代码实现 /** * 使用数组实现线段树 */ public class SegmentTree<E> { private Node[] data; private int size; private Merger<E> merge

  • java 数据结构并查集详解

    目录 一.概述 二.实现 2.1 Quick Find实现 2.2 Quick Union实现 三.优化 3.1基于size的优化 3.2基于rank优化 3.2.1路径压缩(Path Compression ) 3.2.2路径分裂(Path Spliting) 3.2.3路径减半(Path Halving) 一.概述 并查集:一种树型数据结构,用于解决一些不相交集合的合并及查询问题.例如:有n个村庄,查询2个村庄之间是否有连接的路,连接2个村庄 两大核心: 查找 (Find) : 查找元素所在

  • Java数据结构之堆(优先队列)详解

    目录 堆的性质 堆的分类 堆的向下调整 堆的建立 堆得向上调整 堆的常用操作 入队列 出队列 获取队首元素 TopK 问题 例子 数组排序 堆的性质 堆逻辑上是一棵完全二叉树,堆物理上是保存在数组中 . 总结:一颗完全二叉树以层序遍历方式放入数组中存储,这种方式的主要用法就是堆的表示. 并且 如果已知父亲(parent) 的下标, 则: 左孩子(left) 下标 = 2 * parent + 1; 右孩子(right) 下标 = 2 * parent + 2; 已知孩子(不区分左右)(child

  • Java数据结构之KMP算法详解以及代码实现

    目录 暴力匹配算法(Brute-Force,BF) 概念和原理 next数组 KMP匹配 KMP全匹配 总结 我们此前学了前缀树Trie的实现原理以及Java代码的实现.Trie树很好,但是它只能基于前缀匹配实现功能.但是如果我们的需求是:一个已知字符串中查找子串,并且子串并不一定符合前缀匹配,那么此时Trie树就无能为力了. 实际上这种字符串匹配的需求,在开发中非常常见,例如判断一个字符串是否包括某些子串,然后进行分别的处理. 暴力匹配算法(Brute-Force,BF) 这是最常见的算法字符

  • Java数据结构之对象比较详解

    目录 1. PriorityQueue中插入对象 2. 元素的比较 2.1 基本类型的比较 2.2 对象比较的问题 3. 对象的比较 3.1 覆写基类的equals 3.2 基于Comparble接口类的比较 3.3 基于比较器比较 3.4 三种方式的对比 4.集合框架中PriorityQueue的比较方式 本篇博客主要内容: Java中对象的比较 集合框架中PriorityQueue的比较方式 模拟实现PriorityQueue 1. PriorityQueue中插入对象 优先级队列在插入元素

  • Java数据结构之栈的详解

    目录 栈的抽象定义 顺序栈-----------使用数组表示栈空间 总结 栈是先进后出的特殊线性表,只允许在表的末端进行插入和删除,后面将介绍两种实现栈的方式,分别是基于数组的实现.基于链表的实现. 栈的抽象定义 class Mystack { public: Mystack() {} virtual void push(int &x) = 0; virtual bool pop(int &x) = 0; virtual bool Top(int &x) const = 0; vi

随机推荐