玩转数据结构(13)-- 优先队列和堆的添加/取出操作

优先队列和堆

一、优先队列

普通队列:先进先出,后进后出

优先队列:出队顺序和入队顺序无关;和优先级相关;(例如医院排队和操作系统动态选择优先级最高的任务执行)

图解:关键词“动态”;队列中的元素是在不断变化的,不断有新元素入队,不仅仅是按照优先级排序;【优先级可以具体定义】

与普通队列的区别:出队元素是优先级最高的元素;队首元素也是优先级最高的元素,而不是最早进入队列的元素;

二、堆的基础结构

1.二叉堆(Binary Heap)

二叉堆要满足的条件:二叉堆是一颗完全二叉树;完全二叉树:把元素顺序排列成树的形状;【最下面一层都是叶子节点,但在最后一层上面可能还有叶子节点,但这些叶子节点必须全在树的右侧】

二叉堆的性质:【节点大小和所处层次之间并没有必然的联系】

1.最大堆:堆中某个节点的值总是小于其父节点的值;【父亲节点的值要大于左右孩子的值】

2.最小堆:堆中某个节点的值总是大于其父节点的值;【父亲节点的值要小于左右孩子的值】

可以用数组来实现完全二叉树:

代码实现:最大堆  MaxHeap.java

public class MaxHeap<E extends Comparable<E>> {	//要具有可比较性Comparable

    private Array<E> data;

    public MaxHeap(int capacity){
        data = new Array<>(capacity);
    }

    public MaxHeap(){
        data = new Array<>();
    }

    // 返回堆中的元素个数
    public int size(){
        return data.getSize();
    }

    // 返回一个布尔值, 表示堆中是否为空
    public boolean isEmpty(){
        return data.isEmpty();
    }

    // 返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
    private int parent(int index){
        if(index == 0)	//根节点,没有父亲节点
            throw new IllegalArgumentException("index-0 doesn't have parent.");
        return (index - 1) / 2;
    }

    // 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
    private int leftChild(int index){
        return index * 2 + 1;
    }

    // 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
    private int rightChild(int index){
        return index * 2 + 2;
    }
}

三、 向堆中添加元素

堆中元素上浮(Sift up)

1.在堆中添加元素 52

2. 添加 52 后,52 > 16 ,不满足条件 父亲节点 > 孩子节点;要进行调整:从 52 开始一路与其父亲节点做比较并交换顺序

将 52 与 16 交换顺序;但 52 > 41 ,继续交换位置

这时,62 > 52 ,符合堆的性质  

堆中元素上浮(Sift up):52 从底部逐渐上浮,直到其合适的位置依然可以维持堆的性质

代码实现:

MaxHeap.java

public class MaxHeap<E extends Comparable<E>> {

    private Array<E> data;

    public MaxHeap(int capacity){
        data = new Array<>(capacity);
    }

    public MaxHeap(){
        data = new Array<>();
    }

    // 返回堆中的元素个数
    public int size(){
        return data.getSize();
    }

    // 返回一个布尔值, 表示堆中是否为空
    public boolean isEmpty(){
        return data.isEmpty();
    }

    // 返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
    private int parent(int index){
        if(index == 0)
            throw new IllegalArgumentException("index-0 doesn't have parent.");
        return (index - 1) / 2;
    }

    // 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
    private int leftChild(int index){
        return index * 2 + 1;
    }

    // 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
    private int rightChild(int index){
        return index * 2 + 2;
    }

    // 向堆中添加元素(新增代码)
    public void add(E e){
        data.addLast(e);
        siftUp(data.getSize() - 1);
    }

    private void siftUp(int k){

        while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0 ){ 
		//K所在的父亲元素与K做比较,如果父亲元素更小的话,二者交换位置
            data.swap(k, parent(k));	//使用 swap 方法进行k 与k父亲元素交换
            k = parent(k);
        }
    }
}

Array.java


public class Array<E> {

    private E[] data;
    private int size;

    // 构造函数,传入数组的容量capacity构造Array
    public Array(int capacity){
        data = (E[])new Object[capacity];
        size = 0;
    }

    // 无参数的构造函数,默认数组的容量capacity=10
    public Array(){
        this(10);
    }

    // 获取数组的容量
    public int getCapacity(){
        return data.length;
    }

    // 获取数组中的元素个数
    public int getSize(){
        return size;
    }

    // 返回数组是否为空
    public boolean isEmpty(){
        return size == 0;
    }

    // 在index索引的位置插入一个新元素e
    public void add(int index, E e){

        if(index < 0 || index > size)
            throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");

        if(size == data.length)
            resize(2 * data.length);

        for(int i = size - 1; i >= index ; i --)
            data[i + 1] = data[i];

        data[index] = e;

        size ++;
    }

    // 向所有元素后添加一个新元素
    public void addLast(E e){
        add(size, e);
    }

    // 在所有元素前添加一个新元素
    public void addFirst(E e){
        add(0, e);
    }

    // 获取index索引位置的元素
    public E get(int index){
        if(index < 0 || index >= size)
            throw new IllegalArgumentException("Get failed. Index is illegal.");
        return data[index];
    }

    // 修改index索引位置的元素为e
    public void set(int index, E e){
        if(index < 0 || index >= size)
            throw new IllegalArgumentException("Set failed. Index is illegal.");
        data[index] = e;
    }

    // 查找数组中是否有元素e
    public boolean contains(E e){
        for(int i = 0 ; i < size ; i ++){
            if(data[i].equals(e))
                return true;
        }
        return false;
    }

    // 查找数组中元素e所在的索引,如果不存在元素e,则返回-1
    public int find(E e){
        for(int i = 0 ; i < size ; i ++){
            if(data[i].equals(e))
                return i;
        }
        return -1;
    }

    // 从数组中删除index位置的元素, 返回删除的元素
    public E remove(int index){
        if(index < 0 || index >= size)
            throw new IllegalArgumentException("Remove failed. Index is illegal.");

        E ret = data[index];
        for(int i = index + 1 ; i < size ; i ++)
            data[i - 1] = data[i];
        size --;
        data[size] = null; // loitering objects != memory leak

        if(size == data.length / 4 && data.length / 2 != 0)
            resize(data.length / 2);
        return ret;
    }

    // 从数组中删除第一个元素, 返回删除的元素
    public E removeFirst(){
        return remove(0);
    }

    // 从数组中删除最后一个元素, 返回删除的元素
    public E removeLast(){
        return remove(size - 1);
    }

    // 从数组中删除元素e
    public void removeElement(E e){
        int index = find(e);
        if(index != -1)
            remove(index);
    }

    public void swap(int i, int j){

        if(i < 0 || i >= size || j < 0 || j >= size)
            throw new IllegalArgumentException("Index is illegal.");

        E t = data[i];
        data[i] = data[j];
        data[j] = t;
    }

    @Override
    public String toString(){

        StringBuilder res = new StringBuilder();
        res.append(String.format("Array: size = %d , capacity = %d\n", size, data.length));
        res.append('[');
        for(int i = 0 ; i < size ; i ++){
            res.append(data[i]);
            if(i != size - 1)
                res.append(", ");
        }
        res.append(']');
        return res.toString();
    }

    // 将数组空间的容量变成newCapacity大小
    private void resize(int newCapacity){

        E[] newData = (E[])new Object[newCapacity];
        for(int i = 0 ; i < size ; i ++)
            newData[i] = data[i];
        data = newData;
    }
}

四、从堆中取出元素

取出元素,只取出堆顶元素(二叉树根节点),这个元素是二叉树中存储的最大的元素【取出操作只能取出该元素,不能取出其他元素】;

过程:1.访问根节点,数组中索引为 0 的元素;将其拿走之后,对整个堆来说,可以看成是有两颗子树;

2.将堆中的最后一个元素(即 16)顶到堆顶去;数组0变为16并删掉数组中10对应的16

3.但16在堆顶的话,打破了堆的性质,16 < 52 也 < 30 ;不符合堆中父亲节点大于左右孩子的性质;

Sift Down:堆中元素下沉;

下沉过程1.父亲节点和左右孩子进行比较(16与52和30进行比较);

2.选择孩子中较大的元素,交换位置;

3.父亲节点继续与左右孩子对比,重复上面步骤

4.16 > 15,满足堆的性质,最终形态为下图

代码实现:MaxHeap.java

public class MaxHeap<E extends Comparable<E>> {

    private Array<E> data;

    public MaxHeap(int capacity){
        data = new Array<>(capacity);
    }

    public MaxHeap(){
        data = new Array<>();
    }

    // 返回堆中的元素个数
    public int size(){
        return data.getSize();
    }

    // 返回一个布尔值, 表示堆中是否为空
    public boolean isEmpty(){
        return data.isEmpty();
    }

    // 返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
    private int parent(int index){
        if(index == 0)
            throw new IllegalArgumentException("index-0 doesn't have parent.");
        return (index - 1) / 2;
    }

    // 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
    private int leftChild(int index){
        return index * 2 + 1;
    }

    // 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
    private int rightChild(int index){
        return index * 2 + 2;
    }

    // 向堆中添加元素
    public void add(E e){
        data.addLast(e);
        siftUp(data.getSize() - 1);
    }

    private void siftUp(int k){

        while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0 ){
            data.swap(k, parent(k));
            k = parent(k);
        }
    }

    // 看堆中的最大元素
    public E findMax(){
        if(data.getSize() == 0)
            throw new IllegalArgumentException("Can not findMax when heap is empty.");
        return data.get(0);
    }

    // 取出堆中最大元素(新增代码)
    public E extractMax(){

        E ret = findMax();

        data.swap(0, data.getSize() - 1);
        data.removeLast();	//删掉最大元素
        siftDown(0);	//进行下沉操作,对应索引为0

        return ret;
    }

    private void siftDown(int k){

        while(leftChild(k) < data.getSize()){//左孩子的索引比数组元素总数还要小,k节点没有孩子了
            int j = leftChild(k); // 在此轮循环中,data[k]和data[j]交换位置
            if( j + 1 < data.getSize() &&
                    data.get(j + 1).compareTo(data.get(j)) > 0 )
                j ++;	//j 存储的实际上是右孩子的索引
            // data[j] 是 leftChild 和 rightChild 中的最大值

            if(data.get(k).compareTo(data.get(j)) >= 0 ) //k 与孩子中最大元素j进行对比
                break;

            data.swap(k, j);	//若 j>k,则交换二者位置
            k = j;
        }
    }
}

Main.java(测试)

import java.util.Random;

public class Main {

    public static void main(String[] args) {

        int n = 1000000;	//随机数数量

        MaxHeap<Integer> maxHeap = new MaxHeap<>();
        Random random = new Random();
        for(int i = 0 ; i < n ; i ++)
            maxHeap.add(random.nextInt(Integer.MAX_VALUE));//对1000000个随机数进行排序(从大到小)

        int[] arr = new int[n];
        for(int i = 0 ; i < n ; i ++)
            arr[i] = maxHeap.extractMax();

        for(int i = 1 ; i < n ; i ++)
            if(arr[i-1] < arr[i])	//比较相邻数(右边>左边)
                throw new IllegalArgumentException("Error");

        System.out.println("Test MaxHeap completed.");
    }
}

输出:

时间复杂度分析:添加(add) 和 取出操作(extractMax) 的时间复杂度都是:O(log n)【还是二叉树高度这个级别,但对于堆来说,由于其是完全二叉树,故永远不会变为链表形态,不存在最差时为 n 的情况】

猜你喜欢

转载自blog.csdn.net/jianghao233/article/details/82732278
今日推荐