二叉搜索树
二叉搜索树又叫二叉查找树,二叉排序树;它具有以下特点:
- 如果它的左子树不为空,则左子树上结点的值都小于根结点
- 如果它的右子树不为空,则右子树上结点的值都大于根结点
- 子树同样也要遵循以上两点
只要一颗树是二叉搜索树,那么它的中序遍历一定是有序的
看上面的这颗二叉树,它的中序遍历为:
0 3 4 5 6 8
这种二叉搜索树的查询效率十分的高,假如一共有4个数据,那么最多要找2次,如果有20亿个数据,那么最多要找32次。
这种二叉搜索树查找的时间复杂度为logn,或者说树的深度就是查找的时间复杂度
下面是二叉搜索树的插入数据遍历数据和查找数据的代码
public class BinarySearchTree {
int date;
BinarySearchTree left;
BinarySearchTree right;
public BinarySearchTree(int date) {
this.date = date;
}
/**
* 向树中插入一个树
* @param root
* @param date
*/
public void insert(BinarySearchTree root, int date){
if (root.date < date){
if (root.right == null){
root.right = new BinarySearchTree(date);
}else {
insert(root.right,date);
}
}else{
if (root.left == null){
root.left = new BinarySearchTree(date);
}else {
insert(root.left,date);
}
}
}
/**
* 中序遍历
* @param tree
*/
public void printInTree(BinarySearchTree tree){
if (tree != null){
if (tree.left != null){
printInTree(tree.left);
}
System.out.println(tree.date);
if (tree.right != null){
printInTree(tree.right);
}
}
}
/**
* 前序遍历
* @param tree
*/
public void printPreTree(BinarySearchTree tree){
System.out.println(tree.date);
if (tree != null){
if (tree.left != null){
printPreTree(tree.left);
}
if (tree.right != null){
printPreTree(tree.right);
}
}
}
/**
* 后序遍历
* @param tree
*/
public void printPosTree(BinarySearchTree tree){
if (tree != null){
if (tree.left != null){
printPosTree(tree.left);
}
if (tree.right != null){
printPosTree(tree.right);
}
System.out.println(tree.date);
}
}
/**
* 判断一个数是否在树中
* @param root
* @param date
*/
public void find(BinarySearchTree root,int date){
if (root != null){
if (root.date == date){
System.out.println("找到了");
}else if (root.date < date){
find(root.right,date);
}else {
find(root.left,date);
}
}else{
System.out.println("没有找到");
}
}
public static void main(String[] args) {
BinarySearchTree root = new BinarySearchTree(7);
int date[] = {6,5,9,10,8};
for(int i=0;i<date.length;i++){
root.insert(root,date[i]);
}
root.find(root,10);
}
}
二叉搜索树的局限性
上面说到树的深度就是查找的时间复杂度,那么二叉搜索树的结构基本决定了二叉树查找时间复杂度,那么就有可能出现极端的情况导致查询时间变长。
如下两张图所示,表示的都是同一组数据,但是却是两种不同的树结构,第一个树的时间复杂度为6,第二个数是3。
基本可以认定第一张图的树已经退化成了链表,所以查询很慢。
为了解决这种问题,因此就有了AVL树和红黑树的出现。
AVL树
平衡二叉树,它的左右子树高度之差不超过1
这样确实可以避免一条直线型的结构,但还不是我们最理想的,因为插入删除开销太大。
红黑树
红黑树的特征如下
- 每个结点不是红色就是黑色
- 不可能有连在一起的红色结点
- 根结点都是黑色
- 每个红色结点的两个子结点都是黑色
- 任一结点到其子树中每个叶子节点的路径都有相同数量的黑色结点
那么问题来了,如何在删除和插入数据的时候保证以上性质呢,红黑树的策略就是改变颜色和旋转,改变颜色很好理解,那么旋转是什么呢?
旋转分为左旋和右旋
- 左旋
如下图所示左图为旋转前,右图是旋转后,是以5为根结点从左向右旋转,就是逆时针旋转
- 右旋
如下图所示左图为旋转前,右图是旋转后,是以5为根结点从右向左旋转,就是顺时针旋转
旋转和颜色变换规则
1.变颜色的情况:当前结点的父亲是红色,且它的祖父结点的另一个子结点(叔叔结点)也是红色。
(1)把父节点设为黑色
(2)把叔叔也设为黑色
(3)把祖父也就是父亲的父亲设为红色
(4)把指针定义到祖父结点设为当前要操作的
2.左旋:当前父结点是红色,叔叔是黑色的时候,且当前的结点是右子树,以父结点作为左旋。不需要改变颜色。
3.右旋:当前父结点是红色,叔叔是黑色的时候,且当前的结点是左子树。
(1)把父结点变为黑色
(2)把祖父结点变为红色 (爷爷)
(3)以祖父结点旋转(爷爷)
插入数据示例
假设有如下的红黑树,符合红黑树的特征
现在插入数据6,颜色假设为红色,这样就不符合红黑树的特征,所以就要对其进行变换
变换过程如下:
1.因为父结点7和叔叔结点10都是红色,所以首先颜色变换,将父结点7和叔叔结点10变为黑色,祖父结点8变为红色,当前结点变为祖父结点8,依然不符合红黑树特征,继续变换
2.因为父结点5为红色,叔叔结点20为黑色,且当前结点为右子树,那么对父结点5进行左旋操作,并且当前结点设置为父结点5,依然不符合红黑,继续变换
3.因为父结点8为红色,叔叔结点20为黑色,当前结点为左子树,那么先将父结点8变为黑色,祖父结点15变为红色,那么再对祖父结点15进行右旋操作,同样当前结点变为祖父结点15,至此现在的红黑树已经符合特征,变换完成
可以看出变换完的红黑树结构依然稳定,所以红黑树就解决了插入和删除的问题
红黑树的应用
- JDK HashMap
- JDK TreeMap
- JDK TreeSet
- Windows文件搜索
Btree
M阶的Btree的几个重要特性:
- 结点最多含有m颗子树(指针),m-1个关键字(存的数据)( m>=2)
- 除根结点和叶子结点外,其它每个结点至少有ceil(m / 2)个子节点,ceil为上取整
- 若根节点不是叶子节点,则至少有两颗子树
例子:
创建一个5阶的Btree。插入的数据有:
3 14 7 1 8 5 11 17 13 6 23 12 20 26 4 16 18 24 25 19
根据Btree特性,5阶则一个磁盘空间最多有5个指针(存的查找路径 ),4个关键字(mysql存的数据)。那么具体的插入如下所示:
插入3
插入14
插入7
插入1
插入8,此时发现空间不够,这时会出现一个分裂,移动中间元素到根节点即得到如下图:
插入5
插入11,17
插入13:需要注意,此时Btree又会进行一次分裂,这分裂需要注意下13会移到根节点
后面的插入就不一一举例了,自己验证下。最后得到的这棵树如下所示:
以上操作为Btree的详细构建过程,在构建的时候主要需要注意的就是分裂,分裂后一定要判定是否符合Btree的特性,是否是一颗有序的数,左边一定小于右边,叶子节点是否已经大于阶数,叶子节点的最小数目为ceil(m(这里为5)/2)-1=2等。有时候一个数字插入的时候会进行多次分裂操作,需要特别注意。
B+Tree和BTree类似,只是B+Tree非叶子节点不会存储数据,所有的数据都存储在叶子节点,其目的主要增加了系统的稳定性。
Mysql索引采用的就是B+Tree实现的。
下面是Btree的实现代码
class BTreeNode {
int T; //表示每个节点最少的值个数 m/2
int count; //节点值个数
int key[]; //关键字个数 数据
BTreeNode childs[]; //指针个数
Boolean isleaf; //叶子节点
public BTreeNode(int T) {
this.T = T;
key = new int[2 * T - 1];
childs = new BTreeNode[2 * T];
isleaf = true;
count = 0;
}
}
public class MyBTree {
int M;
BTreeNode root;
public MyBTree(int order) {
this.M = order;
root = new BTreeNode(order);
}
public BTreeNode search(BTreeNode root, int key) {
int i = 0;
while (i < root.count && key > root.key[i]) {
i++;
}
if (i <= root.count && key == root.key[i]) {
return root;
}
if (root.isleaf) { //搜到根节点了直接返回null
return null;
} else { //继续搜下一层
return search(root.childs[i], key);
}
}
public void insert(MyBTree tree, int key) {
BTreeNode r = tree.root;
if (r.count == 2 * M - 1) { //判断根节点是否已经填满
BTreeNode newNode = new BTreeNode(M);
tree.root = newNode;
newNode.childs[0] = r; //根节点分裂成一个新的,将第一个子节点指向原来的root节点,并将左边赋给第一个子节点
newNode.isleaf = false;
newNode.count = 0;
split(newNode, 0, r); //进行分裂操作
fillNodeIn(newNode, key); //分裂完成后插入数据
} else{
fillNodeIn(r, key);
}
}
//核心部分就是这个分裂,最容易出问题,我在这里也卡了很久,spiltNode 为需要分裂的点 newNode分裂后的结果
public void split(BTreeNode newNode, int i, BTreeNode spiltNode) {
BTreeNode tempNode = new BTreeNode(M); //分裂节点的临时存储数据节点,存储分裂节点右边数据和右子树的数据
tempNode.isleaf = spiltNode.isleaf;
tempNode.count = M - 1;
for (int j = 0; j < M - 1; j++) {
tempNode.key[j] = spiltNode.key[j + M]; //把分裂的节点右边数据先赋值到临时节点
}
if (!spiltNode.isleaf) {
for (int k = 0; k < M; k++) { //如果分裂节点不是叶子节点则要把分裂节点的右子树全部赋值给临时节点
tempNode.childs[k] = spiltNode.childs[k + M];
}
}
spiltNode.count = M - 1;
for (int j = newNode.count; j > i; j--) { //源节点数据进行平移
newNode.childs[j + 1] = newNode.childs[j];
}
newNode.childs[i + 1] = tempNode; //将临时存储了数据的节点插入源节点
for (int j = newNode.count; j >= i && j < newNode.key.length - 1; j--) {
newNode.key[j + 1] = newNode.key[j];
}
newNode.key[i] = spiltNode.key[M - 1]; //将分裂出来的那个key插入到源节点去
spiltNode.key[M - 1] = 0; //分裂节点分出去的key重置
for (int j = 0; j < M -1 ; j++) { //分裂节点右边key重置
spiltNode.key[j + M] = 0;
}
for (int j = 0; j < M ; j++) { //分裂节点右子树重置
spiltNode.childs[j + M] = null;
}
newNode.count++; //新节点数据大小+1
}
public void fillNodeIn(BTreeNode node, int key) { //将数据插入到节点
int i = node.count;
if (node.isleaf) {
while (i >= 1 && key < node.key[i - 1]) {
node.key[i] = node.key[i - 1];
i--;
}
node.key[i] = key;
node.count++;
} else {
int j = 0;
while (j < node.count && key > node.key[j]) {
j++;
}
if (node.childs[j].count == M * 2 - 1) {
split(node, j, node.childs[j]);
if (key > node.key[j]) {
j++;
}
}
fillNodeIn(node.childs[j], key);
}
}
public void print(BTreeNode node) {
for (int i = 0; i < node.count; i++) {
System.out.print(node.key[i] + " ");
}
if (!node.isleaf) {
for (int j = 0; j <= node.count; j++) {
if (node.childs[j] != null) {
System.out.println();
print(node.childs[j]);
}
}
}
}
public void searchKey(MyBTree T, int x) {
BTreeNode temp = new BTreeNode(M);
temp = search(T.root, x);
if (temp == null) {
System.out.println("not exist in this tree");
}
else {
print(temp);
}
}
public static void main(String[] args) {
int t = 3;
MyBTree tree = new MyBTree(t);
int arr[ ] = { 3,14,7,1,8,5,11,17,13,6,23,12,20,26,4,16,18,24,25,19,9,10,15,27,28,2} ;
for (int i = 0; i < arr.length; i++) {
tree.insert(tree, arr[i]);
System.out.println(arr[i] + "插入后的Btree");
tree.print(tree.root);
System.out.println();
System.out.println("----------");
}
tree.searchKey(tree, 2);
}
}