数据结构_堆排序实例_详细注释_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 @ 2023-12-10 17:31  xuchaoxin1375  阅读(19)  评论(0)    收藏  举报  来源