js版數據結構_07散列表
ps:今天晚上寫之前沒考慮這一節代碼量的問題,爲保證時間沒有測試。但是主要都是邏輯問題,看邏輯便ok
知識點
- 散列表是什麼
- hash表的基本實現
- hash表完善之使用分離鏈接
- hash表完善之使用線性探查
1. 散列表(哈希表)
找的這兩個圖比較能體現散列表的特點,我們至今已經總結了不少的數據結構。但是回想一樣在他們的取值實現中,我們爲取到一個值往往是要對整個結構進行遍歷。這種操作效率是非常差的,我們希望可能有一種數據結構的數據獲取十分迅速。最好時間複雜度爲O(1)。於是散列表這種數據結構出現了。
散列表的實現,拿獲取值來說。它需要我們提供一個哈希哈數處理我們的key。使其能夠快速定位到我們存儲數據的存儲單元。
這裏我們會介紹兩種哈希函數,當然好理解的有缺陷,不好理解的最好用。
djb2HashCode(目前最好用的哈希函數)
djb2HashCode(key) {
const tableKey = this.toStrFn(key);
let hash = 5381;
for (let i = 0; i < tableKey.length; i++) {
hash = (hash * 33) + tableKey.charCodeAt(i);
}
return hash % 1013;
}
loseloseHashCode(好理解,它只是將字符串的ascii碼與37取餘)
loseloseHashCode(key) {
if (typeof key === 'number') {
return key;
}
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash += tableKey.charCodeAt(i);
}
return hash % 37;
}
2. hash表的基本實現
它的基本實現很簡單,不做解釋了。先說一個它的漏洞也是loseloseHashCode的不足,使用loseloseHashCode這種哈希函數,總會使得一些字符串的ascii碼值是相同的。這便會產生數據覆蓋的問題。顯然這不是我們希望遇見到。下面的兩節便是爲了解決這個問題。先來看它的基本實現
class HashTable {
constructor() {
this.table = {};
}
// 先創建散列函數
loseloseHashCode(key) {
if (typeof key === "number") {
return key
} else {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = hash + key.charCodeAt(i);
}
return hash % 37;
}
}
// 新增一條到散列表
put(key, val) {
if (key != null && val != null) {
const position = this.loseloseHashCode(key);
this.table[position] = val;
return true;
}
return false;
}
// 取值
get(key) {
const position = this.loseloseHashCode(key);
if (this.table[position]) {
return this.table[position];
} else {
return undefined;
}
}
// 移出值
remove(key) {
const position = this.loseloseHashCode(key);
if (this.table[position]) {
delete this.table[position];
return true;
} else {
return false;
}
}
}
3. hash表完善之使用分離鏈接
如上醜圖,鍵key1key2經過loseloseHashCode處理之後定位到的資源空間是同一塊,爲了防止數據覆蓋的問題,我們將使用鏈表存放它們。雖然這裏在鏈表中查找key1或key2時還是使用了線性查找。但是在這裏也是沒有法子的我們只能犧牲一下。不過注意的是我們這是在鏈表中保存的不止是val了。爲了方便定位之後的線性查詢我們要連key,val一塊存在鏈表中。
看實現:
// 創建一個節點類
class LNode {
constructor(e) {
this.element = e;
this.next = null;
}
}
//鏈表類
class LinkList {
constructor() {
this.count = 0;
this.head = null;
}
getHead() {
return this.head;
}
push(e) {
const node = new LNode(e);
let current = this.head;
// 先看鏈表是不是空的
if (current == null) {
this.head = node;
} else {
// 想一下最後一個節點的特點,最後一個節點的next爲null
while (current.next) {
current = current.next;
}
current.next = node;
}
this.count++;
}
removeAt(index) {
// 先判讀這個index是否越界
if (index >= 0 && index < this.count) {
let current = this.head;
// 看index是不是head位置
if (index === 0) {
this.head = current.next;
} else {
let previous;
// 找位置
for (let i = 0; i < index; i++) {
previous = current;
current = current.next;
}
previous.next = current.next;
}
this.count--;
return current.element;
} else {
return undefined;
}
}
getElementAt(index) {
// 判斷是否越界
if (index >= 0 && index < this.count) {
let node = this.head;
for (let i = 0; i < index && node != null; i++) {
node = node.next;
}
return node;
} else {
return undefined;
}
}
// 在任意位置插入
insert(e, index) {
// 還是先判斷是否越界
if (index >= 0 && index < this.count) {
let node = new LNode(e);
// 0號位置判斷
if (index == 0) {
let current = this.head;
node.next = current;
this.head = node;
} else {
// 先找到索引的前一號位置,一截兩瓣
let pre = this.getElementAt(index - 1);
let current = pre.next;
pre.next = node;
node.next = current;
}
this.count++;
return true;
} else {
return undefined;
}
}
// 返回一個元素的位置
indexOf(e) {
// 到循環遍歷鏈表就ok
let current = this.head;
for (let i = 0; i < this.count && current != null; i++) {
if (current.element == e) {
return i;
} else {
current = current.next;
}
}
// 循環完鏈表仍然沒有找到
return -1;
}
// 刪除指定元素
remove(e) {
// 我們前面實現一個根據位置刪除
// 還實現了一個根據指定元素返回位置
let index = this.indexOf(e);
return this.removeAt(index);
}
size() {
return this.count;
}
isEmpty() {
return this.count == 0;
}
}
class LinkListNode {
constructor(key, val) {
this.key = key;
this.val = val;
}
}
class HashTable {
constructor() {
this.table = {};
}
// 先創建散列函數
loseloseHashCode(key) {
if (typeof key === "number") {
return key
} else {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = hash + key.charCodeAt(i);
}
return hash % 37;
}
}
// 新增一條到散列表
put(key, val) {
if (key != null && val != null) {
const position = this.loseloseHashCode(key);
if (!this.table[position]) {
this.table[position] = new LinkList();
}
this.table[position].push(new LinkListNode(key, val));
return true;
} else {
return false;
}
}
// 取值
get(key) {
const position = this.loseloseHashCode(key);
if (this.table[position]) {
// 遍歷鏈表取值
if (!this.table[position].isEmpty()) {
let current = this.table[position].getHead();
while (current != null) {
if (current.element.key === key) {
return current.element.val;
}
current = current.next;
}
return undefined;
}
} else {
return undefined;
}
}
// 移出值
remove(key) {
const position = this.loseloseHashCode(key);
let linkList = this.table[position];
if (linkList != null && !linkList.isEmpty()) {
let current = linkList.getHead();
// 鏈表我們實現兩種刪除元素1根據元素值2根據索引
// 顯然這裏用值好
while (current) {
if (current.element.key === key) {
linkList.remove(current.element);
// 刪完如鏈表爲空則刪除此位置
if (linkList.isEmpty()) {
delete this.table[position]
}
return true;
}
current = current.next;
}
}
return false;
}
}
4. hash表完善之使用線性探查
這個比分離鏈表還要簡單一些,它是key定位到15的位置時發現爲空就把它的信息節點保存到這,key也是定位15的,但是它來的晚一看已經被人佔了。就向下找,找到一個空地就把自己的信息節點保存下來。
信息節點中的東西還是key和val。(方便查詢)
邏輯實現還是很順的,只不過有一個新的點要考慮。就是在節點刪除的時候,比如上圖我把key1刪掉了。那麼key2就會很尷尬。因爲我們定位都是定位到key1的位置的,一判斷key1的位置是空那我們下面實現的get和remove還怎麼玩。我們連key2的信息都拿不到。因此在數據移出的時候我們要對它下面的元素位置進行修復
實現:
class Node {
constructor(key, val) {
this.key = key;
this.val = val;
}
}
class LineHashTable {
constructor() {
this.table = {};
}
// 先創建散列函數
loseloseHashCode(key) {
if (typeof key === "number") {
return key
} else {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = hash + key.charCodeAt(i);
}
return hash % 37;
}
}
verifyRemoveSideEffect(key, removedPosition) {
const hash = this.hashCode(key);
let index = removedPosition + 1;
while (this.table[index] != null) {
const posHash = this.hashCode(this.table[index].key);
if (posHash <= hash || posHash <= removedPosition) {
this.table[removedPosition] = this.table[index];
delete this.table[index];
removedPosition = index;
}
index++;
}
}
// 新增一條到散列表
put(key, val) {
if (key != null && val != null) {
const position = this.loseloseHashCode(key);
if (this.table[position] == null) {
// 爲空放入節點
this.table[position] = new Node(key, val);
} else {
// 向下找空的儲存空間
let index = position + 1;
while (!this.table[index]) {
index++;
}
this.table[index] = new Node(key, val)
}
return true;
}
return false;
}
// 取值
get(key) {
const position = this.loseloseHashCode(key);
// 先直接到lost函數所能快速定位的地方
if (this.table[position] != null) {
// 再判是不是我們要的值
if (this.table[position].key === key) {
return this.table[position].val;
}
// 不是則需要在此地的基礎上向下尋找了
let index = position + 1;
while (this.table[index] != null && this.table[index].key != key) {
index++;
}
// 上面出來了有兩種可能
// 1.找到目標
// 2.有一個空的地址
if (this.table[index] != null && this.table[index].key === key) {
return this.table[index].val;
}
}
return undefined;
}
// 移出值
remove(key) {
const position = this.loseloseHashCode(key);
if (this.table[position] != null) {
// 判此時這個位置上的使我們要的嗎
if (this.table[position].key === key) {
delete this.table[position];
// 修復下面的位置
this.verifyRemoveSideEffect(key, position);
return true;
}
let index = position + 1;
while (this.table[index] != null && this.table[index].key !== key) {
index++;
}
if (this.table[index] != null && this.table[index].key === key) {
delete this.table[index];
// 修復下面的位置
this.verifyRemoveSideEffect(key, index);
return true;
}
}
return false;
}
}