数据结构和算法之【堆】

目录

认识堆

堆的定义

大顶堆

小顶堆

堆的特性

父节点和子节点之间的位置关系

已知子节点的位置,求其父节点的位置

已知父节点的位置,求其左右子节点的位置

自实现堆

定义接口

定义抽象类

大顶堆(具体实现类)

小顶堆(具体实现类)

总结

堆的应用场景

Java中的优先级队列(PriorityQueue)

堆排序

核心思路

代码实现

求第K个最大元素(LeetCode-215题)

题解

测试结果


认识堆

堆的定义

计算机科学中,堆是一种基于树的数据结构,通常用完全二叉树(除了最后一层,其他层的节点都是满的,且最后一层的节点从左至右是连续的二叉树)来实现

大顶堆

小顶堆

堆的特性

  • 大顶堆中,任意节点C和它的父节点P满足:P.value >= C.value
  • 小顶堆中,任意节点C和它的父节点P满足:P.value <= C.value
  • 最顶层的节点没有父节点,称为根节点

父节点和子节点之间的位置关系

已知子节点的位置,求其父节点的位置

  • 索引从0开始存储节点数据,索引为i的节点的父节点的索引为:(i-1) / 2,i > 0
  • 索引从1开始存储节点数据,索引为i的节点的父节点的索引为:i / 2,i > 1

已知父节点的位置,求其左右子节点的位置

  • 索引从0开始存储节点数据,索引为i的节点的左子节点的索引为:2i + 1;右子节点的索引为:2i + 2,左右子节点的索引都 < size
  • 索引从1开始存储节点数据,索引为i的节点的左子节点的索引为:2i;右子节点的索引为:2i + 1,左右子节点的索引都 < size

自实现堆

定义接口

java 复制代码
package algorithm.heap;

/**
 * 接口定义
 */
public interface Heap {
    /**
     * 堆化
     */
    void heapIfy(int[] values);

    /**
     * 添加元素
     */
    boolean offer(int value);

    /**
     * 移除堆顶元素
     */
    int poll();

    /**
     * 替换堆顶元素
     */
    int replace(int value);

    /**
     * 查看堆顶元素
     */
    int peek();
}

定义抽象类

java 复制代码
package algorithm.heap;

import java.util.Arrays;

/**
 * 定义抽象类
 */
public abstract class AbstractHeap implements Heap {
    protected int size;
    protected int[] container;
    public static final int CONTAINER_CAPACITY = 128;

    @Override
    public void heapIfy(int[] values) {
        // check param
        if (values == null) {
            throw new NullPointerException();
        }
        if (values.length == 0) {
            return;
        }
        container = values;
        size = container.length;
        // 找到最后一个非叶子节点的位置
        int lastNonLeaf = (size - 1) >> 1;
        for (int i = lastNonLeaf; i >= 0; i--) {
            // 从最后一个非叶子节点开始到root节点依次执行下潜
            siftDown(i);
        }
    }

    /**
     * 下潜:交给子类去实现,不同的实现类有不同的算法
     */
    protected abstract void siftDown(int parent);

    /**
     * 交换i位置和j位置上的值
     */
    protected void swap(int i, int j) {
        if (i == j) {
            return;
        }
        container[i] = container[i] ^ container[j];
        container[j] = container[i] ^ container[j];
        container[i] = container[i] ^ container[j];
    }

    @Override
    public boolean offer(int value) {
        // 懒加载
        if (container == null) {
            container = new int[CONTAINER_CAPACITY];
        }
        // 堆已满
        if (size == container.length) {
            return false;
        }
        container[size++] = value;
        // 执行上浮操作
        siftUp(size - 1, value);
        return true;
    }

    /**
     * 上浮
     */
    private void siftUp(int child, int value) {
        // 找到新元素的存放位置
        int targetIndex = getUpTargetIndex(child, value);
        container[targetIndex] = value;
    }

    /**
     * 交给子类去实现,不同的实现类有不同的实现方式
     */
    protected abstract int getUpTargetIndex(int child, int val);

    @Override
    public int poll() {
        int top = peek();
        // 交换顶堆元素和最后一个元素,并移除
        swap(0, --size);
        // 执行下潜操作,使整体满足堆的特性
        siftDown(0);
        return top;
    }

    @Override
    public int replace(int value) {
        if (size == 0) {
            throw new RuntimeException("堆已空!");
        }
        int top = container[0];
        container[0] = value;
        // 执行下潜操作,使整体满足堆的特性
        siftDown(0);
        // 返回原来的堆顶元素
        return top;
    }

    @Override
    public int peek() {
        if (size == 0) {
            throw new RuntimeException("堆已空!");
        }
        // 堆顶元素
        return container[0];
    }

    @Override
    public String toString() {
        return Arrays.toString(Arrays.copyOfRange(container, 0, size));
    }
}

大顶堆(具体实现类)

java 复制代码
package algorithm.heap;

/**
 * 大顶堆实现类
 */
public class MaxHeap extends AbstractHeap {

    /**
     * 大顶堆下潜
     * 将父节点的值和它的两个子节点的值进行比较,和比它大的且值为最大的那个子节点交换,直到没有比它大的子节点
     */
    @Override
    protected void siftDown(int parent) {
        // 通过父节点找子节点
        int leftChild = (parent << 1) + 1;
        int rightChild = leftChild + 1;
        // 假设值最大的是父节点
        int max = parent;
        // 如果左子节点值大
        if (leftChild < size && container[max] < container[leftChild]) {
            max = leftChild;
        }
        // 如果右子节点值大
        if (rightChild < size && container[max] < container[rightChild]) {
            max = rightChild;
        }
        // 最大值不是父节点值,需要交换
        if (max != parent) {
            swap(parent, max);
            // 继续下潜
            siftDown(max);
        }
    }

    @Override
    protected int getUpTargetIndex(int child, int value) {
        // 递归结束条件一
        if (child == 0) {
            return child;
        }
        // 通过子节点获取父节点位置
        int parent = (child - 1) >> 1;
        // 递归结束条件二
        if (container[parent] >= value) {
            return child;
        }
        container[child] = container[parent];
        child = parent;
        return getUpTargetIndex(child, value);
    }
}

小顶堆(具体实现类)

java 复制代码
package algorithm.heap;

/**
 * 小顶堆实现类
 */
public class MinHeap extends AbstractHeap {

    /**
     * 小顶堆下潜
     * 将父节点的值和它的两个子节点的值进行比较,和比它小的且值为最小的那个子节点交换,直到没有比它小的子节点
     */
    @Override
    protected void siftDown(int parent) {
        // 通过父节点找子节点
        int leftChild = (parent << 1) + 1;
        int rightChild = leftChild + 1;
        // 假设值最小的是父节点
        int min = parent;
        // 如果左子节点值小
        if (leftChild < size && container[min] > container[leftChild]) {
            min = leftChild;
        }
        // 如果右子节点值小
        if (rightChild < size && container[min] > container[rightChild]) {
            min = rightChild;
        }
        // 最大值不是父节点值,需要交换
        if (min != parent) {
            swap(parent, min);
            // 继续下潜
            siftDown(min);
        }
    }

    @Override
    protected int getUpTargetIndex(int child, int value) {
        // 递归结束条件一
        if (child == 0) {
            return child;
        }
        int parent = (child - 1) >> 1;
        // 递归结束条件二
        if (container[parent] <= value) {
            return child;
        }
        container[child] = container[parent];
        child = parent;
        return getUpTargetIndex(child, value);
    }
}

总结

通过对大顶堆和小顶堆的实现,不难发现有几个方法是比较关键的

  • 建堆/堆化:heapIfy
  • 下潜:siftDown
  • 上浮:siftUp

堆的应用场景

这里简单介绍几个堆的应用场景

Java中的优先级队列(PriorityQueue)

Java中提供的优先级队列的底层就是使用堆来实现的,这里简单看下源码,通过offer和poll方法就能看到堆的上浮(siftUp)和下潜(siftDown)操作

java 复制代码
package java.util;

// ...

public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable {

    // ...

    transient Object[] queue;


    // 添加元素
    public boolean offer(E e) {
        // ...

        if (i == 0)
            queue[0] = e;
        else
            // 上浮
            siftUp(i, e);
        return true;
    }


    // 移除堆顶元素
    public E poll() {
        // ...
        if (s != 0)
            // 下潜
            siftDown(0, x);
        return result;
    }

    // ...
}

堆排序

核心思路

堆排序的思路如下

  • heapIfy堆化建立堆
  • 交换堆顶和堆底元素(最值被交换到了堆底),缩小并执行下潜操作调整堆
  • 重复上一步操作直至堆中只剩下一个元素

代码实现

java 复制代码
package algorithm.heap;

public class SortUtil {

    /**
     * 堆排序
     */
    public static void heapSort(int[] data) {
        if (data == null || data.length == 1) {
            return;
        }
        // 1、heapIfy堆化建立堆
        heapIfy(data);
        // 2、交换堆顶和堆底元素(最值被交换到了堆底),缩小并执行下潜操作调整堆
        // 3、重复上一步操作直至堆中只剩下一个元素
        int size = data.length;
        while (size > 1) {
            swap(data, 0, size - 1);
            size--;
            siftDown(0, data, size);
        }
    }

    /**
     * 堆化
     */
    private static void heapIfy(int[] data) {
        int size = data.length;
        int lastNonLeafIndex = (size - 1) >> 1;
        for (int i = lastNonLeafIndex; i >= 0; i--) {
            siftDown(i, data, size);
        }
    }

    /**
     * 下潜
     */
    private static void siftDown(int parent, int[] data, int size) {
        int childLeft = (parent << 1) + 1;
        int childRight = childLeft + 1;
        int maxIndex = parent;
        if (childLeft < size && data[maxIndex] < data[childLeft]) {
            maxIndex = childLeft;
        }
        if (childRight < size && data[maxIndex] < data[childRight]) {
            maxIndex = childRight;
        }
        if (maxIndex != parent) {
            swap(data, parent, maxIndex);
            siftDown(maxIndex, data, size);
        }
    }

    /**
     * 交换数组中索引i和索引j位置上的元素
     */
    private static void swap(int[] data, int i, int j) {
        if (i == j) {
            return;
        }
        data[i] = data[i] ^ data[j];
        data[j] = data[i] ^ data[j];
        data[i] = data[i] ^ data[j];
    }
}

求第K个最大元素(LeetCode-215题)

题解

java 复制代码
class Solution {
    public int findKthLargest(int[] nums, int k) {
        // check param
        if (nums == null || nums.length < k) {
            throw new IllegalArgumentException();
        }
        // 这里使用小顶堆来解题
        MinHeap minHeap = new MinHeap();
        // 将数组中前K个元素堆化
        minHeap.heapIfy(Arrays.copyOfRange(nums, 0, k));
        // 接下来的元素,只要是满足大于堆顶元素,就执行小顶堆的replace方法替换堆顶元素
        for (int i = k; i < nums.length; i++) {
            int num = nums[i];
            if (num <= minHeap.peek()) {
                continue;
            }
            minHeap.replace(num);
        }
        // 此时堆顶元素就是数组中第K个最大元素
        return minHeap.peek();
    }

    /**
     * 小顶堆
     */
    private static class MinHeap {
        private int[] container;
        private int size;

        /**
         * 堆化
         */
        public void heapIfy(int[] array) {
            if (array == null || array.length == 0) {
                throw new IllegalArgumentException();
            }
            container = array;
            size = container.length;
            int lastNonLeaf = (size - 1) >> 1;
            for (int i = lastNonLeaf; i >= 0; i--) {
                siftDown(i);
            }
        }

        /**
         * 下潜
         */
        private void siftDown(int parent) {
            int leftChild = (parent << 1) + 1;
            int rightChild = leftChild + 1;
            int minIndex = parent;
            if (leftChild < size && container[minIndex] > container[leftChild]) {
                minIndex = leftChild;
            }
            if (rightChild < size && container[minIndex] > container[rightChild]) {
                minIndex = rightChild;
            }
            if (minIndex != parent) {
                swap(minIndex, parent);
                siftDown(minIndex);
            }
        }

        /**
         * 交换两位置上的值
         */
        private void swap(int i, int j) {
            if (i == j) {
                return;
            }
            container[i] = container[i] ^ container[j];
            container[j] = container[i] ^ container[j];
            container[i] = container[i] ^ container[j];
        }

        /**
         * 替换堆顶元素
         */
        public void replace(int value) {
            container[0] = value;
            siftDown(0);
        }

        /**
         * 查看堆顶元素
         */
        public int peek() {
            return container[0];
        }
    }
}

测试结果

相关推荐
计算机安禾2 分钟前
【数据结构与算法】第42篇:并查集(Disjoint Set Union)
c语言·数据结构·c++·算法·链表·排序算法·深度优先
码界奇点2 分钟前
基于Spring Boot的前后端分离商城系统设计与实现
java·spring boot·后端·java-ee·毕业设计·源代码管理
一叶飘零_sweeeet4 分钟前
深度剖析:Java 并发三大量难题 —— 死锁、活锁、饥饿全解
java·死锁·活锁·饥饿
IT乐手10 分钟前
java 对比分析对象是否有变化
android·java
云烟成雨TD14 分钟前
Spring AI Alibaba 1.x 系列【18】Hook 接口和四大抽象类
java·人工智能·spring
Hachi被抢先注册了22 分钟前
Docker学习记录
java·云原生·eureka
YuanDaima204826 分钟前
二分查找基础原理与题目说明
开发语言·数据结构·人工智能·笔记·python·算法
devilnumber1 小时前
Spring Boot 2 vs Spring Boot 3:50 条核心区别 + 升级优势 + 避坑指南
java·spring boot·springboot升级
武超杰1 小时前
Spring Cloud Alibaba Nacos 进阶:配置隔离、集群、持久化与开机自启
java·开发语言
Venhoul1 小时前
@Scheduled(cron = “1 0 0 * * ?“用法介绍
java