RxSwift PriorityQueue 优先级队列的实现

在 RxSwift 框架中,在 PriorityQueue.swift 文件中,使用数组实现了一个优先级队列 PriorityQueue

优先级队列(priority queue)是0个或者多个元素的集合,每个元素有一个优先级,可以在集合中查找或删除优先级最高的元素。对优先级队列执行的操作有:

  1. 查找。一般情况下,查找操作用来搜索优先权最大的元素。
  2. 插入一个新元素。
  3. 删除。一般情况下,删除操作用来删除优先权最大的元素。
    对于优先权相同的元素,可按先进先出次序处理或按任意优先权进行。

RxSwift 是通过数组实现优先级队列的。在有元素入队列和出队列的之后必须对数组中的元素进行排序,才能在获取队列中优先级最高的元素时非常快速。RxSwift 使用的是最大堆最小堆)的排序算法,对数组中的元素进行排序的。

堆树

堆树(最大堆或者最小堆)的定义如下:

  1. 堆树是一棵完全二叉树;
  2. 堆树中某个节点的值总是不大于(或不小于)其子节点的值;
  3. 堆树中每个节点的子树都是堆树;

当父节点的值总是大于或等于任何一个子节点的值时为最大堆,当父节点的值总是小于或等于任何一个子节点的值时为最小堆

最大堆示例
最小堆示例

构造最大树

怎样将一个未排序的数组构造成堆树呢?现在以最大堆为例进行讲解(最小堆同理)。
加入我们拿到的未排序的数组为:

var numbers = [6, 2, 5, 4, 20, 13, 14, 15, 9, 7]

数组对应的完全二叉树如下图所示:

未排序数组对应的完全二叉树

如果堆树中的每个非子节点的值都大于它的两个(或一个)相近的子节点的值,那么整个堆树就满足了每个节点的值总是不小于其子节点的值。所以构造堆树的基本思路是:首先找到最后一个节点的父节点,从这个父节点开始调整树,保证父节点的值大于子节点的值。然后从这个父节点继续向前调整树,直到所有的非子节点的值都不小于于它的相邻的子节点的值,这样最大堆就构造完毕了

假设树中有n个节点,从0开始给节点编号,到n-1结束,对于编号为i的节点,其父节点为(i-1)/2;左子节点的编号为i2+1,右子节点的编号为i2+2。最后一个节点的编号为n-1,其父节点的编号为(n-2)/2,所有从编号为(n-2)/2的节点开始调整树。

如下图所示,最后一个节点为7,其父节点为20,从20这个节点开始构造最大堆;构造完毕之后,转移到下一个父节点,直到所有父节点都构造完毕。


最大堆构造过程1

最大堆构造过程2

思路已经梳理完成,下面我们看 RxSwift 具体是怎样实现的。

PriorityQueue的初始化方法中传入对比两个元素优先级和判断元素是否相等的方法。_elements用于保存队列中的元素。

struct PriorityQueue<Element> {

    private let _hasHigherPriority: (Element, Element) -> Bool
    private let _isEqual: (Element, Element) -> Bool

    fileprivate var _elements = [Element]()

    init(hasHigherPriority: @escaping (Element, Element) -> Bool, isEqual: @escaping (Element, Element) -> Bool) {
        _hasHigherPriority = hasHigherPriority
        _isEqual = isEqual
    }
}

获取优先级最高的元素,即返回 _elements 最前的元素:

func peek() -> Element? {
    return _elements.first
}

有元素进入队列时,首先将元素添加到数组_elements的末尾,相当于在堆树的末尾又添加了一个节点,这时需要根据这个节点的优先级和其父节点的优先级调整数组。因为在添加元素之前的树结构已经满足最大堆的要求,所以现在只需要关注最后一个节点和它的父节点的优先级,如果最后一个节点的优先级较高,则将最后一个节点和它的父节点进行调整。以此类推,一直向上调整到树的顶端。

mutating func enqueue(_ element: Element) {
    _elements.append(element)
    bubbleToHigherPriority(_elements.count - 1)
}

// 从下标为 initialUnbalancedIndex 处向高的优先级处调整元素
private mutating func bubbleToHigherPriority(_ initialUnbalancedIndex: Int) {
    // 确保 initialUnbalancedIndex 在 _elements 的索引范围内
    precondition(initialUnbalancedIndex >= 0)
    precondition(initialUnbalancedIndex < _elements.count)

    var unbalancedIndex = initialUnbalancedIndex

    while unbalancedIndex > 0 {
        // unbalancedIndex 为未排序的索引
        // parentIndex 为未排序节点的父节点的索引
        let parentIndex = (unbalancedIndex - 1) / 2
        guard _hasHigherPriority(_elements[unbalancedIndex], _elements[parentIndex]) else { break }
        #if swift(>=3.2)
        _elements.swapAt(unbalancedIndex, parentIndex)
        #else
        swap(&_elements[unbalancedIndex], &_elements[parentIndex])
        #endif
        unbalancedIndex = parentIndex
    }
}

将优先级最高的元素出队列,也就是将数组中的第一个元素移除,同时我们也有可能需要移除数组中的任意一个的元素。进而我们可以将问题归结为:怎样移除指定索引处的元素。

当要移除指定索引的元素时,首先将指定索引处的元素和数组的最后一个元素交换位置,再将最后一个元素移除,这样原本在数组最后的元素移到了指定的索引处,这样有可能破坏了的最大堆的规则,所以要将指定位置的元素根据其优先级向下浮动,让它的子节点中值最大的一个和其交换位置,确保指定索引的优先级大于它所有子节点的优先级,然后将指定索引处的元素根据优先级向上浮动,确保指定索引处的元素优先级小于或等于其父节点的优先级。代码如下:

// 优先级最高的元素出队列
mutating func dequeue() -> Element? {
    guard let front = peek() else {
        return nil
    }

    removeAt(0)

    return front
}

// 移除任意一个元素
mutating func remove(_ element: Element) {
    for i in 0 ..< _elements.count {
        if _isEqual(_elements[i], element) {
            removeAt(i)
            return
        }
    }
}

// 移除指定索引的元素
private mutating func removeAt(_ index: Int) {

    let removingLast = index == _elements.count - 1
    if !removingLast {
        #if swift(>=3.2)
        _elements.swapAt(index, _elements.count - 1)
        #else
        swap(&_elements[index], &_elements[_elements.count - 1])
        #endif
    }

    _ = _elements.popLast()

    if !removingLast {
        bubbleToHigherPriority(index)
        bubbleToLowerPriority(index)
    }
}


// 向低优先级冒泡
private mutating func bubbleToLowerPriority(_ initialUnbalancedIndex: Int) {
    precondition(initialUnbalancedIndex >= 0)
    precondition(initialUnbalancedIndex < _elements.count)

    var unbalancedIndex = initialUnbalancedIndex
    while true {
        //
        let leftChildIndex = unbalancedIndex * 2 + 1
        let rightChildIndex = unbalancedIndex * 2 + 2

        var highestPriorityIndex = unbalancedIndex

        if leftChildIndex < _elements.count && _hasHigherPriority(_elements[leftChildIndex], _elements[highestPriorityIndex]) {
            highestPriorityIndex = leftChildIndex
        }

        if rightChildIndex < _elements.count && _hasHigherPriority(_elements[rightChildIndex], _elements[highestPriorityIndex]) {
            highestPriorityIndex = rightChildIndex
        }

        guard highestPriorityIndex != unbalancedIndex else { break }

        #if swift(>=3.2)
        _elements.swapAt(highestPriorityIndex, unbalancedIndex)
        #else
        swap(&_elements[highestPriorityIndex], &_elements[unbalancedIndex])
        #endif
        unbalancedIndex = highestPriorityIndex
    }
}

到此,RxSwift 的优先级队列 PriorityQueue 的所有功能已经实现,它使用最大堆排序,插入和删除元素的时间复杂度都是O(log(n))。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容