internal class PrioritySet(private val list: MutableList<Int> = mutableListOf())
从 PrioritySet 的声明可以了解到的信息包括:
- 这是一个内部类;
- 包含一个可变列表参数,默认为空列表,列表保存 Int 类型的元素。
PrioritySet 类方法列表:
internal class PrioritySet(private val list: MutableList<Int> = mutableListOf()) {
// 添加数据
fun add(value: Int)
// 判空
fun isEmpty(): Boolean
fun isNotEmpty(): Boolean
// 查看堆顶元素
fun peek(): Int
// 从堆中移除并返回最大值(不重复)
fun takeMax(): Int
// 验证堆(Heap)数据结构是否满足堆性质的函数
fun validateHeap()
}
向堆中添加元素
// Add a value to the heap
fun add(value: Int) {
// Filter trivial duplicates
if (list.isNotEmpty() && (list[0] == value || list[list.size - 1] == value)) return
var index = list.size
list.add(value)
// Shift the value up the heap.
while (index > 0) {
val parent = ((index + 1) ushr 1) - 1
val parentValue = list[parent]
if (value > parentValue) {
list[index] = parentValue
} else break
index = parent
}
list[index] = value
}
- 第一步先过滤重复项,若 list 不为空且插入值与 list 的第一个元素的值或最后一个元素值相等,则直接 return。否则继续执行下一步;
- 第二步取列表的长度然后将其作为 index 添加到可变列表 list;
- 当 index 大于 0 时,开启一个循环
- 计算当前节点的父节点的索引,
(index + 1) ushr 1当前值 + 1 再右移(相当于除以 2),即取出 index 到 0 之间的中间值,为父节点的索引。 - 然后通过父节点的索引取出父节点中的值。
- 比较父节点的值和当前插入值,如果当前插入值大于父节点中的值,更新列表的最后一个元素为父节点的值,然后将插入值的索引 index 更新为父节点的索引值。
- 否则跳出循环。
- 计算当前节点的父节点的索引,
- 结束循环后,将插入值更新到最最新的索引值 index 位置上。
这是一个通过列表维护一个二叉树的结构,维护一个最大堆的特性,操作如下:
- 计算当前节点的父节点索引。
- 比较当前值与父节点的值。
- 如果当前值大于父节点的值,则将父节点的值下移到当前节点,并更新当前索引为父节点索引。
- 如果当前值不大于父节点的值,则退出循环。
最大堆
最大堆是一个完全二叉树,即除了最后一层,其他层都是满节点的。并且最后一层是从左到右排序。
在最大堆中,每个节点的值都大于或等于其子节点的值。这意味着根节点的值最大。
每个节点的左子节点大于右子节点。
常见的实现方法是使用数组来表示堆。对于索引为 i 的节点,其父节点和子节点的位置可以通过以下公式计算:
- 父节点索引:
P(i) = (i - 1) // 2(//运算为整数除法向下取整) - 左子节点索引:
L(i) = 2 * i + 1 - 右子节点索引:
R(i) = 2 * i + 2
最大堆数组通常是层次遍历的顺序保存二叉树的值。
判空函数
判空方法是直接通过判断参数 list 是否为空:
fun isEmpty() = list.isEmpty()
fun isNotEmpty() = list.isNotEmpty()
查看堆顶
fun peek() = list.first()
最大堆的堆顶一般是第一个元素。
验证堆
@Suppress("ExceptionMessage")
fun validateHeap() {
val size = list.size
for (index in 0 until size / 2) {
val left = (index + 1) * 2 - 1
val right = (index + 1) * 2
check(list[index] >= list[left])
check(right >= size || list[index] >= list[right])
}
}
根据 list 的元素数量进行 0 到 size 的一半遍历,为什么是一半呢?因为在一个堆结构中,所有的叶子节点都位于数组的后半部分,而非叶子节点(即具有至少一个子节点的节点)位于数组的前半部分。
验证方法也是安装左右节点的索引公式,计算出左右子节点的索引,然后根据最大堆的两个特性进行检查:
- 每个节点的值都大于或等于其子节点的值。
- 每个节点的左子节点大于右子节点。
移除并返回最大值
从堆中取出最大值(移除并返回),同时处理堆中的重复值。
fun takeMax(): Int {
runtimeCheck(list.size > 0) { "Set is empty" }
val value = list[0]
while (list.isNotEmpty() && list[0] == value) {
list[0] = list.last()
list.removeAt(list.size - 1)
var index = 0
val size = list.size
val max = list.size ushr 1
while (index < max) {
val indexValue = list[index]
val left = (index + 1) * 2 - 1
val leftValue = list[left]
val right = (index + 1) * 2
if (right < size) {
val rightValue = list[right]
if (rightValue > leftValue) {
if (rightValue > indexValue) {
list[index] = rightValue
list[right] = indexValue
index = right
continue
} else break
}
}
if (leftValue > indexValue) {
list[index] = leftValue
list[left] = indexValue
index = left
} else break
}
}
return value
}
首先进行 list 检查:
internal inline fun runtimeCheck(value: Boolean, lazyMessage: () -> Any) {
if (!value) {
val message = lazyMessage()
composeRuntimeError(message.toString())
}
}
internal fun composeRuntimeError(message: String): Nothing {
throw ComposeRuntimeError(
"Compose Runtime internal error. Unexpected or incorrect use of the Compose " +
"internal runtime API ($message). Please report to Google or use " +
"https://goo.gle/compose-feedback"
)
}
如果没有元素抛出 ComposeRuntimeError。
取出第一个元素,开启一个循环,当 list 不为空且 list 的第一个元素与预先取出的值相等时进行循环:
// in takeMax()
list[0] = list.last() // 将最后一个元素移动到堆顶
list.removeAt(list.size - 1) // 移除最后一个元素
var index = 0 // 内部声明一个 index 指针
val size = list.size // 取出新的 list 长度
val max = list.size ushr 1 // list 长度的一半,也就是之前用来验证堆性质时遍历的数量
// 开启一个循环,遍历所有非叶子结点
while (index < max) {
val indexValue = list[index] // 取出 index 位置的值
val left = (index + 1) * 2 - 1 // index 索引节点的左子节点索引
val leftValue = list[left] // index 索引节点的左子节点的值
val right = (index + 1) * 2 // index 索引节点的右子节点索引
// 右子节点索引小于最大索引值,防止右子节点索引超出 list 最大长度
if (right < size) {
val rightValue = list[right] // index 索引节点的右子节点的值
// 如果右子节点的值大于左子节点的值
if (rightValue > leftValue) {
// 右子节点大于父节点的值
if (rightValue > indexValue) {
// 交换位置,右子节点变更为父节点
list[index] = rightValue
// 交换位置,父节点的值移动到右节点
list[right] = indexValue
index = right // index 索引指向右子节点,继续下一轮节点交换
continue
} else break // 右子节点本身小于左子节点的值,满足最大堆特性,无需其他操作
}
}
// 如果左子节点的值大于父节点的值,两者交换位置,index 指向新的左子节点
if (leftValue > indexValue) {
list[index] = leftValue
list[left] = indexValue
index = left
} else break
}
return value
通过阅读代码,这个函数的基本逻辑就是:取出最大堆中最大的值,重新对二叉树进行更新排序,确保满足最大堆的特性。
总结
PropertySet 实现了一个集合,允许将整数记录到集合中,并高效地从集合中提取最大的最大值。它使用堆结构来实现堆排序,确保即使值反复添加和移除,添加或移除值的操作也是 O(log N) 的时间复杂度。
它是 Compose 底层数据结构 SlotTable 中使用到的一环,本质上还是学习了解最大堆的实现逻辑。