哈希表和搜索树
一、哈希表
-
哈希表(hash table):哈希表是基于数组取下标的方式来快速进行增删改查的一种数据结构。
-
哈希函数:将 key 映射成数组下标的函数。
-
哈希冲突:不同的 key ,映射到相同的下标上。
-
处理哈希冲突的方法:①闭散列;②开散列。
-
闭散列:
1)若出现哈希冲突,则继续往后找下一个空闲位置。(可将int类型的数组换为对象数组);
2) 若数组上元素较多,比较拥挤,此时算法性能严重下降,就需频繁对数组进行扩容,使得数组上保持稀疏。 -
开散列(哈希桶):
1) 数组上的每个元素为一个链表的头节点,若出现哈希冲突,就将此对象插入到对应位置的链表上;
2) 使用开散列也可能会导致每个下标位置的链表过长,此时可通过:①扩容;②将这个较长的链表转换成红黑树或哈希表;
3)字符串映射的哈希值求解算法:md5、md4、sha1、sha256…;
4)md5 计算哈希值的特点:①定长。得到的哈希值都是固定长度;
②分散。输入字符串有微小变化,得到的哈希值差别都会很大;(若字符串 str1 和 str2 的 md5 值相同,则可认为 str1 和 str2 相同。)
③不可逆。可根据字符串计算 md5 值,但根据 md5 值找到对应的字符串原串,理论上是不可能的。md5 的应用:
①作为字符串hash值的算法;
②用于加密领域;
③校验文件传输结果是否正确;(对比文件传输前后的md5 值)5)负载因子:衡量当前哈希表中的元素拥挤程度,可用来判断当前数组是否需要进行扩容。
负载因子的选取:通常是根据实验来选取的。负载因子太小,空间利用率越低;负载因子太大,性能效率会降低。
6)性能分析:虽然哈希表一直在和冲突做斗争,但在实际使用过程中,哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,哈希表的插入/删 除/查找时间复杂度是 O(1) 。
哈希表的实现:
- 插入键值对;
- 根据 key,获取 value 值;
- 删除给定 key;
以下采用开散列/哈希桶的方式来处理哈希冲突。
class HashNode{
public int key;
public int value;
public HashNode next;
public HashNode(int key,int value) {
this.key = key;
this.value = value;
}
}
//哈希表:数组上的每个元素是一个链表
//采用开散列/哈希桶的方式来处理哈希冲突
public class MyHashMap {
private HashNode[] array = new HashNode[16];
private int size = 0;
//1.插入键值对
public void put(int key,int value){
//根据key,计算下标位置
int index = key % array.length;
//查看index位置的链表中是否存在key,存在直接修改value,不存在插入新节点。
for(HashNode cur = array[index];cur != null;cur = cur.next){
if(cur.key == key){
cur.value = value;
return;
}
}
//循环结束,即没找到,需创建新节点插入到链表中(此处采用头插)
HashNode newNode = new HashNode(key,value);
newNode.next = array[index];
array[index] = newNode;
size++;
if(loadFactor() > 0.75){
resize();
}
}
private double loadFactor(){
return size / array.length;
}
private void resize(){
//创建一个更长的新数组,将原数组拷贝进去
HashNode[] newArray = new HashNode[2 * array.length];
for(int i = 0;i < array.length;i++){
HashNode next = null;
//外层循环拷贝数组
//for(HashNode cur = array[i];cur != null;cur = cur.next){
for(HashNode cur = array[i];cur != null;cur = next){
next = cur.next;//修改cur之前需提前备份之前的位置,
// 下面的cur.next已指向的是新链表,不能当做循环条件中的旧链表指向了
//里层循环拷贝i下标位置的链表
int indexNew = cur.key % newArray.length;
cur.next = newArray[indexNew];
newArray[i] = cur;
}
}
array = newArray;
}
//2.根据key获取value
public Integer get(int key){
int index = key % array.length;
//遍历哈希表寻找key
for(int i = 0;i < array.length;i++){
for(HashNode cur = array[index];cur != null;cur = cur.next){
if(cur.key == key){
return cur.value;
}
}
}
return null;
}
//3.删除key
public void remove(int key){
int index = key % array.length;
for(int i = 0;i < array.length;i++){
HashNode prev = array[index];
while(prev != null && prev.next != null && prev.next.key == key){
prev = prev.next;
}
if(prev == null && prev.next == null){
return;
}
HashNode cur = prev.next;
prev.next = cur.next;
}
}
}
二、搜索树:
- 二叉搜索树是 TreeSet 和 TreeMap 的底层实现;
- 二叉搜索树的特点:左子树小于根节点,右子树大于根节点,中序遍历结果是一个有序序列;
- 二叉搜索树的相关方法实现:①查找元素;②插入元素;③删除元素。
- 性能分析:
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为: l o g 2 N log_2 N log2N;
最差情况下,二叉搜索树退化为单支树,其平均比较次数为: N 2 \frac{N}{2} 2N。
删除元素的8种情况:
二叉搜索树的简单实现:
class BinaryNode{
public int key;
public int value;
public BinaryNode left;
public BinaryNode right;
public BinaryNode(int key,int value) {
this.key = key;
this.value = value;
}
}
public class BinarySearchTree {
private BinaryNode root = null;
//1.查找节点
public Integer get(int key){
//创建一个引用cur 从root出发
BinaryNode cur = root;
while(cur != null){
if(key < cur.key ){
cur = cur.left;
}else if(key > cur.key){
cur = cur.right;
}else{
return cur.value;
}
}
return null;
}
//2.插入节点
public void put(int key,int value){
if(root == null){
root = new BinaryNode(key,value);
return;
}
//先找到要插入节点的位置
BinaryNode cur = root;
BinaryNode parent = null;
while(cur != null){
if(key < cur.key){
parent = cur;
cur = cur.left;
}else if(key > cur.key){
parent = cur;
cur = cur.right;
}else{
cur.value = value;
return;
}
}
BinaryNode newNode = new BinaryNode(key,value);
if(key < parent.key){
parent.left = newNode;
}else{
parent.right = newNode;
}
}
//3.删除节点
public void remove(int key){
//先查找待删除节点的位置
BinaryNode cur = root;
BinaryNode parent = null;
while(cur != null){
if(key < cur.key){
parent = cur;
cur = cur.left;
}else if(key > cur.key){
parent = cur;
cur = cur.right;
}else{
removeNode(parent,cur);
return;
}
}
}
private void removeNode(BinaryNode parent,BinaryNode cur){
if(cur.left == null){
//1.待删除节点的左子树为空
if(cur == root){
//1.1待删除节点是根节点
root = cur.right;
}else if(cur == parent.left){
//1.2待删除节点不是根节点,是父节点的左节点
parent.left = cur.right;
}else if(cur == parent.right){
//1.3待删除节点不是根节点,是父节点的右节点
parent.right = cur.right;
}
}else if(cur.right == null){
//2.待删除节点的右子树为空
if(cur == root){
//2.1待删除节点是根节点
root = cur.left;
}else if(cur == parent.left){
//2.2待删除节点是父节点的左节点
parent.left = cur.left;
}else if(cur == parent.right){
//2.3待删除节点是父节点的右节点
parent.right = cur.left;
}
}else{
//3.待删除节点的左右子树都不为空
//需在其右子树中找一个最小节点作为替罪羊节点
//将替罪羊节点复制给待删除节点,再将原替罪养节点删除
BinaryNode goat = cur.right;
BinaryNode goatParent = null;
while(goat.left != null){
goatParent = goat;
goat = goat.left;
}
cur.key = goat.key;
cur.value = goat.value;
if(goat == goatParent.left){
goatParent.left = goat.right;
}else{
goatParent.right = goat.right;
}
}
}
}