算法与数据结构-算法篇-堆排序

堆排序

快有一个月没有写博客了,还是得把这些东西好好梳理一下,今天写下堆排序的相关内容
堆排序和快速排序相比,可能相对会复杂一些,不那么容易理解
先给出结论,堆排序和快速排序一样,平均的时间复杂度都是o(N * logN),但是常数项的大小一般要大过快速排序,因此快速排序的速度通常比堆排序更快

一 堆结构

在开始了解堆排序前,首先需要知道的是,这种算法是基于堆的数据结构设计而来的一种排序算法
so,啥是堆咧?
堆是一种具备特殊性质的完全二叉树! 那啥又是二叉树,啥又是完全二叉树??

1.二叉树

  • 二叉树其实指的就是子节点最多两个的一种树形结构,简单的来看有以下几种

在这里插入图片描述

  • 层级多了之后 ,二叉树就有可能演变成这样

在这里插入图片描述

  • 简单的说,一个完全二叉树就是从上向下,从左向右依次挂载成一棵二叉树的样子,中间不能缺失任何一个,但是不要求是满二叉树,即叶子节点只存在于层级为max和max-1的层次上

2.大根堆 & 小根堆

大/小根堆是在完全二叉树的基础上,引入了一些对数据的要求:
若每一个根节点都大于等于它的左右孩子节点,就认为这是一个大根堆结构
若每一个根节点都小于等于它的左右孩子节点,就认为这是一个小根堆结构

在这里插入图片描述

3.程序中的大根堆表示

因为堆排序用到的是大根堆结构,后面不再讲小根堆
在大根堆的结构上,在程序中可以用两个基本数据结构来表现
一是使用链表,不过这样操作指针过于麻烦
二是使用数组,因为它是一种完全二叉树的结构,只要约定好数组与根位置的约定,即可实现数组—完全二叉树节点的映射

在这里插入图片描述

4.堆结构中,每一个小二叉树的节点间的关系

0为根,1,2为子, (1+1)/2=(0+1) , (2+1)/2=(0+1) 即(子节点序号+1)/2 = 根节点序号+1
1为根,3,4为子,(3+1)/2=(1+1),(4+1)/2=(1+1) 即(子节点序号+1)/2 = 根节点序号+1
2为根,5,6为子,(5+1)/2 =(2+1),(6+1)/2+(2+1) 即(子节点序号+1)/2 = 根节点序号+1
3为根,7,8为子,(7+1)/2=(3+1),(8+1)/2=(3+1) 即(子节点序号+1)/2 = 根节点序号+1
注:以上算法均为int类型的计算,因此小数位会舍去

  • 因此可以得出两个结论
  1. 任意一个给定的根节点,它的左子节点序号一定是 (自身序号+1)*2 -1 ,右子节点序号一定是(自身序号+1)*2
  2. 对于任意一个给定的子节点, 根节点的序号一定是 (自身序号+1)/2

二 如何在现有的大根堆结构上插入一个数之后,还可以保持大根堆结构

  1. 在将数据向堆内添加的时候,保证每一个数进来,都是一个大根堆结构,就能保证最终获得的一定是大根堆
  2. OK,先假设我们已经有了一个大根堆结构,就像上面那样,那么新来一个数后,如何计算才能保证调整后的结构一定是大根堆呢?
  3. 假设我们的新值为80,比赛正式开始
    在这里插入图片描述
  4. 直接找到自己父亲4号位,和它进行比较,如果小于等于父亲,就不必再动了. 如果比父亲大,直接和父亲交换
    在这里插入图片描述
  5. 此时继续刚才的逻辑,它在4号位,再向父亲发起挑战,发现自己还是比父亲大,再交换
    在这里插入图片描述
  6. OK,继续,再和父亲干一架,发现父亲比自己厉害,那就找到自己的位置了-----1号位

总结一下,如果想在原大根堆结构中保持大根堆结构,只需要按以下步骤来:
1.找自己父亲PK,只要比自己父亲大就交换
2.交换之后就继续按1步骤进行比较,直到没有父节点,或者父节点比自己大就结束

   /**
     * @param heap 现成的大根堆结构
     * @param currentLength 新来的值的索引位置
     */
    private void heapInsert(int[] heap, int currentLength) {
        //和父节点PK
        while (heap[currentLength] > heap[(currentLength - 1) / 2]) {
            //大于父亲就交换
            swap(heap, currentLength, (currentLength - 1) / 2);
            //交换索引位置
            currentLength = (currentLength - 1) / 2;
        }
    }

三 如何在移除了最大的根节点之后,让余下的数组织成大根堆

  1. 以上面的大根堆为例子,先看下初始状态

在这里插入图片描述

  1. 还是以上面那个大根堆为例,将根节点0号位和最后一个数44交换,表示移除了最大值
    在这里插入图片描述

  2. 这时候因为99已经确定是最大值,放到了队尾,因此10号索引已经不需要参与排序,接下来只要处理0~9即可,从0号位开始,算出自己的左节点的位置为(0+1)*2 -1 =1,先看下有没有右节点,如果有右节点,和右边比较一下,看看左右两个节点谁比较大,返回谁的索引,如果没有右节点,返回左节点索引,对于上例来讲,2号索引位比较大,将0位与2位进行交换
    在这里插入图片描述

  3. 再次2号位当成根节点,找出左节点 (2+1)*2 -1 = 5 ,5号位和6号位比较,发现5号位大,则由5号位和2号位PK,2号位再次落败,交换
    在这里插入图片描述

  4. 此时发现最大的86已经又到最上方了,再快速跑一轮,将0号位和9号位交换
    在这里插入图片描述

  5. 1,2号位,1号最大,0号和1号交换
    在这里插入图片描述

  6. 3,4号位PK,4号获胜,1,4号位PK,4号获胜,交换
    在这里插入图片描述

  7. 此时的最大值80已经来到了0号位置,后面再将0~8号交换,因此能看出来,每一次的几步比较之后,就可以让当前的最大值来到根节点上,将最大值与当前的数组最后一个值进行交换,同时缩小数组范围,就可以不断的让每一次数组内的最大值放到当前数组的尾巴上,从而达到升序排序的结果

总结一下:

  1. 将根节点同现有大根堆最后一位进行交换
  2. 再从根节点出发,找到自己的左节点,如果找不到,则比较结束
  3. 如果找到了,再看下自己和右节点谁比较大(如果右节点不存在,认为左节点大),拿大的和父亲比较
  4. 如果父亲没有子节点中的最大值大,那么父节点和子节点中较大的交换,如果父亲大于子节点中较大值,比较结束
  5. 交换完成后,继续从2进行下一轮比较
    /**
     * 让数组从0-R范围组织成大根堆
     * @param heap 已经将最大值和大根堆尾部交换过的堆结构
     * @param index 交换后根的索引
     * @param heapSize 堆的大小
     */
    private void heapify(int[] heap, int index, int heapSize) {
        //找到左节点索引
        int leftIndex = index * 2 + 1;
        //左节点索引如果越界了说明没有左节点了
        while (leftIndex < heapSize) {
            //右节点(不一定存在)
            int rightIndex = leftIndex + 1;
            //判断左右节点谁更大,返回对应的索引(右节点不存在的情况下以左节点为准)
            int largerIndex = rightIndex < heapSize && heap[rightIndex] > heap[leftIndex] ? rightIndex : leftIndex;
            //判断孩子中较大的值和根节点相比,哪个大
            int compareWithRoot = heap[index] > heap[largerIndex] ? index : largerIndex;
            //如果根节点大于自己的孩子,就停止执行
            if (compareWithRoot == index) {
                break;
            }
            //将较大的值上提
            swap(heap, index, largerIndex);
            //走到这里,说明根节点是比较小的,因此交换后,largerIndex里就是原来的根节点
            index = largerIndex;
            //计算新的左节点,重复
            leftIndex = index * 2 + 1;
        }
    }

四 堆排序

在有了上面的基础之后,我们就可以将堆排序归纳为以下几步

  1. 将给定的数组组织为大根堆
  2. 将最大值扔到数组尾部,同时将要组织堆结构的数组大小减一
    3.每一次堆排序结束后,都会将最大值置顶,因此再将最大值向数组尾部交换即可,直到数据循环完
public void sort (int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        for (int i = arr.length - 1; i >= 0; i--) {
            //倒序组织大根堆,时间复杂度最低
            heapify(arr, i, arr.length);
        }
        int heapSize = arr.length;
        //将最大的数扔到屁股上
        swap(arr, 0, --heapSize);
        // O(N)
        while (heapSize > 0) {
            // O(logN)
            heapify(arr, 0, heapSize);
            swap(arr, 0, --heapSize);
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值