美文网首页
[老实李] 算法初探:堆排序 Heap Sort

[老实李] 算法初探:堆排序 Heap Sort

作者: 老实李 | 来源:发表于2017-04-24 22:11 被阅读0次

    1、为什么要使用堆?

    首先我们可以理解一下堆和栈之间的区别,第一从空间的分配来看的话,堆是手动申请和释放空间,一般用new来分配;栈,空间由操作系统自动分配和适配,空间有限。比如Object a = null;就是在栈中分配空间,Object a = new Object()就是在堆中分配空间了。第二从对任务的调度策略来看的话,队列是先进先出,栈是先进后出,而堆是每个任务分配了一个优先权,根据优先权进行任务的执行。
    因此,堆又被称为优先队列。 大家知道普通的队列是先进先出,后进后出,而优先队列则是出队顺序和入队顺序无关,而只和优先级有关。在计算机领域就有大量的地方使用到了优先队列。
    典型的应用比如在操作系统中执行任务,大家知道操作系统可以同时执行很多任务,实际上操作系统是将CPU的执行周期划分为了时间片,在每个时间片里只能执行一个任务。那究竟执行哪一个任务呢?操作系统就会动态的每一次选择优先级最高的任务来执行,也就是说操作系统中的每一个任务都会动态的加到这样一个优先队列之中。这里请大家一定注意“动态”两个字,如果是静态的任务队列那么很简单,只要我们对这个队列排一次序然后按照优先级大小依次执行即可,但是实际在我们使用优先队列的时候情况是非常复杂的。例如操作系统中的任务是会随时增加的,而如果使用排序的方式那么只要增加一个任务,我们就需要重新排序一次,甚至当一个任务运行结束的时候我们也要重新的排序一次,而这么做耗时是巨大的。
    优先队列这种模型不仅仅适用于操作系统,在生活中其他方面使用此模型的也很多。比如用户访问同一个网页的时候,而服务器端就要依次回应这些请求,回应的顺序是怎样的呢,一般就会使用优先队列,在媒体服务,视频播放,音乐播放等领域都要使用优先队列。再比如在人工智能领域的应用,我们在打枪战游戏的时候,视野前有很多的敌人,使用优先队列我们就可以优先攻击血少的敌人,有的敌人被消灭了就执行出队操作,而有新的敌人进入视野了就执行入队操作。
    之前我们一直强调使用优先队列在处理动态的数据上是很有优势的,但其实在一些静态问题上也是很有优势的。比如“在100000000个元素中选出前100名?“这样的问题。按照正常的排序,在N个元素中选出前M个元素,这个算法是NlogN复杂度的,而使用优先队列则就变成了NlogM的复杂度。

    2、优先队列的实现方式

    ScreenClip.png

    优先队列的实现方式有哪些呢?根据之前本文对优先队列的一个讲解,有了如下几个思路
    第一个思路是使用一个普通数组,这样元素入队的时候就直接插入到数组的末尾,算法复杂度是O(1),而出队的时候要排一下序选择优先级最高的出队,复杂度就是O(n);
    第二个思路是使用一个顺序的数组,这样元素入队的时候要进行一个排序让其插入到合适的位置,出队的时候就直接将数组最头的出队即可;
    但是上面两种思路都是有局限性的,我们伟大的计算机科学家发明了一种新的数据结构,那就是堆。可以看到,在入队的事件复杂度上面堆不如普通数组,在出队的复杂度上不如顺序数组,但是堆可以很好的平衡入队和出队的时间复杂度。在极端情况下,使用数组来实现优先队列最差的情况是O(n^2)的时间复杂度,而使用堆则稳定在O(lgn)的时间复杂度上面。

    3、堆的基本存储

    堆所对应的插入和删除都是lgN级别的,所以堆一定相应的是一个树形结构。最为经典的一个堆的实现就是二叉堆。
    首先二叉堆就是一个二叉树结构的,二叉树的特点就是一个父节点两个子节点,且子节点要小于自己的父节点。

    ScreenClip.png ScreenClip.png

    其次,二叉堆又是一棵完全二叉树。完全二叉树就是除了最后一层节点,其余层级的节点都必须是满的,并且最后一层节点要靠左边。

    满足这两个条件就构造了一棵二叉堆。

    ScreenClip.png

    4、一个堆的基本实现

    class MaxHeap{
         private int[] data;
         private int count;
         public MaxHeap(int capacity){
               data = new int[capacity];
               count = 0;
         }
         public int getSize(){
               return count;
         }
         public boolean isEmpty(){
               return count==0;
         }
    }
    

    其实就是在构造函数里面new了一个数组而已。

    5、在堆中插入一个数据Shift up

    ScreenClip.png

    比如我们在数组的末尾加上一个52,此时这个堆就不满足一个二叉堆的定义了,我们就需要让这个二叉堆满足二叉堆的定义,也就是要让这个52放到合适的位置。
    首先和其父节点16比较,比16大,交换位置,再与41比较,比41大,交换位置,再与62比较,比62小,ShiftUp结束。这个过程就是ShiftUp。
    具体代码实现:

    class MaxHeap {
         private int[] data;
         private int count;
         private int capacity;
    
         public MaxHeap(int capacity) {
               data = new int[capacity];
               count = 0;
               this.capacity = capacity;
         }
    
         public void print() {
               for (int i = 0; i < getSize(); i++) {
                    System.out.print(data[i] + ",");
               }
         }
    
         public int getSize() {
               return count;
         }
    
         public int[] getData() {
               return data;
         }
    
         public boolean isEmpty() {
               return count == 0;
         }
    
         public void insert(int item) {
               assert (count + 1 <= capacity);
               data[count + 1] = item;
               count++;
               shiftUp(count);
         }
    
         private void shiftUp(int k) {
               while (k > 1 && data[k / 2] < data[k]) {
                    int temp = data[k / 2];
                    data[k / 2] = data[k];
                    data[k] = temp;
                    k /= 2;
               }
         }
    
    }
    
    

    测试代码:

    public class Heap {
         public static void main(String[] args) {
               MaxHeap maxHeap = new MaxHeap(100);
               Random rand = new Random();
               for (int i = 0; i < 10; i++) { 
                    int randNum = rand.nextInt(100) + 1;
                    maxHeap.insert(randNum);
               }
                   
               System.out.println(maxHeap.getSize() + "");
               maxHeap.print();
    
         }
    }
    

    输出结果:

    10
    0,99,71,47,42,63,44,17,42,39,
    

    6、从堆中移除一个元素Shift Down

    ScreenClip.png

    从堆中只能移除根节点的元素,比如上面的例子中第一次移除的元素是索引为1的元素62,然后由堆中最底层的元素16填补空缺。然后对此时的根节点16进行ShiftDown操作。ShiftDown操作就是将16放到合适的位置,首先另其左右子节点比较,较大的一个与自己再进行比较,然后交换位置,直到16到达它合适的位置停止。
    具体代码实现:

    public int extractMax() {
            assert (count > 0);
            int ret = data[1]; // 1、获取堆中的第一个元素
    
            // 2、swap第一个元素和最后一个元素
            int temp = data[1];
            data[1] = data[count];
            data[count] = temp;
            // 3、数组大小减1
            count--;
            // 4、对第一个元素执行ShiftDown
            shiftDown(1);
            return ret;
        }
        public int getMax() {
            assert (count > 0);
            return data[1];
        }
    
        private void shiftDown(int k) {
            while (2 * k <= count) {// 1、 如果此节点有子节点
                int j = 2 * k; // 2、j代表左节点索引
                if (j + 1 <= count && data[j + 1] > data[j])// 3、如果右节点大于左节点,且右节点在索引范围内
                    j++; // 4、j变成代表右节点索引
    
                // data[j] 是data[2*k]和data[2*k+1]中的最大值
                if (data[k] >= data[j]) {
                    break;
                }
                // 5、swap
                int temp = data[k];
                data[k] = data[j];
                data[j] = temp;
                // 6、下一次从原右索引位置处开始比较
                k = j;
            }
        }
    

    测试代码:

    public static void main(String[] args) {
            MaxHeap maxHeap = new MaxHeap(100);
            Random rand = new Random();
            for (int i = 0; i < 10; i++) {
                int randNum = rand.nextInt(100) + 1;
                maxHeap.insert(randNum);
            }
    
            while (!maxHeap.isEmpty()) {
                int num = maxHeap.extractMax();
                System.out.print(num + ",");
            }
        }
    

    7、HeapSort

    基础堆排序:

    public static void heapSort(int arr[], int n) {
            MaxHeap maxHeap = new MaxHeap(n+1);
            for (int i = 0; i < n; i++) {
                maxHeap.insert(arr[i]);
            }
            for (int i = n - 1; i >= 0; i--) {
                arr[i] = maxHeap.extractMax();
            }
        }
    

    测试用例:

    Random rand = new Random();
            int arr[] = new int[9000000];
            for (int i = 0; i < 9000000; i++) {
                int randNum = rand.nextInt(9000000) + 1;
                arr[i] = randNum;
            }
            long currentTimeMillis = System.currentTimeMillis();
            heapSort(arr, 9000000);
            long currentTimeMillis2 = System.currentTimeMillis();
            System.out.println("基础堆排序所用时间:"+(currentTimeMillis2-currentTimeMillis));
    

    输出:

    基础堆排序所用时间:2488
    

    可以看到这个基础堆排序的速度还是可以接受的。

    相关文章

      网友评论

          本文标题:[老实李] 算法初探:堆排序 Heap Sort

          本文链接:https://www.haomeiwen.com/subject/sfhmzttx.html