伸展樹簡介
伸展樹是二叉查找樹,滿足左子樹的key<=根節點的<=右子樹的key的特點。不保證樹是平衡的,但各種操作的平攤的複雜度是logn。從效率上上和平衡樹的效率差不多。從編碼複雜度上伸展樹比紅黑樹和AVL簡單了很多。
伸展樹的思想:二八原則 ,也就是把那些訪問頻率高的的字段儘可能移到離根節點近的位置,所以每次訪問節點都會通過各種旋轉的方法將訪問節點旋轉到根節點。
伸展樹的旋轉
要將訪問節點旋轉到根節點,主要通過zig、zag、zag-zag、zig-zig、zag-zig和zig-zag這幾種方式的針對不同情況的旋轉到根節點。zig和zag通常稱爲單層旋轉,zag-zag、zig-zig、zag-zig和zig-zag稱爲雙層旋轉,雙層旋轉都是由單層旋轉組合成的。
zig和zag
zig和zag明明是右旋和左旋爲啥重新起個名字,大家還都這麼叫。沒找到這麼找到這麼叫的原因,但搜到一個單詞 zigzagging,意思是“之字形運動”,也許與這單詞有關係吧。 網上也有直接利用zig和zag將訪問節點旋轉到根節點,稱之爲單層旋轉,這也符合伸展樹的定義。操作流程是這樣的:如果訪問節點在父節點左支上,則進行zig操作,在父節點右支上,則進行zag操作,但是效率有時候不太好。如下圖,這種旋轉結果令人不太滿意爲了避免這種狀況,經常只有當父節點是根節點時,我們用這種單層旋轉,其他情況我們都用雙層旋轉
zag-zag和zig-zig
RR型:孩子節點在父節點右支,父節點在祖父節點右支
LL型:孩子節點在父節點左支,父節點在祖父節點左支
伸展樹中,再將節點旋轉到根節點的時候,遇到RR型時,進行zag-zag操作
zig-zig.png
伸展樹中,再將節點旋轉到根節點的時候,遇到LL型時,進行zig-zig操作
zag-zig和zig-zag
LR型:孩子節點在父節點右支,父節點在祖父節點左支
RL型:孩子節點在父節點左支,父節點在祖父節點右支
伸展樹中,再將節點旋轉到根節點的時候,遇到LR型時,進行zag-zig操作
zig-zag.png
伸展樹中,再將節點旋轉到根節點的時候,遇到RL型時,進行zig-zag操作
代碼實現
zag、zig、zag-zag、zig-zag、zag-zig、zig-zig
這些都是上面各種旋轉的代碼化,需要注意的是,zig-zag zag-zig 旋轉不是一個節點。
/**
* 左旋轉
*
* @param node
*/
public void zag(SplayNode node) {
if (node == null) {
return;
}
SplayNode pNode = node.getParent();
if (pNode == null) {
return;
}
SplayNode lChild = node.getLeft();
node.setParent(pNode.getParent());
if (pNode.getParent() != null) {
if (pNode.getParent().getLeft() == pNode) {
pNode.getParent().setLeft(node);
} else {
pNode.getParent().setRight(node);
}
}
pNode.setParent(node);
node.setLeft(pNode);
pNode.setRight(lChild);
if (lChild != null)
lChild.setParent(pNode);
}
/**
* 右旋轉
*
* @param node
*/
public void zig(SplayNode node) {
if (node == null) {
return;
}
SplayNode pNode = node.getParent();
if (pNode == null) {
return;
}
SplayNode rChild = node.getRight();
node.setParent(pNode.getParent());
if (pNode.getParent() != null) {
if (pNode.getParent().getLeft() == pNode) {
pNode.getParent().setLeft(node);
} else {
pNode.getParent().setRight(node);
}
}
pNode.setParent(node);
node.setRight(pNode);
pNode.setLeft(rChild);
if (rChild != null)
rChild.setParent(pNode);
}
// 分爲左旋(zag),右旋(zig),
private void zigZag(SplayNode node) {
zig(node);
zag(node);
}
private void zigZig(SplayNode node) {
zig(node.getParent());
zig(node);;
}
private void zagZag(SplayNode node) {
zag(node.getParent());
zag(node);
}
private void zagZig(SplayNode node) {
zag(node);
zig(node);
}
伸展操作
伸展操作其實就是針對伸展中遇到的LL,LR,RR,RL以及父節點是根節點的各種情況處理,處理方式就是伸展樹的各種旋轉。
public void splay(SplayNode node){
if(node.getParent() ==null){
return;
}
SplayNode parent = node.getParent();
if(parent.getParent() ==null){//父節點是根節點
if(node == parent.getLeft()){
zig(node);
}else {
zag(node);
}
return;
}
if(parent == parent.getParent().getLeft()){
if(node == parent.getLeft()){//LL型
zigZig(node);
}else {
zigZag(node);//LR型
}
}else {
if(node == parent.getParent()){
zagZag(node);//RR型
}else {
zagZig(node);//RL型
}
}
}
基礎操作
插入
伸展樹也是二叉樹,所以插入操作的和二叉樹一樣,增加的只是插入完成後,對插入節點一次伸展操作
public void insert(int value){
SplayNode node = new SplayNode();
node.setValue(value);
if (root == null) {//如果樹爲空
root = node;
return;
}
SplayNode insertNode = insertNode(node, getRoot());
if(insertNode ==null){
return;
}
splay(insertNode);
}
//將節點插入樹中
public SplayNode insertNode(SplayNode node,SplayNode root){
if(root.getValue()>node.getValue()){//插入左子樹
if(root.getLeft()!=null){
return insertNode(node,root.getLeft());
}else {
root.setLeft(node);
node.setParent(root);
return node;
}
}else if (root.getValue() <node.getValue()){//插入右子樹
if(root.getRight() !=null){
return insertNode(node,root.getRight());
}else {
root.setRight(node);
node.setParent(root);
return node;
}
}else{//這個節點廢棄掉
return null;
}
}
查找
查找操作和二叉樹的查找一樣,只是在查找到結果之後將,查找節點通過伸展操作旋轉到根節點
public SplayNode search(int key){
SplayNode node = searchNode(getRoot(),key);
if(node ==null){
return;
}
splay(node);
}
public SplayNode searchNode(SplayNode root ,int key){
if(root.getValue()>key){//去左子樹查找
if(root.getLeft()!=null){
return searchNode(root.getLeft(),key);
}else {
return null;//未查找到
}
}else if (root.getValue() <key){//去右子樹查找
if(root.getRight() !=null){
return searchNode(root,key);
}else {
return null;//未查找到
}
}else{//等於key
return root;
}
}
刪除
刪除操作就是,查找到刪除節點後,然後通過伸展操作旋轉到根節點,然後再刪除。
刪除操作
- 找到該節點後繼節點(中序遍歷它後面的那個節點)
- 沒有後繼節點,將根節點左孩子當做根節點
- 有後繼節點,將後繼節點key賦值給根節點。1、後繼節點是葉子節點,直接刪除2、後繼節點有右孩子,右孩子替換後繼節點的位置
public void deleteKey(int key){
SplayNode node = searchNode(getRoot(),key);
if(node ==null){
return;
}
splay(node);
SplayNode succeedNode = getSucceedNode(node);
if(succeedNode == null){
root = node.getLeft();
if(root != null){
root.setParent(null);
}
}else {
if(succeedNode.getRight() == null){//後繼節點是葉子節點
root.setValue(succeedNode.getValue());//將後繼節點值賦給根節點
SplayNode parent = succeedNode.getParent();
if(parent.getLeft().getValue() == succeedNode.getValue()){
parent.setLeft(null);
}else {
parent.setRight(null);
}
succeedNode.setParent(null);
}else {
root.setValue(succeedNode.getValue());//將後繼節點值賦給根節點
SplayNode parent = succeedNode.getParent();
SplayNode right = succeedNode.getRight();
right.setParent(null);
if(parent.getLeft().getValue() == succeedNode.getValue()){
parent.setLeft(right);
right.setParent(parent);
}else {
parent.setRight(right);
right.setParent(parent);
}
}
}
}
/**
* 獲取後繼節點
* @param node
* @return
*/
public SplayNode getSucceedNode(SplayNode node){
if(node ==null){
return null;
}
SplayNode rightTree = node.getRight();
if(rightTree == null){
return null;
}
SplayNode currentNode = rightTree;
SplayNode succeedNode =currentNode;
while (currentNode != null){
succeedNode =currentNode;
currentNode =currentNode.getLeft();
}
return succeedNode;
}
思考一個問題
現在訪問一個節點就會將它旋轉到根節點,是不是會存在訪問一次之後就不再訪問它了?這樣的情況是不是就會浪費性能,在實際應用中,是不是增加個熱度的字段,根據這個熱度值來判斷,它值不值得旋轉到根節點。熱度值的值設置,是不是可以讓前N次數據訪問 ,這個值不起作用,只用來改變數據熱度,N次之後才起作用,這樣的設計是否效率更高些?
總結
整篇文章核心就伸展樹的伸展操作,伸展樹的核心就是數據的二八原則。如果數據二八選擇不明顯,就看旋轉操作能不能將樹變得相對平衡了,雙層旋轉還是可以達到這個效果的。