一、递归算法
1.方法定义中调用方法本身的现象
2.递归注意实现
(1)要有出口,否则就是死递归
(2)次数不能太多,否则就内存溢出
(3)构造方法不能递归使用
具体代码如下:
package cn.tedu;
public class DiGuiDemo {
public static void main(String[] args) {
//计算1~5的和,使用递归完成
int n = 5 ;
//调用求和的方法
int sum = getSum(n);
//输出结果
System.out.println(sum);
}
/*
通过递归算法实现
*/
private static int getSum(int n) {
/*
n为1时,方法返回1,
相当于时方法的出口,n总有是1的情况
*/
if (n==1){
return 1;
}
/*
n不为1时,方法返回n+(n+1)的累和
递归调用getSum方法
*/
return n+getSum(n-1);
}
}
package cn.tedu;
public class DiGuiDemo2 {
public static void main(String[] args) {
int n = 5;
//调用函数5的阶乘
int result= jc2(n);
System.out.println(result);
}
private static int jc2(int n) {
//设置的出口
if (n<=1){
return 1;
}
return n*jc2(n-1);//涉及阶乘公式 n!=(n-1)!*n
}
}
二、二分法
1.二分法算法是建立在排序的基础之上的,即没有排序的数据是无法查找的;
2.二分法查找的效率高于“一个挨着一个”的这种查找方式;
3.二分法查找原理?举例:
int[] arr = {0,5,9,10,20,60,70,80,90,100};
目标:找出100的下标;
数组元素为:0(下标为0)5 9 10 20 60 70 80 90 100(下标为9)
3.1、找出中间元素:(0+9)/2 -->(中间元素)
3.2、用找到的中间元素跟目标元素比较:20<100,此时说所要查找的元素在中间元素的右边,则开始下标就变成了:4+1
3.3、继续循环,找出中间值:(5+9)/2-->(中间元素)
3.4、用找到的中间元素跟目标元素比较:80 < 100, 此时说所要查找的元素在中间元素的右边,则开始下标就变成了:7 + 1;
3.5、继续循环,找出中间值:(8+9) / 2 --> 8(中间元素);
3.6、用找到的中间元素跟目标元素比较:90 < 100, 此时说所要查找的元素在中间元素的右边,则开始下标就变成了:8 + 1;
3.7、继续循环,找出中间值:(9+9) / 2 --> 9(中间元素);
3.8、用找到的中间元素跟目标元素比较:100 = 100, 此时说明目标元素已被找到;
4、具体代码实现:
package cn.tedu;
import java.util.Arrays;
public class EFF {
public static void main(String[] args) {
int[] arr = {0,5,9,10,20,60,70,80,90,100};
//找出这个数组中9所在的下标
//调用方法
//Arrays.binarySearch(arr,9);//掉用了SUN公司的数组工具类
int index = binarySearch(arr,9);
System.out.println(index == -1?"该元素不存在":"该元素的下标为"+index);
}
private static int binarySearch(int[] arr, int i) {
//开始下标
int begin = 0;
//结束下标
int end = arr.length-1;
//开始元素的下标只要在结束元素下表的左边,就可以继续循环
while (begin<=end){
//中间元素下标
int mid = (begin+end)/2;
if (arr[mid] == i) {
return mid;
}else if(arr[mid]<i){
//目标在中间元素的右边
//开始元素下标元素发生改变
begin = mid + i;//一直增
}else {
//arr[mid]>i;
//目标在中间元素的左边
end = mid - i;//一直减
}
}
return -1 ;
}
}
package cn.tedu;
public class EFF2 {
/*
有序的二维数组二分法
*/
public static void main(String[] args) {
int[][] arr = {{1,2,3},
{4,5,6},
{7,8,9}};
int[] res = search(arr,1);
String str = java.util.Arrays.toString(res);
System.out.println(str);
}
static int[] search(int[][] arr, int a){
int minIndex_x = 0;
int minIndex_y = 0;
int maxIndex_x = arr.length-1;
int maxIndex_y = arr[0].length-1;
int halfIndex_x = minIndex_x + (maxIndex_x - minIndex_x)/2;
int halfIndex_y = minIndex_y + (maxIndex_y - minIndex_y)/2;
int x = arr.length-1;
int y = arr[0].length-1;
while (minIndex_y <= maxIndex_y && minIndex_x <= maxIndex_x){
if (a==arr[halfIndex_x][halfIndex_y]){
int[] res = {halfIndex_x,halfIndex_y};
return res;
}else if (a>arr[halfIndex_x][halfIndex_y]){
// 搜索值比中间值大
if (halfIndex_y==y & halfIndex_x != x){
minIndex_x = halfIndex_x + 1;
minIndex_y = 0;
}else{
minIndex_y = halfIndex_y + 1;
}
}else{// 搜索值比中间值小
if (halfIndex_y==0 & halfIndex_x != 0){
maxIndex_x = halfIndex_x - 1;
maxIndex_y = y;
}else{
maxIndex_y = halfIndex_y - 1;
}
}
halfIndex_x = minIndex_x + (maxIndex_x - minIndex_x)/2;
halfIndex_y = minIndex_y + (maxIndex_y - minIndex_y)/2;
}
int[] res_null = {-1,-1};
return res_null;
}
}
总结:在需要查找元素时,如果是一对数字,首选方法为二分法,因为二分法的效率高,而且也比较便捷,用起来更方便,但是,最重要的一点还是"要对目标数组进行排序",有关于数组的排序,可翻我的上一篇博客。在Java中,sun公司也帮我们写好了二分法的代码,我们可以通过"Arrays.binarySearch(目标数组,目标元素);",我们可以通过"Arrays.sort(目标数组);"进行对数组的排序,然后在通过"Arrays.binarySearch(目标数组,目标元素);"进行查找,又方便效率也高。
三、二叉树
树(tree)是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点通过连接它们的边组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的树。
二叉树是一种特殊的树,每个节点最多只能有两个子节点。
二叉搜索树要求:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树
二叉搜索树作为一种数据结构,那么它是如何工作的呢?它查找一个节点,插入一个新节点,以及删除一个节点,遍历树等工作效率如何,下面我们来一一介绍
二叉树节点类(节点一般存储是对象,这里为了方便演示用的是int)
/**
* 二叉树节点
*/
public class Node {
public int data;
public Node leftNode;
public Node rightNode;
public Node(int key) {
data = key;
}
}
准备实现的几个方法
/**
* 二叉树
*
* @author hh
*/
public abstract class AbstractTree {
public int count = 0;
/**
* 查询
*
* @return
*/
public abstract Node find(int o);
/**
* 插入
*
* @param o
*/
public abstract boolean insert(int o);
/**
* 删除
*
* @param o
*/
public abstract boolean delete(int o);
/**
* 节点个数
*
* @return
*/
public int count() {
return this.count;
}
}
查找节点
查找节点从根节点开始遍历查找
1 、查找值比当前节点值大,则搜索右子树;
2、 查找值等于当前节点值,停止搜索(终止条件);
3、查找值小于当前节点值,则搜索左子树;
public Node find(int key) {
Node current = root;
while (current != null) {
if (current.data > key) {
//当前值比查找值大 则继续遍历左子树
current = current.leftNode;
} else if (current.data < key) {
//当前值小于查找值 则继续遍历右子树
current = current.rightNode;
} else {
//查找到 返回
return current;
}
}
//查不到返回null
return null;
}
插入节点
要插入节点,必须先找到插入的位置。与查找操作相似,由于二叉搜索树的特殊性,待插入的节点也需要从根节点开始进行比较,小于根节点则与根节点左子树比较,反之则与右子树比较,直到左子树为空或右子树为空,则插入到相应为空的位置,在比较的过程中要注意保存父节点的信息 及 待插入的位置是父节点的左子树还是右子树,才能插入到正确的位置
public boolean insert(int data) {
count++;
//如果第一个节点为空 设置第一个节点
Node newNode = new Node(data);
if (root == null) {
root = newNode;
return true;
}
Node current = root;
Node parentNode = null;
while (current != null) {
parentNode = current;
//当前值比新插入值大
if (current.data > data) {
current = current.leftNode;
//若左节点为空 则直接插入即可
if (current == null) {
parentNode.leftNode = newNode;
return true;
}
} else {
//当前值小于新插入值
current = current.rightNode;
if (current == null) {
parentNode.rightNode = newNode;
return true;
}
}
}
return false;
}
删除节点
删除节点相对于查询和删除稍微复杂点,主要是删除时考虑的情况多一点点。
1、删除节点没有子节点
2、删除节点有一个子节点
3、删除节点有两个子节点
(1)删除节点没有子节点
这种的话就直接删除当前节点就好了,只需讲当前节点的父节点指向的右节点置为null即可
(2)删除节点有一个子节点
删除有一个子节点的节点,我们只需要将其父节点原本指向该节点的引用,改为指向该节点的子节点即可。
(3)删除节点有两个子节点
当删除的节点存在两个子节点,那么删除之后,两个子节点的位置我们就没办法处理了。既然处理不了,我们就想到一种办法,用另一个节点来代替被删除的节点,那么用哪一个节点来代替呢?
这实际上就是要找比删除节点关键值大的节点集合中最小的一个节点,只有这样代替删除节点后才能满足二叉搜索树的特性。
删除代码如下
/**
* 删除共三种情况
* 1 该节点是叶子节点
* 2 该节点有一个叶子节点
* 3 该节点有两个叶子节点
*
* @param data
*/
@Override
public boolean delete(int data) {
Node current = root;
Node parentNode = root;
//当前节点是否为左节点
boolean isLeftNode = false;
//定位data的位置
while (current.data != data) {
parentNode = current;
if (current.data > data) {
isLeftNode = true;
current = current.leftNode;
} else {
isLeftNode = false;
current = current.rightNode;
}
if (current == null) {
return false;
}
}
// 1 第一种情况 此节点为叶子节点
if (current.leftNode == null && current.rightNode == null) {
if (current == root) {
root = null;
} else if (isLeftNode) {
//如果要删除的节点为父节点的左节点 把父节点的左节点置为空
parentNode.leftNode = null;
} else {
parentNode.rightNode = null;
}
return true;
}
//2 当前节点有一个节点
if (current.leftNode == null && current.rightNode != null) {
if (root == current) {
root = current.rightNode;
} else if (isLeftNode) {
parentNode.leftNode = current.rightNode;
} else {
parentNode.rightNode = current.rightNode;
}
} else if (current.leftNode != null && current.rightNode == null) {
if (root == current) {
root = current.leftNode;
} else if (isLeftNode) {
parentNode.leftNode = current.leftNode;
} else {
parentNode.rightNode = current.leftNode;
}
}
//3 当前节点有两个节点
if(current.leftNode != null && current.rightNode != null){
//获取删除节点的后继结点
Node successor = getSuccessor(current);
if (root == current) {
root = successor;
} else if (isLeftNode) {
parentNode.leftNode = successor;
} else {
parentNode.rightNode = successor;
}
}
return false;
}
/**
* 获取要删除节点的后继节点
*
* @param delNode
* @return
*/
public Node getSuccessor(Node delNode) {
Node successorParent = delNode;
Node successor = delNode;
Node current = delNode.rightNode;
while (current != null) {
successorParent = successor;
successor = current;
current = current.leftNode;
}
if (successor != delNode.rightNode) {
successorParent.leftNode = successor.rightNode;
successor.rightNode = delNode.rightNode;
}
return successor;
}
完整二叉树实现代码如下
Node类
package com.example.demo.BinaryTree;
/**
* 二叉树节点
*/
public class Node {
public int data;
public Node leftNode;
public Node rightNode;
public Node(int key) {
data = key;
}
}
抽象类AbstractTree
package com.example.demo.BinaryTree;
/**
* 二叉树
*
* @author hh
*/
public abstract class AbstractTree {
public int count = 0;
/**
* 查询
*
* @return
*/
public abstract Node find(int o);
/**
* 插入
*
* @param o
*/
public abstract boolean insert(int o);
/**
* 删除
*
* @param o
*/
public abstract boolean delete(int o);
/**
* 节点个数
*
* @return
*/
public int count() {
return this.count;
}
}
二叉树实现类Tree
package com.example.demo.BinaryTree;
/**
*
*/
public class Tree extends AbstractTree {
private Node root;
@Override
public Node find(int key) {
Node current = root;
while (current != null) {
if (current.data > key) {
current = current.leftNode;
} else if (current.data < key) {
current = current.rightNode;
} else {
return current;
}
}
return null;
}
@Override
public boolean insert(int data) {
count++;
//如果第一个节点为空 设置第一个节点
Node newNode = new Node(data);
if (root == null) {
root = newNode;
return true;
}
Node current = root;
Node parentNode = null;
while (current != null) {
parentNode = current;
//当前值比新插入值大
if (current.data > data) {
current = current.leftNode;
//若左节点为空 则直接插入即可
if (current == null) {
parentNode.leftNode = newNode;
return true;
}
} else {
//当前值小于新插入值
current = current.rightNode;
if (current == null) {
parentNode.rightNode = newNode;
return true;
}
}
}
return false;
}
/**
* 删除共三种情况
* 1 该节点是叶子节点
* 2 该节点有一个叶子节点
* 3 该节点有两个叶子节点
*
* @param data
*/
@Override
public boolean delete(int data) {
Node current = root;
Node parentNode = root;
//当前节点是否为左节点
boolean isLeftNode = false;
//定位data的位置
while (current.data != data) {
parentNode = current;
if (current.data > data) {
isLeftNode = true;
current = current.leftNode;
} else {
isLeftNode = false;
current = current.rightNode;
}
if (current == null) {
return false;
}
}
// 1 第一种情况 此节点为叶子节点
if (current.leftNode == null && current.rightNode == null) {
if (current == root) {
root = null;
} else if (isLeftNode) {
//如果要删除的节点为父节点的左节点 把父节点的左节点置为空
parentNode.leftNode = null;
} else {
parentNode.rightNode = null;
}
return true;
}
//2 当前节点有一个节点
if (current.leftNode == null && current.rightNode != null) {
if (root == current) {
root = current.rightNode;
} else if (isLeftNode) {
parentNode.leftNode = current.rightNode;
} else {
parentNode.rightNode = current.rightNode;
}
} else if (current.leftNode != null && current.rightNode == null) {
if (root == current) {
root = current.leftNode;
} else if (isLeftNode) {
parentNode.leftNode = current.leftNode;
} else {
parentNode.rightNode = current.leftNode;
}
}
//3 当前节点有两个节点
if(current.leftNode != null && current.rightNode != null){
//获取删除节点的后继结点
Node successor = getSuccessor(current);
if (root == current) {
root = successor;
} else if (isLeftNode) {
parentNode.leftNode = successor;
} else {
parentNode.rightNode = successor;
}
}
return false;
}
/**
* 获取要删除节点的后继节点
*
* @param delNode
* @return
*/
public Node getSuccessor(Node delNode) {
Node successorParent = delNode;
Node successor = delNode;
Node current = delNode.rightNode;
while (current != null) {
successorParent = successor;
successor = current;
current = current.leftNode;
}
if (successor != delNode.rightNode) {
successorParent.leftNode = successor.rightNode;
successor.rightNode = delNode.rightNode;
}
return successor;
}
}
测试类Test
package com.example.demo.BinaryTree;
/**
* Created by HH on 2019/7/2.
*/
public class Test {
public static void main(String[] args) {
Tree t=new Tree();
t.insert(80);
t.insert(70);
t.insert(100);
t.insert(90);
Node node = t.find(100);
System.out.println(node.leftNode.data);
System.out.println(t.count());
}
}
总结:
树是由边和节点构成,根节点是树最顶端的节点,它没有父节点;二叉树中,最多有两个子节点;某个节点的左子树每个节点都比该节点的关键字值小,右子树的每个节点都比该节点的关键字值大,那么这种树称为二叉搜索树,其查找、插入、删除的时间复杂度都为logN;删除一个节点只需要断开指向它的引用即可;
四、红黑树
红黑树是带有着色性质的二叉查找树。
性质如下:
① 每一个节点或者着成红色或者着成黑色。
② 根节点为黑色。
③ 每个叶子节点为黑色。(指的是指针指向为NULL的叶子节点)
④ 如果一个节点是红色的,那么它的子节点必须是黑色的。
⑤ 从一个节点到一个NULL指针的每一条路径必须包含相同数目的黑色节点。
推论: 有n个节点的红黑树的高度最多是2log(N+1) 。
1.红黑树的基本概念与数据结构表示
首先红黑树来个定义:
红黑树定义:红黑树又称红-黑二叉树,它首先是一颗二叉树,它具体二叉树所有的特性。同时红黑树更是一颗自平衡的排序二叉树(平衡二叉树的一种实现方式)。
我们知道一颗基本的二叉排序树他们都需要满足一个基本性质:即树中的任何节点的值大于它的左子节点,且小于它的右子节点。
按照这个基本性质使得树的检索效率大大提高。我们知道在生成二叉排序树的过程是非常容易失衡的,最坏的情况就是一边倒(只有右/左子树),这样势必会导致二叉树的检索效率大大降低(O(n)),所以为了维持二叉排序树的平衡,大牛们提出了各种平衡二叉树的实现算法,如:AVL,SBT,伸展树,TREAP ,红黑树等等。
平衡二叉树必须具备如下特性:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一个子节点,其左右子树的高度都相近。下面给出平衡二叉树的几个示意图:
红黑树顾名思义就是结点是红色或者是黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。对于一棵有效的红黑树而言我们必须增加如下规则,这也是红黑树最重要的5点规则:
1、每个结点都只能是红色或者黑色中的一种。
2、根结点是黑色的。
3、每个叶结点(NIL节点,空节点)是黑色的。
4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5、从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。
这些约束强制了红黑树的关键性质: 从根到叶子最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。所以红黑树它是复杂而高效的,其检索效率O(lg n)。下图为一颗典型的红黑二叉树:
上面关于红黑树的概念基本已经说得很清楚了,下面给出红黑树的结点用Java表示数据结构:
private static final boolean RED = true;
private static final boolean BLACK = false;
private Node root;//二叉查找树的根节点
//结点数据结构
private class Node{
private Key key;//键
private Value value;//值
private Node left, right;//指向子树的链接:左子树和右子树.
private int N;//以该节点为根的子树中的结点总数
boolean color;//由其父结点指向它的链接的颜色也就是结点颜色.
public Node(Key key, Value value, int N, boolean color) {
this.key = key;
this.value = value;
this.N = N;
this.color = color;
}
}
/**
* 获取整个二叉查找树的大小
* @return
*/
public int size(){
return size(root);
}
/**
* 获取某一个结点为根结点的二叉查找树的大小
* @param x
* @return
*/
private int size(Node x){
if(x == null){
return 0;
} else {
return x.N;
}
}
private boolean isRed(Node x){
if(x == null){
return false;
}
return x.color == RED;
}
2.红黑树的三个基本操作
红黑树在插入,删除过程中可能会破坏原本的平衡条件导致不满足红黑树的性质,这时候一般情况下要通过左旋、右旋和重新着色这个三个操作来使红黑树重新满足平衡化条件。
旋转
旋转分为左旋和右旋。在我们实现某些操作中可能会出现红色右链接或则两个连续的红链接,这时候就要通过旋转修复。
通常左旋操作用于将一个向右倾斜的红色链接(这个红色链接链连接的两个结点均是红色结点)旋转为向左链接。对比操作前后,可以看出,该操作实际上是将红线链接的两个结点中的一个较大的结点移动到根结点上。
左旋的示意图如下:
左旋的Java实现如下:
/**
* 左旋转
* @param h
* @return
*/
private Node rotateLeft(Node h){
Node x = h.right;
//把x的左结点赋值给h的右结点
h.right = x.left;
//把h赋值给x的左结点
x.left = h;
//
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = 1+ size(h.left) + size(h.right);
return x;
}
左旋的动画效果如下:
右旋其实就是左旋的逆操作:
右旋的代码如下:
/**
* 右旋转
* @param h
* @return
*/
private Node rotateRight(Node h){
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = 1+ size(h.left) + size(h.right);
return x;
}
右旋的动态示意图:
颜色反转
当出现一个临时的4-node的时候,即一个节点的两个子节点均为红色,如下图:
我们需要将E提升至父节点,操作方法很简单,就是把E对子节点的连线设置为黑色,自己的颜色设置为红色。颜色反转之后颜色如下:
/**
* 颜色转换
* @param h
*/
private void flipColors(Node h){
h.color = RED;//父结点颜色变红
h.left.color = BLACK;//子结点颜色变黑
h.right.color = BLACK;//子结点颜色变黑
}
注意:以上的旋转和颜色反转操作都是针对单一结点的,反转或则颜色反转操作之后可能引起其父结点又不满足平衡性质。
红黑树插入节点
关于红黑树插入节点后为了保持红黑树的特性,主要进行的操作就是换颜色和旋转操作。
情况1:父节点是黑色,插入新节点为红色。
解决办法:
由于新节点父节点为黑色,直接插入即可。
情况2:父节点为红色,叔叔节点为红色,插入新节点为红色,插入新节点为父节点的左孩子节点。(红叔左父左插入情况)
解决办法:
为了保持新节点为红色,如果此时父节点为红色就不满足性质4.所以父节点需要调整为黑色,但是这样祖节点到NL叶子节点,就会出现数目不同的黑色节点了。不满足性质5了。所以祖节点需要调整为红色节点满足性质。这样叔叔也就得调整为黑色了,要不就不满足性质4和性质5.
总结:红父->黑父 红叔->黑叔 黑祖->红祖
情况3:父节点为红色,叔叔节点为红色,插入新节点为红色,插入新节点为父节点的右孩子节点。(红叔左父右插入情况)
解决办法:
这种情况同情况2一样,无须旋转,只是颜色不满足性质,只需调整颜色即可。
总结: 红父->黑父 红叔->黑叔 黑祖->红祖
情况4:父节点为红色,叔叔节点为黑色,插入新节点为红色,插入新节点为父节点的左孩子节点。(黑叔左父左插入情况)
解决办法:
这种情况单纯通过换色是无法控制红黑树性质的满足。通过右旋来满足平衡条件。
总结:右旋 红父->黑父 黑祖->红祖
情况5:父节点为红色,叔叔节点为黑色,插入新节点为红色,插入新节点为父节点的右孩子节点。(黑叔左父右插入情况)
解决办法:
这种使用上面的右旋会发现,就会出现两个值的节点和三个子节点的情况。我们先局部左旋父节点。然后将祖节点和叔节点右旋转。
总结:左旋 右旋 黑祖->红祖 红新->黑新
情况6:父节点为红色,叔叔节点为红色,插入新节点为红色,插入新节点为父节点的左孩子节点。(红叔右父左插入情况)
解决办法:
这种情况和情况2差不多,就是调整颜色就可以了,主要原因就可以发现父节点和叔叔节点都是红色,这样直接调整颜色,就会满足红黑树性质。
总结:红父->黑父 红叔->黑叔 黑祖->红祖
情况7:父节点为红色,叔叔节点为红色,插入新节点为红色,插入新节点为父节点的右孩子节点。(红叔右父右插入情况)
解决办法:
这种情况和情况3一样,都是只是调整下颜色就好。
总结:红父->黑父 红叔->黑叔 黑祖->红祖
情况8:父节点为红色,叔叔节点为黑色,插入新节点为红色,插入新节点为父节点的右孩子节点。(黑叔右父右插入情况)
解决办法:
这种情况会出现旋转了,因为叔节点和父节点颜色不同,所以单纯调整颜色不能满足性质了。就采用将祖节点和叔节点左旋,作为父节点的左孩子树。
总结:左旋 红父->黑父 黑祖->红祖
情况9:父节点为红色,叔叔节点为黑色,插入新节点为红色,插入新节点为父节点的左孩子节点。(黑叔右父左插入情况)
解决办法:
这种情况类似于情况5,如果我们直接左旋祖节点和叔节点,那么就会出现2节点,3孩子情况。所以为了避免这种冲突。就先进行局部旋转,先将父节点右旋,然后将祖节点和叔节点左旋。
总结:右旋 左旋 黑祖->红祖 红新->黑新
全面总结
黑父,直接插入新节点即可。
红叔,就调整颜色即可。
黑叔,需要进行旋转操作。
红黑树插入元素完整代码实现
颜色枚举:
public enum Color {
RED("0","红色"),
BLACK("1","黑色");
private String name = ;
private String value =;
private Color(String name, String value) {
this.name=name;
this.value=value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public static Color getEnumByName(String name){
if ( == name) {
return null;
}
for (Color type : values()) {
if (type.getName().equals(name.trim()))
return type;
}
return null;
}
public static Map<String, String> toMap() {
Map<String, String> enumDataMap = new LinkedHashMap<String, String>();
for (Color type : values()) {
enumDataMap.put(type.getName(), type.getValue());
}
return enumDataMap;
}
}
红黑树节点数据的结构定义:
public class RBNode {
private Integer data;
//红黑树中节点对应的颜色
private Color color;
//红黑树中当前节点的左孩子节点
private RBNode lchild;
//红黑树中当前节点的右孩子节点
private RBNode rchild;
//红黑树中当前节点的父节点
private RBNode parent;
public RBNode(){}
public RBNode(Integer data){
this.data =data;
}
public RBNode(Integer data,Color color,RBNode parent,RBNode lchild,RBNode rchild){
this.data =data;
this.color =color;
this.parent =parent;
this.lchild =lchild;
this.rchild =rchild;
}
public RBNode getParent() {
return parent;
}
public void setParent(RBNode parent) {
this.parent = parent;
}
public Integer getData() {
return data;
}
public void setData(Integer data) {
this.data = data;
}
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
public RBNode getLchild() {
return lchild;
}
public void setLchild(RBNode lchild) {
this.lchild = lchild;
}
public RBNode getRchild() {
return rchild;
}
public void setRchild(RBNode rchild) {
this.rchild = rchild;
}
}
主程序代码:
public class RBTree {
private static RBNode root =;
//顺时针右旋
public static void rotateByRight(RBNode rbn){
RBNode b =rbn.getLchild();
rbn.setLchild(b.getRchild());
if(b.getRchild() !=null){
b.getRchild().setParent(rbn);
}
b.setParent(rbn.getParent());
if(rbn.getParent() ==null){
root =;
}else{
if(rbn ==rbn.getParent().getRchild()){
rbn.getParent().setRchild(b);
}else{
rbn.getParent().setLchild(b);
}
}
b.setRchild(rbn);
rbn.setParent(b);
}
//逆时针左旋
public static void rotateByLeft(RBNode rbn){
RBNode d =rbn.getRchild();
rbn.setRchild(d.getLchild());
if(d.getLchild() !=null){
d.getLchild().setParent(rbn);
}
d.setParent(rbn.getParent());
if(rbn.getParent() ==null){
root =;
}else{
if(rbn ==rbn.getParent().getLchild()){
rbn.getParent().setLchild(d);
}else{
rbn.getParent().setRchild(d);
}
}
d.setLchild(rbn);
rbn.setParent(d);
}
//红黑树插入节点
private static void insertRBNode(RBNode insertNode){
RBNode tempRoot =root;
//给红黑树插入节点,先不考虑局部平衡问题
while(tempRoot !=null){
if(insertNode.getData()<tempRoot.getData()){
if(tempRoot.getLchild() !=null){
tempRoot =tempRoot.getLchild();
}else{
tempRoot.setLchild(insertNode);
insertNode.setParent(tempRoot);
break;
}
}else if(insertNode.getData()>tempRoot.getData()){
if(tempRoot.getRchild() !=null){
tempRoot =tempRoot.getRchild();
}else{
tempRoot.setRchild(insertNode);
insertNode.setParent(tempRoot);
break;
}
}
}
//插入节点设置为红色
insertNode.setColor(Color.RED);
insertNode.setLchild(null);
insertNode.setRchild(null);
adjustRBTree(insertNode);
}
//调整红黑树
public static void adjustRBTree(RBNode rbNode){
//定义父节点
RBNode parent =rbNode.getParent();
while(parent !=null && parent.getColor().equals(Color.RED)){
//定义祖父节点
RBNode grandParent =parent.getParent();
//如果父节点是祖父节点的左孩子
if(parent.equals(grandParent.getLchild())){
RBNode uncleNode =grandParent.getRchild();
//情况2、情况3:红叔
if(uncleNode !=null && uncleNode.getColor().equals(Color.RED)){
uncleNode.setColor(Color.BLACK);
parent.setColor(Color.BLACK);
grandParent.setColor(Color.RED);
}else if(uncleNode !=null && uncleNode.getColor().equals(Color.BLACK) && parent.getLchild().equals(rbNode)){
//情况4:黑叔,当前节点是左孩子
rotateByRight(grandParent);
parent.setColor(Color.BLACK);
grandParent.setColor(Color.RED);
}else if(uncleNode !=null && uncleNode.getColor().equals(Color.BLACK) && parent.getRchild().equals(rbNode)){
//情况5:黑叔,当前节点是右孩子
rotateByLeft(parent);
rotateByRight(grandParent);
rbNode.setColor(Color.BLACK);
grandParent.setColor(Color.RED);
}else{
break;
}
}else{//如果父节点是祖父节点的右孩子
RBNode uncleNode =grandParent.getLchild();
//情况6、情况7:红叔
if(uncleNode !=null && uncleNode.getColor().equals(Color.RED)){
uncleNode.setColor(Color.BLACK);
parent.setColor(Color.BLACK);
grandParent.setColor(Color.RED);
}else if(uncleNode !=null && uncleNode.getColor().equals(Color.BLACK) && parent.getRchild().equals(rbNode)){
//情况8:黑叔,当前节点是右孩子
rotateByLeft(grandParent);
parent.setColor(Color.BLACK);
grandParent.setColor(Color.RED);
}else if(uncleNode !=null && uncleNode.getColor().equals(Color.BLACK) && parent.getLchild().equals(rbNode)){
//情况9:黑叔,当前节点是左孩子
rotateByRight(parent);
rotateByLeft(grandParent);
grandParent.setColor(Color.RED);
rbNode.setColor(Color.BLACK);
}else{
break;
}
}
}
root.setColor(Color.BLACK);
}
public static void queryRBNodeByPre(RBNode root){
if(root !=null){
System.out.print(root.getData()+"["+root.getColor().getValue()+"]"+" - ");
queryRBNodeByPre(root.getLchild());
queryRBNodeByPre(root.getRchild());
}else{
return;
}
}
/*递归方式遍历红黑树
* root:为遍历红黑树的根节点
* 中序方式
* */
public static void queryRBNodeByOrder(RBNode root) {
if(root !=null ){
queryRBNodeByOrder(root.getLchild());
System.out.print(root.getData()+"["+root.getColor().getValue()+"]"+" - ");
queryRBNodeByOrder(root.getRchild());
}else{
return;
}
}
public static void main(String[] args) {
root =new RBNode(13,Color.BLACK,null,null,null);
insertRBNode(new RBNode(8));
insertRBNode(new RBNode(17));
insertRBNode(new RBNode(1));
insertRBNode(new RBNode(11));
insertRBNode(new RBNode(15));
insertRBNode(new RBNode(25));
insertRBNode(new RBNode(6));
insertRBNode(new RBNode(22));
insertRBNode(new RBNode(27));
/*initRBTree(new RBNode(6), root);
initRBTree(new RBNode(5), root);
initRBTree(new RBNode(4), root);
queryRBNodeByOrder(root);
System.out.println();
queryBSTNodeByPre(root);
System.out.println();
System.out.println("旋转值:");
//rotateByLeft(root.getLchild());
rotateByRight(root.getRchild());*/
queryRBNodeByPre(root);
System.out.println();
System.out.println("----------------");
queryRBNodeByOrder(root);
/*RBNode rbNode =getMinRchild(root);
System.out.println(rbNode.getData());*/
}
}
测试用例
测试结果
问题
在插入节点的时候,我直接使用root全局变量来操作的,发现程序的根节点被替换了,程序出现了问题。
后来才想到全局变量是在堆中开辟空间的,而堆是共享区域。方法体内定义的变量是在栈中开辟空间的,是在每个线程私有的区域,如果为了防止全局变量被修改,那么在方法中调用全局变量时,可以单独复制一份,以防止出现全局变量被修改。
五、雪花算法
一般情况,实现全局唯一ID,有三种方案,分别是通过中间件方式、UUID、雪花算法。
方案一,通过中间件方式,可以是把数据库或者redis缓存作为媒介,从中间件获取ID。这种呢,优点是可以体现全局的递增趋势(优点只能想到这个),缺点呢,倒是一大堆,比如,依赖中间件,假如中间件挂了,就不能提供服务了;依赖中间件的写入和事务,会影响效率;数据量大了的话,你还得考虑部署集群,考虑走代理。这样的话,感觉问题复杂化了
方案二,通过UUID的方式,java.util.UUID就提供了获取UUID的方法,使用UUID来实现全局唯一ID,优点是操作简单,也能实现全局唯一的效果,缺点呢,就是不能体现全局视野的递增趋势;太长了,UUID是32位,有点浪费;最重要的,是插入的效率低,因为呢,我们使用mysql的话,一般都是B+tree的结构来存储索引,假如是数据库自带的那种主键自增,节点满了,会裂变出新的节点,新节点满了,再去裂变新的节点,这样利用率和效率都很高。而UUID是无序的,会造成中间节点的分裂,也会造成不饱和的节点,插入的效率自然就比较低下了。
方案三,基于redis生成全局id策略,因为Redis是单线的天生保证原子性,可以使用原子性操作INCR和INCRBY来实现,注意在Redis集群情况下,同MySQL一样需要设置不同的增长步长,同时key一定要设置有效期,可以使用Redis集群来获取更高的吞吐量
方案四,通过snowflake算法如下:
SnowFlake算法生成id的结果是一个64bit大小的整数,它的结构如下图:
1位
,不用。二进制中最高位为1的都是负数,但是我们生成的id一般都使用整数,所以这个最高位固定是0-
41位
,用来记录时间戳(毫秒)。- 41位可以表示$2^{41}-1$个数字,
- 如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 $2^{41}-1$,减1是因为可表示的数值范围是从0开始算的,而不是1。
- 也就是说41位可以表示$2^{41}-1$个毫秒的值,转化成单位年则是$(2^{41}-1) / (1000 * 60 * 60 * 24 * 365) = 69$年
-
10位
,用来记录工作机器id。- 可以部署在$2^{10} = 1024$个节点,包括
5位datacenterId
和5位workerId
5位(bit)
可以表示的最大正整数是$2^{5}-1 = 31$,即可以用0、1、2、3、....31这32个数字,来表示不同的datecenterId或workerId
- 可以部署在$2^{10} = 1024$个节点,包括
-
12位
,序列号,用来记录同毫秒内产生的不同id。12位(bit)
可以表示的最大正整数是$2^{12}-1 = 4095$,即可以用0、1、2、3、....4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号
由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的。
SnowFlake可以保证:
- 所有生成的id按时间趋势递增
- 整个分布式系统内不会产生重复id(因为有datacenterId和workerId来做区分)
以下是Twitter官方原版的,用Scala写的:
Java版:
package com.test.util;
/**
* Twitter_Snowflake<br>
* SnowFlake的结构如下(每部分用-分开):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
* 加起来刚好64位,为一个Long型。<br>
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
*/
public class SnowflakeIdWorker {
// ==============================Fields===========================================
/** 开始时间截 (2015-01-01) */
private final long twepoch = 1420041600000L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据标识id,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~31) */
private long workerId;
/** 数据中心ID(0~31) */
private long datacenterId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
//==============================Test=============================================
/** 测试 */
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 100; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}