目录
一. 搜索树
1.1 概念
1.2 操作1 查找
1.3 操作2 插入
1.4 操作3 删除
1.5 性能分析
1.6 和 java 类集的关系
二.哈希表
2.1 概念
2.2 哈希冲突
2.3常见哈希函数
2.4 负载因子
2.5 闭散列
2.6 开散列
2.7 哈希表的实现
push 添加数据
getVal() 获取key对应的val值
2.8 性能分析
2.9 和 java 类集的关系
一. 搜索树
1.1 概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
1.2 操作1 查找
思路:
- 从根节点出发, 如果根节点大于val值, 说明要找的val小于根在根的左边, 所以cur = cur.left
- 如果根节点小于val值, 说明要找的val大于根在根的右边, 所以cur = cur.right
- 如果根节点等于val值, 说明找到了, 直接返回true
- 循环搜索下去, 直到cur == null停止, 说明找不到val值,返回false
public boolean search(int val){
TreeNode cur = root;
while(cur != null){
if(cur.val > val){
cur = cur.left;
}else if(cur.val < val){
cur = cur.right;
}else{
return true;
}
}
return false;
}
1.3 操作2 插入
思路:
- 如果是一颗空树, 直接将结点插入即可
- 找到合适的位置: 和查找的方法类似, 先循环找到该节点应该插入的位置, 如果根节点大于val值, cur = cur.left, 根节点小于val值, cur = cur.right, 根节点等于val值, 则直接返回, 因为搜索树是不允许有相同的节点的, 直到cur== null, 说明该节点应该插入在此
- 但是, 如果我们不知道此节点的父亲节点, 那么我们将没法插入, 所以我们要定义一个parent结点, parent始终等于cur的父亲节点
- 如果val值 < parent.val, 则插入到parent.left, 如果val值 > parent.val, 则插入到parent.right
public void insert(int val){
if(root == null){
root = new TreeNode(val);
return;
}
TreeNode node = new TreeNode(val);
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{
return;
}
}
if(parent.val > val){
parent.left = node;
}else{
parent.right = node;
}
}
1.4 操作3 删除
思路:
设删除节点为cur, cur的父亲节点为parent
cur结点一共分为三种情况:
情况1. cur.left == null
cur.left == null时, 也分成三种情况:
1)cur == root, 只需将root = cur.right
2)cur ! =root && cur == parent.left, 将parent.left = cur.right
3)cur ! =root && cur == parent.right, 将parent.right = cur.right
情况2. cur.right == null
cur.right == null时, 同样也分成三种情况:(与上面相对应)
1)cur == root, 只需将root = cur.left
2)cur ! =root && cur == parent.left,将parent.left = cur.left
3)cur ! =root && cur == parent.right,将parent.right = cur.left
情况3. cur左右节点都不为空
当cur左右节点都不为空时, 我们采用的方法是替换法:
法一: 找到cur结点左子树的最大值, 将cur的值替换成这个值, 再将此结点删除
法二: 找到cur结点右子树的最小值, 将cur的值替换成这个值, 再将此结点删除
问题一:为什么要找左子树的最大值或右子树的最小值?
答:
上面这棵树, 删除15, 正常我们要找的14或20, 得到:
可以看到, 替换之后, 依旧满足搜索树的定义, 左子树都比根小, 右子树都比根大
如果我们选择11, 那么:
发现违背了搜索树的定义!
问题二:为什么要使用替换法?
答:假设我们使用法一, 因为此节点时左子树的最大值, 那么说明该节点没有右子树, cur.right == null, 那么删除该节点的方法就非常简单啦, 就是上面的第2种情况
思路(假设使用法一, 找左子树的最大值):
- 找到要删除的结点cur
- 找左子树最大值: 定义一个指针t, 令t = cur.left(因为我们要找左子树), 让t去找左子树的最大值, 即如果t有右子树, 则t = t.right, 直到t没有右子树
- 替换: 此时t为左子树的最大值, 将t.val = cur.val
- 删除: 还要定义一个tp, 使tp始终等于t的父亲节点, 就回到了情况2: 如果t == tp.left, tp.left = t.left ; 如果t == tp.right, tp.right = t.left
public void remove(int val){ 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{ removeNode(parent,cur); return; } } } public void removeNode(TreeNode parent, TreeNode cur){ //情况1 if(cur.left == null){ if(cur == root){ root = cur.right; }else if(cur == parent.left){ parent.left = cur.right; }else if(cur== parent.right){ parent.right = cur.right; } //情况2 }else if(cur.right == null){ if(cur == root){ root = cur.left; }else if(cur == parent.left){ parent.left = cur.left; }else if(cur == parent.right){ parent.right = cur.left; } //情况3 }else{ TreeNode t = cur.left; TreeNode tp = cur; while(t.right != null){ tp = t; t = t.right; } cur.val = t.val; if(t == tp.right){ tp.right = t.left; }else{ tp.left = t.left; } } }
1.5 性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:
最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N/2
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,都可以是二叉搜索树的性能最佳?
答:AVL树(后续介绍)
1.6 和 java 类集的关系
(在下一篇博客介绍)
TreeMap 和 TreeSet 即 java 中利用搜索树实现的 Map 和 Set;实际上用的是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证.
注意: 使用TreeMap和TreeSet时, 所插入的元素必须是可比较的
二.哈希表
2.1 概念
- 插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
- 搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
2.2 哈希冲突
2.3常见哈希函数
2.4 负载因子
2.5 闭散列
插入:
- 通过哈希函数获取待插入元素在哈希表中的位置
- 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
-
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4 ,如果直接删除掉, 44 查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
2. 二次探测
2.6 开散列
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。
冲突严重时的解决方法:
2.7 哈希表的实现
(开散列)
规则:
先做好准备工作, 定义好结点, 数组, usedSize, 负载因子
public class HashBuck {
public class Node{
public int key;
public int val;
public Node next;
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
public Node[] arr = new Node[10];
public int usedSize;
public static final double LOAD_FACTOR = 0.75;
}
push 添加数据
思路:
- 首先找到添加数据的位置index
- 创建结点node
- 定义一个cur去遍历链表, 如果有和key重复的, 就更新val值, 直到cur == null
- 假设我们使用头插法进行插入, 因为arr[index]中存放的是链表第一个元素的地址, 所以将node.next = arr[index], 然后再将arr[index] = node, 插入成功usedSize++
- 此时需要判断负载因子是否超过了0.75, 如果超过需要扩容, 方法为reSize()
reSize():
- 重新定义一个数组newArray, 将数组的长度定为arr.length*2
- 用cur遍历原数组arr, 将原数组中所有的数重新添加到newArray, 找到cur在新数组的下标, 直接进行头插法, 但需要用curNext记录cur.next的值, 因为头插法会修改cur.next, 直到cur == null, 继续遍历数组下一个下标
- 最后将新数组赋给原数组
代码:
public void push(int key,int val){
Node node = new Node(key,val);
int index = key % arr.length;
Node cur = arr[index];
while(cur != null){
if(cur.key == key){
cur.val = val;
return ;
}
cur = cur.next;
}
node.next = arr[index];
arr[index] = node;
usedSize++;
if(doLoadFactor() >= LOAD_FACTOR){
reSize();
}
}
private double doLoadFactor(){
return usedSize*1.0 / arr.length;
}
private void reSize(){
Node[] newArray = new Node[arr.length * 2];
for (int i = 0; i < arr.length; i++) {
Node cur = arr[i];
while(cur != null){
int index = cur.key % newArray.length;
Node curNext = cur.next;
cur.next = newArray[index];
newArray[index] = cur;
cur = curNext;
}
}
arr = newArray;
}
getVal() 获取key对应的val值
思路:
- 找到key对应的index值
- 定义cur遍历链表, cur.key == key, 返回val, 直到cur == null
- 找不到返回-1
代码:
public int getVal(int key){
int index = key % arr.length;
Node cur = arr[index];
while(cur != null){
if(cur.key == key){
return cur.val;
}
cur = cur.next;
}
return -1;
}
2.8 性能分析
虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是 O(1) 。
2.9 和 java 类集的关系
(在下一篇博客介绍)
1. HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
2. java 中使用的是哈希桶方式解决冲突的
3. java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
4. java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。