红黑树

在前面介绍了一棵高度为h的二叉搜索树,其相关操作的时间复杂度均为O(h)。因此搜索树的高度较低时,这些集合操作会执行得较快。然而,如果树的高度较高时,这些集合操作可能并不比链表上执行的快。

红黑树(red black tree)是许多“平衡”搜索树中的一种,可以保证在最坏情况下基本动态集合操作的时间复杂度为O(lgn)。

1 红黑树的性质

1.1 简介

红黑树是一棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是red或black。通过对任何一条从根到叶子结点的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而近似于平衡的

树中每个结点包含5个属性:color、key、left、right和p。如果一个结点没有子结点或父结点,则该结点相应指针属性的值为nil。我们可以把这些nil视为指向二叉搜索树的叶结点(外部结点)的指针,而把带关键字的结点视为树的内部结点。

如下图表示就是一颗红黑树,NIL为指向外结点的指针。(外结点视为没有key的结点,这里省略显示)

1.2 特性

一棵红黑树是满足下面红黑性质的二叉搜索树:

  1. 每个结点或是红色的,或是黑色的;
  2. 根结点是黑色的;
  3. 每个叶结点(NIL)是黑色的;
  4. 如果一个结点是红色的,则它的两个孩子都是黑色的;
  5. 对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。

从某个结点x出发(不含该结点)到达一个叶结点的任意一条简单路径上的黑色结点个数称为该结点的黑高,记为bh(x)。

一棵有n个内部结点的红黑树的高度至多为2lg(n+1)。

1.3 结点YJRedBlackNode

根据红黑树中结点的相关特性设计类YJRedBlackNode。

//
//  YJRedBlackNode.swift
//  RedBlackTree
//
//  CSDN:http://blog.csdn.net/y550918116j
//  GitHub:https://github.com/937447974/Blog
//
//  Created by yangjun on 15/11/20.
//  Copyright © 2015年 阳君. All rights reserved.
//

import Cocoa

/// 结点颜色
enum YJNodeColor {
    case Red
    case Black
}

/// 红黑树的结点
class YJRedBlackNode: NSObject {

    ///
    var key: Int!
    /// 颜色,默认YJNodeColor.Black
    var color: YJNodeColor = YJNodeColor.Black
    /// 父结点
    var parent: YJRedBlackNode?
    /// 左结点
    var left: YJRedBlackNode!
    /// 右结点
    var right: YJRedBlackNode!

    // MARK: - 初始化
    /// 初始化
    ///
    /// - parameter key : 值
    ///
    /// - returns: YJRedBlackNode
    init(key: Int) {
        self.key = key
    }

}

1.4 红黑树YJRedBlackTree

这里我们使用YJRedBlackTree作为红黑树。

//
//  YJRedBlackTree.swift
//  RedBlackTree
//
//  CSDN:http://blog.csdn.net/y550918116j
//  GitHub:https://github.com/937447974/Blog
//
//  Created by yangjun on 15/11/20.
//  Copyright © 2015年 阳君. All rights reserved.
//

import Cocoa

/// 红黑树
class YJRedBlackTree {

    /// 根结点
    var root: YJRedBlackNode?
    /// 哨兵
    private var sentinel = YJRedBlackNode(key: 8888)

    init() {
        self.root = self.sentinel
    }

    // MARK: - 中序遍历
    /// 中序遍历
    ///
    /// - returns: void
    func inorderWalk() {
        if self.root != nil {
            self.inorderWalk(self.root!, height: 0)
        }
    }

    private func inorderWalk(node: YJRedBlackNode, height: Int) {
        if node != self.sentinel {
            // 中序遍历
//            print("\(node.key); color:\(node.color) ; height:\(height)") // 测试
            self.inorderWalk(node.left, height: height+1)
            print(node.key)
            self.inorderWalk(node.right, height: height+1)
        }
    }

    // MARK: - 查找结点
    /// 查找结点
    ///
    /// - parameter key : 要查找的key
    ///
    /// - returns: YJRedBlackNode
    func search(key: Int) -> YJRedBlackNode? {
        var node = self.root
        while node != nil && node != self.sentinel && key != node!.key {
            if node!.key > key { // 当前结点大于key时搜索左孩子
                node = node!.left
            } else { // 否则进入右孩子
                node = node?.right
            }
        }
        node = node == self.sentinel ? nil : node
        return node
    }

}

在YJRedBlackNode已经有了两个方法inorderWalk和search,这些方法来源于二叉搜索树,如果你想了解关于二叉搜索树的相关信息,可以阅读我的另一篇博文《二叉搜索树》。

为了便于处理红黑树代码中的边界条件,这里还使用了哨兵sentinel,使用sentinel代替树中的nil左右孩子结点。sentinel可以让结点x的nil孩子视为一个普通结点,其父结点为x。所有的nil孩子结点都会指向这个哨兵sentinel。

2 旋转

红黑树操作insert和delete在含n个关键字的红黑树上,运行花费时间为O(lgn)。由于这两个操作对树做了修改,结果可能违反红黑性质。为了维护这些性质,必须要改变树中某些结点的颜色以及指针结构。

指针结构的修改是通过旋转来完成的,其中有左旋和右旋。

2.1 左旋

当在某个结点pivot上,做左旋操作时,我们假设它的右孩子y不是self.sentinel,pivot可以为树内任意右孩子而不是self.sentinel的结点。左旋以pivot到y之间的链为“支轴”进行,它使y成为该孩子树新的根,而y的左孩子b则成为pivot的右孩子。

// MARK: - 左旋
private func leftRotate(x:YJRedBlackNode) {
    let y = x.right
    if x != self.sentinel && y != self.sentinel { // x和y都不是哨兵,执行左旋操作
        // 连接x和y.left
        x.right = y.left
        y.left?.parent = x
        // 连接x.parent和y
        y.parent = x.parent
        if x.parent == nil {
            self.root = y
        } else if x == x.parent?.left {
            x.parent!.left = y
        } else {
            x.parent?.right = y
        }
        // 连接x和y
        y.left = x
        x.parent = y
    }
}

2.2 右旋

右旋与左旋差不多,在此不做详细介绍。

// MARK: 右旋
private func rightRotate(y:YJRedBlackNode) {
    let x = y.left
    if x != self.sentinel && y != self.sentinel { // x和y都不是哨兵,执行左旋操作
        // 连接y和x.right
        y.left = x.right
        x.right?.parent = y
        // 连接y.parent和x
        x.parent = y.parent
        if y.parent == nil {
            self.root = x
        } else if y == y.parent!.left {
            y.parent!.left = x
        } else {
            y.parent!.right = x
        }
        // 连接y和x
        x.right = y
        y.parent = x
    }
}

3 插入

我们可以在O(lgn)的时间内完成向一棵含n个结点的红黑树中插入一个新结点。利用insert完成这个操作,和插入普通的二叉搜索树一样,然后将z着为红色。为保证红黑性质能继续保持,我们调用一个辅助方法insertFixup来对结点重新着色并旋转。

如图所示,在使用insertFixup重新着色时,会遇到3种情况:

  1. z的叔结点y是红色的。
  2. z的叔结点y是黑色的且z是一个右孩子。
  3. z的叔结点y是黑色的且z是一个左孩子。
// MARK: - 插入z
/// 插入z
///
/// - parameter z : 插入的YJRedBlackNode
///
/// - returns: void
func insert(z: YJRedBlackNode) {
    var y: YJRedBlackNode?
    // 设置z.parent
    var x = self.root
    while x != nil && x != self.sentinel {
        y = x
        if z.key < x!.key {
            x = x!.left
        } else {
            x = x!.right
        }
    }
    z.parent = y
    // 设置z为root、y.lefty.right
    if y == nil {
        self.root = z
    } else if z.key < y!.key {
        y!.left = z
    } else {
        y!.right = z
    }
    // 设置z的相关属性
    z.color = YJNodeColor.Red
    z.left = self.sentinel
    z.right = self.sentinel
    // 使用insertFixup(z: YJRedBlackNode)维护红黑
    self.insertFixup(z)

}

// MARK: 插入后保持红黑性质
private func insertFixup(var z: YJRedBlackNode) {
    /* 3种情况:
    1. z的叔结点y是红色的。
    2. z的叔结点y是黑色的且z是一个右孩子。
    3. z的叔结点y是黑色的且z是一个左孩子
    */
    while z.parent?.color == YJNodeColor.Red {
        if z.parent == z.parent!.parent?.left { // z.parentz的祖父结点的左边
            let y = z.parent!.parent!.right
            if y?.color == YJNodeColor.Red { // case 1
                z.parent?.color = YJNodeColor.Black
                y?.color = YJNodeColor.Black
                z.parent!.parent!.color = YJNodeColor.Red
                z = z.parent!.parent!
            } else if z == z.parent!.right { // case 2
                z = z.parent!
                self.leftRotate(z)
            } else { // case 3
                z.parent?.color = YJNodeColor.Black
                if z.parent?.parent != nil { // 祖父结点存在
                    z.parent!.parent!.color = YJNodeColor.Red
                    self.rightRotate(z.parent!.parent!)
                }
            }
        } else if z.parent == z.parent!.parent?.right { // z.parentz的祖父结点的右边
            let y = z.parent!.parent!.left
            if y?.color == YJNodeColor.Red { // case 1
                z.parent!.color = YJNodeColor.Black
                y!.color = YJNodeColor.Black
                z.parent!.parent?.color = YJNodeColor.Red
                z = z.parent!.parent!
            } else if z == z.parent!.left { // case 2
                z = z.parent!
                self.rightRotate(z)
            } else { // case 3
                z.parent?.color = YJNodeColor.Black
                if z.parent?.parent != nil { // 祖父结点存在
                    z.parent!.parent!.color = YJNodeColor.Red
                    self.leftRotate(z.parent!.parent!)
                }
            }
        }
    }
    // 设根为黑色
    self.root!.color = YJNodeColor.Black
}

4 删除

与n个结点的红黑树上的其他基本操作一样,删除一个结点要花费O(lgn)时间。与插入操作相比,删除操作要复杂些。

删除结点后,需要为替代的x结点重新着色,此时会遇到四种情况,w = x的兄弟结点。

  1. w是红色的。
  2. w是黑色的,而且w的两个子结点都是黑色的。
  3. w是黑色的,w的左孩子是红色的,w的右孩子是黑色的。
  4. w是黑色的,切w的右孩子是红色的。
// MARK: - 删除结点
/// 删除结点
///
/// - parameter z : 要删除的YJRedBlackNode
///
/// - returns: void
func delete(z: YJRedBlackNode) {
    // 原理:尽量让将z调整到红色叶节点上,删除
    var x: YJRedBlackNode! // 需要调整的点
    var yColor = z.color   // 删除的点的颜色
    if z.left == self.sentinel { // 1 如果z没有左孩子,则用其右孩子代替z
        x = z.right
        self.transplant(z, v: z.right)
    } else if z.right == self.sentinel { // 2 如果z没有右孩子,则用其左孩子代替z
        x = z.left
        self.transplant(z, v: z.left)
    } else { // 3 z即有左孩子又有右孩子,则用z的后继y替换它
        let y = self.minimum(z.right!) // 后驱
        yColor = y.color
        x = y.right
        x.parent = y // 如果x为哨兵,需要临时设置其父结点
        //4 如果yz的右孩子,用y替换z,并仅留下y的右孩子
        if y.parent != z{
            //5 否则,y位于z的右子树中但不是z的右孩子,在这种情况先用y的右孩子替换y。再用y替换z。
            self.transplant(y, v: y.right)
            y.right = z.right
            y.right?.parent = y
        }
        // y替换z,即删除z
        self.transplant(z, v: y)
        y.left = z.left
        y.left?.parent = y
        y.color = z.color
    }
    // 删除为黑结点时,破坏了红黑性质,需要使用deleteFixup维护红黑性质
    if yColor == YJNodeColor.Black {
        self.deleteFixup(x)
    }
}

// MARK: v替换u
private func transplant(u: YJRedBlackNode, v: YJRedBlackNode?) {
    // u.parent连接v
    if u.parent == nil {
        self.root = v
    } else if u == u.parent!.left {
        u.parent!.left = v
    } else {
        u.parent!.right = v
    }
    // v连接u.parent
    v?.parent = u.parent
}

// MARK: 根据结点获取其最小结点
private func minimum(var node:YJRedBlackNode) -> YJRedBlackNode {
    while node != self.sentinel && node.left != self.sentinel {
        node = node.left!
    }
    return node
}

// MARK: 删除后保持红黑性质
private func deleteFixup(var x: YJRedBlackNode) {
    /* 4种情况,w = x的兄弟结点
    1. w是红色的
    2. w是黑色的,而且w的两个子结点都是黑色的
    3. w是黑色的,w的左孩子是红色的,w的右孩子是黑色的
    4. w是黑色的,且w的右孩子是红色的
    */
    while x != self.root && x.color == YJNodeColor.Black {
        if x == x.parent!.left { // x是左孩子
            var w = x.parent!.right!
            if w.color == YJNodeColor.Red { // case 1
                w.color = YJNodeColor.Black
                x.parent!.color = YJNodeColor.Red
                self.leftRotate(x.parent!)
                w = x.parent!.right!
            }
            if w.left?.color == YJNodeColor.Black && w.right?.color == YJNodeColor.Black { // case 2
                w.color = YJNodeColor.Red
                x = x.parent!
            } else {
                if w.right?.color == YJNodeColor.Black { // case 3
                    w.left?.color = YJNodeColor.Black
                    w.color = YJNodeColor.Red
                    self.rightRotate(w)
                    w = x.parent!.right!
                } else { // case 4
                    w.color = x.parent!.color
                    x.parent?.color = YJNodeColor.Black
                    w.right?.color = YJNodeColor.Black
                    self.leftRotate(x.parent!)
                    x = self.root!
                }
            }
        } else { // x是右孩子
            var w = x.parent!.left!
            if w.color == YJNodeColor.Red { // case 1
                w.color = YJNodeColor.Black
                x.parent!.color = YJNodeColor.Red
                self.rightRotate(x.parent!)
                w = x.parent!.left!
            }
            if w.left?.color == YJNodeColor.Black && w.right?.color == YJNodeColor.Black { // case 2
                w.color = YJNodeColor.Red
                x = x.parent!
            } else {
                if w.left?.color == YJNodeColor.Black { // case 3
                    w.right?.color = YJNodeColor.Black
                    w.color = YJNodeColor.Red
                    self.leftRotate(w)
                    w = x.parent!.left!
                } else {
                    // case 4
                    w.color = x.parent!.color
                    x.parent?.color = YJNodeColor.Black
                    w.left?.color = YJNodeColor.Black
                    self.rightRotate(x.parent!)
                    x = self.root!
                }
            }
        }
    }
    // 最后x设为黑色
    x.color = YJNodeColor.Black
}

删除结点delete方法,涉及到三个辅助方法,结点替换transplant、最小结点minimum、删除后重新着色deleteFixup。

5 小结

本篇博文讲解了红黑树的增加和删除操作,其中利用到了旋转这个概念。红黑树是平衡搜索树的一种,可以保证在最坏情况下基本动态集合操作的时间复杂度为O(lgn)。
 


其他

源代码

Algorithms

参考资料

算法导论

文档修改记录

时间描述
2015-11-20完成到1.2章节
2015-11-22完成红黑树研发
2015-11-23完成博文

版权所有

CSDN:http://blog.csdn.net/y550918116j

GitHub:https://github.com/937447974/Blog

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值