搜索树
任意节点的左子树的值都小于节点的值,右子树的值都大于节点的值 ,二叉搜索树中没有重复的节点。
- 平衡搜索树:插入删除 查找的时间复杂度O(log(n))
因为搜索树的高度不确定,所以时间复杂度不确定,因此引入平衡树:为了解决搜索树高度不确定的缺憾,平衡树中,要求树中每个节点的左子树的高度和右子树的高度差的绝对值不超过1。
1.平衡树:AVL树:
在AVL树中,每个节点会记录一项属性:平衡因子(左子树的高度-右子树的高度):-1 0 1
难度:随着插入的进行,树可能不平衡(平衡因子变为2时),因此采用旋转来使树达到平衡:当插入到平衡因子为1的节点的左子树上则需要右旋,插入到平衡因子为1的节点的右子树上则需要左旋。
需要旋转的情况:
往左子树插入节点,插入后,左子树的高度-右子树的高度==2
往右子树插入节点,插入后,左子树的高度-右子树的高度==-2:
AVL树的插入与调整:
往左子树插入:
node.left的左子树 比node.left的右子树 高:对node做右旋 转
node.left的左子树比node.left的右子树低:对node.left做左旋转,对node做右旋转
往右子树插入:
node.right的左子树 比node.right的右子树 高:对node做右旋 转
node.right的左子树比node.right的右子树低:对node.right做左旋转,对node做右旋转
旋转的是常数时间,不影响插入和删除的时间复杂度,仍然是O(log(n))
左旋:
右旋:
更新平衡因子:
节点中保存平衡因子,一个比特位即可,保存高度则需要更大的空间保存
自己实现的AVL树
package www.sweet.search;
import java.util.function.Predicate;
/**
* Author:sweet
* Created:2019/6/1
*/
public class AVLTree {
public static class Node {
int key;
int value;
//int factor;-1,0,1
int height = 1;
Node left = null;
Node right = null;
Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private Node root;
public boolean insert(int key, int value) {
try {
root = insertInternal(root, key, value);
return true;
} catch (Exception e) {
return false;
}
}
private Node insertInternal(Node node, int key, int value) throws Exception {
if (node == null) {
return new Node(key, value);
} else if (node.key == key) {
throw new Exception("key值冲突了");
} else if (node.key < key) {
//要插入的值比根节点大,插入做子树中
node.left = insertInternal(node.left, key, value);
//判断调整AVL树
//若果不是平衡树了
if (height(node.left) - height(node.right) >= 2) {
//判断左子树高了,还是右子树高了,进行旋转调整
if (height(node.left.left) > height(node.left.right)) {
//如果插入左子树,并且左子树的左边大于左子树的右边,则对根节点进行右旋
node = rightRotate(node);
} else {
//插入到了左子树的右节点,对左子树进行左旋,根节点变为左子树节点
node.left = leftRotate(node.left);
//然后再对当前node做右旋
node = rightRotate(node);
}
}
} else {
//如果插入的值小于根节点的值,那么就需要插入了进右子树
if (height(node.right.left) > height(node.right.right)) {
//如果右子树的左边比右边高
//先对右子树进行右旋,再对根节点进行左旋
node.right = rightRotate(node.right);
node = leftRotate(node);
} else {
//右节点的右子树比左子树高
//对根节点进行左旋即可
node = leftRotate(node);
}
}
//更新根节点的高度
node.height = updateHeight(node);
return node;
}
private Node rightRotate(Node node) {
//右旋的结果为:当前节点的左子树的右子树 成为根节点的左子树,
// 根节点成为根节点左子树的右子树
Node original = node.left;
node.left = original.right;
original.right = node;
node.height = updateHeight(node);
original.height = updateHeight(original);
return original;
}
private int updateHeight(Node node) {
return max(height(node.right),height(node.left))+1;
}
private int max(int a,int b){
return a>b?a:b;
}
private int height(Node node) {
return node==null? 0:node.height;
}
/*
* 左旋
* */
private Node leftRotate(Node node) {
Node originalRigth = node.right;
node.right = originalRigth.left;
originalRigth.left = node;
node.height = updateHeight(node);
return originalRigth;
}
/**
* 1. 验证每个结点中的高度 == 真实的高度
* 2. 每个结点的左右子树高度差不能超过 |1|
* 3. 中序是有序的
*/
private void inorder(Node node, Predicate<Node> tester) {
if (node != null) {
inorder(node.left, tester);
if (!tester.test(node)) {
throw new RuntimeException("key: " + node.key);
}
inorder(node.right, tester);
}
}
public void verifyHeights() {
inorder(root, (Node node) -> {
int actualHeight = getHeight(node);
return actualHeight == node.height;
});
}
public void verifyBalance() {
inorder(root, (Node node) -> {
int differ = height(node.left) - height(node.right);
return differ >= -1 && differ <= 1;
});
}
public void verifyOrdering() {
class Cache {
public int lastKey = Integer.MIN_VALUE;
}
final Cache cache = new Cache();
inorder(root, (Node node) -> {
// 要保证 node.key 永远是大于等于上一个给过来的 node.key
boolean r = node.key >= cache.lastKey;
cache.lastKey = node.key;
return r;
});
}
private int getHeight(Node node) {
if (node == null) {
return 0;
}
int left = getHeight(node.left);
int right = getHeight(node.right);
return max(left, right) + 1;
}
public static void main(String[] args) {
AVLTree tree = new AVLTree();
for (int i = 1000; i > 0; i--) {
tree.insert(i, i);
}
tree.verifyHeights();
tree.verifyBalance();
tree.verifyOrdering();
}
}
2.红黑树
红黑树是:
每个节点有颜色,红或黑(0/1)
红色不能和红色相邻
根节点一定是黑色的
叶子节点(null节点)是黑色的
从根到每一个叶子,所有这样的路径上,黑色节点的数量一样多
通过这样一系列的规定,会使红黑树达到平衡二叉树的特性,最长的一条长度不会超过最短的一条的两倍,因此能够保证一个相对的长度。
红黑树的插入与调整(TreeMap内部的排序算法):
在红黑树中插入的节点一定是红色的,插入节点会遇到以下几种情况(调整策略的基本原则是尽量不改变黑色节点的个数):
1.如果父亲是黑色的,插入成功
2.如果父亲是红色的,破坏了红黑树的性质,需要进行调整:
- 若存在父亲节点,则一定没达到根节点,因为根节点一定是黑的
- 若存在祖父节点:祖父颜色一定是黑的,会出现以下四种情况
- 有叔叔节点:并且叔叔节点是红的,需要改变父亲和叔叔的颜色,才能满足红黑树的性质
- 没有叔叔:对父亲节点进行右旋,并调整父亲节点为根节点(黑色),祖父节点改为红色
- 若叔叔是黑色的:对父亲节点进行右旋调整,把祖父节点调整为红色
- 有叔叔且为黑色,我插入的位置是父亲节点的右节点:对父亲节点进行左旋(然后就回到了第二种情况,再继续进行调整)
- 有叔叔节点:并且叔叔节点是红的,需要改变父亲和叔叔的颜色,才能满足红黑树的性质
AVL树和红黑树的对比:理论上来讲:AVL查找比红黑树好一点(AVL高度更平衡,更低),插入删除比红黑树差一点(调整的次数更多)
3.B-树,B+树,B*树
B-树:值除了在叶子节点中保存,中间节点中也保存
B+树:值全部在叶子中
数据库中的索引是为了提升查找效率,能够提升查找效率的途径有:搜索树和哈希,在数据库中使用的是B+树(多叉树)索引
二叉搜索树主要应用在内存上,因为内存的速度快,
B+树具有多分支,路径段的特点,因此常常应用在磁盘上的搜索,因为磁盘读写效率低,B+树可以使得读写磁盘的次数变小,搜索效率变高。(在同等数量的情况下,高度更低,所以访问磁盘次数更少)
在java中应用搜索树的类有:TreeMap和TreeSet,它们都用到了红黑树
哈希表
哈希冲突/哈希碰撞:数组的容量小于要存放的数据,根据Hash(key)生成下标,不同的key经过hash函数后,得到同样的下标,这种情况就称为出现了哈希冲突或哈希碰撞。(由于数据个数n一定大于数组长度N,所以冲突无法避免)
1.如何减少哈希冲突:设计好的哈希函数,控制哈希表中存的数据量
2.遇到哈希冲突怎么办:
-
1)开放地址法(闭散列):遇到冲突了,重新计算一个下标
- 线性探测法:出现冲突了,就依次向后找
- 二次探测法:第一次找12 个格子,第二次找22 个格子。。。。
-
2)用一条链表把冲突的数据串起来:哈希桶(HashMap中的方式)
看源码的方式:
1.看构造方法
2.put()/get()
搜索可以解决的问题:
1.给定key返回value(给定key,返回key出现的次数)
2.判断存在与否key
解决搜索问题的方法:
1.二分查找
- 适用场景
- 针对于有序数组
- 如果给定情况下,数据已经有序,并且不会变更,适合二分查找
- 求N的平方根
2.哈希表
- 优点:具有O(1)的时间复杂度,速度快
3.搜索树(平衡)
- 优点:有序性(B+树应用于数据库,天生具有有序性,可以做范围查找,若使用hash作为数据库索引,那么只能做匹配=查找)
海量数据处理
Q:100Tip日志,找出出现次数最多的K个
1.先切割文件(不能简单的水平、平均切割:这样会使同一个IP地址被切到不同的文件中)
- 要使同一个IP被切进同一个文件中,可以利用hash的方式来切:对应每一个IP都对应同一个下标
- 但是利用hash的切法无法保证每个文件大小都平均,所以最好适当多切分几份