散列表【Java算法(七)】

39 篇文章 2 订阅

写在前面的话:

  1. 参考资料:《漫画算法》52页散列表
  2. 所需知识点:单链表的增删改查
  3. 本章知识难点:多个链表的增删改查
  4. IDE:IntelliJ IDEA 2021.2.1
  5. JDK:Java8

目录

1.散列表

2.测试类 

3.运行截图 


散列表结构图:(代码根据该结构图制作出来)

 

 

项目结构: 

1.散列表

MyHashTable.java
package ex01;

/*
    散列表:
        1.将传入的Key通过哈希函数转换成索引值
        2.当第1步出现哈希冲突时,考虑使用拉链法(链表法)来解决问题

    确定是否进行扩容的问题:
        当size >= capacity * loadFactor时,需要进行扩容。
        1.扩容:
            数组长度变为原来的两倍。
        2.重新Hash:
            将原先在散列表中的值,根据哈希函数规则再次放入新的散列表中
 */

public class MyHashTable<Key, Value> {

    private int size;//存放元素的个数
    //这里体现了多个链表的使用
    private Node<Key, Value>[] head;//头节点,含有多个链表【头节点也有多个】
    private Node<Key, Value>[] tail;//尾结点,跟头节点对应

    private float capacity;//散列表的容量 ==> 散列表的长度【与array.length相等】
    private final float loadFactor = 0.75f;//负载因子 ==> 通常设定为0.75f

    //仓库中相当于1个链表
    private Node storeHouse_H;//放入插入散列表中的所有节点的头【相当于一个仓库】
    private Node storeHouse_T;//放入插入散列表中的所有节点的尾【相当于一个仓库】

    /**
     * 链表的节点
     */
    private class Node<Key, Value> {
        //数据
        Value data;
        //关键key
        Key key;
        //下一个节点
        Node next;

        //创建一个节点时,将数据放进去
        public Node(Key key, Value data) {
            this.data = data;
            this.key = key;
        }

        public Node() {
        }

    }

    //创建散列表时需要用到
    public MyHashTable() {
        this(10);//未指定时,初始值就是10
        this.capacity = 10;//创建完,更新散列表容量(capacity)
    }

    /**
     * 实例化一个散列表时需要
     *
     * @param capacity 初始散列表的容量
     */
    public MyHashTable(int capacity) {
        if (capacity > 0) {
            //创建一个存储空间为capacity的数组
            head = (Node<Key, Value>[]) new Node[capacity];
            tail = (Node<Key, Value>[]) new Node[capacity];
            //初始化链表数组
            for (int i = 0; i < capacity; i++) {
                head[i] = new Node();
                tail[i] = new Node();
            }
        } else {
            //手动抛出异常
            throw new IllegalArgumentException("参数capacity错误!capacity应不小于或等于0:" + capacity);
        }
        this.capacity = capacity;//创建完,更新散列表容量(capacity)
    }

    /**
     * 通过哈希算法转换过来的索引值
     *
     * @param x 传入的Key值
     * @return 返回一个索引值
     */
    private int getHash(Key x) {
        //确保索引值是正数
        return (x.hashCode() & 0x7fffffff) % head.length;
    }

    /**
     * 将数据插入散列表中
     *
     * @param key
     * @param value
     */
    public void insert(Key key, Value value) {

        //判断数组是否需要扩容
        if (size >= capacity * loadFactor) {
            expandHashMap();
        }

        //当散列表中有多个数据,才会出现key相同的情况
        if (size > 0) {
            //判断参数key是否存在
            if (isExist(key)) {
                //若存在,则抛出异常
                throw new IllegalArgumentException("参数key错误!参数key:" + key + "已存在!");
            }
        }

        //先创建一个(将要插入的)节点
        Node<Key, Value> insertedNode = new Node<>(key, value);
        int index = getHash(key);

        if (size == 0) {
            //当散列表中还未有数据时,仓库为0
            storeHouse_H = insertedNode;
            storeHouse_T = insertedNode;
        }

        if (head[index].key == null) {
            //此索引值无节点【还未放入元素】
            //将插入节点设置成该索引值的头节点和尾节点
            head[index] = insertedNode;//该索引值的头节点
        } else {
            //在该索引值位置是一个链表,将其插入到该索引值的链表尾部
            tail[index].next = insertedNode;
        }
        tail[index] = insertedNode;//该索引值的尾节点
        if (size != 0) {
            storeHouse_T.next = insertedNode;//将节点插入到仓库当中进行保存
            storeHouse_T = insertedNode;
        }
        size++;//元素个数增加

    }

    /**
     * 从散列表中获取数据
     *
     * @param key 通过关键key来获取数据
     * @return 返回获取到的数据
     */
    public Value getValue(Key key) {

        /*
            思路:
                1.通过哈希函数将参数key转变成索引值
                2.查看当前索引值的key是否与参数key相等,若相等,返回数据,否则进入第3步
                3.依次遍历当前索引值的整条链表,直至为空
         */

        int index = getHash(key);//获取通过哈希函数转变成的索引值
        Node<Key, Value> node = head[index];//从头节点开始遍历【当前索引值的链表的头节点】

        if (node == null) {
            //该索引值未放元素
            return null;
        }

        for (; node.key != null; node = node.next) {
            //判断key是否相等
            if (node.key.equals(key)) {
                return node.data;//若相等,则返回数据
            } else {
                node = node.next;//遍历下一个节点
            }
        }

        return null;//遍历完,没有遇到匹配值,散列表中无该数据
    }

    /**
     * 通过key来修改散列表
     *
     * @param key      关键key,定位到要修改的节点
     * @param newValue 修改的新值
     */
    public void change(Key key, Value newValue) {

        if (size <= 0) {
            throw new IllegalArgumentException("参数key错误!该散列表没有数据!");
        }

        //判断key是否存在
        if (isExist(key)) {

            //从此处开始,与查询相似【public Value getValue(Key key)】
            int index = getHash(key);//通过哈希算法得到一个索引值
            Node<Key, Value> node = head[index];//从头节点开始遍历【当前索引值的链表的头节点】

            while (node.key != null) {
                //判断key是否相等
                if (node.key.equals(key)) {
                    node.data = newValue;//将新的值赋值给data
                    break;//修改后,跳出循环【没有这句,将成为死循环】
                } else {
                    node = node.next;//遍历下一个节点
                }
            }
        } else {
            throw new IllegalArgumentException("参数key错误!未找到key:" + key);
        }
    }

    /*
    删除思路:
        1.从仓库中删除掉该节点
        2.获取索引值(通过哈希函数把key转换),找到对应链表
        3.利用链表删除的思路(头节点删除、中间删除、尾部删除)
    */
    public void delete(Key key) {

        if (size <= 0) {
            throw new IllegalArgumentException("参数key错误!该散列表没有数据!");
        }

        //判断key是否存在【存在才能够删除】
        if (isExist(key)) {

            int index = getHash(key);//通过哈希算法得到一个索引值
            Node<Key, Value> node = head[index];//从头节点开始遍历【当前索引值的链表的头节点】
            Node<Key, Value> storeNode = storeHouse_H;//仓库节点
            Node<Key, Value> preStoreNode = null;//获取要删除仓库中节点前的一个节点
            Node<Key, Value> preNode = null;//获取前一个节点

            /*
                先将简单的把仓库中的节点删除
             */
            //遍历整个仓库中的节点,发现key相同的节点删除它【插入时已经判断key要唯一】
            while (storeNode != null) {
                if (storeNode.key.equals(key)) {
                    //发现与参数key相同
                    if (storeHouse_H.key.equals(key)) {//当要删除的仓库的头节点时
                        //这里与单个链表不同,需特别注意:这是由多个链表【size==1一定等于头节点,但size != 1不一定不是头节点】
                        if(size == 1){//当仓库中国只有1个元素时
                            size = 0;//元素个数为0
                        }else{
                            //当仓库中元素不只有1个,但是该节点是其中一个链表的头节点时,需特别注意!
                            if(storeNode.next == null){
                                //要删除的节点是刚加入的【要删除的节点为尾节点】
                                preStoreNode.next = null;//将前一个仓库节点的后一个节点设置为null
                                storeHouse_T = preStoreNode;//并将该节点设置为尾部节点
                                break;//删除后,跳出循环
                            }
                            storeHouse_H = storeNode.next;//移动仓库的头节点
                        }
                        break;//删除后,跳出循环
                    } else if (storeNode.next == null) {
                        //要删除的节点是刚加入的【要删除的节点为尾节点】
                        preStoreNode.next = null;//将前一个仓库节点的后一个节点设置为null
                        storeHouse_T = preStoreNode;//并将该节点设置为尾部节点
                        break;//删除后,跳出循环
                    } else {
                        //从中间删除的情况
                        preStoreNode.next = storeNode.next;//前一个节点的下一个 ==> 当前节点的下一个【删除成功】
                    }
                }
                //将此节点记录下来,作为下一节点的前一个节点
                preStoreNode = storeNode;
                storeNode = storeNode.next;
            }

            /*
                从散列表中删除节点
             */
            while (node != null) {
                if (node.key.equals(key)) {
                    //发现与参数key相同
                    if (node.key.equals(key)) {
                        //此时相当于该索引值处的节点==>就是要删除的节点
                        node.key = null;//将key删掉,等同于删除这个节点【插入时,是判断有无key的】
                        break;//删除后,跳出循环
                    } else if (node.next == null) {
                        //要删除的节点是刚加入的【要删除的节点为尾节点】
                        preNode.next = null;//将前一个仓库节点的后一个节点设置为null
                        tail[index] = preNode;//并将该节点设置为尾部节点
                        break;//删除后,跳出循环
                    } else {
                        //从中间删除的情况
                        preNode.next = node.next;//前一个节点的下一个 ==> 当前节点的下一个【删除成功】
                    }
                }
                //将此节点记录下来,作为下一节点的前一个节点
                preNode = node;
                node = node.next;
            }

        } else {
            throw new IllegalArgumentException("参数key错误!未找到key:" + key);
        }

        size--;//删除后,元素个数减一
    }

    /**
     * 散列表进行扩容
     */
    private void expandHashMap() {

        //将整个头节点数组扩大成2倍
        Node<Key, Value>[] newHead = new Node[head.length * 2];

        head = newHead;

        Node<Key, Value> node = storeHouse_H;

        //重新hash
        for (int i = 0; i < size; i++) {
            insert(node.key, node.data);
            node = node.next;
        }
    }

    /**
     * 判断插入的key是否在散列表中存在
     *
     * @param key 传入要插入的key
     * @return 返回true -> 存在  false -> 不存在
     */
    private boolean isExist(Key key) {
        boolean flag = false;
        Node node = storeHouse_H;//从仓库中头节点开始遍历,寻找可能存在的参数key

        while (node != null) {
            //判断是否存在参数key
            if (node.key.equals(key)) {
                flag = true;
            }
            node = node.next;
        }
        return flag;
    }

    @Override
    public int hashCode() {
        return super.hashCode();
    }
}

2.测试类 

Test.java 

package ex01;

public class Test {

    public static void main(String[] args) {
        MyHashTable<Integer,String> myHashTable = new MyHashTable<Integer,String>(20);

        /*
            增加数据
         */
        myHashTable.insert(1001,"张三");
        myHashTable.insert(1002,"李四");
        myHashTable.insert(1003,"王五");
        myHashTable.insert(1004,"刘一");
        myHashTable.insert(1005,"熊大");
        myHashTable.insert(1006,"熊二");
        myHashTable.insert(1007,"光头强");
        myHashTable.insert(1008,"喜羊羊");
        myHashTable.insert(1009,"孙悟空");
        myHashTable.insert(1010,"kitty");

        for(int i = 1001;i <= 1010;i++){
            String name = myHashTable.getValue(i);
            System.out.println(i + " " + name);
        }
        System.out.println("---------------------------add");

        /*
            修改数据
         */
        myHashTable.change(1002,"甲乙");
        for(int i = 1001;i <= 1010;i++){
            String name = myHashTable.getValue(i);
            System.out.println(i + " " + name);
        }
        System.out.println("---------------------------update");

        /*
            删除数据
         */
        myHashTable.delete(1004);
        myHashTable.delete(1005);
        myHashTable.delete(1001);
        myHashTable.delete(1010);
        for(int i = 1001;i <= 1010;i++){
            String name = myHashTable.getValue(i);
            System.out.println(i + " " + name);
        }
        System.out.println("---------------------------delete");

        /*
            再次添加
         */
        myHashTable.insert(1004,"灰太狼");
        for(int i = 1001;i <= 1010;i++){
            String name = myHashTable.getValue(i);
            System.out.println(i + " " + name);
        }
    }
}


3.运行截图 

 增加部分:

修改1002的值改为”甲乙“:

 删除部分:

删除之后,能够添加:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fy哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值