hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,
该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间。它其实就是一个算法,最简单的算法就是加减乘除,比方,我设计个数字算法,输入+7=输出,比如我输入1,输出为8;输入2,输出为9。
哈希算法不过是一个更为复杂的运算,它的输入可以是字符串,可以是数据,可以是任何文件,经过哈希运算后,变成一个固定长度的输出,
该输出就是哈希值。但是哈希算法有一个很大的特点,就是你不能从结果推算出输入,所以又称为不可逆的算法
下面使用一个简答的例子:
print(hash('我爱你'))
# 输出:3471388576844338423
print(hash('我也爱你'))
# 输出:5000768010434506639
如上所示,输入“我爱你”三个字,经过哈希运算后,会得到一个随机数列,而且不管你的输入文件多大,最后得到的结果都是这么一个固定长度的数列,即使你输入的是一部电影,输出也是这么大。而且通过数列不能推导出输入。
不可逆:在具备编码功能的同时,哈希算法也作为一种加密算法存在。即,你无法通过分析哈希值计算出源文件的样子,换句话说:你不可能通过观察香肠的纹理推测出猪原来的样子。
计算极快:20G高清电影和一个5K文本文件复杂度相同,计算量都极小,可以在0.1秒内得出结果。也就是说,不管猪有多肥,骨头多硬,做成香肠都只要眨眨眼的时间。
哈希算法的不可逆特性使其在以下领域使用广泛
密码,我们日常使用的各种电子密码本质上都是基于hash的,你不用担心支付宝的工作人员会把你的密码泄漏给第三方,因为你的登录密码是先经过 hash+各种复杂算法得出密文后 再存进支付宝的数据库里的
文件完整性校验,通过对文件进行hash,得出一段hash值 ,这样文件内容以后被修改了,hash值就会变。 MD5 Hash算法的”数字指纹”特性,使它成为应用最广泛的一种文件完整性校验和(Checksum)算法,不少Unix系统有提供计算md5 checksum的命令。
数字签名,数字签名技术是将摘要信息用发送者的私钥加密,与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文产生一个摘要信息,与解密的摘要信息对比。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改,否则说明信息被修改过,因此数字签名能够验证信息的完整性。
在上面的例子中,我们发现有些在Hash中很多位置可能要存两个甚至多个元素,很明显单纯的数组是不行的,这种两个不同的输入值,根据同一散列函数计算出的散列值相同的现象叫做碰撞,那该怎么解决呢?
常见的方法有: 开放定址法Java里的Threadlocal、链地址法Java里的ConcurrentHashMap、再哈希法(布隆过滤器)、建立公共溢出区。后两种用的比较少,我们重点看前两个。
开放地址法是什么?
开放定址法(Open Addressing)是一种解决哈希冲突的方法,在开放定址法中,所有的元素都存储在一个数组中。当发生哈希冲突时,即多个元素需要存储在同一个位置时,开放定址法会通过一定的探测方法查找下一个可用的位置,直到找到一个空闲的位置来存储冲突的元素。开放定址法的主要思想是通过线性探测、二次探测、双重散列等方法来寻找下一个可用的位置。
在查找或删除操作时,也需要按照同样的规则来查找目标元素。
使用场景
开放定址法的使用场景包括但不限于以下情况:
优点:
缺点:
总体上说,开放定址法是一种高效的解决哈希冲突的方法,适用于数据量较小、分布较均匀的场景。但在面对大规模数据和高负载因子时,可能会导致性能下降。开放定址法的优点是不需要额外的内存来存储链表或指针,可以充分利用哈希表的空间。但是当槽位的装填因子过高时,会导致冲突的概率增加,进而影响性能。因此,在设计哈希表时需要合理选择哈希函数和解决冲突的方法,并考虑调整哈希表的大小来平衡性能和空间的消耗。
链地址法是什么?
链地址法(Chaining)是一种解决哈希冲突的方法,用于实现哈希表。它的主要思想是将哈希桶中的每个位置都设置为一个链表或其他数据结构,当发生哈希冲突时,将冲突的元素插入到对应位置的链表中。在查找或删除操作时,首先计算元素的哈希值,并根据哈希值找到对应的槽。然后遍历槽对应的链表,查找或删除目标元素。
具体操作流程如下:
使用场景
优点:
缺点:
链地址法的优点是简单易实现,对于解决哈希冲突效果较好。但是需要额外的空间来存储链表节点,而且链表的查找、插入和删除操作的时间复杂度与链表的长度成正比,可能会导致性能下降。因此,在设计哈希表时需要合理选择哈希函数和解决冲突的方法,以平衡时间和空间的消耗。
队列的特点是节点的排队次序和出队次序按入队时间先后确定,即先入队者先出队,后入队者后出队,即我们常说的FIFO(first in first out)先进先出。队列实现方式也有两种形式,基于数组和基于链表。对于基于链表,因为链表的长度是随时都可以变的,实现起来比较简单。如果是基于数组的,会有点麻烦.
基于链表实现队列还是比较好处理的,只要在尾部后插入元素,在front删除元素就行了。
public class LinkQueue {
private Node front;
private Node rear;
private int size;
public LinkQueue() {
this.front = new Node(0);
this.rear = new Node(0);
}
/**
* 入队
*/
public void push(int value) {
Node newNode = new Node(value);
Node temp = front;
while (temp.next != null) {
temp = temp.next;
}
temp.next = newNode;
rear = newNode;
size++;
}
/**
* 出队
*/
public int pull() {
if (front.next == null) {
System.out.println("队列已空");
}
Node firstNode = front.next;
front.next = firstNode.next;
size--;
return firstNode.data;
}
/**
* 遍历队列
*/
public void traverse() {
Node temp = front.next;
while (temp != null) {
System.out.print(temp.data + "\t");
temp = temp.next;
}
}
static class Node {
public int data;
public Node next;
public Node(int data) {
this.data = data;
}
}
//测试main方法
public static void main(String[] args) {
LinkQueue linkQueue = new LinkQueue();
linkQueue.push(1);
linkQueue.push(2);
linkQueue.push(3);
System.out.println("第一个出队的元素为:" + linkQueue.pull());
System.out.println("队列中的元素为:");
linkQueue.traverse();
}
}
基于数组实现队列的算法需要维护两个指针,一个指向队列的头部(front),一个指向队列的尾部(rear)。初始时,front 和 rear 都设为 0。
以下是基于数组实现队列的算法步骤:
基于数组实现队列的算法的时间复杂度如下:
需要注意的是,使用基于数组实现的队列时,当队列满时,无法添加新元素,即使队列中有空闲位置。这种情况称为“数组循环(Array Circular)”,可以通过循环利用数组中的空闲位置来解决此问题。
public class ArrayQueue {
private int[] queue; // 用于存储队列元素的数组
private int front; // 队头指针
private int rear; // 队尾指针
public ArrayQueue() {
queue = new int[10]; // 初始化数组大小为10
front = -1; // 初始化队头指针为-1
rear = -1; // 初始化队尾指针为-1
}
/**
* 入队
*/
public void push(int value) {
if (isFull()) {
System.out.println("队列已满,无法添加新元素!");
return;
}
if (isEmpty()) {
front = 0; // 如果队列为空,则将队头指针设置为0
}
rear++;
queue[rear] = value;
}
/**
* 出队
*/
public int pull() {
if (isEmpty()) {
throw new NoSuchElementException("队列为空!");
}
int element = queue[front];
if (front == rear) {
front = -1; // 如果出队后队列为空,将队头指针和队尾指针重置为-1
rear = -1;
} else {
front++;
}
return element;
}
/**
* 判空操作
*/
public boolean isEmpty() {
return front == -1;
}
/**
* 判满操作
*/
public boolean isFull() {
return rear == queue.length - 1;
}
/**
* 获取队头元素
*/
public int getFront() {
if (isEmpty()) {
throw new NoSuchElementException("队列为空!");
}
return queue[front];
}
//测试main方法
public static void main(String[] args) {
ArrayQueue arrayQueue = new ArrayQueue();
arrayQueue.push(1);
arrayQueue.push(2);
arrayQueue.push(3);
System.out.println("第一个出队的元素为:" + arrayQueue.pull());
System.out.println("队列中的元素为:");
while (!arrayQueue.isEmpty()) {
System.out.print(arrayQueue.pull() + "\t");
}
}
}
使用示例:
ArrayQueue queue = new ArrayQueue(5);
queue.push(2);
queue.push(4);
queue.push(6);
System.out.println(queue.poll()); // 输出:2
System.out.println(queue.getFront()); // 输出:4
System.out.println(queue.isEmpty()); // 输出:false
System.out.println(queue.isFull()); // 输出:false
这样就over啦~