js 堆排序

本文详细介绍了堆排序算法,包括堆的概念、保持堆性质的操作、建堆过程及最终的堆排序算法。通过实例解释了如何实现堆排序,并提供了JavaScript代码示例。

原文地址
我感觉这篇写的最容易的理解, 堆排序把我卡了挺久的,对于一个非科班的人,

其实算法和专业没啥关系,但科班的一般都有上过这门课.

主要是利用堆的性质 和 递归,也可以非递归 . 关键是从最后一个非叶节点入手

这里写图片描述
学了一段时间了,都没有把学习心得放网上,现在才发现这样不好不好。。。因此开了博客把自己的学习点滴或者感兴趣的内容记录下来,先讲讲堆排序吧。

根据《算法导论》,理解堆排序需要了解:堆结构、保持堆性质、建堆、最后是堆排序算法。

一、堆

一种数据结构,可视为完全二叉树。如下图

这里写图片描述

一个父节点最多分二叉,圆圈上面数字是节点序号,可以看到一层层由左向右排列。圆圈里面数字是该节点存的值。其中序号1为整个树的根节点,2、3为其子节点。另外,序号2也是子树(将2、4、5视为一个子树)的根节点。6~10为叶节点,叶节点没有子节点。

     二叉堆分为最大堆和最小堆。最大堆指堆中最大元素放在父节点,最小堆指堆中最小元素放在父节点。堆排序中用最大堆。

另外还需要知道堆的几个结论:

1、设某节点序号为i,则其父节点为⌊i/2⌋,2*i为左子节点序号,2*i+1为右子节点序号。其中,⌊⌋为向下取整符号。

2、当存储了n个元素时,⌊n/2⌋+1、⌊n/2⌋+1、···、n为叶节点。

这些可以自己举例验证。就不多说了。

二、保持堆性质

     保持堆性质就是保持最大堆性质,即其子节点不能大于父节点。为了达到这一性质,有以下操作:

从最后一个非叶节点(按照上面的结论2,最后一个非叶节点是⌊n/2⌋)开始,考察该节点和它的子节点的值的大小,如果子节点比该元素大,则交换;这样整个交换过后可以保证最大的值肯定交换到了整个堆的根节点上。

但是这样会产生一个问题:设i为某非叶节点序号(值为A[i]),该节点的某一个子节点值比它大时交换;交换后,值 A[i]成为了子节点的值。

但是当该子节点(交换后的A[i])又是另一个子树的根节点时,可能出现它的子节点大于根节点的值A[i]的情况。

因此,对于值A[i],要继续进行堆调整,即对值A[i]重复上面的工作。这样的结果是确保每一个子树都能符合最大堆。上图就是一个符合了最大堆特性的堆。

到这里仍然没有排列完因为:

1、同一个子树的左右两个子节点没有比较大小并排列;(如上图节点8、9)

2、同一层的不同父节点的子节点间没有进行比较。(如上图节点5、6)

三、建堆

     即从最后一个非叶节点⌊n/2⌋到1,进行保持堆性质操作。

四、堆排序算法

1、在建好了最大堆后,交换节点1(设值为A[1])和最后一个节点;由上面知,A[1]肯定是最大值。

2、交换后,将该节点(值为A[1])移除(从堆结构中移除,不是从数组中移除);

3、移除后,对新堆的序号1(原来最后1一个节点的值)进行堆性质调整(即比较序号1和子节点的值大小,并交换)。调整后重复步骤1、2。

最大值移除的目的在于得到最大值并放入排列结果数组中,并且缩短堆的长度,减少堆调整的时间。继续进行堆调整的目的很明显是为了得到当前堆的最大值。

js实现如下:

其中,因为上面的堆节点序号是从1开始,而数组索引是从0开始,因此有以下调整:

1、左子节点序号为i*2+1,右子节点序号为i*2+2。(如果还是以前的i*2则序号0的左子节点为0*2=0,显然不对)

2、上面堆的最后一个非叶节点⌊n/2⌋,现在为⌊n/2⌋-1。

var len;      
function buildMaxHeap(arr) {   //建堆  
    len = arr.length;  
    // [n/2-1]表示的是最后一个有子节点 (本来是n/2(堆从1数起),但是这里arr索引是从0开始,所以-1)  
    for (var i = Math.floor(len/2)-1; i>=0; i--) {  
        maxHeapify(arr, i);  
    }  
    //对每一个节点(非叶节点),做堆调整  
}  
function maxHeapify(arr, i) {     //堆调整  
    var left = 2*i+1,    
        right = 2*i+2,   
        largest = i;   //i为该子树的根节点  

    if (left < len && arr[left] > arr[largest]) {    
        largest = left;  
    }  

    if (right < len && arr[right] > arr[largest]) {  
        largest = right;  
    }  

    if (largest != i) {  //即上面的if中有一个生效了  
        swap(arr, i, largest);  //交换最大的为父节点  
        maxHeapify(arr, largest);  //交换后,原值arr[i](往下降了)(索引保存为largest),  
        //作为根时,子节点可能比它大,因此要继续调整  
    }    
}   
function swap(arr, i, j) {  
    var temp = arr[i];    
    arr[i] = arr[j];  
    arr[j] = temp;  
}   
function heapSort(arr) {  
    buildMaxHeap(arr);  
    for (var i = arr.length-1; i > 0; i--) {  
        swap(arr, 0, i);  
        len--;  
        maxHeapify(arr, 0);  
    }  
    return arr;  
}  

var arr = [46,12,33,72,68,19,80,33];
heapSort(arr)
console.log(' after: ' + arr);
<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]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值