目录
1. 搜索树
1. 概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树。
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
总结:二叉搜索树的左子树节点 < 父亲节点 < 右子树节点
2.操作
2.1 插入
public void checkInsertLegal() throws insertException{
throw new insertException("待插入的值在树中已存在");
}
public void insert(int val){
TreeNode node = new TreeNode(val);
if (root == null) {
root = node;
return;
}
TreeNode cur = root;
TreeNode parent = null;
//寻找插入位置
while (cur != null) {
if (cur.val > val) {
parent = cur;
cur = cur.left;
}else if (cur.val < val){
parent = cur;
cur = cur.right;
}else {
try {
checkInsertLegal();
}
catch (insertException e){
e.printStackTrace();
}
return;
}
}
//插入
if (parent.val > val) {
parent.left = node;
}
else{
parent.right = node;
}
}
2.2 查找
public boolean search(int key) {
TreeNode cur = root;
while (cur != null) {
if (cur.val < key){
cur = cur.right;
}else if (cur.val > key){
cur = cur.left;
}else {
return true;
}
}
2,3 删除(难)
public void remove(int key) {
TreeNode parent = null;
TreeNode cur = root;
while (cur != null) {
if (cur.val < key) {
cur = cur.right;
} else if (cur.val > key) {
cur = cur.left;
} else {
//删除(三种情况)
//removeNode();
}
}
}
private void removeNode(TreeNode parent,TreeNode cur) {
if(cur.left == null) {
if (cur == root) {
root = cur.right;
} else if (cur == parent.left) {
parent.left = cur.right;
}else {
parent.right = cur.right;
}
} else if (cur.right == null) {
if (cur == root) {
root = cur.left;
} else if (cur == parent.left) {
parent.left = cur.left;
}else {
parent.right = cur.left;
}
}else {
//取左树最大值替代
TreeNode target = cur.right;
TreeNode targetP = cur;
while(cur.left != null){
targetP = target;
target = target.left;
}
cur.val = target.val;
if (target == targetP) {
targetP.left = target.right;
}else {
targetP.right = target.right;
}
}
}
3.实现
public class BinarySearchTree {
static class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
}
4. 性能分析
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:log2N
最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N/2
在退化成单支树后,二叉搜索树便失去了右树,此时我们可以将其改变成AVL树,但这里就不多赘述。
2. 搜索
2.1 概念
Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。
以前常见的查找方式:
- 直接遍历,时间复杂度为O(N),元素如果比较多效率会非常慢
- 二分查找,时间复杂度为 ,但搜索前必须要求序列是有序的
上述的查找方式常用于静态数据的查找,而现实生活中我们我们在查找的过程当中不可避免地需要进行插入和删除的操作,例如:
- 根据姓名查询考试成绩
- 通讯录,即根据姓名查询联系方式
- 不重复集合,即需要先搜索关键字是否已经在集合中
而本节介绍的Map和Set就是应用于动态数据的查找。
2,2 模型
一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以 模型会有两种:
- 纯 key 模型,比如:
- 有一个英文词典,快速查找一个单词是否在词典中
- 快速查找某个名字在不在通讯录:
- Key-Value 模型,比如:
- 统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:
- 梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号
3. Map
3.1 说明
Map是一个接口类,该类没有继承自Collection,该类中存储的是结构的键值对,并且K一定是唯一的,不 能重复。
3.2 Map.Entry<Key,Value>
Map.Entry 是Map内部实现的用来存放键值对映射关系的内部类,该内部类中主要提供了<Key,Value>的获取,value的设置以及Key的比较方式。
方法 | 解释 |
K getKey() | 返回 entry 中的 key |
V getValue() | 返回 entry 中的 value |
V setValue(V value) | 将键值对中的value替换为指定value |
注意:Map.Entry并没有提供设置Key的方法
4. Set
注意:
1. Set是继承自Collection的一个接口类
2. Set中只存储了key,并且要求key一定要唯一
3. TreeSet的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的
4. Set最大的功能就是对集合中的元素进行去重
5. 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础 上维护了一个双向链表来记录元素的插入次序。
6. Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入
7. TreeSet中不能插入null的key,HashSet可以。
5. 哈希表
5.1 概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键 码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( ),搜索的效率取决于搜索过程中 元素的比较次数。
5.2 哈希冲突,避免
对于两个数据元素的关键字 和 (i != j),有 != ,但有:Hash( ) == Hash( ),即:不同关键字通过相同哈 希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞
所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。
5.3 冲突 - 解决
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以 把key存放到冲突位置中的“下一个” 空位置中去。
常见的有线性探测、二次探测,但这不是重点,java内部集合实现的是下面这种。
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子 集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
刚才我们提到了,哈希桶其实可以看作将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味 着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:
1. 每个桶的背后是另一个哈希表
2. 每个桶的背后是一棵搜索树
5.4 实现
public class HashBuck {
static class Node {
public int key;
public int val;
Node next;
public Node(int key,int val) {
this.key = key;
this.val = val;
}
}
public Node[] array;
public double loadFactor = 0.75;
public int usedSize;
public HashBuck() {
array = new Node[10];
}
public void put(int key,int val) {
int index = key % array.length;
Node cur = array[index];
//1.遍历链表,查看是否存在当前值
while (cur != null){
if (cur.key == key){
//key一样更新val
cur.val = val;
return;
}
cur = cur.next;
//2.头插
Node node = new Node(key, val);
node.next = array[index];
array[index] = node;
usedSize++;
}
//3.
if (LoadFactorcount() > loadFactor) {
//扩容
//重新哈希
resize();
}
}
private double LoadFactorcount() {
//负载因子计算
return usedSize * 1.0 / array.length;
}
private void resize() {
Node[] newarray = new Node[array.length*2];
for (int i = 0; i < array.length; i++) {
Node cur = array[i];
//遍历链表
while(cur != null) {
Node curN = cur.next;
int newIndex = cur.key % newarray.length;
cur.next = array[newIndex];
newarray[newIndex] = cur;
cur = cur.next;
cur = curN;
}
}
array = newarray;
}
public int get(int key) {
int index = key % array.length;
Node cur = array[index];
//1.遍历链表,查看是否存在当前值
while (cur != null){
if (cur.key == key){
//key一样更新val
return cur.val;
}
cur = cur.next;
}
return -1;
}
}
5.5 性能分析
虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的, 也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是 O(1) 。
5.6 和java集合的关系
1. HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
2. java 中使用的是哈希桶方式解决冲突的
3. java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
4. java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方 法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。