数据结构(2)堆

什么是堆(二叉堆)?

堆,这里是二叉堆的简称,其实就是一棵二叉树,还是二叉树里比较特殊的完全二叉树。如果不熟悉的二叉树的相关概念的朋友,需要先复习一下,起码要知道二叉树的概念,比如子节点,叶子节点,树的层级这些概念。
完全二叉树:除了最后一层以外的所有层的节点都是被填满的,最后一层的叶子节点如果没有被填满,那么叶子节点的顺序是从左向右排列的。
当然,堆也有区分是 大顶堆小顶堆

  • 大顶堆:根节点是整棵二叉树中最大的数字。
  • 小顶堆:根节点是整棵二叉树中最大的数字。

用堆来实现什么?

有的时候,我们需要利用有限的空间来获取一些Top K这种数据,这种数据可以是最大的,也可以是最小的。
为啥说是有限空间?比如我们有个10G的数据,但是我们只有200M的内存,我们要取出前一万个最大的数字。常规的做法就是排序呗,但是这个内存太小,不足以让你在内存里面排序。
那咋整,每次拿个几万数据来排序,然后再和原来的那个一万个逐个比较?这是一种办法,但是时间复杂度太高。每次排序都要O(NlogN)O(NlogN),然后还要算上和原来的排好序的数组的进行比较,交换,搬移的时间,时间太久了。
那么有没有一个比较好的方法来完成这个任务呢?
答案就是利用优先队列,也就是这个数据结构,至于如何操作我们等等再讲。

为什么要用堆?

如果不是堆这个数据结构,那么可以用哪些来实现优先队列呢?

  1. 数组:入队(插入)时候,时间复杂度O(1)O(1);出队(删除)的时候,需要遍历整个数组,删除,并且把后面的元素前移,时间复杂度为O(n)O(n)
  2. 链表:入队时间复杂度为O(1)O(1);出队的时候,需要遍历整个数组,并且删除,但是不用进行数据搬移。
  3. 二叉查找树:入队的时间复杂度为O(log(n))O(log(n));出队时候时间复杂度为O(1)O(1)。但是有个弊端,就是在出队时候,会出现子树不平衡的现象,容易变成单链表,比如,如果是小顶堆,那么左子树就会一直减少;如果是大顶堆,那么右子树就会一直减少。
  4. 平衡二叉查找树:入队时间O(log(n))O(log(n)),出队的时间复杂度O(1)O(1)。但是平衡二叉查找树也支持许多不需要的特性,使用起来显得有点浪费,特别是不管哪种平衡二叉查找树的实现都是复杂的实现。

如何实现二叉堆?

(1)实现哪些接口

首先,我们要想清楚,这个二叉堆要什么功能?
一般数据结构个,增加,删除,查询,修改,这几个基本操作是必须的。
同样,身为数据结构,二叉堆也有以上几种操作,不过,不同的是,它 “变异” 了。
二叉堆的实现是接口:

public interface Heap<E extends Comparable> {
    /**
     * add element.
     * @param e element
     */
    void add(E e);

    /**
     * delete max element or min element.
     * @return max element or min element.
     */
    E delete();

    /**
     * find max element or min element.
     * @return max element or min element.
     */
    E find();

    /**
     * replace max element or min element.
     * @param e to replacing-element.
     */
    void replace(E e);
}

为什么用Comparable这个排序接口,主要是有compareTo这个接口,我们的这个二叉堆肯定是要去有比较前后大小性质的,所以需要进行排序。
add: 这个不用说,就是增加节点的操作。
注意:以下都是“变异”后的操作了
delete: 这个操作有点特殊,不是删除任意的节点,而是删除二叉堆中最大的或者是最小的点 。至于说到底是最大还是最小,这个取决于你实现的是最大堆还是最小堆
find: 这个不是按照查找任意节点,而是查找最大的或者最小的点。至于说到底是最大还是最小,这个取决于你实现的是最大堆还是最小堆
replace: 就是把最大的,或者最小的那个节点替换掉。至于说到底是最大还是最小,这个取决于你实现的是最大堆还是最小堆

(2)用最大堆作为例子

首先我们需要明白一个概念,虽然二叉堆是个二叉树,但是它如果用树的结构来存储数据是很耗费空间;因为它是个完全的二叉树,所以我们可以用数组来存储数据。
要理解这段话并不容易,首先我们要知道,之所以二叉树树的节点的数据结构,一般都要存一个左节点和右节点,我们可以根据当前节点来获取左节点和右节点,有的二叉树的节点结构有获取父节点的需求,还会增加一个父节点的点。所以,我们可以知道一条信息,就是你如果获取了某个树的节点,那么相应的,你需要获取这个二叉树的左孩子的节点的值,右孩子的节点的值。
因为二叉堆是个满二叉树,所以,我们只要知道某个索引,我们就可以根据索引来算出这个座标。
先来看看下面这张图:
在这里插入图片描述
其实就是把完全二叉树给塞进数组里面。
这里我们要知道计算左孩子,右孩子,父亲节点的索引几个公式。
父亲节点:(index1)/2(index - 1) / 2
左孩子:index2+1index * 2 + 1
右孩子:index2+2index * 2 + 2
我们用19这个节点来作为例子:
19的索引 :2
父亲的索引:0
左孩子的索引:5
右孩子的索引:6
可以带入公式,看看是否正确。
知道如何去保存数据之后,我们再来看看堆需要几个基本操作。
增加:
首先就是我们无论如何都要保存堆的一个性质,我们假设我们增加了一个元素,我们要保证增加元素过后,依然保证最大堆的性质不变,就是随便在什么地方,父亲节点大于孩子节点。
那么这个增加元素在最后,我们要怎么修改呢?
把增加的元素和他的父亲节点去比,如果比他父亲大,就交换,如果比父亲小,那么就结束。代码实现就是:

public void add(E e) {
   data.add(e);
   size++;
   shiftUp(size - 1);
}
/**
 * 上移操作:在底层,开始往上移动。
 * @param index 下移的座标
 */
private void shiftUp(int index) {
    int parentIndex = (index - 1) / 2;
    // 与父亲节点比较,如果大于父亲节点,就交换,
    while (parentIndex >= 0 && data.get(parentIndex).compareTo(data.get(index)) < 0) {
        swap(index, parentIndex);
        index = parentIndex;
        parentIndex = (index - 1) / 2;
    }
}

删除
把最大的节点删除,那么就会空出一个节点,别的节点也要相应的占位上来;其实这样看来,是比较复杂的,但是我们可以吧最后一个节点赋值到第一个节点,然后把最后一个结点删除,
然后去吧第一个节点进行比较,然后归位。

@Override
public E delete() {
    if (size <= 0) {
        throw new IllegalArgumentException("size under 0.");
    }
    E ans = data.get(0);
    data.set(0, data.get(size - 1));
    data.remove(size - 1);
    size--;
    shiftDown(0);
    return ans;
}
/**
 * 下移操作
 * @param index 下移的座标
 */
private void shiftDown(int index) {
    int maxIndex = index;
    while (true) {
        int leftChildIndex = 2 * index + 1;
        if (leftChildIndex < size && data.get(leftChildIndex).compareTo(data.get(maxIndex)) > 0) {
            maxIndex = leftChildIndex;
        }
        int rightChildIndex = 2 * index + 2;
        if (rightChildIndex < size && data.get(rightChildIndex).compareTo(data.get(maxIndex)) > 0) {
            maxIndex = rightChildIndex;
        }
        if (maxIndex == index) {
            break;
        }
        swap(maxIndex, index);
        index = maxIndex;
    }
}

其实堆里面,最重要就是两个操作,一个上移,一个下沉,只要搞懂了这个两个操作,堆你就拿下了。
我们来看看完整的代码。

public class MaxHeapMine<E extends Comparable> implements Heap<E> {

    private ArrayList<E> data;
    private int size;

    public MaxHeapMine() {
        data = new ArrayList<>();
        size = 0;
    }

    public MaxHeapMine(E[] elements) {
        data = new ArrayList<>();
        size = 0;
        for (E item : elements) {
            add(item);
        }
    }
    @Override
    public void add(E e) {
        data.add(e);
        size++;
        shiftUp(size - 1);
    }

    @Override
    public E delete() {
        if (size <= 0) {
            throw new IllegalArgumentException("size under 0.");
        }
        E ans = data.get(0);
        data.set(0, data.get(size - 1));
        data.remove(size - 1);
        size--;
        shiftDown(0);
        return ans;
    }

    @Override
    public E find() {
        if (size <= 0) {
            throw new IllegalArgumentException("size under 0.");
        }
        return data.get(0);
    }

    @Override
    public void replace(E e) {
        data.set(0, e);
        shiftDown(0);
    }

 	/**
     * 上移操作
     * @param index 下移的座标
     */
    private void shiftUp(int index) {
        int parentIndex = (index - 1) / 2;
        while (parentIndex >= 0 && data.get(parentIndex).compareTo(data.get(index)) < 0) {
            swap(index, parentIndex);
            index = parentIndex;
            parentIndex = (index - 1) / 2;
        }
    }

    /**
     * 下移操作
     * @param index 下移的座标
     */
    private void shiftDown(int index) {
        int maxIndex = index;
        while (true) {
            int leftChildIndex = 2 * index + 1;
            if (leftChildIndex < size && data.get(leftChildIndex).compareTo(data.get(maxIndex)) > 0) {
                maxIndex = leftChildIndex;
            }
            int rightChildIndex = 2 * index + 2;
            if (rightChildIndex < size && data.get(rightChildIndex).compareTo(data.get(maxIndex)) > 0) {
                maxIndex = rightChildIndex;
            }
            if (maxIndex == index) {
                break;
            }
            swap(maxIndex, index);
            index = maxIndex;
        }
    }

    private void swap(int a, int b) {
        E temp = data.get(a);
        data.set(a, data.get(b));
        data.set(b, temp);
    }

    public static void main(String[] args) {
        Integer[] test = {1, 2, 3, 4, 0, 9, 8, 7, 6, 5, 20, 19, 18, 17, 16};
        MaxHeapMine<Integer> maxHeapMine = new MaxHeapMine<>(test);
        for (int i = 0, len = test.length; i < len; ++i) {
            System.out.println(maxHeapMine.delete());
            System.out.println(maxHeapMine.find());
        }
    }
}

你可以把代码直接拷贝运行,看看效果如何,最后会打印出一组排好序的数字的。
其实上面的代码可以继续优化,减少交换的次数。不过有些不太好理解。有兴趣的同学可以是试试。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章