目录
堆排序
堆的逻辑结构是一颗完全二叉树 (想象出来的)
堆的物理结构的一个数组(真实内存中存放的) -> 通过下标访问父子节点关系
物理结构图如下:
逻辑结构图如下:
通过上面我们可以观察,总结这两种图的联系
即: leftchild=parent * 2 +1;
rightchild=parent * 2 + 2;
parent=(child - 1) / 2;
(这里的parent,child代表的是在数组中的下标)
学习了上面的结构,下面我们说堆的分类 :最大堆(大顶堆),最小堆(小顶堆)
最大堆要求:树中所以的父亲都大于等于孩子
最小堆要求:树中所以的父亲都小于等于孩子
大堆示意图:
小堆示意图:
那么我们如何使一个无序数组变成一个大或者小堆呢?(下面我们以建大堆为例子)
即:向下调整算法(AjustDown)
向下调整算法有个前提 :就是左右子数必须是大堆,才能使用
例如下图:
上图6节点的左右子数已经是大堆了,但最上面一层不是,这时我们可以用向下调整算法
从根节点开始,选出左右孩子中大的,跟父亲比较,如果父亲小,就交换,然后再继续往下调,调到叶子节点就终止
代码如下,注释详细:
//向下调整算法(使用前提:左右子树必须已经是大堆或者小堆)
void AjustDown(int *arr,int Size,int root)//这里用建大堆做例子
{
int parent=root;
int child=2*parent+1; //先默认左孩子大
while(child < Size)
{
if(child+1 < Size && arr[child+1] > arr[child])//child+1 < Size防止只有一个孩子越界
{
child=child+1;
}
//选孩子中大的和父亲交换
if(arr[child] > arr[parent])
{
Swap(&arr[child],&arr[parent]);
parent=child;
child=2*parent+1;
}
else//如果父亲已经比两个孩子都大已经满足条件了
{
break;
}
}
}
这时又有小伙伴想到了,如果左右子数不是大堆怎么办?
答:我们反着来调不就行了嘛,从倒数第一个叶子节点开始调
我们发现叶子节点只有一个值也不需要调,所以最终,我们可以从最后一个非叶子节点开始调节。
如何找到最后一个非叶子节点呢? -> 最后一个叶子节点的父亲 -> 下标为:(Size-1 -1)/2
代码如下:(Size为数组大小)
for(int i=(Size-1-1)/2; i>=0; i--)
{
AjustDown(arr,Size,i);
}
最后我们然后利用大堆来实现排序呢?
我们发现大堆的第一个是最大的。
只需把第一个交换到最后,然后Size--,重新建一次就行了,循环起来
所以升序需要建大堆。
如果建小堆的话,每次取第一个为最小值,然后第一个不变,从第二个开始重新建堆
这样堆的原始结构都被改变了,建小堆不是不行,是没有了效率优势
代码如下:
int end=Size-1;
while(end > 0)
{
Swap(&arr[0],&arr[end]);
AjustDown(arr,end,0);
end--;
}
最后整体代码如下,注释详细:
#include<stdio.h>
//堆排序 时间复杂度:O(N*logN)
void Swap(int *a,int *b)
{
int temp=*a;
*a=*b;
*b=temp;
}
//向下调整算法(使用前提:左右子树必须已经是大堆或者小堆)
void AjustDown(int *arr,int Size,int root)//这里用建大堆做例子
{
int parent=root;
int child=2*parent+1; //先默认左孩子大
while(child < Size)
{
if(child+1 < Size && arr[child+1] > arr[child])//child+1 < Size防止只有一个孩子越界
{
child=child+1;
}
//选孩子中大的和父亲交换
if(arr[child] > arr[parent])
{
Swap(&arr[child],&arr[parent]);
parent=child;
child=2*parent+1;
}
else//如果父亲已经比两个孩子都大已经满足条件了
{
break;
}
}
}
void HeapSort(int *arr,int Size)
{
//建堆时间复杂度O(N)
//因为向下调整算法前提,所以我们需要从后向前调整,即从最后一棵非叶子节点开始调(叶子不需要调)
for(int i=(Size-1-1)/2; i>=0; i--)//最后一棵非叶子节点 -> 最后一个节点的父亲
{
AjustDown(arr,Size,i);
}
int end=Size-1;
while(end > 0)
{
Swap(&arr[0],&arr[end]);
AjustDown(arr,end,0);
end--;
}
}
int main()
{
int arr[]={5,2,1,4,3,6,9,8,7,0};
HeapSort(arr,10);
for(int i=0;i<10;i++)
{
printf("%d ",arr[i]);
}
return 0;
}
选择排序
选择排序(升序):每次选择最大的放到后面 / 取最小的放到前面
时间复杂度O(N^2),这个排序几乎是最low的了,最好最坏时间复杂度都是O(N^2)
平常我们见到的都是每次选择一个,下面我们学习一下优化一点的版本,优化后依然是O(N^2),每次选择两个数,把最大的和最后一个数交换,把最小的到前面。
但如果每次选两个的话,右可能出现bug,例如 9 2 -1 2 3 0,就会出现-1被排在最后
原因是什么呢?看图
我们来分析一下:
1.选出最大值和最小值
2.首先执行的是mini和begin位置互换
3.继续进行maxi和end互换,但当maxi和begin重叠的时候,第二步进行互换的时候maxi的值已经不是最大的数了,被交换到了mini位置,bug就出现在这里,我们只需要修正一下maxi就OK了
#include<stdio.h>
void Swap(int *a,int* b)
{
int temp=*a;
*a=*b;
*b=temp;
}
//直接选择排序(这个是优化后的一次选两个)
//几乎最差的排序,因为最好最坏时间复杂度都是O(N^2)
void SelectSort(int *arr,int Size)
{
int bagin=0;
int end=Size-1;
while(bagin < end)
{
int mini=bagin,maxi=bagin;
for(int i=bagin; i<=end; i++)
{
if(arr[i] < arr[mini])
{
mini=i;
}
if(arr[i] > arr[maxi])
{
maxi=i;
}
}
Swap(&arr[mini],&arr[bagin]);
//如果bagin和maxi重叠,需要修正一下maxi的位置
if(bagin==maxi)
{
maxi=mini;
}
Swap(&arr[maxi],&arr[end]);
bagin++;
end--;
}
}
int main()
{
int arr[]={9,3,5,2,7,8,6,-1,4,0};
SelectSort(arr,10);
for(int i=0;i<10;i++)
{
printf("%d ",arr[i]);
}
return 0;
}