1.堆的概念
大顶堆:每个节点的值都大于或等于其左右孩子节点的值,下图是完全二叉树是大顶堆
小顶堆:每个节点的值都小于或等于其左右孩子节点的值
如果我们用指针来表示大顶堆,那么每个结点需要三个指针来找到它的上下结点(父结点和两个子结点各需要一个),但如果使用完全二叉树,表达就会边的特别方便,如下图所示。完全二叉树使用数组而不需要指针就可以表示大顶堆。(注意为了方便,是从下标1开始存放元素的),此时下标i对应的节点的双亲节点如果存在,那么在数组中的下标是[i/2],下标i对应的孩子节点存在,那么对应的下标是2i和2i+1.
2.优先队列
提出的背景:许多应用程序都要处理有序的元素,但不一定要求他们全部有序,或者不一定要一次将他们排序,很多情况下我们会收集一些元素,处理元素的最大值,然后收集更多的元素,再处理当前元素的最大的值。
优先队列的操作(大顶堆):
插入某值:插入的元素放在数组尾部,通过上浮操作,将其上浮到合适的位置,构建大顶堆。
删除最大值:获取下标为1的元素(目前的最大值)存入临时变量,然后将下标为1的元素和最后一个元素交换,将最后一个元素设置为空并对下标为1的元素进行下沉操作,重建大顶堆。
代码示例:
class MaxPQ<Key extends Comparable<Key>> //Comparable后面也要加泛型
{
private Key[] pq;
private int N = 0;
//构造函数
public MaxPQ(int maxN)
{
pq = (Key[])new Comparable[maxN+1]; //注意如果数组0坐标不哟把那个,都会有这个操作
}
public boolean isEmpty()
{
return N == 0;
}
public int size()
{
return N;
}
public void insert(Key v)
{
pq[++N] = v; //将元素插入到队尾//0下标并没有放东西
swim(N); //上浮
}
//这边删除一个元素,其实堆排序就是使用了同样的原理
//每次取出的根结点都大顶堆得最大的值,如此反复就达到了排序的目的.
public Key delMax()
{
Key max = pq[1]; //将队头元素取出
exch(1,N); //交换尾元素和队头元素
pq[N] = null; //防止游离
N--; //数目减去一
sink(1); //将队头元素下沉
return max;
}
//这边的比较和交换都是输入的是下标,而不是数值
private boolean less(int i,int j)
{
return pq[i].compareTo(pq[j])<0; //pq[i]<pq[j]
}
private void exch(int i,int j)
{
Key t = pq[i];
pq[i] = pq[j];
pq[j] = t;
}
//上浮操作
private void swim(int k)
{
while(k>1&&less(k/2,k)) //如果父结点小于子结点//如果k为1了就不用比较了
{
exch(k,k/2);
k/=2;
}
}
//下沉操作
private void sink(int k)
{
while(2*k<=N)
{
int j = 2*k;
if(j<N&&less(j,j+1))
j++;
if(!less(k,j))
break;
exch(k,j);
k = j;
}
}
public void show()
{
for(int i=1;i<=N;i++) //下标为0的值并没有使用,因为用起来不方便
{
System.out.print(pq[i]+" ");
}
}
}
class PaiXu
{
public static void main(String[] args)
{
MaxPQ<String> str = new MaxPQ(11);
str.insert("2"); //插入操作用的上浮
str.insert("1");
str.insert("7");
str.insert("3");
str.insert("5");
str.insert("6");
str.insert("9");
System.out.println("打印当前值:");
str.show();
System.out.println(); //换行
//str.delMax();
System.out.println("\n取出当前的最大值");
System.out.println(str.delMax());
System.out.println("\n取出当前的最大值");
System.out.println(str.delMax());
}
}
3.PriorityQueue优先队列
Java已经封装了PriorityQueue类实现了优先队列的操作。
常用构造函数:
PriorityQueue()
//默认是小顶堆
使用默认的初始容量(11)创建一个 PriorityQueue
,并根据其自然顺序对元素进行排序。
PriorityQueue(int initialCapacity, Comparator<? super E> comparator)
//通过自定义比较器设置为大顶堆或小顶堆
使用指定的初始容量创建一个 PriorityQueue
,并根据指定的比较器对元素进行排序。
常用方法:
peek()
获取但不移除此队列的头;如果此队列为空,则返回 null。
poll()
获取并移除此队列的头,如果此队列为空,则返回 null。
4.优先队列的应用
剑指offer两道题,第一题利用小顶堆获取一组数据中的最小值,第二题利用大顶堆+小顶堆获取中位数。
1)题目描述(剑指offer面试题40题)
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。
分析:定义一个大小为4的优先队列(设置为大顶堆),先放入4个元素,从中取出最大值和新放入的元素比较,如果新放入的元素小于该最大值,就将新元素放入优先队列,否则不放入。每次获取最大元素和插入新元素的操作的时间复杂度都是O(logk),对于n个输入数字而言,总的时间复杂度是O(nlogk)。
代码实现:
class Solution{
public ArrayList<Integer> GetLeastNumbers_Solution(int[] array,int k){
ArrayList<Integer> list = new ArrayList<>();
if(k<=0||array==null||array.length<k)
return list;
PriorityQueue<Integer> p = new PriorityQueue<>(k,new myCompare());
for(int i=0;i<array.length;i++){
if(p.size()<k){
p.add(array[i]);
}
else{
int temp = p.peek(); //每次取出的数都是最大数
if(array[i]<temp){ //如果数小于最大值
p.poll();
p.add(array[i]);
}
}
}
for(int i=0;i<k;i++){
list.add(p.poll());
}
return list;
}
class myCompare implements Comparator<Integer>{
public int compare(Integer i1,Integer i2){
return i2-i1; //这里从小到大排序
}
}
}
该代码使用与海量数据的处理,从n(很大的数据)中取出k个最小值或最大值,因为n非常大在硬盘中,无法一次读入内存,所以可以先读取部分数据到内存,从中取出最小的k个数,再读入剩下的部分到内存,比较后取出所有数据的最小的k个数。
2)题目描述 数据流中的中位数(剑指offer面试题41题)
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
分析:
1)使用一个大顶堆和一个小顶堆,如果数据流中的数据array数组数据长度是奇数,将下标为奇数的放入小顶堆,下标为偶数的放入小顶堆,并保证大顶堆的最大值小于小顶堆的最小值,此时大顶堆的最小值就是中位数。如果数据流中的数据array数组数据长度是偶数,将小标为奇数的放入小顶堆,下标为偶数的放入大顶堆,并保证大顶堆的最大值小于小顶堆的最小值,此时取出大顶堆的最大值和小顶堆最小值的和的平均值。
2)如何保证大顶堆的最大值小于最小堆的最小值,放入大顶堆前先放入小顶堆中,然后从小顶堆中取出的值最小值放入大顶堆,这样就保证大顶堆的中最大值小于小顶堆中的最小值。
代码示例:
class PaiXu{
public static void main(String[] args) {
Solution s = new Solution();
s.Insert(5);
System.out.println(s.GetMedian());
s.Insert(2);
System.out.println(s.GetMedian());
s.Insert(3);
System.out.println(s.GetMedian());
s.Insert(4);
System.out.println(s.GetMedian());
}
}
class Solution{
int m = 0;
PriorityQueue<Integer> minQueue = new PriorityQueue<>();
PriorityQueue<Integer> maxQueue = new PriorityQueue<>(new Comparator<Integer>(){
public int compare(Integer i1,Integer i2){
return i2 - i1;
}
});
public void Insert(Integer num){
if((m&1)==0){ //如果是偶数,插入小顶堆
maxQueue.offer(num);
int temp = maxQueue.poll();
minQueue.offer(temp);
}
else{ //如果是奇数,插入大顶堆
minQueue.offer(num);
int temp = minQueue.poll();
maxQueue.offer(temp);
}
m++;
}
public Double GetMedian(){
if(((m-1)&1)==0){
return new Double(minQueue.peek());
}
else{
int temp1 = maxQueue.peek();
int temp2 = maxQueue.peek();
return new Double(temp1+temp2)/2;
}
}
}
总结:
1)一般使用优先队列可以在不排序的情况下获取数组的最大值,最小值,中间值和第k大值。
取最大值通过大顶堆,n个数构建大顶堆的时间复杂度是O(nlogn),取最大值的时间复杂度是O(1)
取最小值通过小顶堆,n个数构造小顶堆的时间复杂度是O(nlogn),取最小值的时间复杂度是O(1)
取中间值通过大顶堆加小顶堆,构建两个堆的时间复杂度O(nlogn),取中间值的时间复杂度O(1)
取第k大元素,可以通过大顶堆,取元素第k次就是第k大的值,总的时间复杂度O(nlogn)
2)对比排序的Partition函数,使用该函数可以通过O(n)的时间复杂度获取未排序的数组第k大的数。
3)n个元素的数组开始构建大顶堆,完全二叉树从最下层最右边的非中断节点开始构建,时间复杂度是O(n),n个数据一个一个插入构建大顶堆时间复杂度O(nlogn),往对里面插入一个数据,删除一个数据的时间复杂度是O(logn),取出最大值时间复杂度O(1)。