散列(一)


定义:
  • 散列是一种用于以常数平均时间执行插入,删除和查找的技术。
  • 我们将每个关键字被映射到从0到TableSize - 1这个范围中的某个数。这个映射称为散列函数。
  • 散列函数需要保证任何两个不同的关键字映射到不同的单元。
  • 当两个关键字散列到同一个值时(这叫做冲突), 应该做什么以及如何确定散列表的大小。
hash_table.png
散列函数:
  • 一般简单合理的方法是直接返回Key mod TableSize, 并且保证表的大小为素数。

a. 假设关键字是字符串,一种选择办法是将字符串的ASCII码(或Unicode码)值加起来

public static int hash(String key, int tableSize){
        int hashVal = 0;
        for (int i = 0; i < key.length(); i ++){
            hashVal += key.charAt(i);
        }
        return hashVal % tableSize;
}

这种方法实现起来简单合理,但如果表很大的话,并不会很好地分配关键字。例如,TableSize = 10007,假设所有的关键字至多8个字符长,由于ASCII码字符的值最多是127,那么它们的值是在0到1016之间,显然这不是一种均匀的分配。

*b. *假设关键字至少有三个字符,可以采用如下方法:

 public static int hash(String key, int tableSize){
        return (key.charAt(0) + key.charAt(1) * 27 + key.charAt(2) * 729) % tableSize;
 }

但这样依旧不能完全均匀地分配,因为3个字母的不同组合数实际只有2851,也就是只有大约表的28%被真正散列到。

c. 根据Horner法则计算一个(37)的多项式函数:

Paste_Image.png

public static int hash(String key, int tableSize){
        int hashVal = 0;
        for (int i = 0; i < key.length(); i ++){
            hashVal = 37 * hashVal + key.charAt(i);
        }
        hashVal %= tableSize;
        //产生负数的情况
        if (hashVal < 0){
            hashVal += tableSize;
        }
        return hashVal;
    }
解决冲突的两种方法:
  • 分离链接法:将散列到同一个值的所有元素保留到一个表中。
  • 开放定址法:尝试另外一些单元,直到找到空的单元为止。

*a. 分离链表法: *

separate_chaining.png
  • 定义表的基本变量:
    //定义默认表的大小
    private static final int DEFAULT_TABLE_SIZE = 101;
    //定义表的存储
    private List<AnyType>[] theLists;
    //定义表当前的大小
    private int currentSize;
  • 定义其构造函数,进行初始化:
    public SeparateChainingHashTable(){
        this(DEFAULT_TABLE_SIZE);
    }

    //进行初始化操作
    public SeparateChainingHashTable(int size){
        //初始化表,这里调用了一个nextPrime函数,以保证其表的大小为素数
        theLists = new LinkedList[nextPrime(size)];
        //初始化表中的链表
        for (int i = 0; i < theLists.length; i ++){
            theLists[i] = new LinkedList<AnyType>();
        }
    }

    //返回下一个素数
    private static int nextPrime(int n){
        while (!isPrime(n)){
            n ++;
        }
        return n;
    }
    //判断是否为素数
    private static boolean isPrime(int n){
        for (int i = 2; i <= Math.sqrt(n); i ++){
            if (n % i == 0 && n != 2){
                return false;
            }
        }
        return true;
    }
  • 进行插入操作:如果这个元素是新元素,则它将被插入到链表的前端,因为往往新近插入的元素最有可能不久被访问。
    public void insert(AnyType x){
        //获取x根据hash后找到所对应的位置
        List<AnyType> whichList = theLists[myHash(x)];
        //如果当前位置链表不包含元素,则进行插入操作
        if (!whichList.contains(x)){
            //进行添加
            whichList.add(x);
            //判断是否达到表的长度,若达到则进行rehash操作,以扩大表的大小
            if (++ currentSize > theLists.length){
                rehash();
            }
        }
    }

    //根据值获取到其对应的hash位置
    private int myHash(AnyType x){
        int hashVal = x.hashCode();
        hashVal %= theLists.length;
        //考虑到负数的情况
        if (hashVal < 0){
            hashVal += theLists.length;
        }
        return hashVal;
    }

    private void rehash(){
        //获取到原来的表
        List<AnyType>[] oldLists = theLists;
        //对现有的表大小进行扩大
        theLists = new List[nextPrime(2 * theLists.length)];
        //初始化新表
        for (int j = 0; j < theLists.length; j ++){
            theLists[j] = new LinkedList<AnyType>();
        }
        //初始化大小
        currentSize = 0;
        //将旧表中的数据重新插入到新表中
        for (int i = 0; i < oldLists.length; i ++){
            for (AnyType item : oldLists[i]){
                insert(item);
            }
        }
    }
  • 删除操作:
    //删除操作
    public void remove(AnyType x){
        //获取到对应位置的链表
        List<AnyType> whichList = theLists[myHash(x)];
        //检查是否包含元素,包含则进行删除操作,并大小--
        if (whichList.contains(x)){
            whichList.remove(x);
            currentSize --;
        }
    }
  • 检查元素:
    //检查是否包含某个元素
    public boolean contains(AnyType x){
        List<AnyType> whichList = theLists[myHash(x)];
        return whichList.contains(x);
    }
  • 打印链表:
    //打印链表结构
    public void printHashTable(){
        for (int i = 0; i < theLists.length; i ++){
            if (theLists[i] != null && theLists[i].size() > 0){
                System.out.println("current position is : " + i );
                for (AnyType x : theLists[i]){
                    System.out.println(x);
                }
            }
        }
    }
  • 进行检测:为了效果更明显,我们强制表的结果为10,当然在实际项目中不可如此,这里只是为了测试作用。
public static void main(String[] args){
        int[] arr = {0, 81,1, 64,4, 25,36,16,49,9};
        SeparateChainingHashTable<Integer> separateChainingHashTable = new SeparateChainingHashTable<>(10);
        //进行插入操作
        for (int i = 0; i < arr.length; i ++){
            separateChainingHashTable.insert(arr[i]);
        }
        //打印链表
        separateChainingHashTable.printHashTable();
    }
  • 运行结果如下:
current position is : 0
0
current position is : 1
81
1
current position is : 4
64
4
current position is : 5
25
current position is : 6
36
16
current position is : 9
49
9
separate chaning.png
  • 当然除了插入默认的基本类型,我们还可以插入对象,但一定要重定义对象的equals()和hashCode()方法
public class Employee {
    private String name;
    private double salary;
    private int seniority;
    //构造函数
    public Employee(String name, double salary, int seniority){
        this.name = name;
        this.salary = salary;
        this.seniority = seniority;
    }
    //判断对象是否相同
    public boolean equals(Object rhs){
        //满足为Employee类型且名字相同
        return rhs instanceof Employee && name.equals(((Employee) rhs).name);
    }
    //重写hashCode
    public int hashCode(){
        return name.hashCode();
    }
    //打印对象
    @Override
    public String toString() {
        return "name: " + name + " salary: " + salary + " seniority: " + seniority + "\n";
    }
}
public static void main(String[] args){
        SeparateChainingHashTable<Employee> hashTable = new SeparateChainingHashTable<>(11);
        hashTable.insert(new Employee("worker", 1000,3));
        hashTable.insert(new Employee("worker", 1500,3));
        hashTable.insert(new Employee("worker", 1600,3));
        hashTable.insert(new Employee("Manager", 2000, 2));
        hashTable.insert(new Employee("Manager", 2500, 2));
        hashTable.insert(new Employee("boss", 3000, 1));
        hashTable.printHashTable();
    }

结果如下:

current position is : 6
name: Manager salary: 2000.0 seniority: 2
current position is : 7
name: boss salary: 3000.0 seniority: 1
current position is : 10
name: worker salary: 1000.0 seniority: 3

我们可以看到对于name相同的对象是不会重复插入的。

完整代码:
public class SeparateChainingHashTable<AnyType> {

    public SeparateChainingHashTable(){
        this(DEFAULT_TABLE_SIZE);
    }

    //进行初始化操作
    public SeparateChainingHashTable(int size){
        //初始化表,这里调用了一个nextPrime函数,以保证其表的大小为素数
        theLists = new LinkedList[nextPrime(size)];
        //初始化表中的链表
        for (int i = 0; i < theLists.length; i ++){
            theLists[i] = new LinkedList<AnyType>();
        }
    }

    public void insert(AnyType x){
        //获取x根据hash后找到所对应的位置
        List<AnyType> whichList = theLists[myHash(x)];
        //如果当前位置链表不包含元素,则进行插入操作
        if (!whichList.contains(x)){
            //进行添加
            whichList.add(x);
            //判断是否达到表的长度,若达到则进行rehash操作,以扩大表的大小
            if (++ currentSize > theLists.length){
                rehash();
            }
        }
    }
    //删除操作
    public void remove(AnyType x){
        //获取到对应位置的链表
        List<AnyType> whichList = theLists[myHash(x)];
        //检查是否包含元素,包含则进行删除操作,并大小--
        if (whichList.contains(x)){
            whichList.remove(x);
            currentSize --;
        }
    }

    //检查是否包含某个元素
    public boolean contains(AnyType x){
        List<AnyType> whichList = theLists[myHash(x)];
        return whichList.contains(x);
    }

    public void makeEmpty(){
        for (int i = 0; i < theLists.length; i ++){
            theLists[i].clear();
        }
    }

    //定义默认表的大小
    private static final int DEFAULT_TABLE_SIZE = 101;
    //定义表的存储
    private List<AnyType>[] theLists;
    //定义表当前的大小
    private int currentSize;

    //根据值获取到其对应的hash位置
    private int myHash(AnyType x){
        int hashVal = x.hashCode();
        hashVal %= theLists.length;
        //考虑到负数的情况
        if (hashVal < 0){
            hashVal += theLists.length;
        }
        return hashVal;
    }

    private void rehash(){
        //获取到原来的表
        List<AnyType>[] oldLists = theLists;
        //对现有的表大小进行扩大
        theLists = new List[nextPrime(2 * theLists.length)];
        //初始化新表
        for (int j = 0; j < theLists.length; j ++){
            theLists[j] = new LinkedList<AnyType>();
        }
        //初始化大小
        currentSize = 0;
        //将旧表中的数据重新插入到新表中
        for (int i = 0; i < oldLists.length; i ++){
            for (AnyType item : oldLists[i]){
                insert(item);
            }
        }
    }

    //返回下一个素数
    private static int nextPrime(int n){
        while (!isPrime(n)){
            n ++;
        }
        return n;
    }
    //判断是否为素数
    private static boolean isPrime(int n){
        for (int i = 2; i <= Math.sqrt(n); i ++){
            if (n % i == 0 && n != 2){
                return false;
            }
        }
        return true;
    }

    //打印链表结构
    public void printHashTable(){
        for (int i = 0; i < theLists.length; i ++){
            if (theLists[i] != null && theLists[i].size() > 0){
                System.out.println("current position is : " + i );
                for (AnyType x : theLists[i]){
                    System.out.println(x);
                }
            }
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,577评论 18 399
  • 一. Java基础部分.................................................
    wy_sure阅读 3,789评论 0 11
  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,723评论 0 33
  • 回溯算法 回溯法:也称为试探法,它并不考虑问题规模的大小,而是从问题的最明显的最小规模开始逐步求解出可能的答案,并...
    fredal阅读 13,623评论 0 89
  • 做寒假工的时候,体会到了在社会生存的不易。 重复的、机械的工作有很多,从事这些工作的人也很容易被替换,例如:服务员...
    008明瑾阅读 238评论 0 0