堆的基本操作及应用

目录

一、堆的基本操作

1. 向下调整

 2. 建堆

二、堆的应用——优先级队列

1. 查看堆顶元素(查看最小/最大元素)

2. 添加元素(向上调整)

3. 删除堆顶元素(删除最小/最大值),返回被删除的元素。(替换 + 向下调整)

4. 完整代码

三、时间复杂度总结


一、堆的基本操作

  堆是一棵逻辑上的完全二叉树,物理存储上表现为一个数组。(不需要按照链式结构进行组织,所以没有结点概念)堆分为大堆和小堆。大堆:在整棵完全二叉树中,任取一棵树上的元素,都大于等于其两个孩子的值。小堆同理。即把最大值维护在跟上是大堆,把最小值维护在根上就为小堆。以下操作中均以小堆为例。

  要实现堆的基本操作,需要先知道下标关系。假设现在已知一个元素的下标为 [k]:

① 这个元素的左孩子的下标:2 * k + 1

     这个元素的右孩子的下标:2 * k + 2

② 这个元素的双亲的下标:

当这个元素是其双亲的左孩子时: ( k - 1 ) / 2

当这个元素是其双亲的右孩子时: ( k - 2 ) / 2

注意:因为在计算机中除法本来就是向下取整的,所以可以直接表示为:( k - 1 ) / 2

  利用下标关系,我们就可以判断出元素是否有孩子(孩子下标是否合法),以及是否为叶子结点等。 

1. 向下调整

  前提:完全二叉树基本已经满足堆的性质,只有要进行操作的元素位置和其孩子的关系还不明确(也许满足,也许不满足)。只有满足这个前提,才能进行向下调整操作。 

操作方法:假设“我”是待调整的元素。

(1)判断“我”是否为叶子结点,“我” 不是叶子 -> “我”有孩子。

(2)找到最小的孩子。

(3)比较 ”我“ 和最小孩子的大小:

     ① 我 <= 最小的孩子:满足堆的性质,调整停止;

     ② 我 > 最小的孩子:交换“我”和最小的孩子。

(4)交换完成后,堆的条件可能破坏了,需要继续向下调整。

时间复杂度:O(log(n))  完全二叉树的高度

空间复杂度:

    非递归:O(1)

    递归:O(n)   调用n次

代码:有两种方式:递归和非递归。其中非递归为常用的向下调整的方法。

    public static void adjustDown递归(long[] array, int size, int index) {
        // index 是要进行向下调整的元素下标
        //1.判断 index 是否为叶子结点:如果为叶子结点,不需要调整,直接结束即可;如果不为叶子结点,进行接下来的操作。
        int leftIndex = 2 * index + 1;
        if (leftIndex >= size) {
            //判断 index 位置元素是否为叶子结点:判断其左孩子下标是否合法
            return;
        }

        //2.找到最小的孩子
        //不是叶子 -> 一定有左孩子
        //当没有右孩子时,左孩子就为最小的孩子
        //当有右孩子时:
        // ① 右孩子 > 左孩子 :最小孩子为左孩子
        // ② 右孩子 = 左孩子 :最小孩子为左孩子(右孩子)
        // ③ 右孩子 < 左孩子 :最小孩子为右孩子
        int minIdx = leftIndex;  //假设最小孩子为左孩子
        int rightIindex = leftIndex + 1;

        if (rightIindex < size && array[rightIindex] < array[leftIndex]) {
            minIdx = rightIindex;
        }

        //3.比较 ”我“ 和最小孩子的值
        //当 我 小于等于 最小孩子的值时,结束
        //当 我 大于最小孩子的值时,进行接下来的操作(向下调整)
        if (array[index] < array[minIdx]) {
            return;
        }

        //此时 我 > 最小孩子,不满足堆的性质,所以将我和最小孩子的位置进行调换
        swap(array, index, minIdx);

        //交换完后需要继续判断,处在新位置的“我 ”是否依然满足堆的性质
        adjustDown递归(array,size,minIdx);

    }

    public static void swap (long[] array, int p, int q) {
        long tmp = array[p];
        array[p] = array[q];
        array[q] = tmp;

    }
    //常用向下调整方法
    public static void adjustDown小堆(long[] array, int size, int index) {
        while (index * 2 + 1 < size) {
            //说明不是叶子结点
            //找到最小孩子
            int minIdx = index * 2 + 1; //假设最小孩子为左孩子
            if (minIdx + 1 < size && array[minIdx + 1] < array[minIdx]) {
                minIdx++;
            }

            //判断我和最小孩子的大小关系
            if (array[index] <= array[minIdx]) {
                return;
            }

            //交换
            swap(array, index, minIdx);
            //以最小孩子的位置再次这个过程
            index = minIdx;
        }

    }

 2. 建堆

  建堆过程就是利用向下调整,从最后一个有孩子的双亲结点开始向下调整,依次到根结点。

时间复杂度:O(n)

代码:

    public static void creatHeap小堆(long[] array, int size) {
        //找到最后一个结点的双亲结点
        // pIdx = ((size - 1) - 1) / 2
        //size - 1 为最后一个结点
        int pIdx = (size - 2) / 2;

        //O(n * log(n)) -> O(n)
        for (int i = pIdx; i >= 0; i--) {
            adjustDown小堆(array, size, i); //O(log(n))
        }
    }

二、堆的应用——优先级队列

  这里的优先级队列中的元素类型只为基础类型。

1. 查看堆顶元素(查看最小/最大元素)

  直接返回根结点元素即可。

时间复杂度:O(1)

代码:

    public long peek() {
        if (size <= 0) {
            throw new RuntimeException("空的");
        }
        return array[0];
    }

2. 添加元素(向上调整)

  先将要插入的元素放到堆的最后,并且 size+1。此时新插入的元素可能不满足堆的性质(这里是小堆,所以可能新插入的元素小于其父母结点元素)
  所以要进行判断:
  (1)判断自己是不是根结点。(如果是根结点,不需要进行操作,调整结束)
  (2)找到自己的双亲结点,进行比较(向上调整)
    ① ”我“ >= 双亲:满足堆的性质,调整结束;
    ② “我” < 双亲:交换我和双亲位置,再持续这个过程。

时间复杂度:O(log(n))

代码:

   public void offer(long e) {
        array[size] = e;
        size++;

        int index = size - 1;
        while (index != 0) {
            int pIdx = (index - 1) / 2;

            if (array[index] >= array[pIdx]) {
                break;
            }

            swap(array, index, pIdx);
            //持续双亲位置继续向上调整
            index = pIdx;
        }
    }

3. 删除堆顶元素(删除最小/最大值),返回被删除的元素。(替换 + 向下调整)

  不能直接删除堆顶元素。要先将堆中的最后一个元素替换堆顶元素,并且 size--。然后再从堆顶开始向下调整。

时间复杂度:O(log(n))

代码:

    public long poll() {
        if (size <= 0) {
            throw new RuntimeException("空的");
        }

        long e = array[0];
        array[0] = array[size - 1];
        array[size - 1] = 0; //可不写,只是为了调试好看
        size--;

        adjustDown小堆(array, size, 0);  //O(log(n))

        return e;
    }

4. 完整代码

public class MyPriorityQueuePrimitiveType {
    //使用小堆
    //不考虑扩容情况
    //long 类型代表元素
    private final long[] array = new long[1000];
    private int size;

    public MyPriorityQueuePrimitiveType(){
        //构造方法:构造一个空的优先级队列
        this.size = 0;
    }

    //查看堆顶元素(查看最小元素)
    //O(1)
    public long peek() {
        if (size <= 0) {
            throw new RuntimeException("空的");
        }
        return array[0];
    }

    //插入元素(向上调整)
    //先将要插入的元素放到堆的最后,并且 size+1。此时新插入的元素可能不满足堆的性质(这里是小堆,所以可能新插入的元素小于其父母结点元素)
    //所以要进行判断:
    //1. 判断自己是不是根结点。(如果是根结点,不需要进行操作,调整结束)
    //2. 找到自己的双亲结点,进行比较(向上调整)
    //  ① ”我“ >= 双亲:满足堆的性质,调整结束;
    //  ② “我” < 双亲:交换我和双亲位置,再持续这个过程
    //O(log(n))
    public void offer(long e) {
        array[size] = e;
        size++;

        int index = size - 1;
        while (index != 0) {
            int pIdx = (index - 1) / 2;

            if (array[index] >= array[pIdx]) {
                break;
            }

            swap(array, index, pIdx);
            //持续双亲位置继续向上调整
            index = pIdx;
        }
    }

    //删除堆顶(最小)元素(trick + 向下调整)返回被删除的元素
    //不能直接删除堆顶元素
    //先将堆中的最后一个元素替换堆顶元素,并且 size--。然后再从堆顶开始向下调整
    //O(log(n))
    public long poll() {
        if (size <= 0) {
            throw new RuntimeException("空的");
        }

        long e = array[0];
        array[0] = array[size - 1];
        array[size - 1] = 0; //可不写,只是为了调试好看
        size--;

        adjustDown小堆(array, size, 0);  //O(log(n))

        return e;
    }

    public void swap(long[] array, int p, int q) {
        long tmp = array[p];
        array[p] = array[q];
        array[q] = tmp;
    }
    public void adjustDown小堆(long[] array, int size, int index) {
        while (index * 2 + 1 < size) {
            //说明不是叶子结点
            //找到最小孩子
            int minIdx = index * 2 + 1; //假设最小孩子为左孩子
            if (minIdx + 1 < size && array[minIdx + 1] < array[minIdx]) {
                minIdx++;
            }

            //判断我和最小孩子的大小关系
            if (array[index] <= array[minIdx]) {
                return;
            }

            //交换
            swap(array, index, minIdx);
            //以最小孩子的位置再次这个过程
            index = minIdx;
        }

    }
}

三、时间复杂度总结

1. 堆的基本操作:

(1)向下调整:O(log(n))    完全二叉树的高度

(2)建堆:O(n)

2. 优先级队列(堆的应用):

(1)查看堆顶元素:O(1)

(2)添加元素(向上调整):O(log(n))

(3) 删除堆顶元素(替换 + 向下调整):O(log(n))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值