数据结构_堆排序实例_详细注释_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]重新调整为大根堆(执行一次该函数即可调整为全局大根堆) */ } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了