哈希算法的介绍和实现

一. 介绍

        百科中,从哈希算法的功能上,对哈希算法进行了定义。百科是这样定义哈希算法的:哈希算法可以将任意长度的二进制值映射为较短的,固定长度的二进制值。我们把这个二进制值成为哈希值。在Java中,哈希(Hash)是一个广泛应用于数据结构和算法中的概念,主要用于快速查找、存储和比较数据。哈希的核心在于哈希函数(Hash Function),它将输入(通常称为键,key)映射到一个固定范围的输出值,这个输出值称为哈希值(Hash Value)或哈希码(HashCode)。哈希的目的在于将原本复杂、不规则的数据转化为简洁的、固定长度的值,使得数据的存储和检索更加高效。

1. 哈希值的特点


  1> 哈希值是二进制值


  2> 哈希值具有一定的唯一性


  3> 哈希值极其紧凑


  4> 要找到生成同一个哈希值的2个不同输入,在一定时间范围内,是不可能的


         正因为哈希值的这些特点,使得哈希算法应用在加密领域成为可能。哈希算法在加密领域的应用,源于哈希算法的不可逆性,对于用户输入的密码,通过哈希算法可以得到一个哈希值。并且,同一个密码,生成的哈希值总是相等的。这样,服务器就可以在不知道用户输入的密码的情况下,判断用户输入的密码是否正确。

2. 哈希函数的特点

  • 哈希函数应该是高效的,即计算速度快。
  • 哈希函数应该尽量均匀分布,以减少哈希冲突。
  • 哈希值虽然可以用于快速比较,但不保证绝对唯一,因此在判断对象相等时,除了比较哈希值外,还需要比较对象的实际内容(通过equals()方法)。
  • 在实现自定义类的hashCode()时,应当遵守与equals()方法的一致性原则,即如果两个对象通过equals()判断为相等,它们的哈希码也必须相等。反之,哈希码相等的对象不一定通过equals()判断相等。

3. 哈希冲突

定义: 当两个不同的数经过哈希函数计算后得到了同一个结果,即他们会被映射到哈希表的同一个位置时,即称为发生了哈希冲突。简单来说就是哈希函数算出来的地址被别的元素占用了。

解决哈希冲突办法

1>  开放定址法

我们在遇到哈希冲突时,去寻找一个新的空闲的哈希地址

举例:就是当我们去教室上课,发现该位置已经存在人了,所以我们应该寻找新的位子坐下,这就是开放定址法的思路。如何寻找新的位置就通过以下几种方法实现。

(1)线性探测法

当我们的所需要存放值的位置被占了,我们就往后面一直加1并对m取模直到存在一个空余的地址供我们存放值,取模是为了保证找到的位置在0~m-1的有效空间之中。

公式:h(x)=(Hash(x)+i)mod (Hashtable.length);(i会逐渐递增加1)

举例:

 存在问题:出现非同义词冲突(两个不相同的哈希值,抢占同一个后续的哈希地址)被称为堆积或聚集现象。

(2)平方探测法(二次探测)

 当我们的所需要存放值的位置被占了,会前后寻找而不是单独方向的寻找。

        公式:h(x)=(Hash(x) +i)mod (Hashtable.length);(i依次为+(i^2)和-(i^2))

        举例:

 2> 再哈希法

        同时构造多个不同的哈希函数,等发生哈希冲突时就使用第二个、第三个……等其他的哈希函数计算地址,直到不发生冲突为止。虽然不易发生聚集,但是增加了计算时间。 

3>  链地址法

        将所有哈希地址相同的记录都链接在同一链表中。

公式:h(x)=xmod(Hashtable.length);

4> 建立公共溢出区

        将哈希表分为基本表和溢出表,将发生冲突的都存放在溢出表中。 

4. 哈希表(Hash Table)

哈希表也叫散列表,哈希表是一种数据结构,它提供了快速的插入操作和查找操作,无论哈希表总中有多少条数据,插入和查找的时间复杂度都是为O(1)。在实现哈希表时,如果只靠数组存储,当需要存储大量元素时,系统很难在内存中找到连续的内存空间。因此需要结合链表来存储大量数据,当链表长度过高时,会转化成红黑树。其次哈希码是可以重复的,当重复时根据数据的值来进行区分。

哈希表,其实是单向链表和数组的结合体,换一种说法呢,就是有一个单向链表数组,里面的每一个元素都是一个单向链表.但是,哈希表也引入了新的代码思想,即底层思想和表层思想,具体的解释是,在底层的单向链表中书写各种方法,然后再在哈希表中书写这些方法,用哈希表的方法去调用单向链表的底层方法,这就是哈希表所能带给我们的新事物,这个应用在后面的二叉树,线索化二叉树中经常可以用到,所以非常的重要,一定要掌握!

哈希表的创建思路:

  •  创建节点类
    •  节点类中应该与单向链表一样有data域和next域
    •  在节点类中提供对应的构造方法,还有重写toString方法,便于输出
  •  创建单向链表类
    • 单向链表类应该有head属性,由于哈希表中不需要头节点,所以head被赋值为第一个节点的内存地址
    • 创建增删插查遍历的底层方法
  • 创建哈希表类
    • 哈希表类中应该有单向链表数组属性还有数组长度属性
    • 创建对应的构造方法,能指定哈希表的底层数组长度
 1> 创建节点类
package com.nianxi.hash;

public class EmpNode {

    public int no;
    public String name;
    public EmpNode next;

    public EmpNode(int no,String name) {
        this.no = no;
        this.name = name;
    }
    //构造方法的创建不能指定其next域,应该有手动设置
    public EmpNode() {

    }

    public String toString() {
        return "该雇员的no[" + no + "] 名字是[" + name + "]";
    }
}
2> 创建单向链表类 (底层)
package com.nianxi.hash;

public class EmpLinedList {
    //head指向的是第一个节点的,不是头节点,哈希表底层的单向链表是无头节点的
    private EmpNode head;
}
3> 在单向链表中创建添加节点的方法
    //添加雇员
    public void add(EmpNode node) {
        //校验意识,先判断是否有第一个节点,如果是,则直接将新节点赋值给head
        if(head == null) {
            head = node;
            return;
        }
        //建立一个辅助引用,避免直接去触碰第一个节点,以防底层链表丢失,若能走到这一步,说明head!=null,可以将head给辅助引用
        var cur = head;
        while(cur.next != null)
            cur = cur.next;
        cur.next = node;
        //通过循环,就可以找到最后一个节点了,找到之后,直接赋值即可(next不为空,说明不是最后一个节点,需要继续循环跳往下一个节点)
    }
4> 在单向链表中创建遍历链表的方法:
    //遍历链表
    public void print(int num) {
        //遍历链表的时候,我们需要先判断一下单向链表是否为空,如果为空,则直接退出,并输出你想要的一句话(校验意识)
        if (head == null) {
            System.out.println("第" + (num + 1) + "个链表为空");
            return;
        }
        //建立一个辅助引用,并赋值为head,通过cur!=null,说明当前节点仍然存在,即输出
        var cur = head;
        System.out.print("第" + (num + 1) + "个链表为:");
        while (cur != null) {
            System.out.print(cur + "  ");
            cur = cur.next;
        }
        //如果cur=null,说明节点已经遍历完了,应该退出循环,结束遍历
        System.out.println();
        //最后的println方法留作悬念,用于哈希表调用时的优化输出
    }
5> 在单向链表中创建查找节点的方法 
    //根据no查找雇员
    public boolean findByNo(int no) {
        //如果第一个节点为空,说明单向链表为空,直接退出,返回false
        if(head == null) {
            return false;
        }
        var cur = head;
        //cur != null 为了防止空指针异常,而且必须写在前面,充分利用短路与的作用
        //只要no匹配不上和cur不为空,就进入循环,往后一个节点去跳
        while(cur != null && cur.no != no){
            cur = cur.next;
        }
        //当循环结束的时候,有两种情况
        //cur为空而出来的,所以最后的结果是cur为空
        //找得到出来的,所以最后的结果是cur不为空
        return cur != null;
        //根据cur是否为空就可以判断是否找得到
    }
 6> 在单向链表中创建删除节点的方法
//根据no删除雇员
    public boolean del(EmpNode node) {
        //可能出现的情况有
        //是否第一个节点为空
        //是否只有一个节点,且该节点是否为我们想要删除的节点
        //是否含有多个节点,且第一个节点是否为我们想要删除的节点
        //是否含有多个节点,且第一个节点不是我们想要删除的节点
        //根据这些情况,可列出上述ifelse语句
        if (head == null) {
            return false;
        } else if (head.next == null) {
            if (head.no == node.no) {
                head = null;
                return true;
            } else {
                return false;
            }
        } else {
            if (head.no == node.no) {
                head = head.next;//表示将第一个节点丢掉
                return true;
            }
        }
        var cur = head;
        boolean loop = false;
        while (cur.next != null) {
            if (cur.next.no == node.no) {
                loop = true;
                break;
            }
            cur = cur.next;
        }
        if (loop) {
            cur.next = cur.next.next;
            return true;
        }
        //删除节点时,必须要找到待删除节点的上一个节点,因为第一个节点比较特殊,所以要特殊处理
        return false;
    }
7> 在单向链表中创建插入节点的方法
    //根据no修改雇员
    public void insert(EmpNode node) {
        //先判断一下第一个节点是否为空,若为空,则直接赋值结课
        if(head == null) {
            head = node;
            return;
        }else {
            if(node.no < head.no) {
                node.next = head;
                head = node;
                return;
            }
        }
        //我们需要判断一下特殊情况,就是插入的节点位于第一个节点之前(单向链表一般都是要找待插入节点的上一个节点,第一个节点没有上一个节点,所以要特殊化处理)
        //如果满足上述node.no < head.no说明确实要插入第一个节点之前,所以第一步:node.next = head将新节点指向原来的第一个节点,第二步:head = node将node变成第一个节点,原来的第一个节点就会变成第二个节点
        boolean loop = false;
        var cur = head;
        while (cur.next != null) {
            if(cur.next.no > node.no) {
                loop = true;
                break;
            }
            cur = cur.next;
        }
        if(loop) {
            node.next = cur.next;
            cur.next = node;
        }
        //按传统的单向链表删除即可
    }
8> 创建一个哈希表类(表层) 
public class HashTable {
    private EmpLinedList[] empLinedLists;
    private int size;
    //哈希表是一个链表数组,所以要有链表数组的属性,还有该数组的大小

    public HashTable() {
    }

    //需要创建一个有参构造,用于初始化该单向链表数组的大小,即哈希表的大小
    public HashTable(int size) {
        this.size = size;
        empLinedLists = new EmpLinedList[size];
        for (int i = 0; i < size; i++) {
            empLinedLists[i] = new EmpLinedList();
        }
        //要给数组的每一个元素都初始化一个单向链表,否则空指针异常
    }
}
9> 创建一个哈希函数(离散函数)
    //该离散函数会根据传进来的no值,通过取模的方式算出其对应的哈希值,即对应的数组下标,让他找到其对应的单向链表
    private int hash(int no) {
        return no % size;
    }
10> 创建各种方法,调用底层方法
//只有调用了该哈希函数,对应的节点才能找到属于他的单向链表,否则数组下标越界异常或者出现其他运行错误与我们想要的结果不符 
    public void add(EmpNode node) {
        var index = hash(node.no);
        empLinedLists[index].add(node);
    }
    public void print() {
        for (int i = 0; i < size; i++) {
            empLinedLists[i].print(i);
        }
    }

    public boolean findByNo(int no) {
        var index = hash(no);
        return empLinedLists[index].findByNo(no);
    }

    public boolean del(EmpNode node) {
        var index = hash(node.no);
        return empLinedLists[index].del(node);
    }

    public void insert(EmpNode node) {
        var index = hash(node.no);
        empLinedLists[index].insert(node);
    }

5. 哈希桶(Hash Bucket)

        哈希桶(Hash Bucket)是哈希表(Hash Table)中用于解决哈希冲突的一种常用方法,它是哈希表数据结构的一个重要组成部分。哈希桶是哈希表中存储元素的地方,通常是一个数组。每个桶都有一个索引,通过哈希函数计算得到的哈希值会决定元素被放置在哪个桶中

        哈希桶解决哈希冲突的方法是,将哈希表的每个槽(或索引)扩展为一个“桶”(Bucket),这个桶本质上是一个数据结构(通常是链表、数组或其他容器),可以存储多个具有相同哈希值的元素。具体来说,当一个键通过哈希函数计算得到的索引已经有其他元素时,新的元素会被添加到这个索引对应的桶中,而不是覆盖原有的元素。

哈希桶的实现细节

  1. 哈希函数:用于将键转换成索引。好的哈希函数能够尽量均匀地分布元素,减少冲突。

  2. 桶的实现:常用的桶实现是链表,因为链表插入和删除操作的时间复杂度较低。但在Java 8以后的HashMap中,当桶中的元素数量达到一定阈值时,会将链表转换为红黑树,以进一步优化查询性能。

  3. 负载因子:表示哈希表中已填入元素的数量与哈希表长度的比例,用于衡量哈希表的填充程度。当负载因子超过某个预设值时,哈希表会进行扩容,重新调整大小,以减少冲突,保持高效性能。

  4. 扩容:扩容通常涉及创建一个新的、更大容量的哈希表,并将原哈希表中的所有元素重新哈希到新表中。这个过程可以确保桶的平均长度减少,从而减少冲突。

  5. 冲突处理:当多个键映射到同一索引时,桶中的链表(或红黑树)结构用于存储这些冲突的键值对,并通过遍历链表(或树)来查找具体的元素。

package com.nianxi.hash;

// key-value 模型
public class HashBucket {
    private static class Node {
        private int key;
        private int value;
        Node next;


        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    private Node[] array;
    private int size;   // 当前的数据个数
    private static final double LOAD_FACTOR = 0.75;
    private static final int DEFAULT_SIZE = 8;//默认桶的大小

    public int put(int key, int value) {
        int index = key % array.length;
        Node cur = array[index];
        //遍历当前列表,看是否存在当前值
        while (cur != null) {
            if (cur.key == key) {
                cur.value = value;
            }
            cur = cur.next;
        }
        //若无当前值,则进行头插法
        Node node = new Node(key, value);
        node.next = array[index];
        array[index] = node;
        size++;
        //判断是否超载
        if (loadFactor()>=LOAD_FACTOR){
            //扩容
            resize();
        }
        return 0;
    }


    private void resize() {
        Node[] newArr=new Node[array.length*2];
        for (int i = 0; i < array.length; i++) {
            Node cur=array[i];
            while(cur!=null){
                //遍历链表,将数据储存到新数组
                int newIndex=cur.key% newArr.length;
                Node curN=cur.next;
                cur.next=newArr[newIndex];
                newArr[newIndex]=cur;
                cur=curN;
            }
        }
        array=newArr;
    }


    private double loadFactor() {
        return size * 1.0 / array.length;
    }


    public HashBucket() {
        array=new Node[10];
    }


    public int get(int key) {
        int index=key%array.length;
        Node cur=array[index];
        while(cur!=null){
            if (cur.key==key){
                return cur.value;
            }
            cur=cur.next;
        }
        return -1;
    }
}

        哈希桶机制通过将冲突的元素组织在一起,而非直接覆盖,保证了哈希表的灵活性和高效性。它允许哈希表在面对大量数据时仍能保持较好的性能,尤其是在冲突较多的情况下。通过调整哈希函数、负载因子和适时的扩容,可以进一步优化哈希表的效率。在Java中,HashMap和HashSet就是使用哈希桶来实现的,它们是Java集合框架中非常重要的组件。

 

ps: 哈希冲突的图引用了解决哈希冲突(四种方法)-CSDN博客

      哈希表借鉴了Java数据结构-哈希表的实现(hash)_java哈希表的实现-CSDN博客

                        ❤️❤️❤️ 

  • 29
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
哈希算法是一种常用的加密算法,可以将输入的任意长度的数据转化成固定长度的哈希值。在C语言中,我们可以通过一些常用的哈希算法实现来进行加密。 一个常用的哈希算法是MD5(Message Digest Algorithm 5)。在C语言中,我们可以使用openssl库中的MD5函数来实现。 首先,我们需要引入openssl头文件并链接openssl库: ``` #include <openssl/md5.h> #include <stdio.h> #include <string.h> #include <stdlib.h> ``` 接下来,我们可以定义一个函数来实现哈希算法的加密功能: ``` void md5_encrypt(char* input, char* output) { unsigned char digest[MD5_DIGEST_LENGTH]; MD5((unsigned char*)input, strlen(input), digest); for(int i = 0; i < MD5_DIGEST_LENGTH; i++) { sprintf(&(output[i*2]), "%02x", (unsigned int)digest[i]); } } ``` 在这个函数中,我们首先定义了一个unsigned char类型的数组digest用于存储MD5算法的哈希值。然后,我们调用了MD5函数来对输入的input进行哈希算法的运算。最后,我们通过sprintf函数将哈希值转化为16进制的字符串并存储在output中。 我们可以在主函数中调用md5_encrypt函数,并输出结果: ``` int main() { char input[] = "Hello World"; char output[MD5_DIGEST_LENGTH*2+1]; md5_encrypt(input, output); printf("MD5 hash: %s\n", output); return 0; } ``` 运行这段代码,我们将会得到输入"Hello World"的MD5哈希值并输出。 除了MD5,C语言中还有其他的哈希算法实现,比如SHA-1(Secure Hash Algorithm 1),SHA-256等等。实现这些哈希算法的原理和步骤类似,只需要将相应的函数和头文件进行替换即可。 需要注意的是,哈希算法是一种单向加密算法,即无法通过哈希值逆向得到原始数据。因此,哈希算法主要用于验证数据的完整性和唯一性,而不是加密敏感信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

晚睡早起₍˄·͈༝·͈˄*₎◞ ̑̑

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

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

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

打赏作者

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

抵扣说明:

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

余额充值