目的:ES6标准下的JS数据结构的一些实现代码。(作为记录和启发)

内容:字典和散列表。(未完成,待继续)

所有源码在我的Github上(如果觉得不错记得给星鼓励我哦):ES6的JavaScript数据结构实现之字典与散列表

注:ES6也是新增加了Map类。此外还提供了WeakMap和WeakSet。基本上,Map和Set与其弱化版本之间仅有的区别是:WeakMap和WeakSet类没有entries、keys、values等方法;只能用对象作为键。而这两个弱化类是为了性能,可以使得JavaScript的垃圾回收器可以从中清除整个入口。此外,由于弱化类键只能是对象,所以不知道键就没办法找到值,这个性质可以用于封装ES6类的私有属性。

一、基础数据结构

1、字典(以[键,值]对的形式存储数据,table[key]={key, value})

//注:由于JavaScript不是强类型的语言,不能保证键一定是字符串,所以需要把所有键名的传入的对象转换为字符串,使得Dictionary类中搜索和获取值更简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
class ValuePair {
constructor(key, value) {
this.key = key;
this.value = value;
}

toString() {
return `[#${this.key}: ${this.value}]`;
}
}

function defaultToString(item) {
if (item === null) {
return 'NULL';
} if (item === undefined) {
return 'UNDEFINED';
} if (typeof item === 'string' || item instanceof String) {
return `${item}`;
}
return item.toString();
}

class Dictionary {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}
hasKey(key) {
return this.table[this.toStrFn(key)] != null;
}
set(key, value) {
if (key != null && value != null) {
const tableKey = this.toStrFn(key);
this.table[tableKey] = new ValuePair(key,value);
return true;
}
return false;
}
remove(key) {
if (this.hasKey(key)) {
delete this.table[this.toStrFn(key)];
return true;
}
return false;
}
get(key) {
const valuePair = this.table[this.toStrFn(key)];
return valuePair == null ? undefined : valuePair.value;
}
keyValues() {
return Object.values(this.table);
}
values() {
return this.keyValues().map(valuePair => valuePair.value);
}

keys() {
return this.keyValues().map(valuePair => valuePair.key);
}
forEach(callbackFn) {
const valuePairs = this.keyValues();
for (let i = 0; i < valuePairs.length; i++) {
const result = callbackFn(valuePairs[i].key, valuePairs[i].value);
if (result === false) {
break;
}
}
}

isEmpty() {
return this.size() === 0;
}

size() {
return Object.keys(this.table).length;
}

clear() {
this.table = {};
}

toString() {
if (this.isEmpty()) {
return '';
}
const valuePairs = this.keyValues();
let objString = `${valuePairs[0].toString()}`;
for (let i = 1; i < valuePairs.length; i++) {
objString = `${objString},${valuePairs[i].toString()}`;
}
return objString;
}


}

const dictionary = new Dictionary();
dictionary.set('Bob','student');
dictionary.set('Alice','teacher');
dictionary.set('Jack','student');
console.log(dictionary);
console.log(dictionary.hasKey('Bob'));
dictionary.remove('Jack');
console.log(dictionary);
console.log(dictionary.get('Alice'));
console.log(dictionary.keyValues());
console.log(dictionary.keys());
console.log(dictionary.values());
dictionary.forEach((k, v) => {
console.log(`forEach: `, `key: ${k},value: ${v}`);
});

2、散列表(HashTable类或HashMap类,它是Dictionary类的一种散列表实现方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
class ValuePair {
constructor(key, value) {
this.key = key;
this.value = value;
}

toString() {
return `[#${this.key}: ${this.value}]`;
}
}

function defaultToString(item) {
if (item === null) {
return 'NULL';
} if (item === undefined) {
return 'UNDEFINED';
} if (typeof item === 'string' || item instanceof String) {
return `${item}`;
}
return item.toString();
}

class HashTable {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}
loseloseHashCode(key) {
if (typeof key === 'number') {
return key;
}
const tableKey = this.toStrFn(key);
let hash = 0;
for (let i = 0; i < tableKey.length; i++) {
hash += tableKey.charCodeAt(i);
}
return hash % 37;
}
hashCode(key) {
return this.loseloseHashCode(key);
}
put(key,value) {
if (key != null && value != null) {
const position = this.hashCode(key);
this.table[position] = new ValuePair(key, value);
return true;
}
return false;
}
get(key) {
const valuepair = this.table[this.hashCode(key)];
return valuepair == null ? undefined : valuepair.value;
}
remove(key) {
const hash = this.hashCode(key);
const valuepair = this.table[hash];
if (valuepair != null ) {
delete this.table[hash];
return true;
}
return false;
}
getTable() {
return this.table;
}

isEmpty() {
return this.size() === 0;
}

size() {
return Object.keys(this.table).length;
}

clear() {
this.table = {};
}

toString() {
if (this.isEmpty()) {
return '';
}
const keys = Object.keys(this.table);
let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
for (let i = 1; i < keys.length; i++) {
objString = `${objString},{${keys[i]} => ${this.table[keys[i]].toString()}}`;
}
return objString;
}



}

const hash = new HashTable();
console.log(hash.hashCode('Bob') + '-Bob');
hash.put('Bob', 20);
console.log(hash);
hash.put('Alice', 21);
hash.put('Jack', 19);
console.log(hash);
console.log(hash.get('Alice'));
hash.remove('Jack');
console.log(hash);

二、散列表的扩展

问题:有时候,一些键会有相同的散列值,不同的值在散列表中对应相同位置的时候,称为冲突。(冲突会导致散列表只保存最新的值,旧的值会被覆盖)

解决冲突:分离链接、线性探查和双散列法。(我们研究前两个)

1、分离链接

//注:分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在里面。(优点:简单;缺点:需要额外的存储空间)

//本质是将Hash表的位置当做链表处理,先找到位置,对元素进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
class ValuePair {
constructor(key, value) {
this.key = key;
this.value = value;
}

toString() {
return `[#${this.key}: ${this.value}]`;
}
}

function defaultToString(item) {
if (item === null) {
return 'NULL';
} if (item === undefined) {
return 'UNDEFINED';
} if (typeof item === 'string' || item instanceof String) {
return `${item}`;
}
return item.toString();
}
//载入之前我们写好的Linkedlist
class Node {
constructor(element, next) {
this.element = element;
this.next = next;
}
}

function defaultEquals(a, b) {
return a === b;
}

class LinkedList {
constructor(equalsFn = defaultEquals) {
this.equalsFn = equalsFn;
this.count = 0;
this.head = undefined;
}

push(element){
const node = new Node(element);
let current;
if (this.head == null){
this.head = node;
} else {
current = this.head;
while (current.next != null) {
current = current.next;
}
current.next = node;
}
this.count++;
}

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;
}
return undefined;
}

insert(element, index) {
if (index >= 0 && index <= this.count) {
const node = new Node(element);
if (index ==0 ) {
const current = this.head;
node.next = current;
this.head = node;
} else {
const previous = this.getElementAt(index - 1);
node.next = previous.next;
previous.next = node;
}
this.count++;
return true;
}
return false;
}

removeAt(index) {
if (index >= 0 && index < this.count) {
let current = this.head;
if (index == 0) {
this.head = current.next;
}else {
const previous = this.getElementAt(index - 1);
current = previous.next;
previous.next = current;

}
this.count--;
return current.element;
}
return undefined;
}

remove(element) {
const index = this.indexOf(element);
return this.removeAt(index);
}

indexOf(element) {
let current = this.head;
for (let i = 0; i < this.count && current != null; i++) {
if (this.equalsFn(element, current.element)) {
return i;
}
current = current.next;
}
return -1;
}

isEmpty() {
return this.size() === 0;
}

size() {
return this.count;
}

getHead() {
return this.head;
}

clear() {
this.head = undefined;
this.count = 0;
}

toString() {
if (this.head == null) {
return '';
}
let objString = `${this.head.element}`;
let current = this.head.next;
for (let i = 1; i < this.size() && current != null; i++) {
objString = `${objString},${current.element}`;
current = current.next;
}
return objString;
}
}

//分离链接
class HashTableSeparateChaining {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}

loseloseHashCode(key) {
if (typeof key === 'number') {
return key;
}
const tableKey = this.toStrFn(key);
let hash = 0;
for (let i = 0; i < tableKey.length; i++) {
hash += tableKey.charCodeAt(i);
}
return hash % 37;
}

hashCode(key) {
return this.loseloseHashCode(key);
}

put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key);
if (this.table[position] == null) {
this.table[position] = new LinkedList();
}
this.table[position].push(new ValuePair(key, value));
return true;
}
return false;
}

get(key) {
const position = this.hashCode(key);
const linkedList = this.table[position];
if (linkedList != null && !linkedList.isEmpty()) {
let current = linkedList.getHead();
while (current != null) {
if (current.element.key === key) {
return current.element.value;
}
current = current.next;
}
}
return undefined;
}

remove(key) {
const position = this.hashCode(key);
const linkedList = this.table[position];
if (linkedList != null && !linkedList.isEmpty()) {
let current = linkedList.getHead();
while (current != null) {
if (current.element.key === key) {
linkedList.remove(current.element);
if (linkedList.isEmpty()) {
delete this.table[position];
}
return true;
}
current = current.next;
}
}
return false;
}

isEmpty() {
return this.size() === 0;
}

size() {
let count = 0;
Object.values(this.table).forEach(linkedList => {
count += linkedList.size();
});
return count;
}

clear() {
this.table = {};
}

getTable() {
return this.table;
}

toString() {
if (this.isEmpty()) {
return '';
}
const keys = Object.keys(this.table);
let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
for (let i = 1; i < keys.length; i++) {
objString = `${objString},{${keys[i]} => ${this.table[
keys[i]
].toString()}}`;
}
return objString;
}
}

const hashTable = new HashTableSeparateChaining();

hashTable.put('Ygritte', 'ygritte@email.com');
hashTable.put('Jonathan', 'jonathan@email.com');
hashTable.put('Jamie', 'jamie@email.com');
hashTable.put('Jack', 'jack@email.com');
hashTable.put('Jasmine', 'jasmine@email.com');
hashTable.put('Jake', 'jake@email.com');
hashTable.put('Nathan', 'nathan@email.com');
hashTable.put('Athelstan', 'athelstan@email.com');
hashTable.put('Sue', 'sue@email.com');
hashTable.put('Aethelwulf', 'aethelwulf@email.com');
hashTable.put('Sargeras', 'sargeras@email.com');

console.log('**** Printing Hash **** ');

console.log(hashTable.toString());

2、线性探查

//注:其处理冲突的方法是将元素直接存储到表中,而不是单独的数据结构中。当在某个位置position添加新元素,出现冲突时,就尝试添加到position+1的位置,如果position+1的位置也被某元素占据,则继续迭代,直到一个“空闲”的位置。

//采用需要检验是否有必要将一个或多个元素移动到之前的位置的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
class ValuePair {
constructor(key, value) {
this.key = key;
this.value = value;
}

toString() {
return `[#${this.key}: ${this.value}]`;
}
}

function defaultToString(item) {
if (item === null) {
return 'NULL';
} if (item === undefined) {
return 'UNDEFINED';
} if (typeof item === 'string' || item instanceof String) {
return `${item}`;
}
return item.toString();
}

//线性探查
class HashTableLinearProbing {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}

loseloseHashCode(key) {
if (typeof key === 'number') {
return key;
}
const tableKey = this.toStrFn(key);
let hash = 0;
for (let i = 0; i < tableKey.length; i++) {
hash += tableKey.charCodeAt(i);
}
return hash % 37;
}

hashCode(key) {
return this.loseloseHashCode(key);
}

put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key);
if (this.table[position] == null) {
this.table[position] = new ValuePair(key, value);
} else {
let index = position + 1;
while (this.table[index] != null) {
index++;
}
this.table[index] = new ValuePair(key, value);
}
return true;
}
return false;
}

get(key) {
const position = this.hashCode(key);
if (this.table[position] != null) {
if (this.table[position].key === key) {
return this.table[position].value;
}
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) {
return this.table[position].value;
}
}
return undefined;
}

remove(key) {
const position = this.hashCode(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;
}

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++;
}
}

isEmpty() {
return this.size() === 0;
}

size() {
return Object.keys(this.table).length;
}

clear() {
this.table = {};
}

getTable() {
return this.table;
}

toString() {
if (this.isEmpty()) {
return '';
}
const keys = Object.keys(this.table);
let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
for (let i = 1; i < keys.length; i++) {
objString = `${objString},{${keys[i]} => ${this.table[
keys[i]
].toString()}}`;
}
return objString;
}
}


const hashTable = new HashTableLinearProbing();
hashTable.put('Jamie', 'jamie@email.com');
hashTable.put('Jack', 'jack@email.com');
hashTable.put('Jasmine', 'jasmine@email.com');
hashTable.put('Jake', 'jake@email.com');
hashTable.put('Nathan', 'nathan@email.com');
hashTable.put('Athelstan', 'athelstan@email.com');
hashTable.put('Sue', 'sue@email.com');
hashTable.put('Aethelwulf', 'aethelwulf@email.com');
hashTable.put('Sargeras', 'sargeras@email.com');

console.log(hashTable);
console.log(hashTable.toString());
console.log(hashTable.get('Nathan')); // nathan@email.com

hashTable.remove('Ygritte');
console.log(hashTable.get('Ygritte')); // undefined
console.log(hashTable.toString());

三、小结

实现的lose lose散列函数并不是一个表现良好的散列函数,因为它会产生太多的冲突。一个表现良好的散列函数是由几个方面构成的:插入和检索元素的时间(即性能),以及较低的冲突可能性。例如djb2函数。

1
2
3
4
5
6
7
8
9
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;
}