算法 堆排序

堆排序从二叉树演化而来。

堆,分为大根堆和小根堆;

大根堆:一个特殊的完全二叉树,他的父节点一定比子节点大;

小根堆:父节点一定比子节点小;

下面的例子都是用大根堆

 

 

 

有两个重点

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。
posted @ 2021-05-25 23:57  luytest  阅读(52)  评论(0编辑  收藏  举报