二叉树的存储方式
一颗完全二叉树逻辑上是以数组的形式进行保存的,是将完全二叉树以二叉树层序遍历的方式保存到数组中的。
完全二叉树的下标之间有一个固定的公式:
已知父结点的下标 parentIndex
左孩子的下标为:2*parentIndex +1
右孩子的下标为:2*parentIndex +2
已知某个结点的下标为 childIndex
其父结点的下标为:(childIndex - 1)/2
堆
- 堆实质上就是在一个数据集合中找最值,能够快速在一个集合中找的最值。
- 堆在逻辑上是一个完全二叉树,但是以数组的形式进行存储。
- 堆可以分为大堆和小堆;
- 任意结点的值都小于等于它的孩子的值(堆顶值最小)的称为小堆;
- 任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆。
- 小堆不一定是有序的,但是有序的一定是小堆。
堆的操作
向下调整
在进行向下调整时有一个前提:
只有需要调整的位置不清楚,但其他位置都已经满足堆的性质了。
(即要调整的位置的左右子树必须已经是一个堆,才能进行调整)
向下调整操作的简单步骤:
- 判断要调整的位置(index)是不是叶子结点,如果是直接return,不需要再进行向下调整
- 否则,找到index位置的两个孩子中值最小的一个;
- 将小孩子的值于index位置的值进行比较:
- 小孩子的值 < index 位置的值
- 小孩子的值 >= index 位置的值,调整结束
- 交换小孩子的值 与 index位置的值
- 把小孩子作为要调整的值(index),进行循环(从第一步开始判断)
这里循环结束的出口有两个:
1.当要调整的值为叶子结点时,结束调整
2.当小孩子的值大于等于要调整的值时,结束调整
向下调整的代码:
public class Heap {
//向下调整
public static void shiftDown(int[] array,int size,int index) {
//1.判断index是不是叶子结点(有没有孩子),看左孩子的下标是否越界,如果越界则没有左孩子
int leftIndex = 2*index +1;
if(leftIndex >= size) {
return;
}
//2.找出两个孩子中最小的
int minIndex = leftIndex;
int rightIndex = leftIndex +1;
if(rightIndex < size && array[rightIndex] < array[leftIndex]) {
minIndex = rightIndex;
}
//3.小孩子的值与index位置的值进行比较
if(array[index] <= array[minIndex]) {
return;
}
//4.交换小孩子的值与index位置的值
int t = array[index];
array[index] = array[minIndex];
array[minIndex] = t;
//4.把最小的孩子作为index,继续循环
index = minIndex;
}
}
建堆
建堆的过程实质上也是进行不断地向下调整。
要将一个完全二叉树变成最小堆的形式,以小堆为例:
如果保证左子树和右子树满足堆的性质,只需要对根结点进行向下调整,同理,对左子树和右子树进行相应的操作,就会得到堆。
需要进行向下调整的位置为从最后一个非叶子结点(最后一个结点的父节点)到根结点。
public static void createHeap(int[] array,int size){
//从最后一个非叶子结点(最后一个结点的父结点)开始进行向下调整,i--
int lastIndex = size -1;
int lastparentIndex = (lastIndex-1)/2;
for(int i = lastparentIndex; i >= 0;i--){
shiftDown(array,size,i);//调用上面的向下调整的方法
}
}
时间复杂度
向下调整:O(log(n)) (最坏情况为比较次数为二叉树的高度)
建堆:O(n*log(n)) (约等于O(n))
log(1000) 约等于 10
log(100w) 约等于 20
log(10亿) 约等于 30
优先级队列
实现了Queue接口
java.util.PriorityQueue implement Queue
优先级队列的操作
实际上是最小堆的形式
queue.add(); 入队列
queue.remove();出队列
queue.element();队首元素
import java.util.PriorityQueue;
public class PriorityQueueDemo {
public static void main(String[] args) {
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.add(3);
queue.add(4);
queue.add(7);
queue.add(8);
queue.add(2);
System.out.println(queue.remove());
System.out.println(queue.remove());
}
}
实现自己的优先级队列
- 返回队首元素,即堆顶元素,也就是数组的第一个元素array[0];
- 删除队首元素:
不是直接将堆顶元素删除,而是用数组的最后一个元素替换堆顶元素,然后通过向下调整的方式重新调整成堆。 - 插入指定元素
先将e插入到二叉树的最后一个位置,然后对该位置进行向上调整。
public class MyPriorityQueue {
Integer[] array = new Integer[100];
int size = 0;
//检索队首元素,但不删除
public Integer element(){
if(size == 0) {
throw new RuntimeException("空");
}
return array[0];
}
//删除队首元素:
//不是直接将堆顶元素删除,而是用数组的最后一个元素替换堆顶元素,然后通过向下调整的方式重新调整成堆
public Integer remove() {
int e = array[0];
array[0] = array[size-1];
size--;
adjustDown(array ,size,0);
return e;
}
public void adjustDown(Integer[] array,int size,int index) {
int leftIndex= 2 * index +1;
if(leftIndex >= size) {
return;
}
int minIndex = leftIndex;
int rightIndex = leftIndex+1;
if(rightIndex < size && array[rightIndex] < array[leftIndex]) {
minIndex = rightIndex;
}
if(array[index] <= array[minIndex]) {
return;
}
int t = array[index];
array[index] = array[minIndex];
array[minIndex] = t;
index =minIndex;
}
//插入指定元素
public void add(Integer e) {
//先将e插入到二叉树的最后一个位置
array[size] = e;
size++;
//将最后一个元素进行向上调整
adjustUp(array,size,size-1);
}
//向上调整
public void adjustUp(Integer[] array,int size,int index) {
//1.先判断index位置是不是树的根
if(index == 0) {
return;
}
//2.找到父结点
int parentIndex = (index-1)/2;
//3.比较(index -1)/2 位置的值与index位置的值
if(array[parentIndex] <= array[index]) {
return;
}
//交换值
int t = array[index];
array[index] = array[parentIndex];
array[parentIndex] = t;
//将父结点作为index,进行循环
index = parentIndex;
}
}
结合自定义类的PriorityQueue
需要用到Comparable 或者 Comparator
public class Person implements Comparable<Person>{
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person o) {
return age - o.age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class PriorityQueueDemo {
public static void main(String[] args) {
PriorityQueue<Person> queue = new PriorityQueue<>(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.name.compareTo(o2.name);
}
});
Person p1 = new Person("a",18);
Person p2 = new Person("b",23);
Person p3 = new Person("c",14);
Person p4 = new Person("d",20);
Person p5 = new Person("e",16);
queue.add(p1);
queue.add(p2);
queue.add(p3);
queue.add(p4);
queue.add(p5);
System.out.println(queue.remove());
}
}
TopK问题
从arr[1, n]这n个数中,找出最大的k个数,这就是经典的TopK问题。
例如:从arr[1, 12]={5,3,7,1,8,2,9,4,7,2,6,6} 这n=12个数中,找出最大的k=5个。
使用堆进行解决
- 先用前k个元素生成一个堆,用于存储当前最大的k个元素。
- 接着,从第k+1个元素开始和堆顶(堆中最小的元素)比较,如果被扫描的元素大于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。
- 直到,扫描完所有n-k个元素,最终堆中的k个元素,就是要求的TopK。