一. 堆
1. 堆的特点:
- 是一棵完全二叉树 : 把元素顺序排列成树的形状。
- 二叉堆有两种:最大堆和最小堆。
- 最大堆:父结点的键值总是大于或等于任何一个子节点的键值;
- 最小堆:父结点的键值总是小于或等于任何一个子节点的键值;
- 作用: 用来查找最大值(最大堆),最小值(最小堆)
- 父节点和左右子节点索引之间的关系:
parent(i) = ( i - 1 ) / 2
left child(i) = 2 * i + 1
right child(i) = 2 * i + 2
2 .堆的时间复杂度分析(以最大堆为例)
插入 O(logN)
插入逻辑:1. 刚在最后一个元素上 。 2。和它的父节点 比较大小,如果比父节点的值大,就互换,递归比较上面的元素,如果不大于父节点的值,就不动。
伪代码如下:
/**
* 向最大堆中添加元素 如果最小堆则改变siftUp的逻辑比较
* sift up 和父节点比较 如果大于父节点,交换和父节点的位置, 继续比较 上一父节点比较
* 时间复杂度O(logn)
* @param e
*/
public void add(E e) {
data.addLast(e);
//维护堆的性质
siftUp(data.getSize() - 1);
}
private void siftUp(int index) {
//todo 索引大于0 且 父亲元素小于当前元素
while (index > 0 && data.get(parent(index)).compareTo(data.get(index)) < 0) {
//交换两个位置的数据
data.swap(index, parent(index));
index = parent(index);
}
}
查询最大(小)的元素 O(1)
查询逻辑:正常查询和数组查询是一样的O(n) ,但是查最大或者最小的元素是O(1),直接去第一个元素。
伪代码日下:
//todo 看最大堆中最大的元素
public E findMax() {
if (data.getSize() == 0) {
throw new IllegalArgumentException("can not findMax when heap is empty");
}
return data.get(0);
}
删除最大(小)元素 O(logN)
伪代码如下:
/**
* 只能取出最大堆最大的元素 如果最小堆最小元素则改变siftDown的逻辑比较
* 堆顶根元素 替换成最后一个元素 然后和两个子结点中最大的交换位置,交换的子结点继续交换位置
* sift down 下沉
* 时间复杂度O(logn)
*/
public E extractMax() {
E ret = findMax();
//交换第一个和最后一个元素
data.swap(0, data.getSize() - 1);
data.removeLast();
siftDown(0);
return ret;
}
/**
* 下沉
*
* @param index
*/
private void siftDown(int index) {
//index位置没有左孩子了
while (leftChild(index) < data.getSize()) {
//左孩子索引
int j = leftChild(index);
//todo 有右孩子 且 比左孩子大
if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) {
j = rightChild(index);
}
if (data.get(index).compareTo(data.get(j)) >= 0) {
break;
}
data.swap(index, j);
index = j;
}
}
初始化一个数组成为一个最大堆(最小堆可以自己实现)
伪代码日下:
/**
* heapify 构造函数
*
* @param arr
*/
//heapify:将任意数组整理成堆的形状
// 将n个元素逐个插入空堆中,时间复杂度为O(nlogn)
//heapify的过程,算法复杂度为O(n)
// 1.首先看非叶子节点 和子结点对比,sift Down
public MaxHeap(E[] arr) {
data = new Array<>(arr);
for (int i = parent(arr.length - 1); i >= 0; i--) {
siftDown((i));
}
}
Java 堆的简单实现(最大堆)
下面的实现用到了前面的数组。
import java.util.Random;
/**
* 堆 本身也是一棵树
* 时间复杂度分析
* 添加:O(logn)
* 删除:O(logn)
* 二叉堆 Binary Heap
* 二叉堆 是一棵完全二叉树 不一定是满二叉树(除叶子节点都有左右子节点),
* 完全二叉树: 把元素顺序排列成树的形状,一层一层的放
* 堆中某个节点的值 总是不大于其父节点的值,最大堆(相应的可以定义最小堆)
* 最大堆:可以以数组的形式表示二叉堆
*
* <p>
* parent(i) = (i-1)/2
* left child(i) = 2*i + 1
* right child(i) = 2*i + 2
* <p>
*
* @author 一直往前走
* @date 2019/08/21
*/
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();
}
//todo 返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
private int parent(int index) {
if (index == 0) {
throw new IllegalArgumentException("index-0 doesn't hava parent.");
}
return (index - 1) / 2;
}
/**
* heapify 构造函数
*
* @param arr
*/
//heapify:将任意数组整理成堆的形状
// 将n个元素逐个插入空堆中,时间复杂度为O(nlogn)
//heapify的过程,算法复杂度为O(n)
// 1.首先看非叶子节点 和子结点对比,sift Down
public MaxHeap(E[] arr) {
data = new Array<>(arr);
for (int i = parent(arr.length - 1); i >= 0; i--) {
siftDown((i));
}
}
private int leftChild(int index) {
return index * 2 + 1;
}
private int rightChild(int index) {
return index * 2 + 2;
}
//todo 向堆中添加元素
//todo sift up 和父节点比较 如果大于父节点,交换和父节点的位置, 继续比较 上一父节点比较
//todo 时间复杂度O(logn)
/**
* 向堆中添加元素
* sift up 和父节点比较 如果大于父节点,交换和父节点的位置, 继续比较 上一父节点比较
* 时间复杂度O(logn)
*
* @param e
*/
public void add(E e) {
data.addLast(e);
//维护堆的性质
siftUp(data.getSize() - 1);
}
private void siftUp(int index) {
//todo 索引大于0 且 父亲元素小于当前元素
while (index > 0 && data.get(parent(index)).compareTo(data.get(index)) < 0) {
//交换两个位置的数据
data.swap(index, parent(index));
index = parent(index);
}
}
//todo 看堆中最大的元素
public E findMax() {
if (data.getSize() == 0) {
throw new IllegalArgumentException("can not findMax when heap is empty");
}
return data.get(0);
}
/**
* 只能取出最大的元素
* 堆顶根元素 替换成最后一个元素 然后和两个子结点中最大的交换位置,交换的子结点继续交换位置
* sift down 下沉
* 时间复杂度O(logn)
*/
public E extractMax() {
E ret = findMax();
//交换第一个和最后一个元素
data.swap(0, data.getSize() - 1);
data.removeLast();
siftDown(0);
return ret;
}
/**
* 下沉
*
* @param index
*/
private void siftDown(int index) {
//index位置没有左孩子了
while (leftChild(index) < data.getSize()) {
//左孩子索引
int j = leftChild(index);
//todo 有右孩子 且 比左孩子大
if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) {
j = rightChild(index);
}
if (data.get(index).compareTo(data.get(j)) >= 0) {
break;
}
data.swap(index, j);
index = j;
}
}
//replace 取出最大元素后,放入一个新元素
//实现1: 可以先extraxtMax,再add,两次O(logn)的擦坐
//todo 实现2: 可以直接将堆顶元素替换以后Sift Down ,一次O(logn)的操作
public E repalce(E e) {
E ret = findMax();
//将堆顶元素替换成e
data.set(0, e);
siftDown(0);
return ret;
}
public static void main(String[] args) {
int n = 100_0000;
MaxHeap<Integer> maxHeap = new MaxHeap<>();
Random random = new Random();
for (int i = 0; i < n; i++) {
maxHeap.add(random.nextInt(Integer.MAX_VALUE));
}
int[] arr = new int[n];
for (int i = 1; i < n; i++) {
if (arr[i - 1] < arr[i]) {
throw new IllegalArgumentException("Error");
}
}
System.out.println("Test MaxHeap completed.");
}
}
二. 优先队列
1. 优先队列的定义
出队顺序和入队顺序无关;和优先级有关。
2. 优先级队列的用处
动态选择优先级最高的任务执行。
3. 优先队列就是用堆来实现的
Java代码如下
import com.mk.coffee.test.dataStructure.heap.MaxHeap;
/**
* 优先队列 底层实现: 最大堆数据
* 操作 入队 出队
* <p>
* 普通线性结构 O(1) O(n)
* 顺序线性结构 O(n) O(1)
* 堆实现优先队列 O(logn) O(logn)
*
* @author makui
* @date 2019/08/21
*/
public class PriorityQueue<E extends Comparable<E>> {
private MaxHeap<E> maxHeap;
public PriorityQueue() {
maxHeap = new MaxHeap<>();
}
public int getSize() {
return maxHeap.size();
}
public boolean isEmpty() {
return maxHeap.isEmpty();
}
public E getFront() {
return maxHeap.findMax();
}
public void enqueue(E e) {
maxHeap.add(e);
}
public E dequeue() {
return maxHeap.extractMax();
}
}
4. 时间复杂度分析
入队: O(logN)
出队: O(logN)