二叉堆的建立、插入、删除

676 阅读7分钟

一、基本概念:

1、完全二叉树:若二叉树的深度为h,则除第h层外,其他层的结点全部达到最大值,且第h层的所有结点都集中在左子树。

2、满二叉树:满二叉树是一种特殊的的完全二叉树,所有层的结点都是最大值。

二叉堆

二叉堆是基于完全二叉树的基础上,加以一定的条件约束的一种特殊的二叉树。
根据约束条件的不同,二叉堆又可以分为两个类型:
大顶堆小顶堆

大顶堆

即任何一个父节点的值,都 大于等于 它左右孩子节点的值。

image.png

小顶堆

即任何一个父节点的值,都 小于等于 它左右孩子节点的值。 二叉堆的根节点叫做 堆顶 ,它是大顶堆里面的最大值,小顶堆里的最小值。

image.png

二、堆的存储与表示

我们在二叉树章节中学习到,完全二叉树非常适合用数组来表示。由于堆正是一种完全二叉树,我们将采用

数组来存储堆。

当使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置。节点指针通过索引映射公式

来实现。

如图 所示,给定索引 𝑖 ,其左子节点索引为 2𝑖 + 1 ,右子节点索引为 2𝑖 + 2 ,父节点索引为 (𝑖 − 1)/2

(向下取整)。当索引越界时,表示空节点或节点不存在。

image.png

我们可以将索引映射公式封装成函数,方便后续使用。

/**
 * 获取左边子节点的索引
 * @param parent 父节点索引
 * @return
 */
private int left(int parent) {
    return 2* parent + 1;
}

/**
 * 获取右边子节点的索引
 * @param parent 父节点索引
 * @return
 */
private int right(int parent) {
    return 2* parent + 2;
}

/**
 * 获取父节点索引
 * @param son 子节点索引
 * @return
 */
private int getParent(int son) {
    return  (son - 1) / 2;
}

三、访问堆顶元素

堆顶元素即为二叉树的根节点,也就是列表的首个元素。

/**
 *  访问堆定元素
 * @return
 */
public T peek() {
    return maxHeap.get(0);
}

四. 元素入堆

给定元素 val ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能

已被破坏。因此,需要修复从插入节点到根节点的路径上的各个节点,这个操作被称为「堆化 heapify」。

考虑从入堆节点开始,从底至顶执行堆化。如图 8‑3 所示,我们比较插入节点与其父节点的值,如果插入节

点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须

交换的节点时结束。

image.png

image.png

image.png

设节点总数为 𝑛 ,则树的高度为 𝑂(log 𝑛) 。由此可知,堆化操作的循环轮数最多为 𝑂(log 𝑛) ,元素入堆操

作的时间复杂度为 𝑂(log 𝑛) 。

/**
 * val 元素入队
 * @param val
 */
public void push (T val) {
    // 尾部添加元素
    maxHeap.add(val);
    // 进行上浮操作
    siftUp(maxHeap.size() - 1);
}

/**
 * 进行上浮操作
 * @param i
 */
private void siftUp(int i) {
    // 节点拥有父节点,才需要上浮
    while (i > 0) {
        // 获取父节点索引
        int parentIndex = getParent(i);
        // 如果父节点小于 子节点,进行上浮
        if (maxHeap.get(parentIndex).compareTo(maxHeap.get(i)) < 0) {
            // 父子,节点数据进行交换
            exchangeData(parentIndex,i);
            // 进行迭代,上浮
            i = parentIndex;
        } else {
            return;
        }
    }
}

/**
 * 交换索引处数据
 * @param j 索引j
 * @param i 索引i
 */
private void exchangeData(int j, int i) {
    T iData = maxHeap.get(i);
    T jData = maxHeap.get(j);
    maxHeap.set(i,jData);
    maxHeap.set(j,iData);
}

五、堆顶元素出堆

堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的

索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采用以下操

作步骤。

  1. 交换堆顶元素与堆底元素(即交换根节点与最右叶节点)。

  2. 交换完成后,将堆底从列表中删除(注意,由于已经交换,实际上删除的是原来的堆顶元素)。

  3. 从根节点开始,从顶至底执行堆化。

image.png

image.png

image.png

与元素入堆操作相似,堆顶元素出堆操作的时间复杂度也为 𝑂(log 𝑛) 。

/**
 * 元素出队处理
 * @return
 */
public T pop () {
    // 堆为空,返回空
    if (maxHeap.isEmpty()) {
        return null;
    }
    // 交换根节点和最后一个节点数据
    exchangeData(0,getHeapSize()-1);
    // 删除最后一个元素
    T val = maxHeap.remove(getHeapSize() - 1);
    // 从顶至底堆化
    siftDown(0);
    return val;
}

/***
 * 进行下沉操作
 * @param i
 */
private void siftDown(int i) {

    while (true) {
        int lIndex = left(i);
        int rIndex = right(i);
        // 左右节点的最大值对应索引
        int maxIndex = lIndex;
        // 没有左节点,不处理
        if (lIndex > maxHeap.size()-1) {
            return;
        }
        // 获取左节点值
        T lData = maxHeap.get(lIndex);
        // 有右节点
        if (rIndex <= getHeapSize() - 1) {
            T rData = maxHeap.get(rIndex);
            // 右节点 > 左节点
            if (rData.compareTo(lData) > 0) {
                // 取右节点
                maxIndex = rIndex;
            }
        }
        // 比较两个节点
        T sonData = maxHeap.get(maxIndex);
        T data = maxHeap.get(i);
        // 父节点小于子节点
        if (data.compareTo(sonData) < 0) {
            // 交换
            exchangeData(maxIndex,i);
            // 进行下一轮迭代
            i = maxIndex;
        } else {
            return;
        }

    }
}

六、堆常见应用

  • 优先队列:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 𝑂(log 𝑛),而建队操作为 𝑂(𝑛) ,这些操作都非常高效。

  • 堆排序:给定一组数据,我们可以用它们建立一个堆,然后不断地执行元素出堆操作,从而得到有序数据。然而,我们通常会使用一种更优雅的方式实现堆排序,详见后续的堆排序章节。

  • 获取最大的 𝑘 个元素:这是一个经典的算法问题,同时也是一种典型应用,例如选择热度前 10 的新闻作为微博热搜,选取销量前 10 的商品等。

七、完整代码

package heapP;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Administrator
 */
public class myHeap<T extends Comparable<T>>  {

    // 存储数据
    private List<T> maxHeap;


    /**
     * 获取堆的大小
     * @return
     */
    public int getHeapSize() {
        return maxHeap.size();
    }

    /**
     * 构建函数
     */
    public myHeap() {
        this.maxHeap = new ArrayList<>();
    }

    /**
     * 获取左边子节点的索引
     * @param parent 父节点索引
     * @return
     */
    private int left(int parent) {
        return 2* parent + 1;
    }

    /**
     * 获取右边子节点的索引
     * @param parent 父节点索引
     * @return
     */
    private int right(int parent) {
        return 2* parent + 2;
    }

    /**
     * 获取父节点索引
     * @param son 子节点索引
     * @return
     */
    private int getParent(int son) {
        return  (son - 1) / 2;
    }

    /**
     *  访问堆定元素
     * @return
     */
    public T peek() {
        return maxHeap.get(0);
    }


    /**
     * val 元素入队
     * @param val
     */
    public void push (T val) {
        // 尾部添加元素
        maxHeap.add(val);
        // 进行上浮操作
        siftUp(maxHeap.size() - 1);
    }

    /**
     * 进行上浮操作
     * @param i
     */
    private void siftUp(int i) {
        // 节点拥有父节点,才需要上浮
        while (i > 0) {
            // 获取父节点索引
            int parentIndex = getParent(i);
            // 如果父节点小于 子节点,进行上浮
            if (maxHeap.get(parentIndex).compareTo(maxHeap.get(i)) < 0) {
                // 父子,节点数据进行交换
                exchangeData(parentIndex,i);
                // 进行迭代,上浮
                i = parentIndex;
            } else {
                return;
            }
        }
    }

    /**
     * 交换索引处数据
     * @param j 索引j
     * @param i 索引i
     */
    private void exchangeData(int j, int i) {
        T iData = maxHeap.get(i);
        T jData = maxHeap.get(j);
        maxHeap.set(i,jData);
        maxHeap.set(j,iData);
    }

    /**
     * 元素出队处理
     * @return
     */
    public T pop () {
        // 堆为空,返回空
        if (maxHeap.isEmpty()) {
            return null;
        }
        // 交换根节点和最后一个节点数据
        exchangeData(0,getHeapSize()-1);
        // 删除最后一个元素
        T val = maxHeap.remove(getHeapSize() - 1);
        // 从顶至底堆化
        siftDown(0);
        return val;
    }

    /***
     * 进行下沉操作
     * @param i
     */
    private void siftDown(int i) {

        while (true) {
            int lIndex = left(i);
            int rIndex = right(i);
            // 左右节点的最大值对应索引
            int maxIndex = lIndex;
            // 没有左节点,不处理
            if (lIndex > maxHeap.size()-1) {
                return;
            }
            // 获取左节点值
            T lData = maxHeap.get(lIndex);
            // 有右节点
            if (rIndex <= getHeapSize() - 1) {
                T rData = maxHeap.get(rIndex);
                // 右节点 > 左节点
                if (rData.compareTo(lData) > 0) {
                    // 取右节点
                    maxIndex = rIndex;
                }
            }
            // 比较两个节点
            T sonData = maxHeap.get(maxIndex);
            T data = maxHeap.get(i);
            // 父节点小于子节点
            if (data.compareTo(sonData) < 0) {
                // 交换
                exchangeData(maxIndex,i);
                // 进行下一轮迭代
                i = maxIndex;
            } else {
                return;
            }

        }
    }
}

八、测试代码

package heapP;

/**
 * @author Administrator
 */
public class TestDemo {

    public static void main(String[] args) {
        myHeap<Integer> myHeap = new myHeap<>();
        for (int i = 1; i < 10;i++) {
            myHeap.push(i);
        }

        while (myHeap.getHeapSize() > 0) {
            Integer pop = myHeap.pop();
            System.out.println(pop);
        }
    }
}

输出

输出:
9
8
7
6
5
4
3
2
1