阅读 21

快速入门数据结构与算法之散列

前言

本篇我们将要介绍一个非常有用的数据结构,那就是散列。为什么说它有用呢,因为它既能巧妙的解决一些算法问题,在日常的生活和工作中也是扮演者重要的角色。下面我们就详细的去了解它。

定义和实现

说起定义我们就先看一下百度百科是怎么说的

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

也就是说散列表就是一个数组,但是这个数组不一定是每个key都有值的,需要通过一个散列函数计算哪个数据存到哪个key中。下面还是老规矩,用JavaScript来实现一下。

//键值对数据
class ValuePair {
    constructor(key, value) {
        this.key = key;
        this.value = value;
    }
    toString() {
        return `[#${this.key}: ${this.value}]`;
    }
}
class HashTable {
    //构造函数
    constructor() {
        this.table = {};
    }
    //最重要的散列函数
    hashFn(key) {
        //如果是数字直接返回
        if (typeof key === 'number'){
            return key;
        }
        //将其他结果全部转换为字符串
        let tableKey
        if (key === null) {
            tableKey =  'NULL';
        } else if (key === undefined) {
            tableKey =  'UNDEFINED';
        } else if(typeof key === 'string' || key instanceof String) {
            tableKey =  `${key}`;
        } else {
            tableKey = key.toString();
        }
        let hash = 0;
        for (let i = 0; i < tableKey.length; i++) {
            //转换成字母的ASCII码并相加求出相应的数字
            hash += tableKey.charCodeAt(i);
        }
        return hash % 36; //防止数字太大
    }
    //向散列表中加入键和值
    put(key, value) {
        if(key != null && value != null) {
            //通过散列函数求出相应的存储位置
            const position = this.hashFn(key);
            this.table[position] = new ValuePair(key, value);
            return true;
        }
        return false;
    }
    //获取一个值
    get(key) {
        const position = this.hashFn(key),
              valuePair = this.table[position];
        return valuePair == null ? undefined : valuePair;
    }
    //移除一个值
    remove(key) {
        const hash = this.hashFn(key);
        const valuePair = this.table[hash];
        if (valuePair != null) {
            delete this.table[hash];
            return true;
        }
        return false;
    }
复制代码

这里的散列函数是散列的关键,他决定了数据的存储位置,既然是一个固定的函数,那么一定会出现重复位置的可能,重复的位置我们就把它叫作冲突,那么我们怎么去解决冲突呢,下面我们将详细并实现两个解决冲突的方法。

冲突的解决

在这里会介绍两种方法,都相对简单,也非常好理解。

线性探测

线性探测,顾名思义就是如果出现冲突就呈线性的向后一个一个探测位置,有位置就把数据放进入,没有就继续向后查找,直至找到位置。流程是不是很简单呢,那我们就改一下上面的方法

put(key, value) {
	if (key !== null && value !== null) {
		let position = this.hashFn(key);
		//当前位置有值就向后查找
		while (this.table[position] !== null) {
			position++;
		}
		this.table[position] = new ValuePair(key, value);
		return true;
	}
	return false;
}

get(key) {
	let position = this.hashFn(key)
	if (this.table[position] != null) {
	    //当前位置有数据且数据的key和传入的key不相等则向后查找
		while (this.table[index] != null && this.table[index].key !== key) {
			position++
		}
		if (this.table[index] != null && this.table[index].key === key) {
			return this.table[position].value
		}
	}
	return undefined
}

remove(key) {
	let position = (hash = this.hashFn(key))
	if (this.table[position] != null) {
		while (this.table[position] != null && this.table[position].key !== key) {
			position++
		}
		if (this.table[position] != null && this.table[position].key === key) {
			delete this.table[position];
			//将后面的数据进行整理
			const newPosition = position + 1;
			//如果后面的位置上有数据
			while (this.table[newPosition] != null) {
				const posHash = this.hashFn(this.table[newPosition].key)
				//如果当前位置的数据的hash值小于等于删除位置数据的hash值或者小于等于删除的位置
				if (posHash <= hash || posHash <= position) {
				    //数据前移
					this.table[position] = this.table[newPosition];
					delete this.table[newPosition];
					position = newPosition;
				}
				newPosition++;
			}
			return true;
		}
	}
	return false;
}
复制代码

数据项链

第二种解决冲突的方法就是数据项链,这里指的是数据项的链,也就是将用散列函数解析出的相同位置的数据组成一个链表,这样就能把他们都存储到一个位置上了。下面咱们用这个思想再来改进一下上面的方法。

put(key, value) {
	if (key != null && value != null) {
		const position = this.hashFn(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.hashFn(key),
		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.hashFn(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;
}
复制代码

在实现时用到的LinkedList是我的线性结构文章中的链表结构,如果没有看过可以点击这里,到这里我们就讲述了两种解决冲突的方法。对于散列表来说,最重要的还是它的散列函数,我们这里的散列函数只是简单的实现,有很多很有名的散列函数,更好的散列函数能让散列表发挥更强大的作用。

应用

学习数据结构是枯燥的,我们讲完了它的基本实现后,来讲讲他的实际应用,这里应该会有趣些。散列在生活中的应用是比较多的,比如我们经常遇到的密码加密技术,MD5、SHA等加密都是利用散列函数将任何长度饿数据变为固定长度的“摘要”。我们下面将会介绍一个更加高端的技术,那就是区块链技术。

区块链顾名思义就是将一个个的区块链接起来,那么什么是区块呢?区块就是一个用来保存数据的容器,它由头和体组成,那么怎么将他们链接起来呢,这里就用到咱们的散列函数,每个区块头中会存储一个由前一个区块头+区块体组成的散列值。这样他们就链接起来了。这只是数据进行了链接,我们所看到的向下图那样的网状结构是各个节点通过网络连接组成的。可不是每个节点上存储一个数据,而是每个节点都会存储所有的数据。形成一个分布式的数据库。

任何节点的数据改变都会同步到所有的节点上。这也是区块链的去中心化,即所有节点平等,不存在控制中心。因为一旦某个区块细微的改动,它的散列值都会改动,这样会导致后面所有的区块都要改动,因为有“工作量证明”机制,所以这种操作是不可能完成的。这样看来区块链中的数据是很安全的。

那么共工作量证明是什么东西呢,其实就是计算出一个有效的散列值。只要计算出这个散列值,就能把一个区块连接到另一个区块后面,但是这么多节点全部同步是非常慢的,所以需要控制区块的生成速度,怎么控制呢?就提高计算这个散列值的难度就行了。说了这么多,是不是还是对他的应用有点模糊。世界上最大规模的区块链我们肯定是非常熟悉的,那就是比特币,它的区块内的数据其实就是一个账本,来记录交易记录的,这对于一个货币体系是非常重要的,所以公司会为每个帮他们记账,也就是把那个区块挂上去的人一笔“记账奖励”。现在知道为什么矿工们不惜买几千台机器来计算这个散列值了吧。讲到这里,是不是对区块链有了比较深入的认识呢?如果对它感兴趣的话可以去查阅资料更详细的学习。

小结

本篇文章就介绍了一个数据结构,本来一直纠结要不要写,但是想了想还是写吧,毕竟它与我们现在的前沿技术还是有很大的联系的,很有必要去了解一下。觉得作者写的还凑合的可以给个小赞。我们一起加油!!!

关注下面的标签,发现更多相似文章
评论