有序表
有序表的分类
基于平衡搜索二叉树实现的有序表
搜索二叉树的节点的插入、查询、删除操作
搜索二叉树的节点的删除操作
(1)叶子节点直接删除;
(2)非叶子节点,删除之后,使用左子树最右的节点,或者右子树最左的节点填补空位。如果用左子树最右的节点,其左子树向上填补它移走后的空缺。如果用右子树最左的节点,其右子树向上填补它移走后的空缺。
删除50这个节点,可以用40或者60填补上去。如果用40填补,35补到40原先的位置。如果用60填补,65补到60原先的位置。
左旋和右旋
左旋和右旋用来调整二叉树的高度,通过适当的旋转操作可以让二叉树达到平衡。
对于搜索二叉树,左旋、右旋之后仍然是搜索二叉树。
左旋
节点T左旋,T向左侧倒:
(1)R成为新的头结点;
(2)T成为R的左子树,T的左子树不变,T的右子树为空;
(3)R的右子树T4不变;
(4)R原先的左子树T3挂到T的右侧。
右旋
节点T右旋,T向右侧倒:
(1)L成为新的头结点;
(2)T成为L的右子树,T的右子树不变,T的左子树为空;
(3)L的左子树T1不变;
(4)L原先的右子树T2挂到T的左侧。
平衡搜索二叉树实现有序表的原理
AVL树、Size-balanced-tree、红黑树都是利用平衡搜索二叉树的思想实现的有序表。
这三种有序表使用的搜索二叉树的查询,插入、删除节点,左旋,右旋等基本操作都是一样的,不一样的是它们对平衡性的定义,控制平衡的规则不一样。
AVL树严格遵守平衡二叉树的定义,通过树的高度、左旋和右旋调整平衡。
Size-balanced-tree不严格遵守平衡二叉树的定义,通过树的节点个数、左旋和右旋调整平衡,整体上保持平衡。
红黑树也不严格遵守平衡二叉树的定义,通过节点的颜色和个数、左旋和右旋调整平衡,整体上保持平衡。
二叉树搜索树加上平衡性约束(即使是整体上的平衡)后,整棵树可以认为是满二叉树,查找、插入、删除操作的时间复杂度为O(logN)。
AVL树
平衡性:
任意一个节点的左子树和右子树的高度差不超过1。
有四种形态的调整:LL、RR、LR、RL型。
Size-balanced-tree
平衡性:
每棵子树的大小,不小于其兄弟的任一子树的大小;
每棵叔叔树的大小,不小于任一侄子数的大小。
也是四种形态的调整:LL、RR、LR、RL型。
红黑树
平衡性:
每个节点都有一种颜色:红色、黑色;
头结点为黑色;
叶子节点为黑色;
红色节点不能相邻;
从每一个节点出发往下走,每条到结束的路上黑色节点数目一样多。
注意:
红黑树中的叶子节点是null节点,不是指既没有左子树、也没有右子树的节点
有八种形态的调整:略。
下边是一棵红黑树,但是并不是严格的平衡二叉树。
跳表
查找、插入、删除的时间复杂度O(logN)。跳表的索引有冗余,增加空间开销。
跳表节点的数据结构如下。跳表是多链表结构,每个节点至少有一个指向下一个节点的指针。
public class SkipList { public static class SkipListNode<K extends Comparable<K>, V> { public K key; public V value; public ArrayList<SkipListNode<K, V>> nextNodes; }}
头节点(默认节点)
逻辑上最小的节点,头结点不保存正常的数据,索引指针数可以动态扩充。
数据节点
除头节点之外存储数据的节点,数据节点的索引指针数固定不变。
数据节点索引指针数生成算法
指针数初始为1。随机从0,1两个数中取一个数(取到0和1的概率相等,都是50%),如果取到1指针数加1,继续该过程,如果取到0停止获取。
查找
从头结点开始,从上到下,从左到右的顺序查找。
从上图所示的跳表中查找节点40,查找过程如下:
(1)当前节点cur来到头节点
(2)从cur(头)的最高层开始,从第4层向右找到节点70,>40,在cur(头)处向下一层移动;
(3)来到cur(头)的第3层,向右找到70,>40,在cur(头)处向下一层移动;
(4)来到cur(头)的第2层,向右找到30,<40,cur移动到30这个节点;
(5)从cur(30)的最高层,从第2层向右找到70,>40,在cur(30)处向下一层移动;
(6)来到cur(30)的第1层,向右找到70,>40,在cur(30)处向下一层移动;
(7)来到cur(30)的第0层,向右找到50,>40,到达最下层,没有找到40,查找结束。
插入
在上图所示的跳表中插入节点40,插入过程如下:
(1)为40这个新节点生成索引指针的个数,假设生成的是6个,> 头节点的指针数(4);
(2)将头节点的指针数扩充到6个,新增加的2个指针先指向null;
(3)当前节点cur来到头结点,从最高层第6层开始,指向null,改成指向新节点40,在cur(头)处向下一层移动;
(4)来到cur(头)的第5层,指向null,改成指向新节点40,在cur(头)处向下一层移动;
(5)来到cur(头)的第4层,指向节点70,>40,将40插入cur(头)和70之间,在cur(头)处向下一层移动;
(6)来到cur(头)的第3层,指向节点70,>40,将40插入cur(头)和70之间,在cur(头)处向下一层移动;
(7)来到cur(头)的第2层,指向节点30,<40,cur移动到节点30,从cur(30)的最高层第2层开始,指向节点70,>40,将40插入cur(30)和70之间,在cur(30)处向下一层移动;
(8)来到cur(30)的第1层,指向节点70,>40,将40插入cur(30)和70之间,在cur(30)处向下一层移动;
(9)来到cur(30)的第0层,指向节点50,>40,将40插入cur(30)和50之间,已来到最下一层,插入完成。
删除
在上图所示的跳表中删除节点40,删除过程如下:
(1)在跳表中查找40,如果找不到,删除结束;
(2)当前节点cur来到头结点,从最高层第6层开始,指向40,40节点指向null,释放第6层,头结点层高减少1层;
(3)来到cur(头)的第5层,指向40,节点40指向null,释放第5层,头结点层高再减少1层;
(4)来到cur(头)的第4层,指向40,节点40指向节点70,cur(头)的第4层改成指向节点70,在cur(头)处向下一层移动;
(5)来到cur(头)的第3层,指向40,节点40指向节点70,cur(头)的第3层改成指向节点70,在cur(头)处向下一层移动;
(6)来到cur(头)的第2层,指向30,<40,cur移动到节点30,从cur(30)的最高层第2层开始,指向节点40,节点40指向节点70,cur(30)的第2层改成指向节点70,在cur(30)处向下一层移动;
(7)来到cur(30)的第1层,指向40,节点40指向节点70,cur(30)的第1层改成指向节点70,在cur(30)处向下一层移动;
(8)来到cur(30)的第0层,指向40,节点40指向节点50,cur(30)的第0层改成指向节点50,已来到最下一层,删除完成。
时间复杂度分析
每个数据节点的索引指针数通过每次50%的随机概率产生,如果总共有N个节点,则节点数的分布情况如下
第0层:N
第1层:N/2
第2层:N/4
第3层:N/8
等等......
整体上符合一棵满搜索二叉树,即平衡搜索二叉树,故查询、插入、删除的时间复杂度与平衡搜索二叉树相当,为O(logN)。
跳表的实现比平衡二叉树简单。
缺点:
索引指针数冗余存储,索引指针的空间复杂度是O(N*N),利用空间换时间。
参考资料
《bilibili左程云算法课堂》
《跳跃表Skip List的原理和实现(Java)》 https://blog.csdn.net/DERRANTCM/article/details/79063312