数据结构_堆排序实例_详细注释_python/c实现(极致详尽注释)

版本0(python):

'''
Description:
Version: 2.0
Author: xuchaoxin
Date: 2021-03-06 20:17:12
LastEditors: xuchaoxin
LastEditTime: 2021-03-07 12:58:14
'''
""" 堆排序
堆中定义以下几种操作:
○ 最大堆调整(Max Heapify堆积化):
§ 将堆的末端子节点作调整,使得子节点永远小于父节点
○ 创建最大堆(Build Max Heap):(核心算法)
§ 将堆中的所有数据重新排序
○ 堆排序(HeapSort):
§ 移除位在第一个数据的根节点,并做最大堆调整的递归运算 (可以不递归)
"""
import generate_randomInt
def big_endian(arr,start,end): #big_endian大端法
"""[设计本函数的初衷的直接目的用来解决:某个已经总体上(由于顶端元素被替换,导致堆结构被破坏)已经满足堆的定义的完全二叉树做调整(将堆顶元素通过调整,放到合适的位置,使得该堆真正满足堆的定义),通过配合循环,合理的逆序调用本函数,可以用来初始换一些杂乱无章的数列,使之满足堆的结构(我们届时用build_heap()函数来组织big_endian()的逆序调用,从而实现初始化建堆的目的)
这是核心函数,提供按深度(而非广度)调整大根堆的服务,从给定的start节点开始,向深层的节点执行,直到扫描的end(给定终点);
值得注意的是,从start节点通往end节点的过程中不是逐个遍历的,
(体现在:一颗满足堆性质的完全二叉树是可以看成两部分(两颗兄弟子树,当其中一颗子树从堆结构被破坏后,该子树就不满足堆的性质;但是另一颗子树并没有受到影响,任然满足堆结构),故而,欲使得该树重新满足堆结构只需要调整被破坏的那一棵子树,而不需要去动另一颗子树。即向深处调整,而不是广度调整。)
这个函数有两方面的用途。
那么,一次big_endian调用到底会调整多少次位于不同层次的三元子堆呢(在同一次调用中,不同深度的三元子堆树最多调整一次)
可以确定的是,这个函数是从给定起点start开始向后遍历到给定的终止节点end,
一方面用途(复杂度较高,反复滚动扫描调整),
对于最开始建堆,是一个反复滚动调用big_endian()的过程,
从给定起点start开始向后遍历到最后一个节点end在这时是确定的end=len(arr)-1(即同终点,(是叶子节点))
这些调用存在先后关系,但不存在嵌套调用的关系,后期的调用要比前期的调用执行更多的判断/对堆的结构可能做出更多的调整行为(滚动)]
另一方面(复杂度较低,一次扫描调整),是基于初始建堆完成之后的完全二叉树,这时的二叉树总体上满足堆定义,但是由于堆顶的调换,导致结构被破坏,这时候只需要重新从堆顶处开始调用big_endian()执行一次扫描调整(该过程会沿着不满足堆结构的子树不断深入深层去调整,每一级调整中(如果需要),都只会调整两棵同级子树中的一颗,另一颗任然是满足二叉树的定义,这一点又该二叉树总体上满足堆定义做出前提保证)
Args:
arr (list): [表示待排序数列的堆(列表形式)]
start (int):[堆的完全二叉树形态下的,需要heapify的节点区间的左边界索引(在arr中的位置索引),即从那个节点开始heapify调整)]
end (int): [需要heapify的节点区间的右边界索引(在arr中的位置索引)/可以用来判断何时停止调整循环:child<=end]
"""
""" 注意,这里用的是列表(索引从0开始,0,1,2(即左孩子的索引编号是偶数,右孩子节点的编号是奇数(2*i+1))) """
root=start #当前级别(深度)的(子)树的根节点(root的值是要被不断更新的)
child=root*2+1 #child记录兄弟节点中的较大者的索引,初始化为左孩子元素的编号索引(child的值也要不断更新/重新计算)
""" root的变化趋势是越来越大(child=root*2+(1/2),当然也是变大的趋势) """
""" 采用循环策略 """
# while child<=end:#如果一个节点的左子节点的理论标号>end,说明该节点的左子节点不存在,有因为是完全二叉树,右节点更加不会存在。
# #child比最后一个节点的编号还大,说明此时的root已经是叶子节点了,就应该退出循环
# if child+1<=end :#如果child+1>end,则表明该非叶子节点只有一个孩子节点(左孩子)
# #保证大孩子索引child指向正确
# if arr[child]<arr[child+1]:
# #为了始终让其跟两个子节点元素中的较大值比较(让较大值当左孩子),如果右边大就更新child的值(使得它指向右孩子),左边大的话就默认
# child+=1
# """ 判断是否需要对该三元堆进行节点调整,否则break """
# if arr[root]<arr[child]:
# #父节点小于子节点中的较大者,则直接交换元素位置,同时坐标也得更新
# arr[root],arr[child]=arr[child],arr[root]
# # 同时更新root值并重新计算child的值,这样下次循环可以准确判断:是否为最底层,
# root=child
# child=root*2+1
# else:
# break
""" 采用递归的策略 """
if child<=end:#表明root节点是非叶子节点(其左孩子存在,因为child<=end)
if child+1<end :#如果同时存在右孩子节点,则比较处那个孩子节点较大,并将对应节点的索引赋值给child(更新)
if arr[child]<arr[child+1]:
child+=1
if arr[child]>arr[root]:
"""执行调整操作:交换元素位置 """
arr[child],arr[root]=arr[root],arr[child]
big_endian(arr,child, end)#所有递归目标同终点end,child是变化的
""" build_heap()不是必须,可以直接写在sort中 """
def build_heap(arr):
reverse_first=len(arr)//2 - 1 #第一个非叶子节点元素的索引;或则写reverse_first=(len(arr)-1)//2
# size=len(arr)
lastIndex=len(arr)-1
""" range()步长为-1,产生序列:reverse_first到0,左闭右开 """
#初始化建堆:执行逆序heapify()
for reverse_roll_start in range(reverse_first,-1,-1):#索引变量为reverse_roll_start
#从下到上,从右到左,对每个节点进行调整,循环调用big,得到非叶子节点
#去调整所有的节点;这里的reverse_roll_start跟随着索引变量的前进而前进(变化);所有调用同终点:lastIndex
big_endian(arr,reverse_roll_start,lastIndex)
def heap_sort(arr): #大根堆排序
build_heap(arr)
lastIndex=len(arr)-1
""" 每执行一次循环,就有一个元素被正确排入到结果序列
总共需要排序lastIndex次,即len(arr)-1次
end>=1,end-1>=0"""
for end in range(lastIndex,0,-1): #索引变量为end(表示该趟heapify()的处理区间[start=0,end]的右边界索引) , 从序列的最后一个元素(叶子节点)开始逆序遍历到第2个位置()
arr[0],arr[end]=arr[end],arr[0] #顶部尾部互换位置 ,将为有序区增加一个元素(而在最后一次调换时,次小的元素的有序同时也使得最小的元素放在了合适的位置)
#重新调整子节点,使得整个堆仍然满足堆的定义,每次heapify都从顶开始调整(start=0);所有调用同起点start=0
big_endian(arr,0,end-1)
#可以考虑仿射big_endian函数
return arr
def main():
# l=[111,7,2,9,11,66,22,11]
l=generate_randomInt.generate(50)
print(heap_sort(l))
# if __name__=="__main__":
# main()
main()
'''
Description:
Version: 2.0
Author: xuchaoxin
Date: 2021-03-07 11:00:11
LastEditors: xuchaoxin
LastEditTime: 2021-03-07 11:12:00
'''
import random
from print_some import printSome
# n=input("Enter a integer that represents the number of elements for the sort problem you want to simulate:")
# n=int(n)
def generate(n):
arr=[]
for i in range(n):
arr.append(random.randint(0,n*10))
return arr
# printSome(arr,str="原始序列")

版本1:

#include <stdio.h>
#define N 10
#define ISHEAP 1
/*"排序.c"里的源文件里的函数*/
void heapSort(int r[], int n);/*复合函数只需要声明最外层即可*/
//void percDown(int r[], int i, int n);
/*main.c*/
int main()
{
typedef int elemtype;
elemtype r[] = { 19,15,13,1,6,7,0,3,2,4 };
elemtype heapr[] = {-99, 19,15,13,1,6,7,0,3,2,4 };
printf("testing:\n");
heapSort(heapr,10);
for (int i = 1; i <= N; i++) printf("%d ", heapr[i]);
//
}
/*堆排序*/
/* 排序思想:首先,通过筛选 为 待排序数据 构建一个二叉堆。
然后,整个排序由N - 1趟排序组成。每次排序时,找到堆中最小元素
(即堆顶元素),并放入排序数组的恰当位置;然后通过筛选把剩余的元素重建成一个堆。*/
void percDown(int r[], int i, int n)/*筛选函数(具有较好的通用性 从第i个元素开始,对数组r进行“筛选”(以最大堆(大顶堆)为例)*/
{
/**********************************************************
参数要求:元素从数组下标为1的地方开始存储
*********************************************************/
int tmp,/*暂存元素*/
child;/*保存大孩子元素的索引*/
/*填写别的挖空代码,一定要尽快搞清楚,他所引入的辅助变量的含义/用意,并为变量注释好含义,不然还是容易在使用的过程中出现前后含义不一致的情况:
比如child是索引值还是元素元素值.*/
/*进入循环体前的准备工作(初始化)*/
tmp = r[i]; // 存储待筛选的第i个元素的值
child =2*i ; // 第i个元素的左孩子索引
/*该函数功能的核心:循环:(传入的节点编号是非叶子节点的时候,才能进入循环)*/
while (2*i<=n) { // 如果待筛选元素存在(左)孩子结点,若还能满足2*i+1<=n,那么该结点r[i]左右孩子都有.
// 从r[i], r[2i], r[2i+1] 这三个元素中,找到最大元素
if (child != n && r[child] < r[child + 1]) /* 让child指向较大的孩子结点(当然前一个条件可以为 child+1 <=n (child != n 的写法也是为了照顾child+1的取值范围)
如果第一个条件不满足,说明只有一个孩子结点(左孩子),那就直接在下面作双亲结点于与左孩子节点的比较.*/
child++;
/*判断并处理交换(如果需要的话)*/
if (tmp<r[child]) // 如果较大孩子结点的值大于待筛选元素(双亲结点),交换
r[i] = r[child];//tmp里的值还在;
else/*temp>=child*/
break; // 否则,筛选结束(已经是大顶堆了)
i = child; child = 2 * i; //往下找, 从较大孩子结点开始,迭代i,继续筛选 推动while()循环.
}//while()
r[i] = tmp; // 把最开始筛选的元素值放入最终交换到的结点位置
}
void heapSort(
int r[],
int n)
{
/*在测试排序算法的函数时,可先将形参注释掉,测试数据直接在函数内部定义(这样方便再调试的时候观察变化,尤其时内部的数组,观察方便,而若通过参数出传入数组,就不方便观察整个数组的变化情况.测试完毕后,注释/删除掉内部的测试数据,并恢复形参
要使用全局变量的话也可,若在不同源文件中,声明一下,若是结构体/自定义类型,就写个头文件吧(注意,所在项目的各个项(源文件可能分散在不同的文件夹中,不方便包含.)
typedef int elemtype;
elemtype r[] = { -99, 19,15,13,1,6,7,0,3,2,4 };
int n = 10;*/
/**********************************************************
对数组r[]中的n个元素进行堆排序
******************************************************/
int i = n / 2, temp;
// 创建堆,从第n/2个元素开始到第一个元素,反复筛选
for (; i >= 1; i--)
{
percDown(r,i,n);
}
/*执行N-1趟排序即可,所以i>1(或写作i>=2 */
for (i = n; i>=2; i--)
{
// 删除堆顶,即把堆顶(堆中最大元素)与堆尾交换
temp = r[i];
r[i] = r[1];
r[1] = temp;
// 完成交换后,从堆顶开始,对堆进行“筛选”调整
percDown(r, 1, i-1);/*此时i已经是全局最大值,放到最大编号处,不再参与堆的调整.*/
}
}

版本2(改编自大话数据结构)

/*版本2(改编自大话数据结构)*/
#define MAXSIZE 100 /* 用于要排序数组个数最大值,可根据需要修改 */
typedef struct
{
int r[MAXSIZE+1]; /* 用于存储要排序数组,r[0]用作哨兵或临时变量 */
int length; /* 用于记录顺序表的长度 */
}SqList;
void swap(SqList *L,int i,int j)
{
int temp=L->r[i];
L->r[i]=L->r[j];
L->r[j]=temp;
}
/* 堆排序********************************** */
/* 已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义, */
/* 本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆(筛选)(自上而下的)
合适的使用该函数也可以用来初始化建队.*/
void HeapAdjust(
SqList *L,
int s,/*待排序部分的编号范围左边界(在初始化建堆时,边界往前;在调整部分时,总是取最小值1)*/
int m)/*待排序部分的编号范围右边界(在初始化建堆的时候,总时取最大值;在调整部分时,边界往前)*/
{
int temp,j;
temp=L->r[s];/*保存当前节点元素的值*/
for(j=2*s;j<=m;j*=2) /* 沿关键字较大的 孩子结点 向下筛选 找到最终正确的位置,不断迭代s并保存到s里*/
{
/*找到两个孩子中的较大值.(要么是j,要么是j+1)*/
if(j<m && L->r[j]<L->r[j+1])/*r[j]是r[s]的左孩子节点(注意,孩子节点的编号较其双亲晚,即比双亲结点要大!)*/
++j; /* j为关键字中较大的记录的下标 (当然也可以写作j = j+1*/
/*将待调整结点的值于其较大孩子的值比较,如果比它的孩子大,那么不需要调整位置,直接进入下一层;
否则将大孩子调整为双亲,并更新较大孩子的结点(自上而下的筛选),直到j>m为止*/
if(temp>=L->r[j])
break;
L->r[s]=L->r[j];
s=j;
/*此时尚不着急将源s结点填到孩子处,因为该位置的元素立马要再次弹出比较,可能会马上又被其大孩子覆盖,可带最后写入*/
}/*离开for时,j*=2使得j>m*/
L->r[s]=temp; /* 插入(写在此时的s位置,不要写成j位置(刚越界) */
}
/* 对顺序表L进行堆排序 */
void HeapSort(SqList *L)
{
int i;
/*创建:建堆方法:对初始序列建堆的过程,就是一一个反复进行筛选的过程(从下往上地执行(自上而下的)筛选操作(化归思想)*/
for(i=L->length/2;i>0;i--) /* 把L中的r构建成一个大根堆 (自下而上的(从最后一个非叶子节点往前操作(直到所有非叶子节点被处理)(要知道,非叶子节点只分布在树的下边缘轮廓.)
非叶子节点的层次遍历方式的编号是连续的*/
HeapAdjust(L,i,L->length);/*i都是非叶子结点的*/
/*执行玩所有循环后,才有了一个全局大根堆*/
/*调整/排序 N-1次即可(例如对三个元素进行堆排序)*/
for(i=L->length;i>1;i--)/*i从length起步,而且只要*/
{
swap(L,1,i); /* 将堆顶记录和当前未经排序子序列的 最后一个记录交换 */
HeapAdjust(L,1,i-1); /* 将L->r[1..i-1]重新调整为大根堆(执行一次该函数即可调整为全局大根堆) */
}
}
posted @   xuchaoxin1375  阅读(12)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示