前言

本文主要介绍字典及散列表相关内容,在ES2015 中已经对字典进行了实现 既Map类,本文中不会过多介绍Map ,学习的话可以直接看阮一峰老师的ES6 Map

字典

定义
  • 字典也称做映射,符号表或者关联数组
  • 以[键,值]的形式存储
方法
  • set(key,value):向字典中添加新元素。如果 key 已经存在,那么已存在的 value 会 被新的值覆盖
  • remove(key):通过使用键值作为参数来从字典中移除键值对应的数据值。
  • hasKey(key):如果某个键值存在于该字典中,返回 true,否则返回 false
  • get(key):通过以键值作为参数查找特定的数值并返回。  clear():删除该字典中的所有值
  • size():返回字典所包含值的数量。与数组的 length 属性类似。
  • isEmpty():在 size 等于零的时候返回 true,否则返回 false。
  • keys():将字典所包含的所有键名以数组形式返回。
  • values():将字典所包含的所有数值以数组形式返回。
  • keyValues():将字典中所有[键,值]对返回。
  • forEach(callbackFn):迭代字典中所有的键值对。callbackFn 有两个参数:key 和 value。该方法可以在回调函数返回 false 时被中止(和 Array 类中的 every 方法相似)。
实现
class ValuePair {
  constructor(key, value) {
    this.key = key;
    this.value = value;
  }
  toString() {
    return `[#${this.key}: ${this.value}]`;
  }
}

class Dictionary {
  constructor() {
    this.table = {};
  }
  //将 key 转化为字 符串的函数
  toStrFn(item) {
    if (item === null) {
      return 'NULL';
    } else if (item === undefined) {
      return 'UNDEFINED';
    } else if (typeof item === 'string' || item instanceof String) {
      return `${item}`;
    }
    return item.toString(); // {1} 
  }

  hasKey(key) {
    return this.table[this.toStrFn(key)] != null;
  }

  set(key, value) {
    if (key != null && value != null) {
      const tableKey = this.toStrFn(key); // {1}    

      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; // {2} 
  }
  keyValues() {
    return Object.values(this.table);
  }

  keys() {
    return this.keyValues().map(valuePair => valuePair.key);
  }
  values() {
    return this.keyValues().map(valuePair => valuePair.value);
  }

  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;
      }
    }
  }
  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;
  }
  isEmpty() {
    return this.size() === 0;
  }


}


//使用
const dictionary = new Dictionary();
dictionary.set('Gandalf', 'gandalf@email.com');
dictionary.set('John', 'johnsnow@email.com');
dictionary.set('Tyrion', 'tyrion@email.com');

console.log(dictionary.hasKey('Gandalf'));  //true
console.log(dictionary.size()); //3

console.log(dictionary.keys()); // ["Gandalf", "John", "Tyrion"]
console.log(dictionary.values()); //["gandalf@email.com", "johnsnow@email.com", "tyrion@email.com"]
console.log(dictionary.get('Gandalf')) //gandalf@email.com
复制代码

散列表

定义
  • 散列表即HashTable类,也叫HashMap类,是Dictionary类(字典)的一种散列实现方式。
  • 散列算法的作用是尽可能的在数据结构中找到一个值
  • 常见的散列函数——lose lose 散列函数,方法是简单地将每个键值中的每个字母的 ASCII值相加
  • 非顺序数据结构
方法
  • put(key,value):向散列表增加一个新的项(也能更新散列表)
  • remove(key):根据键值从散列表中移除值。
  • get(key):返回根据键值检索到的特定的值。
实现
class ValuePair {
 constructor(key, value) {
   this.key = key;
   this.value = value;
 }
 toString() {
   return `[#${this.key}: ${this.value}]`;
 }
}


class HashTable {
 constructor() {
   this.table = {};
 }
 //将 key 转化为字 符串的函数
 toStrFn(item) {
   if (item === null) {
     return 'NULL';
   } else if (item === undefined) {
     return 'UNDEFINED';
   } else if (typeof item === 'string' || item instanceof String) {
     return `${item}`;
   }
   return item.toString(); // {1} 
 }
 // key 的 每个字符 ASCII 码之和
 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); // {4}   
   }   
   return hash % 37; // {5} 
 } 

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

}


const hash = new HashTable(); 
hash.put('Gandalf', 'gandalf@email.com'); 
hash.put('John', 'johnsnow@email.com'); 
hash.put('Tyrion', 'tyrion@email.com'); 

console.log(hash.hashCode('Gandalf') + ' - Gandalf'); //19 - Gandalf
console.log(hash.hashCode('John') + ' - John'); //29 - John
console.log(hash.hashCode('Tyrion') + ' - Tyrion') //16 - Tyrion

复制代码

处理散列表中的冲突

定义

有时候一些键会有相同的键值。不同的的值在散列表中对应相同位置的时候,我们称其为冲突。此时,当我们通过相同的散列值去取属性值的时候会出现相互覆盖、数据丢失的情况。处理冲突有几种方法:分离链接,线性探查和双散列法,

分离链接

定义
  • 分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在里面
  • 在 HashTable 实例之外还需要额外的存储空间
实现

class HashTableSeparateChaining  {
  constructor() {
    this.table = {};
  }
  //将 key 转化为字 符串的函数
  toStrFn(item) {
    if (item === null) {
      return 'NULL';
    } else if (item === undefined) {
      return 'UNDEFINED';
    } else if (typeof item === 'string' || item instanceof String) {
      return `${item}`;
    }
    return item.toString(); // {1} 
  }
  // key 的 每个字符 ASCII 码之和
  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); // {4}   
    }   
    return hash % 37; // {5} 
  } 

  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 SinglyLinkedList(); // 为 链表章节中的类   https://.im/post/5e363960f265da3e51531be6#heading-19   
      }     
      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; 
  }
  

}
复制代码

线性探查

定义
  • 处理冲突的方法是将元素直 接存储到表中,而不是在单独的数据结构中

当想向表中某个位置加入一个新元素的时候,如果索引为index的位置已经被占据了,就尝试index+1的位置。如果index+1的位置也被占据了,就尝试index+2的位置,以此类推。示例代码如下

实现
class HashTableSeparateChaining  {
  constructor() {
    this.table = {};
  }
  //将 key 转化为字 符串的函数
  toStrFn(item) {
    if (item === null) {
      return 'NULL';
    } else if (item === undefined) {
      return 'UNDEFINED';
    } else if (typeof item === 'string' || item instanceof String) {
      return `${item}`;
    }
    return item.toString(); // {1} 
  }
  // key 的 每个字符 ASCII 码之和
  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); // {4}   
    }   
    return hash % 37; // {5} 
  } 

  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) { // {5}       
        index++;     
      }     
      if (this.table[index] != null && this.table[index].key === key) { // {6}       
        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]; // {1}       
        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]; // {3}       
        this.verifyRemoveSideEffect(key, index); // {4}       
        return true;     
      }   
    }   
    return false; 
  }
  //副作用验证
  verifyRemoveSideEffect(key, removedPosition) {   
    const hash = this.hashCode(key); // {1}   
    let index = removedPosition + 1; // {2}  
    while (this.table[index] != null) { // {3}     
      const posHash = this.hashCode(this.table[index].key); // {4}     
      if (posHash <= hash || posHash <= removedPosition) { // {5}       
        this.table[removedPosition] = this.table[index]; // {6}       
        delete this.table[index];      
        removedPosition = index;     
      }     
      index++;   
    }
  }
}
复制代码

结语

前端界的一枚小学生