算法 堆排序
堆排序从二叉树演化而来。
堆,分为大根堆和小根堆;
大根堆:一个特殊的完全二叉树,他的父节点一定比子节点大;
小根堆:父节点一定比子节点小;
下面的例子都是用大根堆
有两个重点
1.堆的向下调整
树的根节点的左右都是堆,但自己不是堆,这种时候可以通过向下调整,成为堆。
图1,需要向下调整
图2,将根节点2与它的子节点9和7比较,都比他大,将更大的往上提,2放到9的位置,继续跟8和5比较,以此类推
图3,最终形成堆
2.如何构建堆
一个无序的树,怎么构建成堆?
图1 是一个无序的树,首先我们找到第一个非叶子节点(从后往前找),这里是3
发现3的叶子节点是5,比它大,不符合大根堆条件,因此对3-5这个子树,做一个堆的向下调整操作,变成5-3
因为 9 这个节点 的叶子节点都比它小,所以不需要调整,我们看1这个节点,需要向下调整
再看8这个节点,比9小比5大,因此调整
最后调整6,也就是根节点,一个有序的堆就构建完成了。
堆排序的思路:
1. 通过向下调整的方式,构建大根堆;
2.取出根节点,大根堆的根节点必然是最大的;
3.从后往前取出叶子节点放到根节点上;
4.通过向下调整的方式,逐步取出剩余最大的数。
5.不停的重复步骤3和4,直到剩余一个数,排序完成。
也就是说,向下调整是关键,不管是构建还是排序,都要用上它
向下调整的代码
/// <summary> /// 堆的向下调整 /// </summary> /// <param name="array"></param> /// <param name="low">堆的根节点位置</param> /// <param name="high">堆的最后一个元素的位置</param> public static void dui_partion(int[] array, int low, int high) { int i = low;//表示要比较的父节点位置,默认是堆顶,即根节点 int j = 2 * i + 1;//表示要比较的子节点位置,默认左孩子节点位置 int temp = array[low];//堆顶的值,存入中间变量 //只要j位置没有超出最后一个元素的位置,就可以循环比较 while (j <= high) { //先判断j+1<=high,即右孩子是否存在 //再比较左孩子和右孩子,如果右孩子比左孩子大,我们需要提的是右孩子 if (j + 1 <= high && array[j] < array[j + 1]) { j = j + 1;//j指向右孩子,走右边的子树 } //如果较大的子节点的值大于根节点的值,需要往上提 if (array[j] > temp) { //将j的值赋给i array[i] = array[j]; //更新i和j的位置 i = j; j = 2 * i + 1; } else//temp大,说明不需要再往下比较,结束 { array[i] = temp; break; } } //标识已经找到叶子节点这一层了,temp就应该放在这 //其实这里可以发现,不管是temp大于某个父节点后不需要再往下比较,还是 //一直找到叶子节点不能再往下比较,都要执行array[i] = temp这一步 //因此这里其实可以直接写array[i] = temp,并把while中的array[i] = temp去掉 //但是可能不好理解,因此保留这种写法 if (j > high) { array[i] = temp; } }
构建堆的代码
/// <summary> /// 构建堆 /// </summary> /// <param name="array"></param> public static void dui_build(int[] array) { Console.WriteLine("原数组:" + string.Join(',', array)); int len = array.Length; //根据构建堆的思路,从后往前的第一个非叶子节点开始 //已知最后一个叶子节点的下标是len-1,根据公式它的父节点下标是(len-1-1)/2 for (int i = (len - 1 - 1) / 2; i >= 0; i--) { dui_partion(array, i, len - 1); Console.WriteLine("循环构建中(" + i + "):" + string.Join(',', array)); } Console.WriteLine("构架堆后:" + string.Join(',', array)); } }
测试一下结果
可以动手画一下,这就是构建好后的大根堆。
然后就是通过向下调整取值,直接把排序写到构建堆方法里了,懒。
/// <summary> /// 构建堆 /// </summary> /// <param name="array"></param> public static void dui_build(int[] array) { Console.WriteLine("原数组:" + string.Join(',', array)); int len = array.Length; //根据构建堆的思路,从后往前的第一个非叶子节点开始 //已知最后一个叶子节点的下标是len-1,根据公式它的父节点下标是(len-1-1)/2 for (int i = (len - 1 - 1) / 2; i >= 0; i--) { dui_partion(array, i, len - 1); Console.WriteLine("循环构建堆(" + i + "):" + string.Join(',', array)); } //j每次循环都指向这个堆中无序部分的最后一个位置 int temp = 0; for (int j = len - 1; j >= 0; j--) { //每次都交换无序部分最后一个位置和堆顶位置的值 //因为通过向下调整堆顶一定是无序部分最大的值 temp = array[0]; array[0] = array[j]; array[j] = temp; dui_partion(array, 0, j - 1);//要注意通过交换,j已经被占据了,所以只能是j-1 Console.WriteLine("循环排序:" + string.Join(',', array)); } Console.WriteLine("排序后:" + string.Join(',', array)); }
结果
重点就是向下调整。
时间复杂度,
dui_partion 部分的时间复杂度是 logN,因为是树结构,最多调整树的深度那么多次。
构建堆需要 nlogn,排序需要nlogn,因此时间复杂度就是,nlogn。