哈希表

介绍

1.哈希表,又称散列表,是根据关键码值而直接进行访问的数据结构

2.它通过把关键码值映射到表中一个位置来访问记录, 以加快查找的速度

3.这个映射函数叫做散列函数, 存放记录的数组叫做散列表

 

两种实现方式

1.数组+链表

2.数组+红黑树

 

代码实现(数组+单向链表)

public class HashTable {
    private SingleLinkedList[] hashTable;//LinkedList类型的数组管理链表
    private int size;//表示有多少条链表

    public HashTable(int size) {
        hashTable = new SingleLinkedList[size];//这里是初始化数组
        for (int i = 0; i < size; i++) {//分别初始化每个链表
            hashTable[i] = new SingleLinkedList();
        }
        this.size = size;
    }

    //添加节点
    public void add(Node node) {
        int no = hash(node.id);//根据节点的id,得到该员工应当添加到哪条链条
        hashTable[no].add(node);//将node添加到对应的链表中
    }

    //根据id,删除节点
    public void delete(int id) {
        int no = hash(id);//根据节点的id,得到该员工应当添加到哪条链条
        hashTable[no].delete(id);

    }

    //更新节点
    public void update(Node node) {
        int no = hash(node.id);//根据节点的id,得到该员工应当更新哪条链条
        hashTable[no].update(node);
    }

    // 遍历所有的链表
    public void list() {
        for (int i = 0; i < size; i++) {
            hashTable[i].list(i);
        }
    }

    //根据id查找
    public void findById(int id) {
        int no = hash(id);//先判断在哪条链表
        Node emp = hashTable[no].findById(id);
        if (emp != null) {
            System.out.println("第" + (no + 1) + "链表中找到id为" + id + "节点");
        } else {
            System.out.println("哈希表中没有找到");
        }
    }

    // 编写散列函数,使用取模法
    public int hash(int id) {
        return id % size;
    }
}

//表示节点,存储数据
class Node {
    public int id;
    public String name;//视作data
    public Node next;//下一个节点

    public Node(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

//表示单向链表,管理节点
class SingleLinkedList {
    private Node head;// 头指针,指向第一个节点

    //尾插法,添加节点到链表
    public void add(Node node) {
        //如果添加第一个节点
        if (head == null) {
            head = node;
            return;
        }
        Node cur = head;
        while (true) {
            if (cur.next == null) { //说明链表到最后
                break;
            }
            cur = cur.next;
        }
        cur.next = node;
    }

    //根据id,删除节点
    public void delete(int id) {
        if (head == null) {
            return;//链表为空
        }
        Node cur = head;
        if (id == head.id) {//删除头节点
            head = cur.next;
            return;
        }
        while (true) {
            if (cur.next == null) {//该编号节点不存在
                return;
            }
            if (cur.next.id == id) {
                cur.next = cur.next.next;//被跳过的节点没有引用,被回收
                return;
            }
            cur = cur.next;
        }
    }

    //更新节点
    public void update(Node node) {
        if (head == null) {
            return;//链表为空
        }
        Node cur = head;
        if (node.id == head.id) {//更新头节点
            node.next = cur.next;
            head = node;
            return;
        }
        while (true) {
            if (cur.next == null) {//该编号节点不存在
                return;
            } else if (cur.next.id == node.id) {//找到该编号节点
                node.next = cur.next.next;
                cur.next = node;
                return;
            }
            cur = cur.next;
        }
    }

    //遍历链表的节点data
    public void list(int no) {
        if (head == null) {
            System.out.println("第" + (no + 1) + "条链表为空");
            return;
        }
        System.out.print("第" + (no + 1) + "条链表的信息为");
        Node cur = head;
        while (true) {
            System.out.printf("=> id=%d name=%s\t", cur.id, cur.name);
            if (cur.next == null) { //说明链表到最后
                break;
            }
            cur = cur.next;
        }
        System.out.println();
    }

    //根据id查找节点
    public Node findById(int id) {
        if (head == null) {
            System.out.println("链表为空");
            return null;
        }
        Node cur = head;
        while (true) {
            if (cur.id == id) {
                break;//这时cur就指向要查找的节点
            }
            if (cur.next == null) {//说明遍历当前链表没有找到该节点
                cur = null;
                break;
            }
            cur = cur.next;//以后
        }
        return cur;
    }
}

 

多种数据类型的散列函数

1、假设

(1)有一个能够保存 M 个键值对的数组,那么就需要一个能够将任意键转化为该数组范围内的索引([0, M-1] 范围内的整数)的散列函数

(2)散列函数应该易于计算并且能够均匀分布所有的键,即对于任意键,0 到 M-1 之间的每个整数都有相等的可能性与之对应(与键无关)

2、正整数

(1)将整数散列最常用方法是除留余数法

(2)选择大小为素数 M 的数组,对于任意正整数 k,计算 k 除以 M 的余数

(3)这个函数的计算非常容易(在 Java 中为 k % M)并能够有效地 将键散布在 0 到 M-1 的范围内

(4)如果 M 不是素数,则可能无法利用键中包含的所有信息,这可能导致无法均匀地散列散列值

3、浮点数

(1)如果键是 0 到 1 之间的实数,可以将它乘以 M 并四舍五入得到一个 0 至 M-1 之间的索引值

(2)尽管这个方法很容易理解,但它是有缺陷的,因为这种情况下键的高位起的作用更大,最低位对散列的结果没有影响

(3)修正这个问题的办法是将键表示为二进制数然后再使用除留余数法(Java 就是这么做的)

4、字符串

(1)除留余数法也可以处理较长的键,例如字符串,只需将它们当作大整数即可

(2)例如

int hash = 0; 
for (int i = 0; i < s.length(); i++) 
 hash = (R * hash + s.charAt(i)) % M;

(3)Java 的 charAt() 函数能够返回一个 char 值,即一个非负 16 位整数。如果 R 比任何字符的值都大,这种计算相当于将字符串当作一个 N 位的 R 进制值,将它除以 M 并取余

(4)一种叫 Horner 方法的经典算法用 N 次乘法、加法和取余来计算一个字符串的散列值。只要 R 足够小,不造成溢出,那么结果就能够落在 0 至 M-1 之内,使用一个较小的素数,例如 31,可以保证字符串中的所有字符都能发挥作用

(5)Java 的 String 的默认实现使用了一个类似(4)方法

5、组合键

(1)如果键的类型含有多个整型变量,可以和 String 类型一样将它们混合起来

(2)例如:Date,其中含有几个整型的域:day(2 个数字表示的日),month(2 个数字表示的月)、 year(4 个数字表示的年)

int M = 31; 
int hash = (((day * R + month) % M ) * R + year) % M;

 

基于拉链法的散列表:数组 + 链表

1、假设:使用的散列函数能够均匀并独立地将所有的键散布于 0 到 M - 1 之间 

2、在一张含有 M 条链表和 N 个键的的散列表中,在假设成立的前提下,任意一条链表中的键的数量均在 N / M 的常数因子范围内的概率无限趋向于 1

 

基于线性探测法的散列表

1、开放地址散列表:用大小为 M 的数组保存 N 个键值对,其中 M>N,需要依靠数组中的空位解决碰撞冲突

2、开放地址散列表中最简单的方法叫做线性探测法:当碰撞发生时(当一个键的散列值已经被另一个不同的键占用),直接检查散列表中的下一个位置(将索引值加 1)。这样的线性探测可能会产生三种结果

(1)命中,该位置的键和被查找的键相同

(2)未命中,键为空(该位置没有键)

(3)继续查找,该位置的键和被查找的键不同

3、用散列函数找到键在数组中的索引,检查其中的键和被查找的键是否相同。如果不同则继续查找(将索引增大,到达数组结尾时折回数组的开头),直到找到该键或者遇到一个空元素

4、探测:检查一个数组位置是否含有被查找的键的操作,等价于一直使用的比较,不过有些探测实际上是在测试键是否为空

5、开放地址类的散列表的核心思想是与其将内存用作链表,不如将它们作为在散列表的空元素,这些空元素可以作为查找结束的标志

 

渐进性能的总结

算法(数据结构) 最坏情况下的运行时间的增长数量级(N 次插入之后) 平均情况下的运行时间的增长数量级(N 次插入之后) 内存使用(字节)
查找 插入 查找 插入
顺序查询(无序链表) N N N / 2 N 48 * N
二分查找(有序数组) lgN N lgN N / 2 16 * N
二叉树查找(BST) N N 1.39 * lgN 1.39 * lgN 64 * N
2-3 树查找(红黑树) 2 * lgN 2 * lgN 1.00 * lgN 1.00 * lgN 64 * N
拉链法*(链表数组) < lgN < lgN N / (2 * M) N / M 48 * N + 32 * M
线性探测法*(并行数组) c * lgN c * lgN < 1.5 < 2.5 32 * N ~ 128 * N

1、M:数组长度

2、*:需要均匀并独立的散列函数

posted @   半条咸鱼  阅读(27)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示