为什么我们通过堆而不是二叉搜索树进行排序?
Why do we sort via Heaps instead of Binary Search Trees?
堆可以在O(n logn)时间内从列表中构造出来,因为向堆中插入一个元素需要O(logn)时间,而且有n个元素。
同样,二叉搜索树可以在O(n logn)时间内从列表构造出来,因为向BST中插入一个元素平均需要logn时间,而且有n个元素。
从最小到最大遍历堆需要 O(n logn) 时间(因为我们必须弹出 n 个元素,并且每个弹出都需要 O(logn) 接收器操作)。从最小到最大遍历 BST 需要 O(n) 时间(字面上只是中序遍历)。
因此,在我看来构建两个结构花费的时间相同,但 BST 的迭代速度更快。那么,为什么我们使用 "Heapsort" 而不是 "BSTsort"?
编辑:感谢 Tobias 和 lrlreon 的回答!综上所述,以下是我们使用堆而不是 BST 进行排序的几点。
- 堆的构造实际上可以在O(n) 时间内完成,而不是O(nlogn) 时间。这使得堆构建比 BST 构建更快。
- 此外,数组可以很容易地就地转换为堆,因为堆始终是完整的二叉树。 BST 不能简单地实现为数组,因为不能保证 BST 是完整的二叉树。这意味着 BST 需要额外的 O(n) space 分配来排序,而堆只需要 O(1).
- 堆上的所有操作都保证是O(logn)时间。除非平衡,否则 BST 可能具有 O(n) 操作。堆比平衡 BST 更容易实现。
- 如果您需要在创建堆后修改一个值,您需要做的就是应用sink 或swim 操作。修改 BST 中的值在概念上要困难得多。
我可以想象您更喜欢(二进制)堆而不是搜索树的原因有很多:
- 构造:通过从最小到最大子树应用heapify操作bottom-up,二叉堆实际上可以在O(n)时间内构造。
修改:二叉堆的所有操作都比较简单:
- 最后插入了一个元素?筛选直到堆条件成立
- 将最后一个元素调换到开头? Swift 直到堆条件成立
- 更改了条目的密钥?根据变化的方向向上或向下筛选
概念简单:由于其隐式数组表示,任何了解基本索引方案的人都可以实现二叉堆(2i+1,2i+2是i)的children,没有考虑很多困难的特殊情况。
如果您在二叉搜索树中查看这些操作,理论上
它们也很简单,但是必须显式存储树,例如使用指针,并且大多数操作都需要树
重新平衡 以保持 O(log n) 高度,这需要复杂的旋转(红色 black-trees)或 splitting/merging
节点 (B-trees)
编辑:存储:正如 Irleon 指出的那样,要存储 BST,您还需要更多存储空间,因为除了值本身,这可能是一个很大的存储开销,尤其是对于小值类型。同时,堆不需要额外的指针。
回答你关于排序的问题:BST 需要 O(n) 时间来遍历 in-order,构造过程需要 O(n log n) 操作,如前所述,它要复杂得多。
同时Heapsort实际上可以实现in-place通过在O(n)时间内从输入数组构建一个max-heap然后重复交换最大元素到tbe并缩小堆。您可以将 Heapsort 视为具有有用数据结构的插入排序,可以让您在 O(log n) 时间内找到下一个最大值。
如果排序方法包括将元素存储在数据结构中并在以排序方式提取之后,那么,尽管两种方法(堆和 bst)具有相同的渐近复杂度 O(n log n),堆往往会更快。原因是堆总是一棵完美平衡的树,它的操作总是 O(log n),以一种确定的方式,而不是平均。对于 bst,根据平衡的方法,插入和删除往往比堆花费更多的时间,无论使用哪种平衡方法。另外,堆通常是用一个数组来实现的,数组存储树的层级遍历,而不需要存储任何类型的指针。因此,如果您知道元素的数量(通常是这种情况),则堆所需的额外存储空间少于 bst。
在对数组进行排序的情况下,有一个非常重要的原因,即堆比bst更可取:您可以使用相同的数组来存储堆;无需使用额外内存。
堆可以在O(n logn)时间内从列表中构造出来,因为向堆中插入一个元素需要O(logn)时间,而且有n个元素。
同样,二叉搜索树可以在O(n logn)时间内从列表构造出来,因为向BST中插入一个元素平均需要logn时间,而且有n个元素。
从最小到最大遍历堆需要 O(n logn) 时间(因为我们必须弹出 n 个元素,并且每个弹出都需要 O(logn) 接收器操作)。从最小到最大遍历 BST 需要 O(n) 时间(字面上只是中序遍历)。
因此,在我看来构建两个结构花费的时间相同,但 BST 的迭代速度更快。那么,为什么我们使用 "Heapsort" 而不是 "BSTsort"?
编辑:感谢 Tobias 和 lrlreon 的回答!综上所述,以下是我们使用堆而不是 BST 进行排序的几点。
- 堆的构造实际上可以在O(n) 时间内完成,而不是O(nlogn) 时间。这使得堆构建比 BST 构建更快。
- 此外,数组可以很容易地就地转换为堆,因为堆始终是完整的二叉树。 BST 不能简单地实现为数组,因为不能保证 BST 是完整的二叉树。这意味着 BST 需要额外的 O(n) space 分配来排序,而堆只需要 O(1).
- 堆上的所有操作都保证是O(logn)时间。除非平衡,否则 BST 可能具有 O(n) 操作。堆比平衡 BST 更容易实现。
- 如果您需要在创建堆后修改一个值,您需要做的就是应用sink 或swim 操作。修改 BST 中的值在概念上要困难得多。
我可以想象您更喜欢(二进制)堆而不是搜索树的原因有很多:
- 构造:通过从最小到最大子树应用heapify操作bottom-up,二叉堆实际上可以在O(n)时间内构造。
修改:二叉堆的所有操作都比较简单:
- 最后插入了一个元素?筛选直到堆条件成立
- 将最后一个元素调换到开头? Swift 直到堆条件成立
- 更改了条目的密钥?根据变化的方向向上或向下筛选
概念简单:由于其隐式数组表示,任何了解基本索引方案的人都可以实现二叉堆(2i+1,2i+2是i)的children,没有考虑很多困难的特殊情况。
如果您在二叉搜索树中查看这些操作,理论上 它们也很简单,但是必须显式存储树,例如使用指针,并且大多数操作都需要树 重新平衡 以保持 O(log n) 高度,这需要复杂的旋转(红色 black-trees)或 splitting/merging 节点 (B-trees)编辑:存储:正如 Irleon 指出的那样,要存储 BST,您还需要更多存储空间,因为除了值本身,这可能是一个很大的存储开销,尤其是对于小值类型。同时,堆不需要额外的指针。
回答你关于排序的问题:BST 需要 O(n) 时间来遍历 in-order,构造过程需要 O(n log n) 操作,如前所述,它要复杂得多。
同时Heapsort实际上可以实现in-place通过在O(n)时间内从输入数组构建一个max-heap然后重复交换最大元素到tbe并缩小堆。您可以将 Heapsort 视为具有有用数据结构的插入排序,可以让您在 O(log n) 时间内找到下一个最大值。
如果排序方法包括将元素存储在数据结构中并在以排序方式提取之后,那么,尽管两种方法(堆和 bst)具有相同的渐近复杂度 O(n log n),堆往往会更快。原因是堆总是一棵完美平衡的树,它的操作总是 O(log n),以一种确定的方式,而不是平均。对于 bst,根据平衡的方法,插入和删除往往比堆花费更多的时间,无论使用哪种平衡方法。另外,堆通常是用一个数组来实现的,数组存储树的层级遍历,而不需要存储任何类型的指针。因此,如果您知道元素的数量(通常是这种情况),则堆所需的额外存储空间少于 bst。
在对数组进行排序的情况下,有一个非常重要的原因,即堆比bst更可取:您可以使用相同的数组来存储堆;无需使用额外内存。