一、哈希表
(一)哈希表的定义
哈希表(Hash table,也叫散列表) 是根据关键码值(Key value)而直接进行访问的数据结构,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
关键码值(Key value)也可以当成是key的hash值
这个映射函数叫做散列函数
存放记录的数组叫做散列表
数组,链表,哈希表的区别:
- 数组(顺序表):寻址容易,只要使用下标即可;插入和删除难,会有大量的元素进行移动
- 链表:插入与删除容易,但是查询难
- 哈希表:寻址容易,插入删除也容易,综合了数组和链表的特性。
(二)哈希表的图解
Key : {14, 19, 5, 7, 21, 1, 13, 0, 18} 散列表: 大小为13 的数组 a[13]; 散列函数: f(x) = x mod 13; 遇到hash冲突则hash值+1(直到加到没有hash冲突的位置,此处先用简单的方式来解决hash冲突)
- 散列表的大小:是需要根据自己的需求而定的
- 散列函数:为了key计算后的结果hash值,能够不超出定义的散列表的大小
- hash冲突:不同的key通过散列函数计算的hash值一样。自定义的哈希表需要设计好hash冲突解决的方法。
- 装填因子:放入散列表中的数据个数 / 散列表的大小
[1] 假设装填因子=0.7,散列表中的数据个数 / 散列表的大小 = 9/13 = 0.7,如果散列表中数据个数达到了9个,就需要对散列表进行扩容,扩容后再存放新的数据。HashMap设置的装填因子是0.75。
[2] 那么为什么不全部放满了再扩容呢,需要根据装填因子?因为数据越接近散列表的大小,产生冲突的可能会越来越大 - 缺点:扩容需要消耗大量的空间和性能。
扩容会导致散列函数重新变化,[ 比如扩容后大小为50,那么f(x) = x mod 50 ],原本存入哈希表的key值都需要重新计算。
HashMap的扩容就是2的n次方计算的。 - 应用:散列表应用场景,是在数组大小变化的可能性不大的情况下:电话号码,字典,点歌系统,QQ,微信的好友等。
比如系统的电话号码存放有上限值的,这样增删改查就能保持性能最快的一种方式。

(三)哈希表的拉链法
要能够手写一套性能优异的哈希表,需要了解所有的树结构。不同的语言,设计的哈希表也是有些差异的,HashMap就是采用了拉链法。
1、JDK1.8以前:数组 + 链表
优点:
- 查找快:时间复杂度 O(n) ,( O(n/线性表的size) ) ,是一个线性的查找
[e.g.] f(337) = 1,查找到数组角标1,然后再单链表查询到337。- 插入快:查找到数组角标,单链表的插入
- 删除快:查找到数组角标,单链表的删除
缺点:
- 大数据时代,如果数组容量不是很大,很有可能出现某一行链表个数超过几万几十万。(解决方法见数组 + 链表 + 红黑树)

2、JDK1.8以后:数组 + 链表 + 红黑树 (大数据时代)
阈值:链表长度最长的值。
当链表长度超过阈值,就转换成红黑树。
HashMap从JDK1.8以后采用了这种方式,更适应大数据时代。
红黑树和链表对比(如果查找第1000个数据):
- 链表:需要查询1000次才能查找到
- 红黑树:每次都是两个两个找,查找的次数:log21000 = 9次 (210=1024)
也就是寻找的力度越大,查找的速度就会越快

二、树的入门
(一)什么是树,森林
1、树(Tree)是n(≥0)个结点的有限集。n=0时成为空树。
在任意一颗非空树中:
- 有且仅有一个特定的称为根(Root)的结点
- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集 T1、T2、…Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)
(一个结点只会有一个父亲)
2、森林(Forest)是m(m≥0)棵互不相交的树的集合。
【树的图示】:

【辨别以下哪些为树】:

(二)树的概念
1、结点与树的度
- 结点的度:结点拥有的子树数
- 叶子结点(终端结点):度为0的结点
- 分支结点(非终端结点):度不为0的结点
- 内部结点:除根结点以外的分支结点
- 树的度:树内各结点的度的最大值。

2、层次和深度
- 结点的层次(Level):从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第L层,则其子树的跟就在L+1层。其双亲在同一层的结点互为堂兄弟。
- 树的深度(Depth)或高度:树中结点的最大层次。

(三)树的存储结构
1、双亲表示法 (少用)
平时会很少用到,启发式寻路会使用到双亲表示法

2、孩子表示法(常用)
- 【方法一】树的度最大值为N,那么每个结点都会带N个指针,在使用的时候非常消耗空间。
二叉树采用的是这种方式:因为二叉树最多只有两个度,所以空间浪费没有这么多。

- 【方法二】标记总共有N个孩子,那么每个结点都会带N个指针:多叉树可以采用这种方式

3、双亲孩子表示法(少用)
把每个结点的孩子结点排列起来,以单链表作为存储结构, 则n个结点有n个孩子链表,如果是叶子结点则此单链表为空, 然后n个头指针又组成一个线性表,采用顺序存储结构,存放在一个一维数组中
- 父亲只有一个,用数组存放
- 孩子有很多,用单链表存放
和HashMap有点相似,但是底层结构完全不一样。

4、孩子兄弟表示法(少用)
孩子兄弟表示法为每个节点设计三个域: 一个数据域,一个该节点的第一个孩子节点域,一个该节点的下一个节点的兄弟指针域

三、二叉树的入门
(一)二叉树的定义
1、二叉树
二叉树(Binary Tree)是n(≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
度:最大为2。
[e.g.] 三维建模,各种优异的算法能解决实际问题的都是采用二叉树。

2、斜树

3、满二叉树
除最后一层无任何子节点外,每一层上的所有结点都有两个子结点二叉树

4、完全二叉树
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
(只缺少最右边过来的子二叉树,编号是连续的)
完全二叉树是满二叉树的子集。

【以下哪些是完全二叉树】:就看编号是否连续的

(二)二叉树的存储结构
1、顺序存储(少用)
- 将每个结点存放到数组中
- 如果结点为空,则存放null

2、链式存储(常用)

(三)二叉树的遍历
1、树的创建
/**
* 功能:二叉树
* <p>
* Created by danke on 2018/12/3.
*/
public class BinaryTree<E> {
transient Node<E> root; // 树的根结点
public BinaryTree(E e) {
this.root = new Node<>(e, null, null);
}
/**
* 创建二叉树
* @param root
*/
public void createTree(Node<String> root) {
// 创建每个结点
Node<String> bNode = new Node<>("B", null, null);
Node<String> cNode = new Node<>("C", null, null);
Node<String> dNode = new Node<>("D", null, null);
Node<String> eNode = new Node<>("E", null, null);
Node<String> fNode = new Node<>("F", null, null);
Node<String> gNode = new Node<>("G", null, null);
Node<String> hNode = new Node<>("H", null, null);
Node<String> iNode = new Node<>("I", null, null);
// 设置每个结点的左右孩子
root.leftChild = bNode;
root.rightChild = cNode;
bNode.leftChild = dNode;
dNode.leftChild = gNode;
dNode.rightChild = hNode;
cNode.leftChild = eNode;
cNode.rightChild = fNode;
eNode.rightChild = iNode;
}
/**
* 结点
*/
public class Node<E> {
E data;
Node<E> leftChild; // 左孩子
Node<E> rightChild; // 右孩子
public Node(E data, Node<E> leftChild, Node<E> rightChild) {
this.data = data;
this.leftChild = leftChild;
this.rightChild = rightChild;
}
}
}
2、前序遍历 DLR
规则是若二叉树为空,则空操作返回,否则先访问跟结点,然后前序遍历左子树,再前序遍历右子树(根—左---右)
[e.g.] 快速排序就是标准的前序遍历。 Arrays.sort();

/**
* 前序遍历 DLR
* @param root
*/
public void preOrderTraverse(Node<E> root) {
if (root == null) {
return;
}
System.out.print(root.data); // 根 -- 输出
preOrderTraverse(root.leftChild); // 左 -- 逻辑
preOrderTraverse(root.rightChild); // 右 -- 逻辑
}
3、中序遍历 LDR
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点), 中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树(左–根--右)
[e.g.]汉诺塔问题(可以看一下上一章递归)

/**
* 中序遍历 LDR(采用了递归的方式)
* @param root
*/
public void midOrderTraverse(Node<E> root) {
if (root == null) {
return;
}
midOrderTraverse(root.leftChild); // 左 -- 逻辑
System.out.print(root.data); // 根 -- 输出
midOrderTraverse(root.rightChild); // 右 -- 逻辑
}
4、后序遍历 LRD
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点(左—右---根)

/**
* 后序遍历 LRD
* @param root
*/
public void postOrderTraverse(Node<E> root) {
if (root == null) {
return;
}
postOrderTraverse(root.leftChild); // 左 -- 逻辑
postOrderTraverse(root.rightChild); // 右 -- 逻辑
System.out.print(root.data); // 根 -- 输出
}
本文介绍了哈希表的定义、拉链法处理冲突,包括JDK1.8前后的实现方式;详细讲解了树的概念,如结点度、层次和深度;并探讨了二叉树的定义、存储结构以及遍历方法。
1649

被折叠的 条评论
为什么被折叠?



