<think>我们被要求实现堆排序算法,并给出JavaScript代码。堆排序是一种基于二叉堆(通常使用最大堆或最小堆)的排序算法。其基本思想是:
1. 将待排序的序列构造成一个最大堆(或最小堆),此时整个序列的最大值(或最小值)就是堆顶的根节点。
2. 将根节点与末尾节点进行交换,此时末尾就为最大值(或最小值)。
3. 然后将剩余n-1个序列重新构造成一个堆,这样会得到n个元素中的次大值(或次小值)。如此反复执行,便能得到一个有序序列了。
在JavaScript中,我们通常使用数组来表示堆,因为堆是一个完全二叉树。对于数组中的第i个元素(索引从0开始):
- 它的父节点索引为:`Math.floor((i-1)/2)`
- 它的左子节点索引为:`2*i+1`
- 它的右子节点索引为:`2*i+2`
堆排序的步骤:
1. 构建最大堆(Build Max Heap):将无序数组构建成最大堆。
2. 调整堆(Heapify):从最后一个非叶子节点开始(即索引为`Math.floor(arr.length/2)-1`)向前调整,使得每个节点都满足最大堆的性质(父节点大于等于子节点)。
3. 堆排序:将堆顶元素(最大值)与当前堆的最后一个元素交换,然后将堆的大小减一(即排除最后一个元素),再对堆顶元素进行向下调整(Heapify)以维持最大堆性质。重复此过程直到堆中只剩下一个元素。
下面我们根据引用中提供的代码示例,结合堆排序的标准实现,给出一个清晰、正确且高效的JavaScript堆排序实现。
注意:我们使用最大堆来实现升序排序。
代码实现:
```javascript
function heapSort(arr) {
// 1. 构建最大堆(从最后一个非叶子节点开始调整)
let n = arr.length;
// 最后一个非叶子节点的索引:Math.floor(n/2)-1
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 2. 堆排序:将堆顶元素(最大值)与当前堆的最后一个元素交换,然后调整堆
for (let i = n - 1; i > 0; i--) {
// 将堆顶元素(最大值)与当前堆的最后一个元素交换
[arr[0], arr[i]] = [arr[i], arr[0]];
// 调整堆(此时堆的大小为i,即排除已经排序好的元素)
heapify(arr, i, 0);
}
return arr;
}
// 调整堆,使其满足最大堆性质
// n: 堆的大小(即数组中需要调整的部分长度)
// i: 当前需要调整的节点索引
function heapify(arr, n, i) {
let largest = i; // 初始化当前节点为最大值
let left = 2 * i + 1; // 左子节点
let right = 2 * i + 2; // 右子节点
// 如果左子节点存在且大于当前节点,则更新最大值索引
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// 如果右子节点存在且大于当前最大值节点,则更新最大值索引
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值不是当前节点,则交换并递归调整
if (largest !== i) {
[arr[i], arr[largest]] = [arr[largest], arr[i]];
// 递归调整受影响的子树
heapify(arr, n, largest);
}
}
// 测试
let arr = [12, 11, 13, 5, 6, 7];
console.log(heapSort(arr)); // 输出: [5, 6, 7, 11, 12, 13]
```
代码说明:
1. `heapSort`函数:主函数,负责构建初始最大堆和进行堆排序。
- 第一步:从最后一个非叶子节点开始,向前遍历,对每个节点调用`heapify`函数,构建最大堆。
- 第二步:将堆顶元素(最大值)与当前堆的最后一个元素交换,然后将堆的大小减一(即排除最后一个元素),再对堆顶元素进行`heapify`调整。重复此过程直到堆中只剩下一个元素。
2. `heapify`函数:调整堆,使其满足最大堆性质。它接收三个参数:数组`arr`、堆的大小`n`、当前要调整的节点索引`i`。
- 首先,假设当前节点`i`是最大值,然后比较其左右子节点,如果子节点大于当前节点,则更新最大值索引。
- 如果最大值索引不是当前节点,则交换当前节点和最大值节点,并递归调整受影响的子树。
时间复杂度:构建堆的时间复杂度为$O(n)$,每次调整堆的时间复杂度为$O(\log n)$,总共进行n-1次调整,所以堆排序的时间复杂度为$O(n \log n)$。
空间复杂度:由于是原地排序,除了递归调用栈(可优化为迭代)外,不需要额外空间,所以空间复杂度为$O(1)$。但递归版本的堆排序在最坏情况下递归深度为$O(\log n)$,所以递归栈空间为$O(\log n)$。但通常我们将其视为原地排序算法。
注意:上面的代码使用了递归,在实际应用中,如果数据量很大,递归可能导致栈溢出。我们可以将递归的`heapify`改为迭代版本以避免递归深度过大。下面是迭代版本的`heapify`:
```javascript
function heapify(arr, n, i) {
let largest = i;
while (true) {
let left = 2 * i + 1;
let right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest === i) break;
[arr[i], arr[largest]] = [arr[largest], arr[i]];
i = largest; // 继续调整受影响的子树
}
}
```
使用迭代版本的`heapify`可以避免递归调用栈过深的问题,更加健壮。
因此,完整的堆排序代码(使用迭代调整)如下:
```javascript
function heapSort(arr) {
let n = arr.length;
// 构建最大堆
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 堆排序
for (let i = n - 1; i > 0; i--) {
[arr[0], arr[i]] = [arr[i], arr[0]];
heapify(arr, i, 0);
}
return arr;
}
function heapify(arr, n, i) {
let largest = i;
while (true) {
let left = 2 * i + 1;
let right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest === i) break;
[arr[i], arr[largest]] = [arr[largest], arr[i]];
i = largest;
}
}
```
这样,我们就实现了一个健壮的堆排序算法。
§§相关问题§§
1. 堆排序的时间复杂度是多少?为什么是$O(n \log n)$?
2. 堆排序在什么情况下表现最好?
3. 堆排序和快速排序相比有什么优缺点?
4. 除了堆排序,还有哪些常见的排序算法?它们的时间复杂度和空间复杂度分别是多少?</think>### JavaScript 堆排序算法实现
堆排序是一种基于二叉堆(完全二叉树)的高效排序算法,通过构建最大堆/最小堆实现升序/降序排序。其核心思想是将数组视为完全二叉树,通过调整节点位置实现堆化。以下是完整的 JavaScript 实现:
#### 算法步骤
1. **构建最大堆**:将无序数组转化为最大堆(父节点 ≥ 子节点)
2. **堆排序**:
- 交换堆顶(最大值)与末尾元素
- 缩小堆范围并重新调整堆
- 重复直到堆大小为 1
#### JavaScript 代码实现
```javascript
function heapSort(arr) {
// 构建最大堆(从最后一个非叶子节点开始)
for (let i = Math.floor(arr.length / 2) - 1; i >= 0; i--) {
heapify(arr, arr.length, i);
}
// 堆排序:依次将堆顶元素移到末尾
for (let i = arr.length - 1; i > 0; i--) {
[arr[0], arr[i]] = [arr[i], arr[0]]; // 交换堆顶与末尾元素
heapify(arr, i, 0); // 调整剩余堆
}
return arr;
}
// 堆调整函数(最大堆)
function heapify(arr, n, i) {
let largest = i; // 初始化最大值为当前节点
const left = 2 * i + 1; // 左子节点索引
const right = 2 * i + 2; // 右子节点索引
// 比较左子节点
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// 比较右子节点
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// 若最大值不是当前节点,交换并递归调整
if (largest !== i) {
[arr[i], arr[largest]] = [arr[largest], arr[i]];
heapify(arr, n, largest); // 递归调整子树
}
}
// 示例用法
const arr = [12, 11, 13, 5, 6, 7];
console.log(heapSort(arr)); // 输出: [5, 6, 7, 11, 12, 13]
```
#### 关键点解析
1. **堆构建起点**:从最后一个非叶子节点(索引 `Math.floor(n/2)-1`)开始调整,因为叶子节点无需调整[^1]。
2. **堆调整**:`heapify` 函数确保以 `i` 为根的子树满足最大堆性质,时间复杂度 $O(\log n)$。
3. **原地排序**:直接在原数组操作,空间复杂度 $O(1)$(递归栈空间不计入)。
4. **时间复杂度**:
- 建堆阶段:$O(n)$
- 排序阶段:$O(n \log n)$
- 总复杂度:$O(n \log n)$
#### 算法特点
- **稳定性**:非稳定排序(交换可能破坏相同元素顺序)
- **适用场景**:大数据量且对最坏时间复杂度有要求时(如实时系统)
- **优势**:时间复杂度稳定为 $O(n \log n)$,无需额外空间
> 注:此实现使用递归调整堆,大数据量时可改用迭代避免栈溢出风险[^2]。