Front end learning data structures and algorithms quick start series - sets, dictionaries, and hash tables

Keywords: data structure

Sets, dictionaries, and hash tables

aggregate

Collection: consists of an unordered and unique set of items.

Tip: aggregate It is a concept in mathematics, but it is applied in the data structure of computer science.

Create collection class

Generally, the collection has the following methods:

  • add(element): adds a new element to the collection
  • delete(element) or remove(): removes an element from the collection
  • clear(): remove all elements in the collection
  • has(element) or contains(): returns true if the element is in the collection
  • size(): returns the number of elements contained in the collection
  • values() or iterator(): returns an item that contains all the values in the collection

The author realizes the following:

class Set {
    constructor() {
        this.items = {}
    }
    has(item) {
        return this.items.hasOwnProperty(item)
    }
    add(item) {
        // Check whether it already exists before adding
        if (!this.has(item)) {
            this.items[item] = item;
            return true;
        }
        // Indicates that it has not been added
        return false;
    }
    delete(item) {
        // Check whether it already exists before adding
        if (this.has(item)) {
            delete this.items[item];
            return true;
        }
        return false;
    }
    size() {
        // Returns an array of its own enumerable properties
        return Object.keys(this.items).length
    }
    values() {
        return Object.values(this.items)
    }
    clear() {
        this.item = {}
    }
}

Test:

let set = new Set()
console.log(set.add(5)) // true 
console.log(set.add("5")) // false 
set.add(6)
console.log(set.values()) // [5, 6] 
console.log(set.size()) // 2

One flaw in this implementation is that the number 5 and the string "5" are considered the same element. Because the key name of the attribute, even if a number is passed in, will be automatically converted into a string, set[5] and set["5"] are the same in the view of Set.

Tip: some students may say that if I use an array to represent a set (this.items = []), I can't distinguish between the number 5 and the string "5". Like this:

class Set {
    constructor() {
        this.items = []
    }
    add(item) {
        if (!this.items.includes(item)) {
            this.items.push(item)
            return true
        }
        // Indicates that it has not been added
        return false;
    }
}

Finally, es6 added the Set type, allowing you to store unique values of any type. Of course, there are other features. For example, the Set type can be iterated

Using the Set class in es6

The core usage of the Set class in es6 is very similar to our Set class. See example:

  • Create a collection through new Set()
  • Call the add() method to add elements to the collection
  • Access the size attribute to get the number of elements in the collection
  • By calling the has() method, you can detect whether a value exists in the Set set
  • You can remove an element from the collection by calling the delete() method
let set = new Set()

set.add(2)
set.add("2")
console.log(set.size) // 2
console.log(set.has(2)) // true
set.delete(2)
console.log(set.values()) // [Set Iterator] { '2' }
set.clear()
console.log(set.size) // 0

There are some differences between the Set of es6 and our implementation. For example:

  • The values() method in es6 returns Iterator
  • size is an attribute in es6

Note: Set constructor can accept all iteratable objects as parameters. For example, array, Set set and Map are all iteratable objects.

forEach() in the array is very easy to use, so es6 also adds the same method to the Set set:

new Set(["a", "b", "c"]).forEach(item => console.log(item))
// a
// b
// c

We can't access the elements in the Set through the index like an array. If necessary, we can convert the Set into an array first:

const set = new Set(["a", "b", "c"])
const arr = [...set]
// arr:  [ 'a', 'b', 'c' ]
console.log('arr: ', arr)

Tip: for more usage, see mdn Set

Set operation

We can perform the following operations on the set:

  • Union: for two sets, returns a new set containing all the elements in both sets
  • Intersection: for two sets, returns a new set containing elements common to both sets
  • Difference set: for two sets, return a new set containing all elements that exist in the first set and do not exist in the second set
  • Subset: if each element in one set (S1) is in another set (S2), S1 is a subset of S2
  • Superset: if each element in one set (S2) is in another set (S1), and S1 may contain elements not in S2, S1 is a superset of S2
// Union
function union(setA, setB) {
    let _union = new Set(setA);
    for (let elem of setB) {
        _union.add(elem);
    }
    return _union;
}

// intersection
function intersection(setA, setB) {
    let _intersection = new Set();
    for (let elem of setB) {
        if (setA.has(elem)) {
            _intersection.add(elem);
        }
    }
    return _intersection;
}

// Difference set
function difference(setA, setB) {
    let _difference = new Set(setA);
    for (let elem of setB) {
        _difference.delete(elem);
    }
    return _difference;
}

// Superset
function isSuperset(set, subset) {
    for (let elem of subset) {
        if (!set.has(elem)) {
            return false;
        }
    }
    return true;
}

Test:

const set1 = new Set([2, 3, 4])
const set2 = new Set([3, 4, 5])
const set3 = new Set([3, 4])

// Set(4) { 2, 3, 4, 5 }
console.log(union(set1, set2)) 

// Set(2) { 3, 4 }
console.log(intersection(set1, set2))

// Set(1) { 2 }
console.log(difference(set1, set2))

// true
console.log(isSuperset(set1, set3))
Extension operator

A simple way to calculate Union, intersection, difference and superset is to use extension operators. Rewrite the above method:

function union(setA, setB) {
    return new Set([...set1, ...set2])
}

function intersection(setA, setB) {
    return new Set([...setA].filter(item => setB.has(item)))
}

function difference(setA, setB) {
    return new Set([...setA].filter(item => !setB.has(item)))
}

function isSuperset(set, subset) {
    return [...subset].every(item => set.has(item))
}

WeakSet

Storing an object in an instance of Set is exactly the same as storing it in a variable. As long as the reference in the instance of Set exists, the garbage collection mechanism cannot free the space of the object:

let set = new Set()
let key = {}
set.add(key)

key = null // {1}
console.log(set.size) // 1 {2}

Sometimes, however, we hope that when all other references do not exist (line {1}), these references in the Set will disappear (line {2} output is 0). Therefore, es6 adds a WeakSet type, which is similar to the use of Set. The main difference is that the WeakSet saves weak references to objects. Rewrite the above example:

let set = new WeakSet()
let key = {}
set.add(key)
// Remove the last strong reference of the object key (the reference in the Weak Set will also be removed automatically)
key = null

Testing is difficult, but you have to trust the javascript engine.

The WeakSet Set is different from the normal Set in the following aspects:

  • A WeakSet can only store objects
  • Only three methods are supported: add(), has(), and delete()
  • forEach is not supported
  • It is not iteratable and does not expose any iterators (such as the keys() and values() methods)
  • The size attribute is not supported

Dictionaries

Dictionaries are very similar to collections. Collections store elements in the form of [value, value], and dictionaries store elements in the form of [key, value].

In the set, we are interested in each value itself and regard it as the main element; For dictionaries, we usually query specific elements by key names.

In real life, the applications of this data structure are: dictionary, address book and so on

Create dictionary class

Generally, the dictionary has the following methods:

  • set(key, value): adds a new element to the dictionary. If the key exists, the existing value will be overwritten
  • get(key): returns the corresponding value
  • has(key): check whether the specified key name already exists in the Map
  • remove(key): removes the specified key name and the corresponding value
  • values(): returns the values contained in the dictionary as an array
  • keys(): returns the key names contained in the dictionary as an array
  • keyValues(): returns the [key, value] pairs in the dictionary as an array
  • isEmpty(): whether the dictionary is empty. Returns true when size is 0
  • size(): returns the number of members contained in the dictionary
  • clear(): method removes all key value pairs in the Map
class Dictionary {
    constructor() {
        this.table = {};
    }
    set(key, value) {
        if (key != null && value != null) {
            this.table[key] = value;
            return true;
        }
        return false;
    }
    get(key) {
        return this.table[key]
    }
    has(key) {
        return this.table.hasOwnProperty(key)
    }
    remove(key) {
        if (this.hasKey(key)) {
            delete this.table[key];
            return true;
        }
        return false;
    }
    values() {
        return Object.values(this.table)
    }
    keys() {
        return Object.keys(this.table)
    }
    keyValues() {
        // Computable attribute names can be used in object literals. The syntax is to use square brackets ([])
        return this.keys().map(key => ({ [key]: this.table[key] }))
    }
    isEmpty() {
        return this.size() === 0;
    }
    size() {
        return Object.keys(this.table).length;
    }
    clear() {
        this.table = {};
    }
}

Test:

const d = new Dictionary()
d.set('age', 18)
d.set('name', 'aaron')
console.log(d)                    // Dictionary { table: { age: 18, name: 'aaron' } }
console.log(d.hasKey('age'))      // true
console.log(d.size())             // 2
console.log(d.keys(), d.values()) // [ 'age', 'name' ] [ 18, 'aaron' ]
console.log(d.keyValues())        // [ { age: 18 }, { name: 'aaron' } ]

Tip: a dictionary is also called an associative array. Perhaps in javascript, we can use square brackets ([]) to get the properties of an object.

Using the Map class in es6

The usage of Map in es6 is very similar to our Dictionary class. See example:

  • Create a dictionary class through new Map()
  • Call the set() method to pass in the key name and the corresponding value
  • Call the get() method to return the corresponding value
  • Calling the has(key) method can detect whether the specified key name already exists in the Map
  • Call the delete(key) method to remove the specified key name and the corresponding value
  • Call the clear() method to remove all key value pairs in the Map
  • Call the size property to return the number of members in the Map
const map = new Map()
map.set('age', 18)
map.set('name', 'aaron')
console.log(map.get('age'))   // 18
console.log(map.has('age'))   // true
map.delete('name')
console.log(map.size)         // 1

There are some differences between es6's Map and our implementation. For example:

  • The values() method in es6 returns Iterator
  • size is an attribute in es6
  • There is no isEmpty() method in es6
  • The keyValues() method is called entries() in es6

Note: the key name and value of Map can be of any type, and the Map constructor can be initialized by passing in an array:

const map = new Map([['age', 18], ['name', 'aaron']])
map.set({}, 1)
map.set({}, null)
console.log(map.size) // 4

The forEach() method in Map is similar to the forEach() method in Set and Array. The callback function supports three parameters:

const map = new Map([['age', 18], ['name', 'aaron']])
map.forEach((value, key, aMap) => {
    console.log(key + ' ' + value)
})
// age 18
// name aaron

const set = new Set(['age', 'name'])
set.forEach((value, key, aMap) => {
    console.log(key + ' ' + value)
})
// age age
// name name

const arr = new Array('age', 'name')
arr.forEach((value, key, aMap) => {
    console.log(key + ' ' + value)
})
// 0 age
// 1 name

Tip: for more usage, see mdn Map

WeakMap

WeakSet is a collection of weak reference sets. In contrast, WeakMap is a weak reference Map, which is also used to store weak references of objects. See example:

let map = new WeakMap()
let key = {}
map.set(key, 'empty object')
key = null

// At this time, the WeakMap is empty

There are several differences between WeakMap and ordinary Map:

  • Key names can only be objects (but cannot be null)
  • Only four methods are supported: set(), get(), has() and delete()
  • forEach is not supported
  • It is not iteratable and does not expose any iterators (for example, keys(), values(), entries())
  • The size attribute is not supported

Note: WeakMap is a special Map. Similarly, WeakSet is also a special Set. It stores weak references of objects. When the strong references of the object are cleared, the keys and corresponding values of weak references will also be automatically garbage collected.

Hash table

Hash table Like dictionaries, data is stored in the form of [key, value] pairs. However, the key name needs to be converted through a function (called hash function).

Tip: generally, to obtain a value in a data structure, you need to iterate the whole data structure to find it. If you use the hash function, you can know the specific location of the value, so you can quickly find the value.

Create hash table

The following is a simple hash table that includes only add, find, and delete:

  • hashCode(): hash function, used to convert key names to hash values
  • put(key, value): adds a new item to the hash table (can also update the hash table)
  • remove(key): removes the value from the hash table based on the key
  • get(key): retrieve a specific value through the key
class HashTable {
    constructor() {
        this.table = {}
    }
    // Hash function
    hashCode(key) {
        // Adds the Unicode encoded values of each character
        const hash = [...String(key)].reduce((pre, curr) => curr.codePointAt(0) + pre, 0)
        return hash

        // Avoid operands exceeding the maximum range of variables
        // return hash % 100
    }
    put(key, value) {
        if (key != null && value != null) {
            const position = this.hashCode(key);
            this.table[position] = value
            return true;
        }
        return false;
    }
    get(key) {
        return this.table[this.hashCode(key)]
    }
    remove(key) {
        const hash = this.hashCode(key)
        if (this.table.hasOwnProperty(hash)) {
            delete this.table[hash]
            return true
        }
        return false
    }
}

Test:

let h = new HashTable()
h.put('abc', 'abc')
h.put('a', 'a')
console.log(h)              // HashTable { table: { '97': 'a', '294': 'abc' } }
console.log(h.remove('a'))  // true
console.log(h.get('abc'))   // abc

Conflicts in hash tables

Sometimes, some keys return the same value:

let h = new HashTable()
h.put('abc', 'abc')         // {1}
h.put('cba', 'cba')         // {2}
console.log(h)              // HashTable { table: { '294': 'cba' } }

Since different keys (abc and cba) correspond to 294, data loss is caused.

Obviously, the purpose of using a data structure to save data is not to lose the data, so we need to solve the conflict.

The simplest way to solve the conflict is the separate link method, that is, the hash table does not store value directly, but stores a linked list data structure, and stores value in the linked list. The schematic diagram is as follows:

class HashTableSeparateChaining {
  constructor(toStrFn = defaultToString) {
    this.table = {};
  }
  hashCode(key) {}
  put(key, value) {
    if (key != null && value != null) {
      const position = this.hashCode(key);
      if (this.table[position] == null) {
        // LinkedList is a linked list data structure
        this.table[position] = new LinkedList();
      }
      this.table[position].push(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 === value) {
          return current.element;
        }
        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 === value) {
          linkedList.remove(current.element);
          if (linkedList.isEmpty()) {
            delete this.table[position];
          }
          return true;
        }
        current = current.next;
      }
    }
    return false;
  }
}

Tip: a good hash function should have a low possibility of conflict

Posted by Savahn on Mon, 22 Nov 2021 18:15:07 -0800