“a-z”的随机字符 - 统计每个字符出现的次数
数组长度为 26
‘a’ => 0
‘z’ => 25
———————————————————————————————————————
String s = “不做限定”; 长度未知
int[] count = new int[1000] //这里1000是随便设定的
for(char ch : s.toCharArray()) {
int index = 根据字符得到下标(ch);
count[index]++;
)
引子:
public static void main(String[] args) {
String s = "azxdbcdewlafkjdofjadsfjlzjoidfjoz";
int [] count = new int [26];
for (char ch : s.toCharArray()) {
int index = ch - 'a';
count[index]++;
}
System.out.println(Arrays.toString(count));
}
相比较于“搜索树”,哈希表的效率更高(平均 O(1));实现简单
// 元素类型,使用Integer
// 使用”拉链法“解决哈希冲突
//哈希表的时间复杂度: 插入/删除/查找 平均O(1)。——> 泊松分布
public class Node {
Integer key;
Node next;
public Node(Integer key) {
this.key = key;
}
}
public class HashTable {
//1、数组
private Node[] array = new Node[11];
//2、维护哈希表中有的元素个数
private int size;
//插入/删除/查找方法的定义...
}
插入:O(1)
1、根据元素,得到合法的下标(根据hash函数)
2、把元素放到 index 位置
// true: key之前不在哈希表中
// false: key之前已经在哈希表中
public boolean insert(Integer key) {
// 1.把对象转成”int类型“
// "hashCode()"方法的调用是核心
int hashValue = key.hashCode();
// 2.把 hashValue 转成合法的下标
int index = hashValue % array.length;
// 3.遍历 index 位置处的链表,确定 key 在不在链表中
Node current = array[index];
while (null != current) {
// "equals()"方法的调用是核心
if (key.equals(current.key)) {
return false;
}
current = current.next;
}
// 4.把key装入节点中,并插入对应的链表中
// 头插尾插都可以,头插相对简单
Node node = new Node(key);
node.next = array[index];
array[index] = node;
//5.维护 元素个数
size++;
// 6.维护负载因子,进而维护较低的冲突率
if ((double)size / array.length >= 0.75) {
//扩容
扩容();
}
return true;
}
//总体上:时间复杂度应算成O(n) ——> 有的部分因为直接为null。
private void 扩容() {
Node[] newArray = new Node[array.length*2];
//搬原来的元素过来
//不能直接按链表搬运,因为元素保存的下标和数组长度有关
//数组长度变了,下标也会变
//所以,需要把每个元素重新计算一次
//遍历整个数组
for (int i = 0;i < array.length;i++) {
//遍历每条链表
Node current = array[i];
while (null != current) {
//高效的办法是搬节点,写起来比较麻烦;
//我们采用复制节点,简单一点
Integer key = current.key;
int hashValue = key.hashCode();
int index = hashValue% newArray.length;
// 头插尾插都可以,头插简单
Node node = new Node(key);
node.next = newArray[index];
newArray[index] = node;
current = current.next;
}
}
array = newArray;
}
删除:O(1)
1、根据元素,得到合法的下标(根据hash函数)
2、把元素从数组中删除
public boolean remove(Integer key) {
//获取hashCode()
int hashValue = key.hashCode();
int index = hashValue % array.length;
//获取元素所在位置
Node current = array[index];
//定义current节点的前驱节点
Node pre = null;
while (current != null) {
//进行比较,查找key
if (key.equals(current.key)) {
//进行删除
if (null == pre) {
array[index] = current.next;
} else {
pre.next = current.next;
}
//维护 元素个数
size--;
return true;
}
pre = current;
current = current.next;
}
return false;
}
查找:O(1)
1、根据元素,得到合法的下标(根据hash函数)
2、判断元素在不在
**哈希冲突:**存在 k1 != k2 , 但是 hash(k1) == hash(k2) 。
public boolean contains(Integer key) {
int hashValue = key.hashCode();
int index = hashValue% array.length;
Node current = array[index];
while (current != null) {
if (key.equals(current.key)) {
return true;
}
current = current.next;
}
return false;
}
如果有人有意识地创造了“哈希冲突”。当链表长度超过8时,碰到不一般人了,
所以不再用链表存储冲突元素,而是用更高效的的搜索数据结构(Java中选用了红黑树(搜索树))
———————————————————————————————————————
问:在保存元素不是固定范围时,”哈希冲突“有可能避免吗?
不能避免(因为存储的元素范围 远远大于 数组长度)
问:怎么解决哈希冲突?
1、线性探测
2、拉链法(Java 的 HashMap 选用这种实现)
拉链法:使用另一种数据结构来解决冲突的元素(链表)
———————————————————————————————————————
如果不是 int 类型怎么办? Person对象
所有的 Object类,都有一个方法int hashCode(); 把一个对象变成"int类型"(不一定是合法下标)
把”int 类型“转成一个合法下标:int index = hashValue % array.length
哈希表的Java实现
1、纯key模型:HashSet
2、key-value模型:HashMap
public class Main {
//"HashMap"
public static void main(String[] args) {
Map<String,Integer> map = new HashMap<>();
System.out.println(map.put("qwer", 123));//返回原来的值,因为原来不存在,所以返回“null”。
System.out.println(map.put("qwer",456));//返回原来的值“123”。
System.out.println();
map.put("wt",666);
map.put("gt",777);
for (String key : map.keySet()) {
System.out.print(key + " ");
}
}
//"HashSet"
public static void main1(String[] args) {
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.add(3);
set.add(4);
System.out.println(set); //不保证顺序
System.out.println(set.add(1));//Set中已经有“1”,所以不允许添加,返回false
System.out.println(set);
System.out.println(set.remove(3));//返回true
System.out.println(set);
System.out.println(set.contains(1));//返回true
System.out.println(set.contains(5));//返回false
}
}
自定义类使用 HashSet 或者 HashMap 的key 时,需要注意:
1.必须重写hashCode() 和 equals()方法
2.如果你认为两个对象相等,则“hashCode()”值相等,并且“equals”返回true
p1.equals(p2)返回true,则最好"p1.hashCode() == p2.hashCode()"
3.p1.hashCode() == p2.hashCode(),需要保证“p1.equals(p2)”返回“true”么?
不保证,因为可能会出现“哈希冲突”。
public class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
//这里实现equals()方法的重写 ...
}
———————————————————————————————————————
继续讨论冲突:
1、冲突无法避免 2、冲突不好 3、目标:尽可能的减少冲突
如何尽可能的减少冲突:
1、数组大小用素数(Java中并没有怎么用)
index = hashValue % array.length;
2、hash 函数(hashCode())尽可能的均匀
@override
public int hashCode() {
return 0;
}
上面的代码是一个错误示范:因为直接返回0,会使所有的元素都“哈希冲突”了。
3、重要
每次插入元素时的冲突率 ~ 元素个数/数组的长度(即负载因子)
通过降低负载因子进而降低冲突率(”负载因子“ 与 “冲突率”成正比。)
1、我们对冲突率有个上限的阈值
所以我们对负载因子有个上限阈值
2、要降低冲突率,需要降低负载因子
负载因子中,元素个数不能动;所以,只能增加数组的长度(即“扩容”)。
3、Java中一般负载因子是“0.75”,达到这个值就会扩容(一般扩充为原来数组的两倍)。
public static void main(String[] args) {
Person p1 = new Person("aaa",20);
Person p2 = new Person("aaa",20);
HashMap<Person,String> map = new HashMap<>();
map.put(p1,"aaa");
System.out.println(map.get(p2));
}
//"HashSet"
public static void main1(String[] args) {
Person p1 = new Person("aaa",20);
Person p2 = new Person("aaa",20);
HashSet<Person> set = new HashSet<>();
set.add(p1);
System.out.println(set.contains(p1));//true
System.out.println(set.contains(p2));//false ——> true
//这里如何使“p2”包含于“set”(即让上面返回true)呢?
//必须重写equals()方法 和 hashCode()方法
}
重写equals()方法 和 hashCode()方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);//在保证 name 和 age 分别相等的情况下,让hashCode值相等。
}
```java
public class Node {
Integer key;
Integer value;
Node next;
public Node(Integer key, Integer value) {
this.key = key;
this.value = value;
}
}
public class MyHashMap {
private Node[] array = new Node[11];
private int size;
public Integer get(Integer key) {
int hashValue = key.hashCode();
int index = hashValue % array.length;
Node current = array[index];
while (null != current) {
if(key.equals(current.key)) {
return current.value;
}
current = current.next;
}
return null;
}
public Integer put(Integer key,Integer value) {
int hashValue = key.hashCode();
int index = hashValue% array.length;
Node current = array[index];
while (null != current) {
if (key.equals(current.key)) {
Integer oldValue = current.value;
current.value = value;
return oldValue;
}
current = current.next;
}
Node node = new Node(key,value);
node.next = array[index];
array[index] = node;
size++;
//判断负载因子,看是否需要扩容
return null;
}
}
**完.......**