Java最高

开始使用Spring 5和Spring Boot 2,通过学习的春天课程:

>>看看这个课程

1.介绍

在本教程中,我们将看到堆排序是如何工作的,我们将用Java实现它。

堆排序是基于堆数据结构。为了正确地理解堆排序,我们将首先深入研究堆以及它们是如何实现的。

2.堆数据结构

堆是一个专门的基于树的数据结构。因此它是由节点组成的。我们将元素分配给节点:每个节点只包含一个元素。

此外,节点可以有子节点。如果一个节点没有任何子节点,我们称它为叶节点。

Heap的特别之处在于两点:

  1. 每个节点的值都必须是小于或等于其子节点中存储的所有值
  2. 这是一个完整的树,这意味着它的可能高度最小

因为第一条规则,最小的元素总是在树的根中

我们如何执行这些规则取决于实现。

堆通常用于实现优先级队列,因为堆是提取最小(或最大)元素的非常有效的实现。

2.1。堆变体

Heap有许多变体,它们在一些实现细节上都有所不同。

例如,我们上面描述的是最小堆,因为父节点总是小于所有子节点。或者,我们也可以定义Max-Heap,在这种情况下,父节点总是大于子节点。因此,最大的元素将位于根节点中。

我们可以从许多树实现中进行选择。最简单的就是二叉树。在二叉树中,每个节点最多可以有两个子节点。我们称之为左子右子

实施第二条规则的最简单方法是使用一个完整二叉树。一个完整二叉树遵循一些简单的规则:

  1. 如果一个节点只有一个子节点,那么它应该是它的左子节点
  2. 只有最深处的最右边的节点才能有一个子节点
  3. 树叶只能在最深处

让我们用一些例子来看看这些规则:

1 2 3 4 5 6 7 8 9 10  () () () () () () () () () () / \ / \ / \ / \ / \ / / / \ () () () () () () () () () () () () () () / \ / \ / \ / / \ () () () () () () () () () / ()

1、2、4、5和7树遵循规则。

树3和树6违反规则1,树8和树9违反规则2,树10违反规则3。

在本教程中,我们将关注二叉树的最小堆实现。

2.2。插入元素

我们应该以一种保持堆不变量的方式实现所有的操作。这样,我们就可以使用重复插入构建Heap,所以我们将重点关注单个插入操作。

插入元素的步骤如下:

  1. 创建一个新的叶子,它是最深处最右边可用的槽,并将项目存储在该节点中
  2. 如果元素小于它的父元素,则交换它们
  3. 继续执行第2步,直到元素小于它的父元素或成为新的根元素

注意,第2步不会违反Heap规则,因为如果我们用一个小于1的值替换一个节点的值,它仍然会小于它的子节点。

让我们来看一个例子!我们想要将4插入到这个Heap中:

2 / \ / \ 3 6 / \ 5 7

第一步是创建一个存储4的新叶子:

2 / \ / \ 3 6 / \ / 5 7 4

因为4小于它的父结点6,我们交换它们:

2 / \ / \ 3 4 / \ / 5 7 6

现在我们检查4是否小于它的父结点。因为它的父结点是2,所以我们停止。堆仍然有效,我们插入了4号。

让我们插入1:

2 / \ / \ 3 4 / \ / 5 7 6 1

我们必须交换1和4:

2 / \ / \ 3 1 / \ / \ 5 7 6 4

现在我们应该交换1和2:

1 / \ / \ 3 2 / \ / \ 5 7 6 4

因为1是新的根,我们停止。

3.Java中的堆实现

因为我们使用完整二叉树,我们可以用数组来实现它:数组中的一个元素就是树中的一个节点。我们从左到右,从上到下用数组索引标记每个节点,方法如下:

0 / \ / \ 1 2 / \ / 3 4 5

我们唯一需要做的就是跟踪树中存储了多少元素。这样,我们想要插入的下一个元素的索引将是数组的大小。

使用这个索引,我们可以计算父节点和子节点的索引:

  • 家长:(index - 1) / 2
  • 左子:2 * index + 1
  • 正确的孩子:2 * index + 2

因为我们不想为数组重新分配而烦恼,所以我们将进一步简化实现,并使用ArrayList

一个基本的二叉树实现是这样的:

class BinaryTree {List元素= new ArrayList<>();void add(E E) {elements.add(E);} boolean isEmpty(){返回元素。isEmpty();} E elementAt(int index) {return elements.get(index);} int parentIndex(int index) {return (index - 1) / 2;} int leftChildIndex(int index){返回2 *索引+ 1;} int rightChildIndex(int index){返回2 *索引+ 2;}}

上面的代码只将新元素添加到树的末尾。因此,如有必要,需要向上遍历新元素。我们可以用下面的代码来做:

class Heap>{//…void add(E E) {elements.add(E);int elementIndex = elements.size() - 1;while (!isRoot(elementIndex) && isCorrectChild(elementIndex)) {int parentIndex = parentIndex(elementIndex);交换(elementIndex parentIndex);elementIndex = parentIndex;}} boolean isRoot(int index) {return index == 0;} boolean isCorrectChild(int index) {return isCorrect(parentIndex(index), index);} boolean isCorrect(int parentIndex, int childIndex) {if (!isValidIndex(parentIndex) || !isValidIndex(childIndex)){返回true;} return elementAt(parentIndex).compareTo(elementAt(childIndex)) < 0; } boolean isValidIndex(int index) { return index < elements.size(); } void swap(int index1, int index2) { E element1 = elementAt(index1); E element2 = elementAt(index2); elements.set(index1, element2); elements.set(index2, element1); } // ... }

注意,因为我们需要比较元素,所以它们需要实现java.util.Comparable

4.堆排序

因为堆的根总是包含最小的元素,堆排序背后的想法非常简单:删除根节点,直到堆变成空

我们唯一需要的是一个删除操作,它使Heap保持一致的状态。我们必须确保我们不违反二叉树的结构或堆属性。

为了保持结构,我们不能删除任何元素,除了最右边的叶节点。因此,方法是将元素从根节点中删除,并将最右边的叶节点存储在根节点中。

但是这个操作肯定会违反Heap属性。所以如果新的根节点大于它的任何一个子节点,我们将它与最小的子节点交换。因为最小的子节点比所有其他子节点都小,所以它不会违反Heap属性。

我们不断交换,直到元素变成叶节点,或者小于所有子节点。

让我们删除这个树的根:

1 / \ / \ 3 2 / \ / \ 5 7 6 4

首先,我们把最后一片叶子放在根上:

4 / \ / \ 3 2 / \ / 5 7 6

然后,因为它比它的两个子结点都大,我们把它和它的最小子结点交换,也就是2:

2 / \ / \ 3 4 / \ / 5 7 6

4比6小,所以我们停止。

5.Java中的堆排序实现

有了这些,删除根(弹出)看起来像这样:

class Heap>{//…E pop() {if (isEmpty()){抛出新的IllegalStateException(“你不能从空堆弹出”);} E result = elementAt(0);int lasElementIndex = elements.size() - 1;交换(0,lasElementIndex);elements.remove (lasElementIndex);int elementIndex = 0;while (!isLeaf(elementIndex) && !isCorrectParent(elementIndex)) {int smallerChildIndex = smallerChildIndex(elementIndex);交换(elementIndex smallerChildIndex);elementIndex = smallerChildIndex; } return result; } boolean isLeaf(int index) { return !isValidIndex(leftChildIndex(index)); } boolean isCorrectParent(int index) { return isCorrect(index, leftChildIndex(index)) && isCorrect(index, rightChildIndex(index)); } int smallerChildIndex(int index) { int leftChildIndex = leftChildIndex(index); int rightChildIndex = rightChildIndex(index); if (!isValidIndex(rightChildIndex)) { return leftChildIndex; } if (elementAt(leftChildIndex).compareTo(elementAt(rightChildIndex)) < 0) { return leftChildIndex; } return rightChildIndex; } // ... }

就像我们之前说的,排序只是创建一个Heap,然后重复地删除根目录:

class Heap>{//…static > List sort(Iterable元素){Heap Heap = of(元素);List result = new ArrayList<>();while (!heap.isEmpty()) {result.add(heap.pop());}返回结果;} static > Heap of(Iterable元素){Heap result = new Heap<>();for (E元素:元素){result.add(元素);}返回结果;} / /……}

我们可以验证它的工作与以下测试:

@Test void givenNotEmptyIterable_whenSortCalled_thenItShouldReturnElementsInSortedList(){//给定List elements = Arrays. getstring ();asList(3, 5, 1, 4, 2);//当List sortedElements = Heap.sort(elements);/ /然后为了sortedElements .isEqualTo(数组。asList(1, 2, 3, 4, 5));}

注意,我们可以提供一个就地排序的实现,这意味着我们在获取元素的同一个数组中提供结果。此外,通过这种方式我们不需要任何中间内存分配。然而,这种实现有点难以理解。

6.时间复杂度

堆排序包括两个关键步骤,插入一个元素,删除根节点。这两个步骤都很复杂O (log n)

因为我们重复这两个步骤n次,总的排序复杂度是O (n log n)

注意,我们没有提到数组重新分配的成本,但由于它是O (n),它不会影响整体的复杂性。同样,正如我们前面提到的,可以实现就地排序,这意味着不需要重新分配数组。

同样值得一提的是,50%的元素是叶子,75%的元素位于最底部的两个层次。因此,大多数插入操作不会超过两个步骤。

注意,在真实数据中,快速排序通常比堆排序的性能更好。幸运的是堆排序总是有最坏的情况O (n log n)时间复杂度。

7.结论

在本教程中,我们看到了二叉堆和堆排序的实现。

即使它的时间复杂性O (n log n)在大多数情况下,它并不是真实数据的最佳算法。

像往常一样,有一些例子在GitHub

Java底部

开始使用Spring 5和Spring Boot 2,通过学习的春天课程:

>>看看这个课程
4评论
最古老的
最新的
内联反馈
查看所有评论
对这篇文章的评论关闭!