02.Java-数据结构
数据结构
1、描述
数据结构是指数据存储结构方式。大致上分为线性表、栈、队列、树、图。
2、线性表
2.1 数组
数组是连续的内存存储区。读取速度非常快。
2.2 链表
链表在java中的实现是LinkedList,内部使用引用的方式来实现,集合内不同通过Node来实现,有指向上家和下家的指针,每个节点上关联了元素。列表存放了first和last元素。
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
3. 栈
栈是一种先进后出的结果,完全可以通过List模拟,手枪的弹夹就是典型的栈结构。
4. 队列
队列是先进先出的结果,也可以通过List实现。
5. 红黑树
红黑树是一种自平衡的二叉查找树,二叉树的每个节点都有一个附属位,该附属位通常解释为节点的颜色(非红即黑)。颜色位用来在树进行插入和删除时保持相应的平衡。
以一种满足特定属性的方式对每个节点进行着色来保持树的平衡。这些属性共同约束了在最坏的情况下,树是如何变得不平衡的。树被修改时,新树被重新排列和回执来恢复其颜色属性。这些属性被设计成能够以非常高效方式进行重新排列和着色。
树的平衡并不完美,但是对能够保证查询时做到$$O(log_2n)$$的时间复杂度来说足够好了,n是元素总数。插入和删除操作,随着树的重新排列和着色,也能够保持在$$O(log_2n)$$的时间复杂度。
跟踪每个节点的颜色信息只需要一个位的成本付出,因此内存消耗非常小,几乎都能同于二叉查找树的内存消耗。之所以使用红黑树冠名一种说法是当时只有红色和黑色两种钢笔用来着色。
5.1 红黑树术语
红黑树是一种特殊类型的二叉树,在计算机科学中用于组织可比较数据的片段,例如文本或数字。红黑树的叶子节点是指不包含数据的节点,可以不需要在计算机内存中显式处理,手段就是编码一个空的孩子指针来标识是节点是叶子节点。但是如果叶子真的是显式节点,就会简化一些在红黑树算法上的操作。为了节省执行时间,有时指向某个特定节点的指针(而不是空指针,类似于java中的单例概念)来行使所有叶节点的角色,所有内部节点到叶节点的引用都指向该特殊节点。
红黑树,像所有的二叉搜索树一样,允许元素的有效遍历(即:按左-根-右)。搜索时间是从根到叶遍历的结果,因此n个树的平衡树具有最小的树高度,从而导致O(log n)搜索时间。
5.2 红黑树属性:
-
每个节点非红即黑
-
根节点是黑的。该规则有时会忽略,因为根总是要从红变成黑色,反之是没有必要,该规则在分析时有些许影响。
-
所有叶子是黑色的
-
如果节点为红,孩子都是黑色的
-
给定节点到所达叶子(NIL)节点的每条路径都含有相同数量的黑色节点
其他定义:从根节点到某个节点之间黑色节点的数量称为该节点的“黑色深度”,从根节点到任一叶子节点的所有路径中黑色节点的个数成为树的高度。注意,由于属性的第5条件,不存在黑色节点数不同的路径。
这些属性强制保证了红黑色的关键属性:从各节点到最远的叶子节点的路径不会超过到最近叶子节点路径的2倍。其结果就是树在高度上大体上是平衡的。由于插入、删除、查找操作有最坏时间的要求,这个要求和数的高度是成比例的,理论上在最差情况下,红黑树仍然是高效的,而不像其他普通二叉搜索树。
这一特性能够得到保证的原因可以考虑属性4和5的共同作用,对于红黑树T来讲,B记做属性5中所说的黑色节点数。从根节点到任意叶子节点中可能的最短路径由B构成,更长的可能路径可以通过插入红色节点来构造。然而,属性4要求不能插入连续一个以上的红色节点。因此,忽律所有黑色NIL(叶子)节点,最长的可能路径上由2 * B个节点组成,交替出现黑和红(这是最糟糕到的情况)。计算黑色NIL节点的个数,最长可能路径由2*B-1个节点组成。
最短可能路径上全是黑色节点,最长可能路径上红黑交替出现,最大路径上也有着相同数量的黑色节点,没有哪条路径是其他路径的2倍以上。
5.3 4阶B树推导
红黑树在结构上同4阶B树相似,每个节点可以包含1到3个值和2到4个子节点。但每个节点中只有一个值和红黑树中的黑色节点的值匹配,该值的前后可以携带一个可选值,和红黑树中的红色节点相匹配。可以看成对红黑树中的红色节点向上提拉产生的效果,如此一来,红色节点和上级黑色节点水平对齐,形成一个水平节点簇。在该树中,所有叶子节点都有相同的深度。
红黑树在结构上相当于4阶的B树,每个簇的最小填充因子为33%,最大容量为3个值。这种B-树类型仍然比红黑树更通用,因为它允许在红黑树转换中产生歧义,可以从等效的4阶B树产生多个红黑树。如果B-树簇只包含1个值,则为最小值,黑色,并有两个子指针。如果一个簇包含3个值,那么中心值将为黑色,并且其边上存储的每个值将为红色。如果集群包含两个值,那么任何一个都可以成为红黑树中的黑节点(而另一个将是红色的)。如下图所示:
4阶B-树的每个簇中并不维护哪个值是根和父代值。尽管如此,红黑树的操作在时间上依然更为经济,因为不需要维护向量值。B-树中如果存放的是值而不是引用的话成本或许更高。B-树在空间上或许更加经济,因为不需要存储颜色属性,但必须要知道簇中的哪个slot被使用,如果使用引用方式存放数据的话,簇可以理解为包含3个slot的向量和包含4个slot的指针集合。此种情况下,B-树在内存中更加紧凑。
5.4 java TreeMap中红黑树实现
java中TreeMap采用红黑树实现,put时,先定位上级元素,如果key存在则替换之前的value即可。如果key不存在,则通过循环找到相应的挂载点,然后将新的kv组装成Entry对象放置到挂载点的left或right的位置。执行完成后要执行最关键的一步就是插入后修正处理,即调用fixAfterInsert()方法。
新节点插入后的修正算法:
-
每次插入的新节点都是红节点
-
红色节点的上级不能是红色节点
-
如果上级是红色节点,如果叔辈也是红色的,改变叔辈和父辈为黑色,祖父改为红色,再处理祖父节点
-
如果父辈是红色节点,叔辈是黑色的,自己和父节点与祖父节点不在一条直线上,则对父节点旋转(目的是拉直),再对父节点和祖父节点变色,最后对祖父节点旋转(自己位于左侧就右旋,反之左旋)。
-
如果父辈是红色节点,叔辈是黑色的,自己和父节点与祖父节点在一条直线上,则只需要改变父节点和祖父节点颜色,再对祖父节点旋转即可。同步骤4相似,不需要进行一步拉直的操作。
-
图例展示如下:
5.4.1 fixAfterInsert方法处理逻辑如下,x为插入的节点
private void fixAfterInsert(x){
x标成红色;
while(x存在 && x不是root && x上级是红色){
//x上级是左节点
if(x上级是左节点?){
y = 取出x上级对应的右节点;
//y是红色的
if(y是红色的?){
设置x上级为黑色 ;
设置y为黑色;
x的上上级为红色;
x = x的上上级;
}
//y不是红色的
else{
if(x本身是右节点?){
x = x上级节点;
对x进行左旋;
}
x上级标黑;
x上上级标红;
x上上级右旋;
}
}
//x上级是右节点
else{
y = x上级对应的左节点 ;
if(y是红色){
x上级标黑;
y标黑;
x上上级标红;
x = x上上级;
}
else{
if(x上级是左节点?){
x = x上级;
对x右旋;
}
x上级标黑;
x上上级标红;
x上上级左旋;
}
}
}
root标黑;
}
5.4.2 左旋处理
节点的左孩子看成女儿,右孩子看成儿子,节点本身可能是女儿,也可能是儿子。根暂看成儿子(女儿也可以,只有一个根)。
private void rotateLeft(Entry<K,V> p) {
if(p存在){
r = p的儿子;
p的孙女成为p的儿子;
if(孙女存在){
孙女的长辈是p;
}
r和p同辈份;
if(p没有父代){
儿子变成root;
}
else{
p原来的儿子r顶替自己的位置;
}
p成了r的女儿;
儿子r成了p的家长;
}
}
下图是对节点2进行左旋的图例解释:
-
向左下方平移节点,自己的孙女(节点3)变成儿子
-
原来的儿子(节点4)向左上方平移,自己变成儿子的女儿。
5.4.3 右旋理
右旋原理同左旋原理相类似,左旋是找孙女,右旋是找外孙。
private void rotateRight(Entry<K,V> p) {
if (p != null) {
//找女儿
Entry<K,V> l = p.left;
//外孙变女儿
p.left = l.right;
//建立和外孙的关系
if (l.right != null) l.right.parent = p;
//女儿和自己同辈
l.parent = p.parent;
//女儿顶替自己的位置
if (p.parent == null)
root = l;
else if (p.parent.right == p)
p.parent.right = l;
else p.parent.left = l;
//p成女儿的儿子
l.right = p;
//女儿成p的家长
p.parent = l;
}
}
旋转过程同左旋完全相似,只是方向相反。这里不做赘述。
5.5 二叉树的遍历
二叉树遍历有四中遍历方式,分别是前序遍历、中序遍历、后续遍历和层序遍历。其中,前中后是指任何一个节点的根在遍历过程中所处的位置。比如前序遍历为根左右,后续遍历为左右根,中序遍历为左根右。由于java的TreeMap没有提供访问根节点、左节点、右节点的方法,需要自行设计方法进行实现。以下代码是分别提取根、key、左节点、右节点的方法,主要是通过反射实现。
-
getRoot
/** * 获取map的根节点 */ public static Map.Entry getRoot(TreeMap map) throws Exception { Field f = TreeMap.class.getDeclaredField("root") ; f.setAccessible(true); Object o = f.get(map) ; return (Map.Entry) o; }
-
getLeft
public static Map.Entry getLeft(Map.Entry e) throws Exception { Field f = e.getClass().getDeclaredField("left") ; f.setAccessible(true); Object o = f.get(e) ; return (Map.Entry) o; }
-
getRight
public static Map.Entry getRight(Map.Entry e) throws Exception { Field f = e.getClass().getDeclaredField("right") ; f.setAccessible(true); Object o = f.get(e) ; return (Map.Entry) o; }A
-
getKey
public static Object getKey(Map.Entry e) throws Exception { Field f = e.getClass().getDeclaredField("key") ; f.setAccessible(true); Object o = f.get(e) ; return o; }
-
getColor
获得节点的颜色,颜色是boolean值,表示是否是黑色,默认是true,即默认黑色。
public static String getColor(Map.Entry e) throws Exception { Field f = e.getClass().getDeclaredField("color") ; f.setAccessible(true); Object o = f.get(e) ; return o.toString(); }
5.5.1 前序遍历
/**
* 递归实现前序遍历
*/
public static void preOrderTravel(Map.Entry e) throws Exception {
if(e != null){
System.out.println(getKey(e));
preOrderTravel(getLeft(e)) ;
preOrderTravel(getRight(e)) ;
}
}
@Test
public void testMidTravel() throws Exception {
TreeMap<Integer, String> map = new TreeMap<Integer, String>();
map.put(8 , "tom1");
map.put(4 , "tom1");
map.put(1 , "tom1");
map.put(2 , "tom1");
map.put(3 , "tom1");
map.put(6 , "tom1");
map.put(7 , "tom1");
map.put(5 , "tom1");
preOrderTravel(getRoot(map));
}
5.5.2 中序遍历
public static void preOrderTravel(Map.Entry e) throws Exception {
if(e != null){
preOrderTravel(getLeft(e)) ;
//中间访问
System.out.println(getKey(e));
preOrderTravel(getRight(e)) ;
}
}
5.5.3 后续遍历
public static void preOrderTravel(Map.Entry e) throws Exception {
if(e != null){
preOrderTravel(getLeft(e)) ;
preOrderTravel(getRight(e)) ;
//最后访问
System.out.println(getKey(e));
}
}
5.5.4 层序遍历
public static void midOrderTravel(int level ,List<Map.Entry> list) throws Exception {
List<Map.Entry> sublist = new ArrayList<Map.Entry>() ;
if(!list.isEmpty()){
System.out.print(level + " ==> ");
for(Map.Entry e : list){
Object key = getKey(e) ;
String red = getColor(e) ;
System.out.print(String.format("(%d:%s)", key , red)) ;
Map.Entry left = getLeft(e);
if(left != null)
sublist.add(left) ;
Map.Entry right = getRight(e);
if(right != null)
sublist.add(right) ;
}
System.out.println();
midOrderTravel(level + 1 , sublist);
}
}