数据结构和算法之【堆】

目录

认识堆

堆的定义

大顶堆

小顶堆

堆的特性

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

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

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

自实现堆

定义接口

定义抽象类

大顶堆(具体实现类)

小顶堆(具体实现类)

总结

堆的应用场景

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];
        }
    }
}

测试结果

相关推荐
Cosmoshhhyyy2 小时前
《Effective Java》解读第45条:谨慎使用Stream
java·开发语言·c#
A Everyman3 小时前
Java 高效生成 Word 文档:poi-tl 的使用
java·pdf·word·poi-tl
短剑重铸之日3 小时前
深入理解Sentinel: 01 一次服务雪崩问题排查经历
java·sentinel·降级熔断
马猴烧酒.3 小时前
【面试八股|操作系统】操作系统常见面试题详解笔记
java·linux·服务器·网络·数据结构·算法·eclipse
薛定谔的悦3 小时前
《储能系统中的故障定位》
java·服务器·前端
六义义3 小时前
SpringBoot 超详细全解(入门 + 实战 + 原理 + 面试)
java·spring boot·面试
im_AMBER3 小时前
Leetcode 146 爬楼梯 | 打家劫舍
数据结构·算法·leetcode
fengxin_rou3 小时前
详解深浅拷贝:从原理到实现的完整指南
java·后端·浅拷贝·深拷贝
㓗冽3 小时前
2026.03.25(第一天)
数据结构