定義:
- 散列是一種用于以常數(shù)平均時(shí)間執(zhí)行插入,刪除和查找的技術(shù)。
- 我們將每個(gè)關(guān)鍵字被映射到從0到TableSize - 1這個(gè)范圍中的某個(gè)數(shù)。這個(gè)映射稱為散列函數(shù)。
- 散列函數(shù)需要保證任何兩個(gè)不同的關(guān)鍵字映射到不同的單元。
- 當(dāng)兩個(gè)關(guān)鍵字散列到同一個(gè)值時(shí)(這叫做沖突), 應(yīng)該做什么以及如何確定散列表的大小。
hash_table.png
散列函數(shù):
- 一般簡單合理的方法是直接返回Key mod TableSize, 并且保證表的大小為素?cái)?shù)。
a. 假設(shè)關(guān)鍵字是字符串,一種選擇辦法是將字符串的ASCII碼(或Unicode碼)值加起來
public static int hash(String key, int tableSize){
int hashVal = 0;
for (int i = 0; i < key.length(); i ++){
hashVal += key.charAt(i);
}
return hashVal % tableSize;
}
這種方法實(shí)現(xiàn)起來簡單合理,但如果表很大的話,并不會(huì)很好地分配關(guān)鍵字。例如,TableSize = 10007,假設(shè)所有的關(guān)鍵字至多8個(gè)字符長,由于ASCII碼字符的值最多是127,那么它們的值是在0到1016之間,顯然這不是一種均勻的分配。
*b. *假設(shè)關(guān)鍵字至少有三個(gè)字符,可以采用如下方法:
public static int hash(String key, int tableSize){
return (key.charAt(0) + key.charAt(1) * 27 + key.charAt(2) * 729) % tableSize;
}
但這樣依舊不能完全均勻地分配,因?yàn)?個(gè)字母的不同組合數(shù)實(shí)際只有2851,也就是只有大約表的28%被真正散列到。
c. 根據(jù)Horner法則計(jì)算一個(gè)(37)的多項(xiàng)式函數(shù):
Paste_Image.png
public static int hash(String key, int tableSize){
int hashVal = 0;
for (int i = 0; i < key.length(); i ++){
hashVal = 37 * hashVal + key.charAt(i);
}
hashVal %= tableSize;
//產(chǎn)生負(fù)數(shù)的情況
if (hashVal < 0){
hashVal += tableSize;
}
return hashVal;
}
解決沖突的兩種方法:
- 分離鏈接法:將散列到同一個(gè)值的所有元素保留到一個(gè)表中。
- 開放定址法:嘗試另外一些單元,直到找到空的單元為止。
*a. 分離鏈表法: *
separate_chaining.png
- 定義表的基本變量:
//定義默認(rèn)表的大小
private static final int DEFAULT_TABLE_SIZE = 101;
//定義表的存儲(chǔ)
private List<AnyType>[] theLists;
//定義表當(dāng)前的大小
private int currentSize;
- 定義其構(gòu)造函數(shù),進(jìn)行初始化:
public SeparateChainingHashTable(){
this(DEFAULT_TABLE_SIZE);
}
//進(jìn)行初始化操作
public SeparateChainingHashTable(int size){
//初始化表,這里調(diào)用了一個(gè)nextPrime函數(shù),以保證其表的大小為素?cái)?shù)
theLists = new LinkedList[nextPrime(size)];
//初始化表中的鏈表
for (int i = 0; i < theLists.length; i ++){
theLists[i] = new LinkedList<AnyType>();
}
}
//返回下一個(gè)素?cái)?shù)
private static int nextPrime(int n){
while (!isPrime(n)){
n ++;
}
return n;
}
//判斷是否為素?cái)?shù)
private static boolean isPrime(int n){
for (int i = 2; i <= Math.sqrt(n); i ++){
if (n % i == 0 && n != 2){
return false;
}
}
return true;
}
- 進(jìn)行插入操作:如果這個(gè)元素是新元素,則它將被插入到鏈表的前端,因?yàn)橥陆迦氲脑刈钣锌赡懿痪帽辉L問。
public void insert(AnyType x){
//獲取x根據(jù)hash后找到所對應(yīng)的位置
List<AnyType> whichList = theLists[myHash(x)];
//如果當(dāng)前位置鏈表不包含元素,則進(jìn)行插入操作
if (!whichList.contains(x)){
//進(jìn)行添加
whichList.add(x);
//判斷是否達(dá)到表的長度,若達(dá)到則進(jìn)行rehash操作,以擴(kuò)大表的大小
if (++ currentSize > theLists.length){
rehash();
}
}
}
//根據(jù)值獲取到其對應(yīng)的hash位置
private int myHash(AnyType x){
int hashVal = x.hashCode();
hashVal %= theLists.length;
//考慮到負(fù)數(shù)的情況
if (hashVal < 0){
hashVal += theLists.length;
}
return hashVal;
}
private void rehash(){
//獲取到原來的表
List<AnyType>[] oldLists = theLists;
//對現(xiàn)有的表大小進(jìn)行擴(kuò)大
theLists = new List[nextPrime(2 * theLists.length)];
//初始化新表
for (int j = 0; j < theLists.length; j ++){
theLists[j] = new LinkedList<AnyType>();
}
//初始化大小
currentSize = 0;
//將舊表中的數(shù)據(jù)重新插入到新表中
for (int i = 0; i < oldLists.length; i ++){
for (AnyType item : oldLists[i]){
insert(item);
}
}
}
- 刪除操作:
//刪除操作
public void remove(AnyType x){
//獲取到對應(yīng)位置的鏈表
List<AnyType> whichList = theLists[myHash(x)];
//檢查是否包含元素,包含則進(jìn)行刪除操作,并大小--
if (whichList.contains(x)){
whichList.remove(x);
currentSize --;
}
}
- 檢查元素:
//檢查是否包含某個(gè)元素
public boolean contains(AnyType x){
List<AnyType> whichList = theLists[myHash(x)];
return whichList.contains(x);
}
- 打印鏈表:
//打印鏈表結(jié)構(gòu)
public void printHashTable(){
for (int i = 0; i < theLists.length; i ++){
if (theLists[i] != null && theLists[i].size() > 0){
System.out.println("current position is : " + i );
for (AnyType x : theLists[i]){
System.out.println(x);
}
}
}
}
- 進(jìn)行檢測:為了效果更明顯,我們強(qiáng)制表的結(jié)果為10,當(dāng)然在實(shí)際項(xiàng)目中不可如此,這里只是為了測試作用。
public static void main(String[] args){
int[] arr = {0, 81,1, 64,4, 25,36,16,49,9};
SeparateChainingHashTable<Integer> separateChainingHashTable = new SeparateChainingHashTable<>(10);
//進(jìn)行插入操作
for (int i = 0; i < arr.length; i ++){
separateChainingHashTable.insert(arr[i]);
}
//打印鏈表
separateChainingHashTable.printHashTable();
}
- 運(yùn)行結(jié)果如下:
current position is : 0
0
current position is : 1
81
1
current position is : 4
64
4
current position is : 5
25
current position is : 6
36
16
current position is : 9
49
9
separate chaning.png
- 當(dāng)然除了插入默認(rèn)的基本類型,我們還可以插入對象,但一定要重定義對象的equals()和hashCode()方法
public class Employee {
private String name;
private double salary;
private int seniority;
//構(gòu)造函數(shù)
public Employee(String name, double salary, int seniority){
this.name = name;
this.salary = salary;
this.seniority = seniority;
}
//判斷對象是否相同
public boolean equals(Object rhs){
//滿足為Employee類型且名字相同
return rhs instanceof Employee && name.equals(((Employee) rhs).name);
}
//重寫hashCode
public int hashCode(){
return name.hashCode();
}
//打印對象
@Override
public String toString() {
return "name: " + name + " salary: " + salary + " seniority: " + seniority + "\n";
}
}
public static void main(String[] args){
SeparateChainingHashTable<Employee> hashTable = new SeparateChainingHashTable<>(11);
hashTable.insert(new Employee("worker", 1000,3));
hashTable.insert(new Employee("worker", 1500,3));
hashTable.insert(new Employee("worker", 1600,3));
hashTable.insert(new Employee("Manager", 2000, 2));
hashTable.insert(new Employee("Manager", 2500, 2));
hashTable.insert(new Employee("boss", 3000, 1));
hashTable.printHashTable();
}
結(jié)果如下:
current position is : 6
name: Manager salary: 2000.0 seniority: 2
current position is : 7
name: boss salary: 3000.0 seniority: 1
current position is : 10
name: worker salary: 1000.0 seniority: 3
我們可以看到對于name相同的對象是不會(huì)重復(fù)插入的。
完整代碼:
public class SeparateChainingHashTable<AnyType> {
public SeparateChainingHashTable(){
this(DEFAULT_TABLE_SIZE);
}
//進(jìn)行初始化操作
public SeparateChainingHashTable(int size){
//初始化表,這里調(diào)用了一個(gè)nextPrime函數(shù),以保證其表的大小為素?cái)?shù)
theLists = new LinkedList[nextPrime(size)];
//初始化表中的鏈表
for (int i = 0; i < theLists.length; i ++){
theLists[i] = new LinkedList<AnyType>();
}
}
public void insert(AnyType x){
//獲取x根據(jù)hash后找到所對應(yīng)的位置
List<AnyType> whichList = theLists[myHash(x)];
//如果當(dāng)前位置鏈表不包含元素,則進(jìn)行插入操作
if (!whichList.contains(x)){
//進(jìn)行添加
whichList.add(x);
//判斷是否達(dá)到表的長度,若達(dá)到則進(jìn)行rehash操作,以擴(kuò)大表的大小
if (++ currentSize > theLists.length){
rehash();
}
}
}
//刪除操作
public void remove(AnyType x){
//獲取到對應(yīng)位置的鏈表
List<AnyType> whichList = theLists[myHash(x)];
//檢查是否包含元素,包含則進(jìn)行刪除操作,并大小--
if (whichList.contains(x)){
whichList.remove(x);
currentSize --;
}
}
//檢查是否包含某個(gè)元素
public boolean contains(AnyType x){
List<AnyType> whichList = theLists[myHash(x)];
return whichList.contains(x);
}
public void makeEmpty(){
for (int i = 0; i < theLists.length; i ++){
theLists[i].clear();
}
}
//定義默認(rèn)表的大小
private static final int DEFAULT_TABLE_SIZE = 101;
//定義表的存儲(chǔ)
private List<AnyType>[] theLists;
//定義表當(dāng)前的大小
private int currentSize;
//根據(jù)值獲取到其對應(yīng)的hash位置
private int myHash(AnyType x){
int hashVal = x.hashCode();
hashVal %= theLists.length;
//考慮到負(fù)數(shù)的情況
if (hashVal < 0){
hashVal += theLists.length;
}
return hashVal;
}
private void rehash(){
//獲取到原來的表
List<AnyType>[] oldLists = theLists;
//對現(xiàn)有的表大小進(jìn)行擴(kuò)大
theLists = new List[nextPrime(2 * theLists.length)];
//初始化新表
for (int j = 0; j < theLists.length; j ++){
theLists[j] = new LinkedList<AnyType>();
}
//初始化大小
currentSize = 0;
//將舊表中的數(shù)據(jù)重新插入到新表中
for (int i = 0; i < oldLists.length; i ++){
for (AnyType item : oldLists[i]){
insert(item);
}
}
}
//返回下一個(gè)素?cái)?shù)
private static int nextPrime(int n){
while (!isPrime(n)){
n ++;
}
return n;
}
//判斷是否為素?cái)?shù)
private static boolean isPrime(int n){
for (int i = 2; i <= Math.sqrt(n); i ++){
if (n % i == 0 && n != 2){
return false;
}
}
return true;
}
//打印鏈表結(jié)構(gòu)
public void printHashTable(){
for (int i = 0; i < theLists.length; i ++){
if (theLists[i] != null && theLists[i].size() > 0){
System.out.println("current position is : " + i );
for (AnyType x : theLists[i]){
System.out.println(x);
}
}
}
}
}