堆排序:
Heapsort类似于 选择排序我们反复选择最大的项目并将其移动到列表的末尾。主要的区别在于,我们不是扫描整个列表来查找最大的项目,而是将列表转换为最大堆(父节点的值总是大于子节点,反之最小堆)以加快速度。
注意:堆一定是一棵完全二叉树;
Heapify堆化:
我们的第一步是将输入列表转换为堆(也称为“堆化”它)
- 把数列的数值视为完全二叉树的结点(从0开始)
- 从倒数第二层开始,进行heapify,即父节点与子节点依次比较,把最大值交换到父节点
- 以此类推,使这颗完全二叉树符合最大堆的性质
规律:父节点的下标 = (i-1)/ 2 例如下图:数值7的下标为3,其父节点的下标为(3-1)/2=1;
左子节点的下标 = 2*i+1 例:数值2的下标为2,其左子节点的下标为 (2*2)+1=5;
右子节点的下标= 2*i+2 例:数值2的下标为2,其左子节点的下标为 (2*2)+2=6;
以此示例输入:
我们可以将它视为完全二叉树中的节点, 而不是像列表那样处理输入。该第0 个位置在堆中是根;
- 我们实际上并没有在这里创建一棵树。我们只是处理输入,就像它指定树中的节点一样。
- 当我们通过移动项目或移除元素来操纵树时,我们实际上正在 重新排列基础输入列表(而不是一些单独的树结构)。
- 我们将向树都和 该名单在我们的图表,但树只是为了帮助我们想象我们如何解释列表作为一个堆。在内存中,我们只是存储 列表。
我们的树需要一些修正才能使它成为一个有效的堆。例如,它现在肯定无效,因为最大的元素(9)不在根。
要将树转换为堆,我们将从树的底部向上工作。我们将每个节点与其子节点进行比较并移动节点,以使父节点始终大于其子节点。
这会导致较小的节点在树中向下移动,“ 向下冒泡 ”以允许较大的值到达顶部。
- 首先,让我们看看叶子。叶节点没有任何子节点,因此它们根本不需要向下移动。
2.让我们看看下一级别的节点:
我们将从左节点(3)及其子节点开始:由于7和9都大于3,我们肯定需要移动。我们将交换3和9以使父母大于其子女。(如果我们将3与7交换,那么我们仍然会遇到问题,因为父节点(7)将小于其子节点(9)
接下来,我们将查看正确的节点(2)及其子节点。由于4大于2,我们将交换它们。
向上移动,我们在根部有一个8。
因为8小于9,所以8个气泡向下,与更大的孩子交换位置:9。
然后,我们需要将8与其两个孩子-7和3进行比较。由于8比他们两个都大,我们已经完成了冒泡并且不需要进行任何额外的交换。
此时,我们已将输入树转换为有效的最大堆
HeapSort堆排序:
反复删除最大值:
每次我们从堆中删除一个元素时,它都是底层列表中的最大项。因此,按排序顺序,它属于列表的末尾。正如我们将看到的,从堆中删除元素可以方便地释放底层列表末尾的空间,我们可以在其中放置已删除的元素。
我们删除max元素:
这留下了需要填补根的空白。我们将最后一个元素放在那里。
但是这样之后就不是一个有效堆了,所以我们要进行heapify,把它堆化,堆化完成后把9放入数组的末尾;如图:
下一个最大的元素是8.我们将它从根中移除,用最底部的最右边的元素(1)填充它的位置
再次heapify,堆化它,
以此类推:7,4,3,2,1;
复杂度:
对于heapify步骤,我们检查树中的每个元素并将其向下移动,直到它比其子项大。因为我们的树高是O(l g(n)),我们可以做到O (l g(n ))移动。所以n个节点,总的时间复杂度为的O (nl g(n ))。
我们已经证明了heapify步骤是 为O (nl g(n ))。通过 更复杂的分析,事实证明它实际上是 上)O (n )↴
将树转换为堆后,我们删除所有的n个元素 - 一次一个元素。从堆中删除需要O (l g(n ))时间,因为我们必须将新值移动到堆的根并向下冒泡。所以删除操作为O (nl g(n ))时间。
更彻底的分析表明这样做 删除仍然是为O (nl g(n ))。 ↴
把这些步骤放在一起,O (nl g(n ))为最坏情况下(平均)的时间。
每次我们从树根中删除一个元素时,替换它的元素根本不会向下冒泡。在这种情况下,每次删除都需要O(1)时间,并做 n次删除操作O (n )。
所以,在最好的情况下,时间复杂度为 O (n )。当输入中的所有内容相同时。
向量O(1)整体空间用于堆垛。
代码:
对于代码部分,我说一下:
有一个前提:对一个节点做heapify的时候,必须保证它的所有子树都已经是堆。
所以,在这个前提下,如果要做heapify的节点已经符合“父节点 > 子节点”的性质,那么这就已经是一个堆了;就没有必要往下走了。
另外,我们的build_heap函数是从最后一个不是叶节点的点开始往前做heapify操作的,所以最后是可以形成一个堆
对于函数Build_Heap()
例如:
如果我们要做heapify,肯定是从下标3开始(因为3结点是最下面的有两个子节点的父节点),然后是2,1,0;
所以才有了这个循环:
for (int i = n - 1; i >= 0; i--)
{
swap(arr[0], arr[i]);
Heapify(arr, i, 0);
}
#include<iostream>
using namespace std;
void Heapify(int* tree, int n, int m) //tree表示数组,n表示数组长度,m表示对第几个结点进行heapify操作
{
if (m >= n) return; //递归出口
int c1 = 2 * m + 1;
int c2 = 2 * m + 2;
int max = m; //假设m为最大值
if (c1<n && tree[c1] > tree[max]) //比较左子节点与父节点
max = c1;
if (c2<n && tree[c2] > tree[max]) //比较右子节点与刚刚比较完之后的父节点进行比较
max = c2;
if (max != m) {
swap(tree[max], tree[m]);
Heapify(tree, n, max);
}
}
void Build_Heap(int* arr, int n) //arr为数组,n为数组长度
{
int last_node = n - 1; //堆的最后一个结点
int last_heapify_node = (n-1)/2; //最后一个结点的父节点,我们需要从这里开始heapify
for (int i = last_heapify_node; i >= 0; i--)
{
Heapify(arr, n, i);
}
}
void heap_sort(int* arr, int n) //排序
{
Build_Heap(arr, n);
for (int i = n - 1; i >= 0; i--)
{
swap(arr[0], arr[i]);
Heapify(arr, i, 0);
}
}
void Show(int* arr, int n) //输出
{
for (int i = 0; i < n; i++)
{
cout << arr[i] << ",";
}
cout << endl;
}
int main()
{
int arr[10] = {1,4,2,6,8,3,9,5,0,7 }; //前提:对一个节点做heapify的时候,必须保证它的所有子树都已经是堆。
heap_sort(arr, 10);
Show(arr, 10);
return 0;
}
十大经典算法复杂度及稳定性比较:https://blog.csdn.net/alzzw/article/details/98100378
冒泡排序:https://blog.csdn.net/alzzw/article/details/97906690
选择排序:https://blog.csdn.net/alzzw/article/details/97964320
插入排序:https://blog.csdn.net/alzzw/article/details/97967278
快速排序:https://blog.csdn.net/alzzw/article/details/97970371
归并排序:https://blog.csdn.net/alzzw/article/details/98047030