堆
堆的概念
堆的性质
1、堆中某个节点的值总是不大于或不小于其父节点的值;
2、堆总是一棵完全二叉树。
最大堆的模拟实现
最大堆中的主要方法实现
1、最大堆的add方法
将元素加入到最后位置,然后通过上浮操作,根据堆的性质,将元素移动到合适位置
public void add(int val) {
// 1.先尾插新元素到数组的尾部
this.data.add(val);
size ++;
siftUp(size - 1);
}
2、上浮操作
将元素通过上浮操作移动到合适的位置。在最大堆中,如果当前元素的值大于其父节点的值,则对其进行上浮操作,最小堆中正好相反。
private void siftUp(int k) {
while (k > 0 && data.get(k) > data.get(parent(k))) {
// 只有当还有父节点且当前节点值 > 父节点时才交换
swap(k,parent(k));
// 继续向上判断
k = parent(k);
}
}
3、取出最大值
在堆中只有堆顶的元素是确定的最值,所以我们能将堆顶的元素取出,这样也衍生出了一种排序方法——堆排,它的时间复杂度为O(nlogn)。当取出堆顶元素后,为了使对堆的影响最小,会叫最后一个叶子结点换到堆顶,在对它进行下沉操作,这样操作之后还是一个最大堆。
public int extractMax() {
if (data.isEmpty()) {
throw new NoSuchElementException("heap is empty!cannot extract!");
}
int val = data.get(0);
// 1.将当前堆的最后一个元素顶到根节点
data.set(0,data.get(size - 1));
data.remove(size - 1);
size --;
// 2.从根节点开始向下调整
siftDown(0);
return val;
}
4.下沉操作
当堆的相对关系被打破后,我们需要对堆进行重建,下浮操作是为了将元素移动到合适的位置。在最大堆中当元素移动到叶子结点或者元素值大于左右孩子的值时,停止下沉操作。
private void siftDown(int k) {
// 首先保证有子树
while (leftChild(k) < size) {
// 判断是否存在右子树且右子树的值比左子树大
int j = leftChild(k);
if (j + 1 < size && data.get(j + 1) > data.get(j)) {
j = j + 1;
}
// 此时j索引一定保存了左右子树的最大值~
if (data.get(k) >= data.get(j)) {
// 已经下沉到合适位置
break;
}else {
swap(k,j);
k = j;
}
}
}
5、查看堆顶元素
public int peekMax() {
if (data.isEmpty()) {
throw new NoSuchElementException("heap is empty!cannot peek!");
}
return data.get(0);
}
堆的实现
public class MaxHeap {
// 具体保存元素的数组
private List<Integer> data;
// 堆中有效元素个数
private int size;
// heapify =》将任意的整型数组调整为最大堆
public MaxHeap(int[] arr) {
this.data = new ArrayList<>(arr.length);
// 先依次将arr中的每个元素放入堆中
for (int i : arr) {
data.add(i);
size ++;
}
// 从当前完全二叉树的最后一个非叶子结点开始向下调整,使得每个子树为堆
for (int i = parent(size - 1);i >= 0;i --) {
siftDown(i);
}
}
// 在当前最大堆中取出最大值
public int extractMax() {
if (data.isEmpty()) {
throw new NoSuchElementException("heap is empty!cannot extract!");
}
int val = data.get(0);
// 1.将当前堆的最后一个元素顶到根节点
data.set(0,data.get(size - 1));
data.remove(size - 1);
size --;
// 2.从根节点开始向下调整
siftDown(0);
return val;
}
// 查看当前最大堆的最大值
public int peekMax() {
if (data.isEmpty()) {
throw new NoSuchElementException("heap is empty!cannot peek!");
}
return data.get(0);
}
// 从当前索引为k的位置开始向下调整
private void siftDown(int k) {
// 首先保证有子树
while (leftChild(k) < size) {
// 判断是否存在右子树且右子树的值比左子树大
int j = leftChild(k);
if (j + 1 < size && data.get(j + 1) > data.get(j)) {
j = j + 1;
}
// 此时j索引一定保存了左右子树的最大值~
if (data.get(k) >= data.get(j)) {
// 已经下沉到合适位置
break;
}else {
swap(k,j);
k = j;
}
}
}
// 向最大堆中添加元素
public void add(int val) {
// 1.先尾插新元素到数组的尾部
this.data.add(val);
size ++;
siftUp(size - 1);
}
// 元素上浮操作
private void siftUp(int k) {
while (k > 0 && data.get(k) > data.get(parent(k))) {
// 只有当还有父节点且当前节点值 > 父节点时才交换
swap(k,parent(k));
// 继续向上判断
k = parent(k);
}
}
private void swap(int i, int j) {
int temp = data.get(i);
data.set(i,data.get(j));
data.set(j,temp);
}
public MaxHeap() {
this(10);
}
public MaxHeap(int capacity) {
this.data = new ArrayList<>(capacity);
}
// 获取父节点
private int parent(int k) {
return (k - 1) >> 1;
}
// 获取左子树的结点编号
private int leftChild(int k) {
return (k << 1) + 1;
}
// 获取右子树的结点编号
private int rightChild(int k) {
return (k << 1) + 2;
}
@Override
public String toString() {
return this.data.toString();
}
}
优先队列
优先级队列的概念
优先级队列(priority queue) 是0个或多个元素的集合,每个元素都有一个优先权,对优先级队列执行的操作有查找、插入、删除一个新元素 一般情况下,查找操作用来搜索优先权最大的元素,删除操作用来删除该元素 。对于优先权相同的元素,可按先进先出次序处理或按任意优先权进行。
优先级队列的实现
主要方法实现
1、建堆
从最后一个非叶子结点开始,对元素位置进行调整,一直持续到到根结点做最后一次调整,讲一个大的问题分解为小问题。
public void createHeap(int[] array) {
for(int i=0;i<array.length&&i<elem.length;i++){
elem[i]=array[i];
usedSize++;
}
for(int k=(usedSize-1)/2;k>=0;k--){
siftDown(k);
}
}
2、下沉操作与上浮操作
具体原理与堆中上浮下沉操作相似
//下沉
public void siftDown(int k) {
int j=2*k+1;
while(j<usedSize){
if(j+1<usedSize&&elem[j+1]>elem[j]){
j=j+1;
}
if(elem[k]>=elem[j]){
break;
}else{
swap(j,k);
k=j;
j=2*k+1;
}
}
}
//上浮
public void siftUp(int k){
while(k>0&&elem[(k-1)/2]<=elem[k]){
swap((k-1)/2,k);
k=(k-1)/2;
}
}
3、入队与出队
原理类似于队列,但是优先级队列元素入队后需要调整元素位置,出队也是元素出队后,将最后一个元素移动到队首,再进行调整位置。虽然优先级队列带着队列,但它的出队顺序并不是按照先入先出的顺序,而是按照优先级来出队。
public int pollHead(){
if(isEmpty()){
throw new NoSuchElementException("MyPriorityQueue is Empyt!cannot poolHead");
}
int oilval=elem[0];
elem[0]=elem[usedSize-1];
usedSize--;
return oilval;
}
public void offer(int val){
if(isFull()){
int n=elem.length;
elem=Arrays.copyOf(elem,2*n);
}
elem[usedSize++]=val;
siftUp(usedSize-1);
}
优先级队列的实现
public class MyPriorityQueue {
public int[] elem;
public int usedSize;
public MyPriorityQueue(){
this(5);
}
public MyPriorityQueue(int capacity){
this.elem=new int[capacity];
}
//建堆
public void createHeap(int[] array) {
for(int i=0;i<array.length&&i<elem.length;i++){
elem[i]=array[i];
usedSize++;
}
for(int k=(usedSize-1)/2;k>=0;k--){
siftDown(k);
}
}
//下沉
public void siftDown(int k) {
int j=2*k+1;
while(j<usedSize){
if(j+1<usedSize&&elem[j+1]>elem[j]){
j=j+1;
}
if(elem[k]>=elem[j]){
break;
}else{
swap(j,k);
k=j;
j=2*k+1;
}
}
}
public void swap(int a,int b){
int tmp=elem[a];
elem[a]=elem[b];
elem[b]=tmp;
}
public void offer(int val){
if(isFull()){
int n=elem.length;
elem=Arrays.copyOf(elem,2*n);
}
elem[usedSize++]=val;
siftUp(usedSize-1);
}
//上浮
public void siftUp(int k){
while(k>0&&elem[(k-1)/2]<=elem[k]){
swap((k-1)/2,k);
k=(k-1)/2;
}
}
public boolean isFull(){
return usedSize==elem.length;
}
public int pollHead(){
if(isEmpty()){
throw new NoSuchElementException("MyPriorityQueue is Empyt!cannot poolHead");
}
int oilval=elem[0];
elem[0]=elem[usedSize-1];
usedSize--;
return oilval;
}
public int peekHead(){
return elem[0];
}
public boolean isEmpty(){
return usedSize==0;
}
public String toString(){
StringBuilder sb=new StringBuilder();
sb.append("[");
for(int i=0;i<usedSize;i++){
sb.append(elem[i]);
if(i!=usedSize-1){
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
}
TopK问题
在日常生生活中我们会遇到排行榜的问题,那么这些问题是如何解决呢?
在这之前我们介绍了优先级队列,而我们的优先级队列,正好能解决这个问题。topK问题一般是去第K个元素或者前K个元素,并且元素不是排序过的。
我们的一般想法是将元素排序,再找出排序后位置的元素,内部排序的时间复杂度最快是O(nlogn),那么还有没有更快的方法呢?我们以LeetCode中的面试题17.14为例
它需要最小的K个数,那么我们就需要建一个大小为K的优先级队列,这个优先级队列是用最大堆实现,当队列内元素小于K时直接入队,而当元素等于K时,就需要判断队首(也就是堆顶)元素与当前元素的关系,若大于队首元素,则直接跳过,小于才入队,并将队首元素出队,重复上述操作,直到元素遍历完,此时内有K个值,最大值在堆顶。
此时时间复杂度变为了O(nlogk)
最小K个数代码实现
public int[] smallestK(int[] arr, int k) {
PriorityQueue<Integer> queue= new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
for (int i:arr) {
queue.offer(i);
if(queue.size()>k){
queue.poll();
}
}
int[] res=new int[k];
for(int i=0;i<k;i++){
res[i]=queue.poll();
}
return res;
}
比较器
jdk中提供的优先级队列是最小堆实现的,而在日常的一些问题中,我们需要将最小堆变为最大堆,这就需要用到比较器。
在java中一旦实现了Comparable接口那么就有了客比较的能力,在jdk中如整形的默认比较为,大于返回大于0的数,等于返回0,小于返回小于0的数。我们想要将最小堆改为最大堆就需要实现Comparable接口并将其返回值修改为大于返回负数,小于返回正数。
如上述使用到jdk的优先队列,需要用到最大堆实现优先队列,那么我使用匿名内部类修改了比较的返回值。Comparable与Comparator接口都是实现元素比较的,只不过Comparable接口将比较代码嵌入自身类中,而Comparator既可以嵌入到自身类中,也可以在一个独立的类中实现比较。