JAVA学习记录(十九)—— 优先级队列(堆)、对象的比较

1、二叉树的存储方式

  使用数组保存二叉树结构,用层序遍历的方式放入数组中,只适合表示完全二叉树,下图数组下标是0~11
在这里插入图片描述

下标关系

已知双亲(parent)的下标:

  • 左孩子(left)下标 = 2 ∗ p a r e n t + 1 2 * parent + 1 2parent+1
  • 右孩子(left)下标 = 2 ∗ p a r e n t + 2 2 * parent + 2 2parent+2

已知孩子(child)下标,则:

  • 双亲(parent)下标 = ( c h i l d − 1 ) / 2 (child - 1) / 2 (child1)/2

2、堆(heap)

  • 1、堆逻辑上是一颗完全二叉树
  • 2、堆物理上是保存在数组中的
  • 3、满足任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆(双亲结点大于孩子结点:大根堆)
  • 4、反之,则是小堆,或者小根堆,或者最小堆(双亲结点小于孩子结点:小根堆)
  • 5、堆的作用:快速找到集合中的最值(最大值、最小值、前K个最大/最小值)
向下调整(一大根堆为例):

  从每一棵子树开始调整,每一棵子树是向下调整,从最下最后的一颗子树开始调整


public class HeapDemo{
    public int[] elem;
    public int useSIze;
    
    public HeapDemo(){
        this.elem = new int[10]; 
    }
    
    // 将数组变成一个大根堆
    public void creatBigHeap(int[] array){
        for(int i = 0; i < array.length; i++){
            this.elem[i] = array[i];
            this.usedSize++;
        }// 此时elem中已经放入了元素
        
        // 开始条件,是最后一个树得双亲节点处开始
        // 结束条件,最初的双亲结点即,0处 
        for(int i = (this.useSize - 1 - 1)/2); i >= 0; i--){
            adjustDown(i, usedSize); // 对每一颗树进行调整
        }
    }
    
    public void show(){
         for(int i = 0; i < this.useSize; i++){
             System.out.print(this.elem[i] + " ");
         }
         System.out.println();
    }
    

    // 要让每棵树都变成大根堆,我们需要向下调整
    // 传入参数len,是因为每棵树的结束位置实际是一样的
    public void adjustDown(int parent, int len){
        // 
        int child = 2 * parent + 1;
        
        // 说明有左孩子
        while(child < len){
            // 在左孩子和右孩子中找到最大值
            if(child + 1 < len && this.elem[child] < this.elem[child + 1]){ //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;
                // 换完之后,还要往当前树的下一个子树去看,看它的子树有没有在useSize以内
                parent = child; // 向它的子树走
                child = 2 * parent + 1;  // 确定新的child和parent后进入下一次while判断
            }else{
                // 因为是从最后面一个子树开始调整的,当遇到
                // parent 大于 child 不用交换的时候
                // 不用进行向子树移动的操作了,其子树一定是大堆
                break;
            }
            
        }
    }
}
  • 测试部分
public class TestDemo{
    public static void main(String[] args){
        HeapDemo heapDemo = new HeapDemo();
    	int[] array = { 27,15,19,18,28,34,65,49,25,37 };
    	System.out.println(Arrays.toString(array));
    	heapDemo.creatBigHeap(array);
    	heapDemo .show();
    }
}

3、堆的应用 — 优先级队列

  • PrioritQueue,堆-优先级队列,其底层默认是一个小根堆
  • 此时poll()拿到队头元素是最小的元素
  • 每次存元素的时候,会保证该堆依然为小根堆,删完之后也要保证(会对每个树都保证)
public class TestDemo{
    public static void main(String[] args){
        HeapDemo heapDemo = new HeapDemo();
        PrioritQueue<Integer> qu = new PriorityQueue<>();
        qu.offer(10);
        qu.offer(100);
        qu.offer(3);
        qu.offer(40);
        qu.offer(6);
       
        System.out.println(qu.poll()); // 输出为3
    }
}
  • 入队,实现给堆内放入元素key
  • 将Key放在数组的最后一个位置(要判满与扩充),然后向上调整
public boolean isFull(){
	return this.useSize == this.elem.length;
}

// 向上调整
public void adjustUp(int child){
	int parent = (child - 1 ) / 2;
	
	while(child > 0){
		if(this.elem[child] > this.elem[parent]){
			int tmp = this.elem[child];
            this.elem[child] = this.elem[parent];
            this.elem[parent] = tmp;
            child = parent;
            parent = (child - 1 ) / 2;
		}else{
			// 与向下调整退出的情况相似
			// 当这里的值parent > child时
			// 上面的已经是大根堆了
			break;
		}
	}
}

public void push(int val){
	if(isFull()){
		//扩容
		this.elem = Ararays.copyOf(this.elem, 2 * this.elem.length);
	}
	this.elem[this.useSize] = val;
	useSize++;
	// 向上调整
	adjustUp(this.usedSize -1); // 拿到的就是最后一个元素的下标
}

  • 出队,出当前最大堆中的最大数
  • 1、将第一个元素和最后一个元素进行交换;
  • 2、useSize–;
  • 3、同时向下调整,只需要调整0号下标即可
public boolean isEmpty(){
    return this.useSize = =0;
}
public int poll(){
    if(isEmpty()){
    	thow new RuntimeException("队列为空");
    }
    
    int ret = this.elem[0];
    // 开始删除操作
    int tmp = this.elem[0];
    this.elem[0] = this.elem[this.useSize - 1];
    this.elem[useSize - 1] = tmp;
    this.useSize--;
    adjustDown(0, this.useSize);
    return ret;
}

  • 拿到最大堆中的最大数
public int peek(){
    if(isEmpty()){
    	thow new RuntimeException("队列为空");
    }
    return this.elem[0];
}

关于PriorityQueue
  • 默认是小堆
  • offer也是进行向上调整,执行逻辑就与前面写的push相似
    在offer中使用的向上调整使用siftUp命名
  • 想要让它以大堆的方式进行排序 :使用自定义比较器
public class TestDemo{
    public static void main(String[] args){
        PrioritQueue<Integer> qu = new PriorityQueue<>(new Comparator(Integer){
        	@Override
        	public int compare(Integer 01, Integer o2){
        		return o1 - o2;   // 小堆
        		  //   o2 - o1;   //大堆
        	}
        });
        qu.offer(10);
        qu.offer(100);
        qu.offer(3);
        qu.offer(40);
        qu.offer(6);
       
        System.out.println(qu.poll()); // 输出为3
    }
}
  • PrioritQueue的offer方法与上述所写的push方法比较:
topK问题(求前K个最大/最小元素)、第K大/小的元素
  • 排序,如在10个元素中,取前3个最大的/最小的
    求前K个最小的元素 —— 建大堆
    求前K个最大的元素 —— 建小堆

  • ①先将前k个元素取出来,建一个小堆

  • ②下标i在第k+1个元素的位置

  • ③将第i个元素与小堆的最小值进行比较,当i下标这个元素大时,将这两个数交换

  • ④对当前堆进行排序成为小堆,然后i向后走1位,同样操作

  • ⑤最后得到的这个小堆,就是前k个最大的元素


  • 1、找前K个最大的元素
public class TestDemo{
	public static void topK(int array){

		// 1、大小为K的小堆
		PriorityQueue<Integer> minHeap = new PriorityQueue<>(k, new Comparator<Integer>(){
			@Override
			public int compare(Integer o1, Integer o2){
				return o1 - o2;
			}
		});
		
		// 2、遍历数组
		for(int i = 0; i < array.length; i++){
			if(minHeap.size() < k){
				minHeap.offer(array[i]);
			}else{
				int top = minHeap.peek();
				if(array[i] > top){
					minHeap.pop();
					minHeap.offer(array[i]);
				}
			}
		}
		for(i = 0; i < k; i++){
			System.out.println(minHeap.poll());
		}
	}
	
}
	public static void main(String[] args){
		
	}
  • 2、找前K个最小的元素,将之中的比较换成小于号<,并且在比较器中是o2 - o1
  • 3、找到数组中第K小的元素 —— 建立大小为K的大堆,等数组遍历完成后,堆顶元素就是第K小的元素
堆排序
  • 要从小到大进行排序,应该建 大堆
    小堆只能知道孩子节点两个值比双亲节点的值大,但并不能有序
    建成一个大堆进行从小到达排序:
  • 1、将0下标与useSize-1下标元素进行交换,此时最大的元素就是useSize-1位置
  • 2、然后进行整棵树位置的调整,调整为大根堆,调整时useSize的大小要减一,即不包含当时移动下来的那个最大的值。
  • 3、再重复进行,让0下标与useSize-1下标元素进行交换,实现上述操作。
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); // 向下调整,调整0下标,大小是树的最后一个位置
		end--; // 每次都不排最后一个上次挪下来的最大的值
	}
}

4、对象的比较

  • 在使用PriorityQueue时,想要插入一个元素使用offer时,会进行比较,我们需要定义比较的规则或在PriorityQueue后面new一个比较器
class Card  implements Comparable<Card>{
	public int rank; // 数值 
	public String suit; // 花色 
	public Card(int rank, String suit) { 
		qthis.rank = rank; this.suit = suit; 
	} 
	@Override
	public int compareTo(Card o){
		return this.rank - o.rank; //以数值的方法进行比较  (此时写法是小堆)
	}
}
  • 或者
class Card{
	public int rank; // 数值 
	public String suit; // 花色 
	public Card(int rank, String suit) { 
		qthis.rank = rank; this.suit = suit; 
	} 
}

public static void main(String[] args){
	Card card1 = new Card(1, "♠");
	Card card2 = new Card(2, "♠");
	Card card3 = new Card(3, "♠");
	PriorityQueue<Card> minHeap = new PriorityQueue<>(k, new Comparator<Card>(){
			@Override
			public int compare(Integer o1, Integer o2){
				return o1.rank - o2.rank;  // 小根堆
			}
		});
}

在比较时使用的方法:
  • compareTo方法可以使用,因为我们已经重写过了
  • equals方法也可以使用,但我们并没有重写,所以这里如果用equals比较的是双方的引用,如果要重写equals方法,则使用alt + ins生成equals and hashCode,进行重写
练习-查找和最小的K对数字
import java.util.*;

public class Solution {
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        // 为了找到最小的K对,使用大根堆
        PriorityQueue<List<Integer>> queue = new PriorityQueue<>(k, new Comparator<List<Integer>>() {
            @Override
            public int compare(List<Integer> o1, List<Integer> o2) {
                return (o2.get(0) + o2.get(1)) - (o1.get(0) + o1.get(1)); // o2 - o1 大根堆, 此处是一个二维数组
            }
        });

        // 遍历所有可能的集合
        for(int i = 0; i < Math.min(nums1.length, k); i++){
            for(int j = 0; j < Math.min(nums2.length, k); j++){
                // 当堆已经放了K个元素且两数之和大于堆顶,因为是升序数组,所以可以直接跳出循环
                if(queue.size() == k && nums1[i]+nums2[j] > queue.peek().get(0) + queue.peek().get(1)){

                    break;
                }

                // 若比堆顶小,则弹出堆顶元素,把当前数对加入
                if(queue.size() == k){
                    queue.poll();
                }
                // 堆中还没有放满K个元素,挨个放进去
                List<Integer> pair = new ArrayList<>();
                pair.add(nums1[i]);
                pair.add(nums2[j]);
                queue.add(pair);
            }
        }
        // 大根堆,堆顶是前K个中最大的,即第K大的
        List<List<Integer>> res = new LinkedList<>();
        for(int i =0; i < k && !queue.isEmpty(); i++){
            res.add(queue.poll()); // 由于大根堆,先出的是最大的,逐个插入即可
        }
        return res;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值