在堆的古枝上,数字舞动如风,交换岁月,序列重塑成升腾的旋律。
一、堆排序
堆排序是一种基于比较的排序算法,它使用了数据结构“堆”。堆排序可以被看作是一种改进的选择排序,它的工作原理是将待排序的序列构造成一个大顶堆或小顶堆,然后移除堆顶元素并放在序列的末尾,再调整剩余元素使之保持堆的性质,如此反复直到所有元素都被排序。
堆排序的主要步骤如下:
- 构造初始堆:将待排序序列构造成一个大顶堆(或小顶堆)。
- 交换数据:将堆顶元素与末尾元素交换,使末尾元素最大(或最小)。然后将剩余元素重新调整为大顶堆。
- 重复步骤 2,直到堆中只剩下一个元素未被排序。
二、发展历史
堆排序是一种经典的排序算法,其发展历史可以追溯到 20 世纪初期。以下是堆排序的主要发展历程:
- 堆的提出(1902 年):堆最早由德国的计算机科学家
卡尔·弗里德里希·高斯
在他的一篇论文中提出。高斯提出了一种数据结构,称为 “堆”,用于解决优先队列的问题。然而,他没有提出堆排序算法。 - 堆排序算法的提出(1964 年):堆排序算法最早由
罗伯特·弗洛伊德(Robert W. Floyd)
在其 1964 年的一篇论文中提出。该论文中详细描述了堆排序算法的实现方法和性能分析。 - 堆排序的进一步优化(1969 年):在唐纳德·克努斯(Donald Knuth)的《计算机程序设计艺术》(The Art of Computer Programming)第 3 卷中,他对堆排序进行了进一步的优化和详细描述。
- 实际应用和性能分析(1970 年代末):随着计算机硬件的发展和堆排序算法的优化,堆排序开始被广泛应用于实际的计算机系统中,并得到了更多的性能分析和改进。
- 算法分析和改进(1980 年代至今):随着对算法性能分析和改进方法的深入研究,堆排序算法在实际应用中的效率和性能得到了进一步提升。同时,人们也提出了一些变种的堆排序算法,如改进的堆数据结构和更高效的实现方式,以应对不同场景下的排序需求。
三、处理流程
场景假设:我们需要将下面的无序序列使用堆排序按从小到大进行排序。
堆排序的流程如下:
- 构造初始堆:我们首先将待排序的序列构造成一个大顶堆。
- 交换数据:我们将堆顶元素(最大的元素)与末尾元素交换,使末尾元素最大。然后我们将剩余元素重新调整为大顶堆。
- 重复步骤 2:我们继续将堆顶元素与当前未排序序列的末尾元素交换,并调整剩余元素为大顶堆。重复这个过程,直到堆中只剩下一个元素未被排序。
四、算法实现
public class HeapSort {
// 堆排序函数
public void sort(int arr[]) {
int arrayLength = arr.length;
// 构建初始堆
for (int i = arrayLength / 2 - 1; i >= 0; i--)
heapify(arr, arrayLength, i);
// 一个个从堆中取出元素
for (int i = arrayLength - 1; i >= 0; i--) {
// 将当前根节点移到数组末尾
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 对剩余的堆进行调整
heapify(arr, i, 0);
}
}
// 堆调整函数
void heapify(int arr[], int heapSize, int rootNode) {
int largestNode = rootNode; // 初始化最大节点为根节点
int leftNode = 2 * rootNode + 1; // 左子节点 = 2*根节点 + 1
int rightNode = 2 * rootNode + 2; // 右子节点 = 2*根节点 + 2
// 如果左子节点大于根节点
if (leftNode < heapSize && arr[leftNode] > arr[largestNode])
largestNode = leftNode;
// 如果右子节点大于当前最大节点
if (rightNode < heapSize && arr[rightNode] > arr[largestNode])
largestNode = rightNode;
// 如果最大节点不是根节点
if (largestNode != rootNode) {
int swap = arr[rootNode];
arr[rootNode] = arr[largestNode];
arr[largestNode] = swap;
// 递归调整受影响的子树
heapify(arr, heapSize, largestNode);
}
}
// 打印数组的函数
static void printArray(int arr[]) {
int n = arr.length;
for (int i = 0; i < n; ++i)
System.out.print(arr[i] + " ");
System.out.println();
}
// 主函数
public static void main(String args[]) {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = arr.length;
HeapSort hs = new HeapSort();
hs.sort(arr);
System.out.println("排序后的数组是");
printArray(arr);
}
}
算法时间复杂度分析:
情况 | 时间复杂度 | 计算公式 | 公式解释 |
---|---|---|---|
最好情况 | O ( n l o g n ) O(nlogn) O(nlogn) | T ( n ) = n l o g n T(n) = nlogn T(n)=nlogn | 这是因为无论输入序列的顺序如何,堆排序都需要进行建堆和调整堆的过程,这两个过程的时间复杂度都是 O ( n l o g n ) O(nlogn) O(nlogn) |
平均情况 | O ( n l o g n ) O(nlogn) O(nlogn) | T ( n ) = n l o g n T(n) = nlogn T(n)=nlogn | 这是因为无论输入序列的顺序如何,堆排序都需要进行建堆和调整堆的过程,这两个过程的时间复杂度都是 O ( n l o g n ) O(nlogn) O(nlogn) |
最坏情况 | O ( n l o g n ) O(nlogn) O(nlogn) | T ( n ) = n l o g n T(n) = nlogn T(n)=nlogn | 这是因为无论输入序列的顺序如何,堆排序都需要进行建堆和调整堆的过程,这两个过程的时间复杂度都是 O ( n l o g n ) O(nlogn) O(nlogn) |
五、算法特性
堆排序(Heap Sort)是一种基于二叉堆数据结构的排序算法。它具有以下特性:
- 稳定性: 由于堆排序的过程中会破坏相同元素之间的相对顺序,所以它是一种非稳定的排序算法。因为在堆的构建和调整过程中,相同元素的相对位置可能会改变。
- 原地性: 堆排序是一种
原地排序算法
,它不需要额外的辅助空间,只需要使用输入数组本身进行排序,因此空间复杂度为 O ( 1 ) O(1) O(1)。 - 不适合小规模数据: 虽然堆排序的时间复杂度较为稳定,但由于它的常数因子较大,因此在数据规模较小的情况下,其性能可能不如插入排序或选择排序等简单排序算法。
- 堆的性质: 堆排序利用了堆的性质,即大顶堆(或小顶堆)中每个节点的值都大于(或小于)其子节点的值,这样通过构建堆和调整堆的过程,可以使得整个数组按照升序(或降序)排列。
六、小结
堆排序是一种高效的排序算法,适用于大规模数据的排序任务。它利用了堆这种数据结构的特性,实现了原地排序和稳定的时间复杂度。尽管堆排序的实现过程相对复杂,但其性能优秀,是算法领域中的经典之作。