关于堆的学习

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,并根据指定的比较器对元素进行排序。

常用方法:

offer(E e)   将指定的元素插入此优先级队列

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)。

猜你喜欢

转载自blog.csdn.net/chenkaibsw/article/details/80734550