引入-二叉树的顺序存储
- 如何顺序存储?就是用层序遍历将二叉树的节点一个个的读取出来,然后依次放入数组。这种存储方式只适合完全二叉树,非完全二叉树有些层的节点可能不满,放入数组会造成空间浪费。
- 以上顺序存储方式可以用来表示堆。
堆
- 堆是什么?堆就是一种数据结构,在逻辑上认为是一棵完全二叉树,但在物理上使用数组来存储的一种数据结构。简单来说,堆就是用数组实现的二叉树。
- 堆分为大根堆和小根堆。满足任意节点的值都大于其子树中节点的值,叫做大根堆。满足任意节点的值都小于其子树中节点的值,叫做小根堆。
- 堆有什么用?堆最常见的作用,就是用来快速寻找一个集合或者一组数据中的最值,最大值或者最小值。
堆属性
- 堆分为大根堆和小根堆,也叫最大堆和最小堆。两者的差别在于节点的排序方式不同。
- 最大堆中, 父节点的值比每一个子节点的值都要大。最小堆中,父节点的值比每一个子节点的值都要小。这种特性就叫堆属性,并且这个属性对堆中的每一个节点都成立。
- 举例:
- 这个堆是最大堆,因为每一个父节点的值都大于子节点。(
10
大于7
和2
,7
又大于5
和1
) - 注意:最大堆总是将最大的值放在根节点,最小堆总是将最小值放在根节点。但其他节点的大小和排序顺序是不确定的,最小的元素未必是最后一个叶子节点。只能确定的是父节点和子节点之间的大小。
堆的实现
- 堆的实现需要用数组(就是二叉树的顺序存储。)
- 这个数组中随便拿出一个元素就是二叉树中的一个节点,那么如何确定其父节点和子节点分别是哪些元素?这需要用到下标。根据父节点和子节点之间的下标关系来确定。
下标关系:
- 可以借助下图来理解数组索引和节点位置之间的关系:
- 上图中二叉树的数字就是对应数组中的下标。
堆的基本操作(向下调整&建堆)
堆中有两个基本操作:向上调整和向下调整。
shiftUp()
:如果一个节点比它的父节点大(最大堆)或者小(最小堆),那么需要将它同父节点交换位置。这样是这个节点在数组的位置上升。shiftDown()
:如果一个节点比它的子节点小(最大堆)或者大(最小堆),那么需要将它向下移动。这个操作也称作“堆化(heapify)”。
public void adjustDown(int root,int len) {
int parent = root;
int child = 2*parent+1;
while(child < len) {
//找到左右孩子的最大值
//1、前提是你得有右孩子
if(child+1 < len && this.elem[child] < this.elem[child+1]) {
child++;
}
//保证,child下标的数据 一定是左右孩子的最大值的下标
if(this.elem[child] > this.elem[parent]) {
int tmp = this.elem[child];
this.elem[child] = this.elem[parent];
this.elem[parent] = tmp;
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
public void adjustUp(int child) {
int parent = (child-1)/2;
while (child > 0) {
if(this.elem[child] > this.elem[parent]) {
int tmp = this.elem[parent];
this.elem[parent] = this.elem[child];
this.elem[child] = tmp;
child = parent;
parent = (child-1)/2;
}else {
break;
}
}
}
插入操作
步骤:
1.将新元素插入到堆的末尾。
2.按照优先顺序,将新元素与其父节点比较,根据大小进行向上或者向下调整。
3.不断进行第2步操作,直到不需要交换新元素和父节点,或者达到堆顶。
4.最后得到一个最小堆。
代码如下:
public void push(int val) {
if(isFull()) {
//扩容
this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
}
this.elem[this.usedSize] = val;//10
this.usedSize++;//11
adjustUp(this.usedSize-1);//10下标
}
public boolean isFull() {
return this.usedSize == this.elem.length;
}
删除操作
堆的删除与插入操作相反,插入是将元素从下往上调整,而删除是将元素从上往下调整
步骤:
1.删除堆定元素
2.比较左右节点的元素,将小的元素上调(向上或向下调整)
3.不断进行步骤2,知道不需要调整或者调整到堆底。
public void pop() {
if(isEmpty()) {
return;
}
int tmp = this.elem[0];
this.elem[0] = this.elem[this.usedSize-1];
this.elem[this.usedSize-1] = tmp;
this.usedSize--;//9 删除了
adjustDown(0,this.usedSize);
}
public int peek() {
if(isEmpty()) {
throw new RuntimeException("队列为空");
}
return this.elem[0];
}
public boolean isEmpty() {
return this.usedSize == 0;
}
建堆操作
创建一个数组,初始化堆,然后调整堆中的数据,不断向上或者向下,最后调整成堆。
以大堆为例:
步骤:
代码如下:
import java.util.Arrays;
public class TestHeap {
public int[] elem;
public int usedSize;
public TestHeap() {
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++;
}
//parent 就代表每颗子树的根节点
for(int parent = (array.length-1-1)/2;parent >= 0;parent--) {
//第2个参数 每次调整的结束位置应该是:this.usedSize.
adjustDown(parent,this.usedSize);
}
}
}
优先级队列
- 队列Queue本身是先入先出的结构,但是某些情况下,数据本身可能带有优先级,就需要先处理优先级更高的数据。比如玩游戏的时候受到电话,那么应该先处理电话,因为电话优先级高
- 这种时候的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,另一个是添加新对象。这种数据结构就是优先级队列(PriorityQueue)
- 优先级队列有很多实现方式,最常见的就是用堆来实现。
PriorityQueue的使用
- JAVA集合框架中提供了PriorityQueue和PriorityBlockingQueue两种优先级队列。前者线程不安全,后者线程安全。这里主要使用PriorityQueue。
- PriorityQueue集合底层是用堆数据结构实现的。
- PriorityQueue中放置的元素必须是能够比较大小(只有实现了 Comparable 和 Comparator 接口的类才能比较大小),不能插入无法比较大小的对象,否则会抛出异常。
- PriorityQueue中可以插入任意多的元素,因为可以自动扩容,但是不能为null对象。
- PriorityQueue默认是小根堆。
- PriorityQueue常见的构造方式如下:
- PriorityQueue常用方法:
使用:
public static void main(String[] args) {
//创建一个空的优先级队列,默认底层容量是11
PriorityQueue<Integer> queue1=new PriorityQueue<>();
//创建一个空的优先级队列
PriorityQueue<Integer> queue2=new PriorityQueue<>(50);
ArrayList<Integer> list=new ArrayList<>();
list.add(4);
list.add(0);
list.add(2);
list.add(3);
//用ArrayList集合来创建一个优先级队列的对象。
PriorityQueue<Integer> queue3=new PriorityQueue<>(list);
System.out.println(queue3.size());
System.out.println(queue3.peek());//默认是最小堆,所以优先级最高的是最小元素
System.out.println(queue3.poll()); //0 优先级最高的元素弹出。
// for(Integer x:queue3){
// System.out.println(queue3.poll());
// }
System.out.println("优先级队列是否为空:"+queue3.isEmpty());
queue1.addAll(list);
System.out.println(queue1.isEmpty());
queue1.clear(); //清空队列
System.out.println(queue1.isEmpty());
}
TopK问题
最小的k个数
思路:将所有元素放入优先级队列,返回前k个。
class Solution {
public int[] smallestK(int[] arr, int k) {
int[] ret=new int[k];
if(arr ==null || k<=0){
return ret;
}
PriorityQueue<Integer> queue=new PriorityQueue<>();
for(int i=0;i<arr.length;i++){
queue.offer(arr[i]);
}
for(int i=0;i<k;i++){
ret[i]=queue.poll();
}
return ret;
}
}
这只是一种,topk问题是在一组数据中求前k个最小元素或者前k个最大元素
比如在N个元素中求前K个最小元素,常见思路:
- 1.建立大小为N的小堆,每次弹出堆定元素,弹K次
- 2.建立大小为K的大堆:
1)将待排序的前K个元素建成大根堆 。
2)遍历剩下待排序元素,每拿到一个元素,就和堆定元素比较
3)如果大于堆定元素,不用管,下一个 。
4)如果小于堆定元素,弹出堆定,然后将这个元素放入堆中。每次放入和弹出都有堆的调整过程。
堆排序
- 由于堆本身就是一个数组,可以利用堆的独特特性来将数组从小到达进行排序。时间复杂度O(nlogn)
- 在排序中都学过,就不详细介绍了。
/**
* 一定是先创建大堆
* 调整每棵树
* 开始堆排序:
* 先交换 后调整 直到 0下标
*/
public void heapSort() {
int end = this.usedSize-1;
while(end > 0) {
int tmp = this.elem[0];
this.elem[0] =this.elem[end];
this.elem[end] = tmp;
adjustDown(0,end);
end--;
}
}