1.散列表
1.1.概念
散列表(Hash Table)又名哈希表/Hash表,是根据键(key)直接访问在内存存储位置的数据结构,它是由数组演化而来,利用了数组支持按照下标进行随机访问数据的特性。
1.2.散列函数
1.2.1.散列函数的要求和特点
散列函数就是一个函数(方法),能够将给定的 key 转换成特定的散列值,我们可以表示为:hashValue = hash(key)
散列函数需要满足以下几个基本要求:
- 散列函数计算得到的散列值,必须是大于等于0的正整数,因为 hash 值需要作为数组的下标;
- 如果 key1== key2,那么经过hash后得到的哈希值也必然相同,即 hash(key1) == hash(key2)
- 如果 key1 != key2,那么经过hash后得到的哈希值也必然不同,即 hash(key1) != hash(key2)
好的散列函数应该满足以下几个特点:
- 散列函数不能太复杂,因为太复杂势必要消耗很多的时间在计算哈希值上,也会间接影响散列表性能
- 散列函数计算得出的哈希值尽可能的随机且均匀的分布,这样能够将散列冲突最小化
1.2.2.散列函数的设计方法
实际工作中,我们还需要综合考虑各种因素。这些因素有关键字的长度,特点,分布,还有散列表的大小等。散列函数各式各样的,我举几个常用的,简单的散列函数的设计方法。
- 直接寻址法
- 除留余数法
- 平方取中法
- 折叠法
1.直接寻址法
比如我们现在要对 0-100 岁的人口数字统计表,那么我们对年龄这个关键字 key 就可以直接用年龄的数字作为地址。此时
hash(key) = key。这个时候,我们可以得出这么个哈希函数 hash(0) = 0,hash(1) = 1,....... ,hash(100) = 100。
比如我们现在要统计的是1980年后出生年份的人口数,那么我们对出生年份这个关键字可以用年份减去1980作为地址。此时
hash(key) = key - 1980。
也就是说,我们可以取关键字 key 的某个线性函数值为散列地址,即 hash(key) = a * key + b,其中a,b为常量。
这样的散列函数优点是简单,均匀,也不会产生冲突,但问题是这需要事先知道关键字key的分布情况,适合查找表较小且连续的情况。由于这样的限制,在现实应用中,直接寻址法虽然简单,但却不常用。
2.除留余数法
除留余数法是最常用的构造散列函数方法。
对于散列表长度为 m 的散列函数公式为:hash(key) = key mod p(p<= m); 注:mod 为求余运算符号
此方法的关键就在于选择合适的 p,如果 p 选择的不合适,就可能会容易产生哈希冲突,比如有12个关键字 key,现在我们针对他设计一个散列表。如果采用除留余数法,那么可以先尝试将散列函数设计为 hash(key) = key mod 12 的方法。比如 29 mod 12 = 5,所以它存储在下标为 5 的位置。
不过这也是存在冲突的可能的,因为 12 = 2 * 6 = 3 * 4。如果关键字中有像 18(3*6),30(5*6),42(7*6),他们的余数都是 6,这样就和 78 所对应的下标位置冲突了。此时如果我们不选择 p = 12,而且选用 p = 11,则结果如下:
使用除留余数法的一个经验是,若散列列表长度为 m,通常 p 为小于或等于表长(最好接近m)的最大质数或不包含小于20质因子的合数。总之实践证明:当P取小于哈希表长的最大质数时,产生的哈希函数较好。
3.平方取中法
这是一种常用的哈希函数构造方法。这个方法是先取关键字的平方,然后根据可使用空间的大小,选择平方数是中间几位为哈希地址。
hash(key) = key 平方的中间几位
这种方法的原理是通过取平方扩大差别,平方值的中间几位和这个数的每一位都相关,则对不同的关键字得到的哈希函数值不易产生冲突,由此产生的哈希地址也较为均匀。
4.折叠法
有时关键码所含的位数很多,采用平方取中法计算太复杂,则可将关键码分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列地址,这种方法称为折叠法,折叠法可分为两种:
移位折叠:将分割后的几部分低位对齐相加。
边界折叠:从一端沿分割界来回折叠,然后对齐相加。
比如关键字为:12320324111220,分成 5 段,123,203,241,112,20,两种方法如下:
当然了,散列函数的设计方法不仅仅只有这些方法,对于这些我们不需要全部掌握,只需要理解其设计原理即可。
1.2.3.散列冲突
两个不同的关键字(key),由于散列函数值相同,因而被映射到同一表位置上。该现象称为 散列冲突 或 哈希碰撞。
散列冲突的解决方案:即使再好的散列函数可能也无法避免散列冲突,那么如果出现了散列冲突,我们该如何解决呢?
在本节我们来介绍两类方法解决散列冲突:开放寻址法,链表法。
1.开放寻址法
开放寻址法的核心思想是:一旦出现了散列冲突,我们就重新去寻址一个空的散列地址。
(1)线性检测
我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
- 支持快速的查询,插入,删除操作
- 内存占用合理,不能浪费过多的内存空间
- 性能稳定,极端情况下,散列表的性能也不会退化到无法接受的情况。
- 设计一个合适的散列函数
- 定义装载因子阈值,并且设计动态扩容策略
- 选择合适的散列冲突解决方法
1.3.散列表的应用
2.哈希算法
2.1.概念
2.2.要求
- 将任何一条不论长短的信息,计算出唯一的一摘要(哈希值)与它相对应,对输入数据非常敏感,哪怕原始数据只修改了一个 Bit,最后得到的哈希值也大不相同
- 摘要的长度必须固定,散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率非常小
- 摘要不可能再被反向破译。也就是说,我们只能把原始的信息转化为摘要,而不可能将摘要反推回去得到原始信息,即哈希算法是单向的
- 哈希算法的执行效率要尽量高效,针对较长的文本,也能快速地计算出哈希值
1.MD5(' 数据结构和算法 ') = 31ea1cbbe72095c3ed783574d73d921e2.MD5(' 数据结构和算法很好学 ')=0fba5153bc8b7bd51b1de100d5b66b0a3.MD5(' 数据结构和算法不好学 ')=85161186abb0bb20f1ca90edf3843c72
3.树
3.1.树的定义及相关概念
3.1.1.定义
- 每个节点都只有有限个子节点或无子节点;
- 没有父节点的节点称为根节点;
- 每一个非根节点有且只有一个父节点;
- 除了根节点外,每个子节点可以分为多个不相交的子树
- 树里面没有环路(cycle)
3.1.2.高度,深度和层
- 节点的高度:节点到叶子节点的最长路径(边数),所有叶子节点的高度为 0。某节点到叶子节点的距离。
- 节点的深度:根节点到这个节点所经历的边的个数,根的深度为 0。某节点到根节点的距离。
- 节点的层数:节点的深度+1。
- 树的高度:根节点的高度。
3.2.二叉树
3.2.1.定义
3.2.2.二叉树的遍历
二叉树经典的三种遍历方式:前序遍历,中序遍历,后续遍历:
3.2.3.二叉查找树
我们以一副图示表示一下该树的结构:
public class SimpleBinarySearchTree {
// 二叉查找树,指向根节点
private Node tree;
/**
* 根据指定的值查找对应的节点
* @param value
* @return
*/
public Node find(int value){
Node parent = tree;
while(parent != null){
if(parent.value > value){
parent = parent.left;
}else if(parent.value < value){
parent = parent.right;
}else {
return parent;
}
}
return parent;
}
/**
* 节点类
* */
private static class Node{
// 节点值
private int value;
// 左节点
private Node left;
// 右节点
private Node right;
protected Node(Node left,int value,Node right){
this.left = left;
this.value = value;
this.right = right;
}
public void setValue(int value) {
this.value = value;
}
public void setLeft(Node left) {
this.left = left;
}
public void setRight(Node right) {
this.right = right;
}
}
}
private Node createNode(Node left,int value,Node right){
return new Node(left,value,right);
}
private Node createNode(int value){
return createNode(null,value,null);
}
/**
* 将value值存入容器
* @param value
* @return
*/
public boolean put(int value) throws Exception {
if(tree == null){
tree = createNode(value);
return true;
}
Node parent = tree;
while (parent != null){
if(parent.value > value){
if(parent.left == null){
parent.left = createNode(value);
return true;
}
parent = parent.left;
}else if(parent.value < value){
if(parent.right == null){
parent.right = createNode(value);
return true;
}
parent = parent.right;
}else {
throw new Exception("查找二叉树不可插入相同值的节点");
}
}
return false;
}
测试代码:
public static void main(String[] args) throws Exception{
// 创建容器
SimpleBinarySearchTree tree = new SimpleBinarySearchTree();
// 向容器中添加值
tree.put(2);
tree.put(4);
tree.put(6);
tree.put(10);
tree.put(15);
tree.put(16);
tree.put(17);
tree.put(18);
tree.put(5);
tree.put(3);
tree.put(9);
tree.put(11);
tree.put(12);
// 从容器中取出节点值为12的节点
SimpleBinarySearchTree.Node node = tree.find(12);
System.out.printf("节点值为12的节点为:"+node);
}
解释一下第三种情况,也就是删除的节点有两个子节点,需要删除的节点的值大于左节点及其以下的所有节点,小于右节点及其以下的所有节点,所以需要再删除节点的所有子节点中选出一个大于左节点及其以下所有节点,且小于右节点及其以下的所有节点,这种情况下,这个合适的节点只能是右节点下的最小值,即右节点下面的最底层的左节点这个节点。
/**
* 删除节点
* @param value
*/
public void remove(int value){
// 记录要删除的节点
Node p = tree;
// 记录要删除节点的父节点
Node p_parent = null;
// 先找到要删除的元素及其父元素
while (p!=null){
if(p.value > value){
p_parent = p;
p = p.left;
}else if(p.value < value){
p_parent = p;
p = p.right;
}else {
break;
}
}
// 如果没有找到则返回
if(p == null){
return;
}
// 要删除的节点有两个子节点,这种情况要用于右子树中最小节点的值替换当前要删除元素的值,然后删除右侧最小节点
if(p.left != null && p.right != null){
// 找到该节点右子树的最小节点-》最左侧的叶子节点
Node rightTree = p.right;
Node rightTree_p = p; // rightTree 的父节点
while (rightTree.left != null){
rightTree_p = rightTree;
rightTree = rightTree.left;
}
// 用右子树最小节点替换当前要删除的节点
p.value = rightTree.value;
// 删除右子树的最小节点,考虑到删除操作的其他两种情况,要删除元素是叶子节点以及要删除元素只有一个子节点都属于元素的删除,
// 这里的思路和逻辑是一样的,为统一代码逻辑编写在此处不直接删除
p = rightTree;
p_parent = rightTree_p;
}
// 删除节点是叶子节点或者仅有一个子节点,都是要删除该节点,将父节点的指针指向当前节点的子节点
Node child = null;
// 计算当前节点的子节点
if(p.right != null){
child = p.right;
}else if(p.left != null){
child = p.left;
}else {
child = null;
}
// 执行删除
if(p_parent == null){ // 要删除根节点
tree = child;
}else if(p_parent.left == p){ // 更新父节点的左指针
p_parent.left = child;
}else {
p_parent.right = child;
}
}
/**
* 获取最小节点
* @param value
* @return
*/
public Node getMin(int value){
if(tree == null){
return null;
}
Node p = tree;
while (p.left != null){
p = p.left;
}
return p;
}
/**
* 获取最大节点
* @param value
* @return
*/
public Node getMax(int value){
if(tree == null){
return null;
}
Node p = tree;
while (p.right != null){
p = p.right;
}
return p;
}