Java 数据结构与算法 堆

    堆也叫做优先级队列。队列是一种先进先出的数据结构,有的时候,要操作的数据有优先级,需要优先级高的先出队列,堆就是这样的数据结构。堆的数据结构提供了两个最基本的操作:返回最高级优先级对象和添加新的对象。堆实际上是一种在完全二叉树的基础上进行元素的调整。

1、堆基本概念

    如果有一个集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一 个一维数组中,并满足:Ki = K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大 堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

    堆的性质:1,堆总是一颗完全二叉树。2,堆中的某个节点的值总是不大于或不小于其父亲节点的值。这里就是指大根堆或小根堆。

    堆是一颗使用数组来存储的完全二叉树,整个的遍历过程使用了层序遍历。和二叉树相比,堆是线性顺序存储的,但是在画图理解的时候,我们画成完全二叉树的形式。

2、堆的创建

    关于堆的创建,需要用到堆的向下调整。因为堆是把元素存在数组中的,如果我们假设i为节点在数组中的下标,那么根据完全二叉树的性质,i是被称为父亲节点的话:

(1)如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2;

(2)如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子;

(3)如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子。

    这个在后面堆的创建中需要用到,下面我们以建立大堆为例。堆(优先级队列)的存储数据的方式是使用数组来存储的,先定义好数组和有效数组的大小。

class TestHeap {
    public int[] elem;
    public int usedSize;

    public TestHeap(int[] elem) {
        this.elem = new int[10];
    }
}

    然后把要建立堆的数组进行拷贝。也可以不拷贝,直接使用。拷贝好之后,从什么地方开调整?这就需要画图来看一看。把使用数组存储的数据画成完全二叉树的形式后,发现:

    直接从0下标开始的话,调整起来不方便。所以从尾节点对应的父节点开始,一步一步调整,一直到0下标。以9下标为例,它的父节点是4下标,是整个数组有效长度的一半。所以调整要从整个数组有效长度的一半开始调整。使用createHeap方法。

class TestHeap {
    public int[] elem;
    public int usedSize;

    public TestHeap(int[] elem) {
        this.elem = new int[10];
    }

    //建堆
    public void createHeap(int[] array) {
        //拷贝过程
        for (int i = 0; i < array.length; i++) {
            this.elem[i] = array[i];
            ++this.usedSize;
        }
        //开始调整过程
        for (int p = (usedSize - 1 - 1) >>> 1; p >= 0; p--) {
            shiftDown(p, usedSize);
        }
    }
}

调整的过程使用一个shiftDown方法,传进去这个下标和数组的有效长度。

shiftDown方法

    这个方法就是具体的调整方法。为了方便,我们定义parent和child变量。其中,parent就是父亲节点,child就是父亲节点对应的孩子节点,有左右孩子之分。他们的关系需要用到前面的性质。其实从图上就能看出来,以3号下标为例,它的左右孩子下标就是7和8。对于左孩子来说,和父节点的下标关系就是child = parent * 2 + 1;右孩子下标就是左孩子下标在加1。要注意的是整个的组织是按照完全二叉树的方式来组织的,调整需要知道父亲和孩子节点的下标关系。而孩子节点的下标在整个数组有效长度里面就说明存在的,比如父节点为5下标的节点。

 private void shiftDown(int root, int len) {
        int parent = root;
        int child = 1 + root << 1;
}

    然后就开始循环调整。整个堆成为大根堆,不能只调整一部分,要调整到从根节点到叶子节点的全部。大根堆是父亲节点的值大于孩子节点。所以需要先找到孩子节点的最大值,在和父亲节点对比交换。

    进入循环的条件是只要有左孩子就可以调整了。父亲节点和孩子节点调整的前提是至少有一个孩子。进入循环后,寻找左右孩子的最大值和父亲节点交换。寻找的前提是要存在左右孩子。

    找到孩子的最大值后,就和父节点对比,交换。如果没有交换,说明已经是大根堆了,就直接结束循环。否则,就移动父节点到孩子节点位置继续调整。

    建立堆的时间复杂度是O(N)。

private void shiftDown(int root, int len) {
        int parent = root;
        int child = 1 + root << 1;
        //进入这个循环,至少有一个孩子
        while (child < len) {
            //如果有右孩子,找到左右孩子的最大值
            if (child + 1 < len && this.elem[child] < this.elem[child + 1]) {
                ++child;
            }
            //child一定是最大值的下标
            //进行交换
            if (this.elem[child] > this.elem[parent]) {
                swap(elem, parent, child);
                parent = child;
                child = 1 + parent << 1;
            } else {
                break;
            }
        }
    }

    private void swap(int[] elem, int parent, int child) {
        int tmp = this.elem[child];
        this.elem[child] = this.elem[parent];
        this.elem[parent] = tmp;
    }

3、堆的插入

堆的插入是把元素插入到堆的尾部,也就是数组的尾部,然后进行调整。插入的时候要判断数组元素的空间够不够,不够的话就进行扩容。因为要调整的是一条路径,需要向上调整。使用push方法。调整的方法使用shiftUp方法。

    public void push(int val) {
        //判断是不是满了
        if (isFull()) {
            elem = Arrays.copyOf(elem,2 * elem.length);
            elem[usedSize] = val;
            //开始调整
            shiftUp(usedSize);
            ++usedSize;
        }
    }
    private boolean isFull() {
        return usedSize == elem.length;
    }

shiftUp方法

    因为整个数据按照完全二叉树的形式组织的,所以,新插入的元素要找到它的父亲节点。对于孩子节点,设下标为i,它的父亲节点下标为 ( i - 1 )/ 2。就然后进行比较交换。这里的交换可以直接使用前面的swap方法。一次比较交换完成后,还要继续比较,一直到整个路径上的元素调整好。如果没有,就结束。

    private void shiftUp(int child) {
        int parent = (child - 1) >>> 1;
        while (child > 0) {
            if (elem[child] > elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                child = parent;
                parent = (child - 1) >>> 1;
            } else {
                break;
            }
        }
    }

4、堆的删除

    堆删除需要删除堆顶的元素,但是不能直接在堆顶上删,而是要把堆顶元素和堆尾元素交换后来删除。减少有效元素个数,同时,对堆顶元素进行向下调整。整个的过程需要保持大根堆。删除之前先要判断这个堆是不是空的堆。

    public void poll() {
        if (isEmpty()) {
            return;
        }
        int tmp = elem[0];
        elem[0] = elem[usedSize - 1];
        elem[usedSize - 1] = tmp;
        --usedSize;
        //调整
        shiftDown(0, usedSize);
    }

    //判断堆是不是空
    private boolean isEmpty() {
        return usedSize == 0;
    }

    从堆的插入和删除来看,堆的操作中的核心就是对堆的向上调整和向下调整方法。

5、获取堆顶元素

直接返回第一个元素下标就可以。如果是空的堆,就返回-1。

    public int peek() {
        if (isEmpty()) {
            return -1;
        }
        return elem[0];
    }

6、堆排序

    要从小到大排序,需要建立大根堆,而从大到小排序,需要建立小根堆。从堆最后一个元素开始调整。每一次调整将该元素与堆顶元素交换,在向下调整。

    public void HeapSort() {
        int end = this.usedSize - 1;
        while (end > 0) {
            swap(this.elem, 0, end);
            shiftDown(0, end);
            --end;
        }
    }

堆中最重要的几个操作都调用了向上调整方法或向下调整方法。搞明白这两个方法是关键。

7、Java官方的实现

    Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,这里介绍PriorityQueue。

使用这个方法的时候,要注意:

(1)PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常;

(2)不能插入null对象,否则会抛出NullPointerException;

(3)PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素。

PriorityQueue (Java Platform SE 7 ) (oracle.com)https://docs.oracle.com/javase/7/docs/api/java/util/PriorityQueue.html

        PriorityQueue<Integer> queue = new PriorityQueue<Integer>();
        queue.add(3);
        queue.add(2);
        queue.add(1);
        System.out.println(queue);

        System.out.println("=============");
        queue.poll();
        System.out.println(queue);

        System.out.println("=============");
        System.out.println(queue.peek());

要改为大堆创建的形式,需要实现接口。

class IntCmp implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
}

public class Main {

    public static void main(String[] args) {
        PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new IntCmp());
        queue.offer(3);
        queue.offer(2);
        queue.offer(1);
        System.out.println(queue);

        System.out.println("=============");
        queue.poll();
        System.out.println(queue);

        System.out.println("=============");
        System.out.println(queue.peek());
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值