数据结构与算法---散列表

122 阅读4分钟

介绍

散列是一种常用的数据存储结构。散列使用的数据结构叫做散列表,也叫做哈希表,是基于数组设计的,数组的长度是预先设定的。

  • 优点:在散列表上插入、删除、取用数据都非常快。
  • 缺点:查找效率低下。

HashTable类

使用一个类来标识散列表,功能包括:

  • 计算散列值
  • 向散列表中插入数据
  • 从散列表中读取数据
  • 显示散列表中数据分布
function HashTable() {
    this.table = new Array(137);
    this.simpleHash = simpleHash;
    this.showDistro = showDistro;
    this.put = put;
    this.get = get;
}

选择散列函数,计算散列值

散列函数的选择依赖于键值的数据类型。如果键是整型,最简单的散列函数就是以数组的长度对键取余。但在一些情况下,比如数组的长度是10,而键值都是10的倍数时,就不推荐使用这种方式了。因此数组的长度最好为质数。如果键是随机的整数,则散列函数应该更均匀地分布这些键。

但在很多应用中,键是字符串类型,选择针对字符串类型的散列函数是很难的。选择时必须小心。将字符串中每个字符的ASCII码值相加似乎是一个不错的选择,这样散列值就是ASCII码值的和处以数组长度的余数。该散列函数的定义如下:

function simpleHash(data) {
    let total = 0;
    for (let i=0;i<data.length;i++) {
        total += data.charCodeAt(i)
    }
    return total % this.table.length;
}

向散列表中插入数据

function put(data) {
    const pos = this.simpleHash(data);
    this.table[pos] = data
}

显示散列表数据分布

function showDistro() {
    for(let i=0;i<this.table.length;i++) {
        if(this.table[i] !== undefined) {
            console.log('i'+this.table[i])
        }
    }
}

从散列表中读取数据

function get(key) {
    return this.table[this.simpleHash(key)]
}

测试

const someNames = ["David", "Jennifer", "Donnie", "Raymond", "Cynthia", "Mike", "Clayton", "Danny", "Jonathan"];
const hTable = new HashTable();
for(let i=0;i<someNames.length;i++) {
    hTable.put(someNames[i]);
}
hTable.showDistro();

输出为:

iCynthia
iClayton
iDonnie
iDavid
iDanny
iMike
iJennifer
iJonathan

细心的你会发现David没有打印出来,这是因为字符串 "Clayton" 和 "Raymond" 的散列值是一样的,一样的散列值引发了碰撞,因为碰撞,只有"Clayton"存入了散列表。

碰撞处理

散列函数对于多个输入产生同样的输出时,就产生了碰撞。主要介绍两种解决碰撞的办法:开链法和线性探测法。

开链法

当碰撞发生时,我们仍然希望将键存储到通过散列算法产生的索引位置上,但实际上,不可能将多份数据存储到一个数据单元中开链法是指实现散列表的底层数组中,每个数组元素又是一个新的数据结构,比如另外一个数组,这样就能存储多个键了。 如图所示: Yx8FpEmp3Y.jpg

线性探测法

线性探测法隶属于一种更一般的散列技术:开发寻址散列。当发生碰撞时,线性探测法检查散列表中的下一个位置是否为空,如果为空,就将数据存入该位置;如果不为空,则继续检查下一个位置,直到找到一个空的位置为止。

哈希应用

哈希算法主要应用下面的场景:

  • 安全加密:日常用户密码加密通常使用的都是 md5、sha等哈希函数,因为不可逆,而且微小的区别加密之后的结果差距很大,所以安全性更好。
  • 唯一标识:比如 URL 字段或者图片字段要求不能重复,这个时候就可以通过对相应字段值做 md5 处理,将数据统一为 32 位长度从数据库索引构建和查询角度效果更好,此外,还可以对文件之类的二进制数据做 md5 处理,作为唯一标识,这样判定重复文件的时候更快捷。
  • 数据校验:比如从网上下载的很多文件(尤其是P2P站点资源),都会包含一个 MD5 值,用于校验下载数据的完整性,避免数据在中途被劫持篡改。

题量练习

两数之和

var twoSum = function(nums, target) {
    const numMap = new Map();
    let result;
    for(let i=0;i<nums.length;i++) {
        let currentNum = nums[i];
        if(numMap.has(currentNum)) {
            result = [numMap.get(currentNum),i];
            break;
        } else {
            numMap.set(target - currentNum,i)
        }
    }
    return result;
};

无重复字符的最长子串

var lengthOfLongestSubstring = function(s) {
    let max = 0;
    let handleMap = new Map();
    for(let i=0,j=0;j<s.length;j++) {
        // 当前位置s[j]是否在handleMap中
        // 如果在,则滑动i,当前字符上一次出现的位置
        // 计算窗口长度,并与max比较
        if(handleMap.has(s[j])) {
            i = handleMap.get(s[j])+1;
        }
        max = Math.max(max,j-i+1);
        handleMap.set(s[j],j)
    }
    return max
};

前K个高频单词

var topKFrequent = function(words, k) {
    let wordMap = new Map();
    for(let i=0;i<words.length;i++) {
        if(wordMap.has(words[i])) {
            wordMap.set(words[i],wordMap.get(words[i])+1);
        } else {
            wordMap.set(words[i],1)
        }
    }
    const wordList = [...wordMap].sort((a,b)=>{
        if(a[1]===b[1]) {
            return a[0].localeCompare(b[0]);
        }
        return b[1]-a[1]
    });
    const results = wordList.map(item=>{
        return item[0]
    })

    return results.splice(0,k)
};