邂逅Hello算法 第四篇(解决哈希冲突)

381 阅读9分钟

image.png

对于数组和链表来说,他们在各种的领域属于是互补型。哈希表就属于是通吃了。

那我就有个疑问了?为什么hash表这么好 还需要数组和链表 直接全部定义为Hash表不好吗?

哈希表的优缺点

优点

  • 快速操作:查找、添加和删除元素的平均时间复杂度为 O(1),高效。
  • 适用于动态数据:适合需要频繁查找和更新的数据集。

缺点

  • 空间复杂度:哈希表可能需要额外的空间来处理冲突和维护内部结构。
  • 哈希冲突:尽管处理冲突的技术(如链式哈希和开放定址)存在,但仍可能影响性能。
  • 不支持排序:哈希表中的元素没有顺序,因此不支持按顺序访问或排序。

适用场景

  • 数据量大,且需要频繁的查找、添加、删除操作。
  • 不需要对数据进行排序或顺序访问。

数组的优缺点

优点

  • 快速索引:通过索引访问元素的时间复杂度为 O(1),非常高效。
  • 内存利用:数组的内存分配是连续的,内存利用效率高。
  • 支持排序:可以方便地对数组进行排序和顺序访问。

缺点

  • 固定大小:传统数组大小固定,不支持动态扩展(尽管动态数组可以扩展)。
  • 插入和删除效率低:在中间位置插入或删除元素需要移动其他元素,时间复杂度为 O(n)。

适用场景

  • 数据量相对固定,且需要高效的随机访问和排序操作。
  • 数据插入和删除操作较少,主要关注访问速度。

链表的优缺点

优点

  • 动态大小:链表可以动态扩展,适合处理大小不确定的数据。
  • 高效插入和删除:在链表中插入和删除元素的时间复杂度为 O(1),只需调整指针即可。

缺点

  • 慢速访问:访问链表中的元素需要从头开始逐个遍历,时间复杂度为 O(n)。
  • 额外开销:链表需要额外的空间来存储指针,内存开销较大。

适用场景

  • 数据量动态变化,且需要频繁插入和删除操作。
  • 对随机访问的需求不高,更多关注插入和删除操作的效率。

选择数据结构的考虑因素

  1. 操作频率:如果主要操作是查找,哈希表是理想选择。如果需要频繁插入和删除,链表更合适。
  2. 数据量大小:数组在处理固定大小的数据时效率高,但链表和哈希表更适合处理动态数据。
  3. 空间与时间权衡:哈希表可能会消耗更多的内存,链表的访问速度较慢,而数组的扩展性有限。
  4. 排序和顺序访问:如果需要排序或顺序访问,数组更适合。

重点就在于:适合需要频繁查找和更新的需要Hash表,如果是只需要查找就用数组,更多需要更新就用链表。

简单介绍Hash

Hash(哈希)是一种将数据映射到固定大小值的技术。它的核心目的是快速存取数据。哈希函数会将输入的任意大小的数据(比如字符串或文件)转换成固定长度的数字(哈希值)。常见的哈希函数有MD5、SHA-1、SHA-256等。

生活中的例子

  1. 图书馆的书籍: 想象图书馆里每本书都有一个唯一的编号,书架上书的排列是按照编号来进行的。这样,当你需要找一本特定的书时,你只需查找编号就可以快速定位。
  2. 密码存储: 当你设置一个密码时,系统不会直接保存你的密码,而是将其通过哈希函数处理后存储。即使黑客拿到存储的哈希值,也很难反推出原始密码,因为哈希函数的逆向是非常困难的。

哈希的特点

  • 唯一性:好的哈希函数能尽量避免不同输入映射到相同的哈希值(这种情况叫做哈希冲突)。
  • 不可逆:从哈希值无法还原出原始输入。
  • 固定长度:不论输入数据多大,哈希值长度是固定的。

使用场景

  • 数据检索:哈希表(Hash Table)中,哈希函数用来快速定位数据存储的位置。
  • 数据完整性:用于检测数据在传输或存储过程中的完整性。
  • 加密:密码存储和验证中常用哈希函数保护用户数据安全。

哈希是数据管理中非常重要的一部分,它提高了数据处理的效率和安全性。

什么是Hash冲突,如何解决Hash冲突

Hash冲突是指不同的输入数据被哈希函数映射到相同的哈希值。由于哈希函数将任意长度的输入映射到固定长度的哈希值,存在输入数据无限多但哈希值有限的情况,这就可能导致不同输入产生相同的哈希值。

解决Hash冲突的方法

  1. 链式哈希(Chaining)

    • 概念:每个哈希表的桶(或槽)都保存一个链表。所有映射到同一个哈希值的元素都会被存储在这个链表中。
    • 优点:简单且有效,冲突时只需在链表中插入新元素即可。
    • 缺点:可能会导致链表过长,查询效率下降。
    class HashTable {
      constructor(size) {
        this.size = size;
        this.table = new Array(size).fill(null).map(() => []);
      }
      
      hash(key) {
        let hash = 0;
        for (let i = 0; i < key.length; i++) {
          hash = (hash + key.charCodeAt(i)) % this.size;
        }
        return hash;
      }
      
      set(key, value) {
        const index = this.hash(key);
        const bucket = this.table[index];
        const item = bucket.find(([k]) => k === key);
        if (item) {
          item[1] = value; // 更新值
        } else {
          bucket.push([key, value]); // 插入新值
        }
      }
      
      get(key) {
        const index = this.hash(key);
        const bucket = this.table[index];
        const item = bucket.find(([k]) => k === key);
        return item ? item[1] : undefined;
      }
    }
    
  2. 开放定址(Open Addressing)

    • 概念:当发生冲突时,哈希表会在表中寻找下一个空位置来存放冲突的元素。

    • 常见策略

      • 线性探测(Linear Probing) :在冲突发生时,线性地探测下一个位置。
      • 二次探测(Quadratic Probing) :探测位置按二次方增加。
      • 双重哈希(Double Hashing) :使用另一个哈希函数来确定探测间隔。
    class OpenAddressingHashTable {
      constructor(size) {
        this.size = size;
        this.table = new Array(size).fill(null);
      }
      
      hash(key) {
        let hash = 0;
        for (let i = 0; i < key.length; i++) {
          hash = (hash + key.charCodeAt(i)) % this.size;
        }
        return hash;
      }
      
      probe(index, i) {
        return (index + i) % this.size;
      }
      
      set(key, value) {
        let index = this.hash(key);
        for (let i = 0; i < this.size; i++) {
          let probeIndex = this.probe(index, i);
          if (this.table[probeIndex] === null || this.table[probeIndex][0] === key) {
            this.table[probeIndex] = [key, value];
            return;
          }
        }
      }
      
      get(key) {
        let index = this.hash(key);
        for (let i = 0; i < this.size; i++) {
          let probeIndex = this.probe(index, i);
          if (this.table[probeIndex] === null) return undefined;
          if (this.table[probeIndex][0] === key) return this.table[probeIndex][1];
        }
        return undefined;
      }
    }
    
  3. 再哈希(Rehashing)

    • 概念:当哈希表的负载因子(元素数量/表大小)过高时,创建一个更大的哈希表,并重新计算所有元素的哈希值以放入新的表中。
    • 优点:减轻了哈希冲突的概率。
    • 缺点:可能会导致性能下降,因为需要重新计算所有元素的哈希值。
    class ResizableHashTable {
      constructor(size) {
        this.size = size;
        this.table = new Array(size).fill(null).map(() => []);
        this.count = 0;
      }
      
      hash(key) {
        let hash = 0;
        for (let i = 0; i < key.length; i++) {
          hash = (hash + key.charCodeAt(i)) % this.size;
        }
        return hash;
      }
      
      resize(newSize) {
        const oldTable = this.table;
        this.size = newSize;
        this.table = new Array(newSize).fill(null).map(() => []);
        this.count = 0;
        
        oldTable.forEach(bucket => {
          bucket.forEach(([key, value]) => this.set(key, value));
        });
      }
      
      set(key, value) {
        if (this.count / this.size > 0.7) this.resize(this.size * 2);
        
        const index = this.hash(key);
        const bucket = this.table[index];
        const item = bucket.find(([k]) => k === key);
        if (item) {
          item[1] = value;
        } else {
          bucket.push([key, value]);
          this.count++;
        }
      }
      
      get(key) {
        const index = this.hash(key);
        const bucket = this.table[index];
        const item = bucket.find(([k]) => k === key);
        return item ? item[1] : undefined;
      }
    }
    

哈希算法

哈希算法是一种将输入数据(如文本或文件)转换为固定长度的输出值(即哈希值)的数学算法。哈希算法在很多领域都很重要,比如数据存储、加密和验证。不同的哈希算法具有不同的特性和用途。

常见的哈希算法

  1. MD5(Message Digest Algorithm 5)

    • 输出长度:128位(16字节)

    • 特点:速度快,广泛用于文件完整性校验,但由于碰撞问题(不同输入产生相同的哈希值),不再推荐用于安全敏感的应用。

    • 示例

      const crypto = require('crypto');
      const hash = crypto.createHash('md5').update('hello world').digest('hex');
      console.log(hash); // 5eb63bbbe01eeed0934d5c6f8f3f3d1e
      
  2. SHA-1(Secure Hash Algorithm 1)

    • 输出长度:160位(20字节)

    • 特点:比MD5安全性稍高,但也存在碰撞问题。现在也不推荐用于安全应用。

    • 示例

      const crypto = require('crypto');
      const hash = crypto.createHash('sha1').update('hello world').digest('hex');
      console.log(hash); // 2ef7bde608ce5404e97d5f042f95f89f1c232871
      
  3. SHA-256(Secure Hash Algorithm 256-bit)

    • 输出长度:256位(32字节)

    • 特点:比SHA-1更安全,广泛应用于数据加密和数字签名。

    • 示例

      const crypto = require('crypto');
      const hash = crypto.createHash('sha256').update('hello world').digest('hex');
      console.log(hash); // a591a6d40bf420404a011733cfb7b190d62c65bf0bcda6d4b037a25d6c8b9f3e
      
  4. SHA-3(Secure Hash Algorithm 3)

    • 输出长度:可选择224位、256位、384位、512位

    • 特点:SHA-3是SHA系列的最新成员,设计上与SHA-2有所不同,提供更高的安全性。

    • 示例

      const crypto = require('crypto');
      const hash = crypto.createHash('sha3-256').update('hello world').digest('hex');
      console.log(hash); // a5a626d4841d89c720aa5f0b53db16b89fa8e7d7cf5b8d8b2b1b673aaf37d6d9
      

哈希算法的特点

  • 固定长度:无论输入数据多大,输出的哈希值长度固定。
  • 唯一性:理想情况下,不同的输入应该产生不同的哈希值,但在实际中存在碰撞的可能性。
  • 不可逆:无法从哈希值反推出原始输入。
  • 快速计算:哈希值的计算应该非常快。
  • 抗碰撞性:好的哈希算法应尽可能减少碰撞的概率。

使用场景

  • 数据完整性:验证数据在传输或存储过程中是否被篡改。
  • 密码存储:将密码通过哈希算法处理后存储,以增加安全性。
  • 数字签名:用于确保消息来源的真实性和完整性。
  • 数据结构:如哈希表中的数据快速定位。