堆排序
对《大话数据结构》P396~P406—堆排序,进行了自己的理解并完善了代码。
一、堆排序之前需要掌握以下知识
1、完全二叉树
例如下图这样的,编号出现空档,就不是完全二叉树。
2、完全二叉树的性质
这三个性质中最重要的是1,看图就很好理解,即如果i>1,i的双亲节点是i/2。
3、堆
4、堆的性质
这个公式的意思是:
1、对于大顶堆,双亲大于左右孩子;对于小顶堆,双亲小于左右孩子。
2、对于有左右孩子的双亲,双亲编号肯定≤n/2。
二、掌握以上四点后,可以讲堆排序的原理。
先看书上的讲解,第一次看的时候有点蒙,看懂堆排序原理之后再看这个定义,发现写得还挺好的。
解读一下思想:
1、先把序列构成一个大顶堆(双亲大于等于左右孩子)。对于上面的图,序列有9个数,那么双亲节点就是1,2,3,4。
A、4,8,9三个节点中,保证节点4最大。
B、3,6,7三个节点中,保证节点3最大。
C、2,4,5三个节点中,保证节点2最大。
D、1,2,3三个节点中,保证节点1最大。
这时候,序列是大顶堆。而且,根节点1肯定是最大的数。
2、把最大的节点1和节点9交换,这样节点9就归位了。然后让节点1~节点8,再构成一个大顶堆。
3、把最大的节点1(这时候只有8个节点,节点9已经归位,可以不考虑了)和节点8(相当于最后一个节点)交换,这样节点8就归位了。然后让节点1~7,再构成一个大顶堆。
............
4、最后把最大的节点1和节点2交换,这样节点2就归位了。结束排序。
举个例子:
代码和解释如下(VS2012测试通过):
1 #include <iostream> 2 using namespace std; 3 #define MAXSIZE 9//用于要排序数组个数最大值,可根据需要修改 4 5 //排序用的顺序表结构 6 typedef struct 7 { 8 int r[MAXSIZE+1];//定义一个数组,用于存储要排序的数,r[0]作为临时变量 9 int length;//用于记录顺序表的长度 10 }SqList; 11 12 //排序表的初始化 13 SqList *InitSqList(SqList *L) 14 { 15 L=new SqList; 16 L->length=MAXSIZE;//本例中长度是9 17 cout<<"input list"<<endl; 18 for(int i=1;i<=L->length;i++) 19 cin>>L->r[i]; 20 return L; 21 } 22 23 //数组遍历输出 24 void PrintSqList(SqList *L) 25 { 26 for(int i=1;i<=L->length;i++) 27 cout<<L->r[i]<<" "; 28 cout<<endl; 29 } 30 31 //交换r[i]和r[j] 32 void swap(SqList *L,int i,int j) 33 { 34 int temp=L->r[i]; 35 L->r[i]=L->r[j]; 36 L->r[j]=temp; 37 } 38 39 //使L->r[s,..,m]成为一个大顶堆 40 void HeapAdjust(SqList *L,int s,int m) 41 { 42 int temp,j; 43 temp=L->r[s];//把要替换的r[s]值放入临时变量 44 for(j=2*s;j<=m;j*=2)//从左孩子开始寻找放入r[s]的最大值 45 { 46 if(j<m && L->r[j]<L->r[j+1]) 47 ++j;//如果左孩子小于右孩子,把j置为右孩子的下标 48 if(temp>=L->r[j]) 49 break;//如果r[s]本身比左右孩子都大,不用替换,直接退出该双亲结点的循环 50 L->r[s]=L->r[j];//如果没有退出循环,说明左右孩子中有一个大于双亲,把孩子的值给双亲 51 s=j;//把孩子的下标给s,方便下面的进行r[s]=temp 52 } 53 L->r[s]=temp;//把双亲的值交换给孩子,如果没有交换双亲和孩子,这一步左右是相等的值,也没有关系 54 cout<<"after one sort"<<endl; 55 PrintSqList(L); 56 } 57 58 //对顺序表L进行堆排序 59 void HeapSort(SqList *L) 60 { 61 int i; 62 for(i=L->length/2;i>0;i--)//把L构建成一个大根堆,i=4,3,2,1 63 HeapAdjust(L,i,L->length);//四次HeapAdjust之后,L是一个大根堆,双亲(4,3,2,1)都大于左右孩子,因为从i=4开始,最后根节点最大 64 for(i=L->length;i>1;i--)//i=9,8,...2 65 { 66 swap(L,1,i);//把大根堆的根和最后一个数交换(最后一个数是相对的,已经归位的数就不算了),依次即把最大数归位 67 HeapAdjust(L,1,i-1);//将L->r[1,..,i-1]重新调整为大根堆,即根节点最大,下一步再把根节点和最后一个数交换,把该根节点的数归位 68 } 69 } 70 71 int main() 72 { 73 SqList *p=NULL; 74 p=InitSqList(p);//初始化 75 HeapSort(p);//排序 76 cout<<"after sort"<<endl; 77 PrintSqList(p);//输出 78 }
运行结果:
时间复杂度:运行时间主要消耗在初始构建堆和重复构建堆。
1、初始构建堆
双亲最多有n/2个,每个双亲和左右孩子以及孩子之间,进行2次比较和互换。因此整个构建堆的时间复杂度是O(n)。
2、重复构建堆
首先看完全二叉树的性质:对一棵有n个结点的完全二叉树(其深度为log2n(向下取整)+1)。
完全二叉树第i个节点到根节点的距离为log2i(向下取整)+1,所以第i次重新构建堆需要O(log2i(向下取整))的时间。(相当于根节点只需要进行log2i(向下取整)次循环,因为i到根节点有log2i(向下取整)+1层,但是不需要和i比较,所以没有+1)
并且需要取n-1次堆顶记录和最后一个数交换。
因此重建构建堆的时间复杂度是(n-1)log2i,即O(nlogn)。