zoukankan      html  css  js  c++  java
  • 数据结构与算法之美-堆和堆排序

    堆和堆排序


    如何理解堆

    堆是一种特殊的树,只要满足以下两点,这个树就是一个堆。

    ①完全二叉树,完全二叉树要求除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。

    ②树中每一个结点的值都必须大于等于(或小于等于)其子树中每个节点的值。大于等于的情况称为大顶堆,小于等于的情况称为小顶堆。


    如何实现堆


    如何存储一个堆

    完全二叉树适合用数组来存储,因为数组中对于下标从1开始的情况,下标为i的节点的左子节点就是下标为i*2的节点,右子节点就是i下标为i*2+1的节点,其父节点时下标为i/2的节点


    堆支持哪些操作

    往堆中插入一个元素

    把新插入的元素放到堆的最后就不符合第二个特性了,所以我们需要进行调整,让其重新满足堆的特性,这个过程我们起了一个名字,就叫作堆化(heapify)。

    堆化就是顺着节点所在的路径,向上或者向下,对比,然后交换。我们先使用从下往上的堆化方法。

    让新插入的节点与父节点对比大小。如果不满足子节点小于等于父节点的大小关系,我们就互换两个节点。一直重复这个过程,直到父子节点之间满足刚说的那种大小关系。

    public class Heap{
        private int[] data;//数组,从下标1开始存储
        private int maxNum;//数组容量
        private int count;//当前数组成员数量
        //构造器初始化数组,大小和数量
        public Heap(int size){
            data = new int[size + 1];
            maxNum = size;
            count = 0;
        }
        public void Insert(int item){
            //堆满返回
            if (count >= maxNum) return;
            //先将节点插入堆尾
            data[count++] = item;
            int i = count;
            //再自下向上堆化,直到堆顶或者父节点比子节点大为止
            while (i / 2 > 0 && data[i] > data[i / 2]){
                //交换位置
                int temp = data[i];
                data[i] = data[i / 2];
                data[i / 2] = temp;
                //更新下标
                i = i / 2;
            }
        }
    }

    删除堆顶元素

    根据对的第二条定义,堆顶元素存储的就是堆中的最大值或最小值。

    这里我们使用从上往下的堆化方法。将最后一个节点放到堆顶,然后利用同样的父子节点对比法,进行互换节点直到父子节点之间满足大小关系为止。

    这样移除的就是数组中的最后一个元素,不会破环完全二叉树的定义。

    public void RemoveMax(){
        //堆空返回
        if (count == 0) return;
        //将最后一个节点提到堆顶
        data[1] = data[count--];
        //进行堆化
        Heapify(data,count,1);
    }
    public static void Heapify(int[] data,int n,int i){
        while (true){
            //记录更大节点的位置,初始化为当前节点的位置
            int maxPos = i;
            //如果其左右子节点存在,且比当前节点大,就将左右节点下标设为更大的节点
            if (i * 2 <= n && data[i] < data[i * 2]) maxPos = i * 2;
            if (i * 2 + 1 <= n && data[maxPos] < data[i * 2 + 1]) maxPos = i * 2 + 1;
            //否则就结束循环,堆化结束
            if (maxPos == i) break;
            //节点交换位置
            int temp = data[i];
            data[i] = data[maxPos];
            data[maxPos] = temp;
            //更新当前节点的下标,循环继续与下一个左右子节点比较
            i = maxPos;
        }
    }

    如何基于堆实现排序

    我们借助于堆这种数据结构实现的排序算法,就叫作堆排序。

    我们可以把堆排序的过程大致分解成两个大的步骤,建堆和排序。


    建堆

    首先将数组原地建成一个堆。借助另一个数组,就在原数组上操作。我们要实现从后往前处理数组,并且每个数据都是从上往下堆化的建堆方法。

    public static void BuildHeap(int[] data, int n){
        //从下标n/2到1开始进行堆化,n/2就是最后一个叶子节点的父节点。
        for (int i = n / 2; i >= 1; --i)
            Heapify(data,n,i);
    }

    我们对下标从n/2开始到 111 的数据进行堆化,下标是n/2+1到n的节点是叶子节点,我们不需要堆化。

    建堆操作的时间复杂度

    排序的建堆过程的时间复杂度是 O(n)。


    排序

    建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的。数组中的第一个元素就是堆顶,也就是最大的元素。我们把它跟最后一个元素交换,那最大元素就放到了下标为n的位置。

    这个过程有点类似删除堆顶元素的操作,当堆顶元素移除之后,我们把下标为n的元素放到堆顶,然后再通过堆化的方法,将剩下的n-1个元素重新构建成堆。

    堆化完成之后,我们再取堆顶的元素,放到下标是的位置,一直重复这个过程,直到最后堆中只剩下标为1的一个元素,排序工作就完成了。

    public static void Sort(int[] data,int n){
        //将数组建造为堆
        BuildHeap(data, n);
        //获取堆尾的下标
        int k = n;
        //循环直到k为1
        while (k > 1){
            //交换堆顶和堆尾的元素
            int temp = data[k];
            data[k] = data[1];
            data[1] = temp;
            //将堆尾的下标递减并对1到k的下标的数组成员进行堆化
            Heapify(data,--k,1);
        }
    }

    堆排序的时间复杂度、空间复杂度以及稳定性

    堆排序是原地排序算法。堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是O(n),排序过程的时间复杂度是O(nlogn)所以,堆排序整体的时间复杂度是O(nlogn)。

    堆排序不是稳定的排序算法,因为在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。

    测试 

    //Main方法
    int[] data = new int[] {0,3,5,2,9,4,7 };
    Heap.Sort(data,data.Length-1);
    for (int i=0;i<data.Length;i++)
        Console.Write(data[i]+",");
    //测试结果
    0,2,3,4,5,7,9,

    数组的第1个成员,即下标0的数据是不作为数据的一部分的,这是为了算法上的方便,如果下标是从0开始,那么左右子节点的下标公式就是i*2+1和i*2+2。


    思考

    在实际开发中,为什么快速排序要比堆排序性能好?

    对于快速排序来说,数据是顺序访问的而对于堆排序来说,数据是跳着访问的。这样对 CPU 缓存是不友好的。

    对于同样的数据,在排序过程中,堆排序算法的数据交换次数要多于快速排序。堆排序的第一步是建堆,建堆的过程会打乱数据原有的相对先后顺序,导致原数据的有序度降低。比如,对于一组已经有序的数据来说,经过建堆之后,数据反而变得更无序了。

  • 相关阅读:
    minicap编译示例
    uniapp H5项目中使用腾讯地图sdk
    腾讯地图打车乘客端小车平滑移动-安卓篇
    地图定位打卡功能示例
    腾讯位置服务个性化图层创建及发布
    腾讯位置服务GPS轨迹回放
    使用腾讯地图实现汽车沿轨迹行驶功能
    地图GPS轨迹录制
    腾讯地图实现微信小程序地图定位教程
    基于腾讯地图定位组件实现周边POI远近排序分布图
  • 原文地址:https://www.cnblogs.com/errornull/p/10054391.html
Copyright © 2011-2022 走看看