目录
1.概念(大根堆和小根堆)
● 在某些情况下,数据要根据优先级出入,就比如我们在看医生排队时,先挂号的人一般先进行诊治,但是突然来了个需要急救的病人,医生就需要先去诊治这个病情重的病人,所以使用普通队列是不合适的,我们需要去使用优先级队列,而优先级队列是用堆(顺序存储的完全二叉树)去实现,堆一般有两种。
① 小堆:堆中父节点值比两个孩子结点的值都小
② 大堆:堆中父节点值比两个孩子结点的值都大
注:假设父节点的下标为i,那么其左孩子的下标为2*i + 1,右孩子的下标为2*i + 2,反推过来,知道孩子结点的下标为j,那么父节点的下标就为(j-1) / 2。
2.使用Java语言来实现大根堆的基本操作
1)建堆(假设将此数组(既不是大根堆也不是小根堆)建堆{55,66,10,77,88,25} )
● 以建大根堆为例,堆的构建有两种方式:
① 向下调整(O(N)) :在堆中,从最后一个非叶子结点开始调整,每个父结点通过比较可能会向下移动。此外,因为根据计算(最坏情况下为满二叉树除最后一层叶子结点外,其余每个结点都需要调整)其时间复杂度比向上调整建堆要快(满二叉树中最后一层的结点数差不多是整个满二叉树的一半,进行向上调整最坏情况下,除根节点之外的结点都要进行,所以时间复杂度会比向下调整建堆时间复杂度大),所以我们采用向下调整。
② 向上调整(O(N)):根据给出的数组,将一个个元素依次入堆(也就是堆中元素个数从0开始,不断往堆中插入元素,在插入前,堆已经是个大根堆或小根堆)。在往堆中添加元素时会用到。
注:根结点无需向上调整,直接入堆。
● 实现代码:
//赋值
public void initQueue(int[] arr) {
for (int i = 0; i < arr.length; i++) {
this.data[i] = arr[i];
}
this.usedSize = arr.length;
}
/**
* 建堆的时间复杂度:O(N)
*
* @param arr 存放要存储到堆中的数据元素的数组
*/
public void createHeap(int[] arr) {
initQueue(arr);
//从最后一个非叶子节点((usedSize - 1) / 2)开始
for (int i = (usedSize - 1) / 2; i >= 0; i--) {
//通过向下调整让每颗子树都变成大根堆
//调整的区间从[i,usedSize)->[i-1,usedSize].....[0,usedSize)
adjustDown(i, usedSize);
}
}
/**
* 向下调整:O(log2n(以2为底))
*
* @param parent 父节点下标(一般都是从每棵子树的根开始向下调整)
* @param usedSize 每棵子树调整结束标志
*/
private void adjustDown(int parent, int usedSize) {
//由父节点下标得到孩子下标
int child = parent * 2 + 1;//左孩子下标
while (child < usedSize) {
//左孩子和右孩子(在[2,usedSize)之间)比较得到较大结点的下标
while (child + 1 < usedSize && data[child] < data[child + 1]) {
child++;
}
//将两个孩子之间的较大值结点与父节点比较
//如果父节点值大则不用继续调整,否则交换后继续调整
if (data[child] < data[parent]) {
break;
} else {
swap(data,child,parent);
// int temp = data[child];
// data[child] = data[parent];
// data[parent] = temp;
parent = child; //更新父节点
child = 2 * parent + 1; //更新孩子结点
}
}
}
//交换数组中对应两个下标位置的值
private void swap(int[] arr,int m, int n) {
int temp = arr[m];
arr[m] = arr[n];
arr[n] = temp;
}
● 过程展示:
2)入队(已完成建大堆:88 77 25 55 66 10)
● 具体思路
① 将新的元素放到堆最后,堆中元素个数+1。
② 从最后一个叶子结点开始进行向上调整,直到child == 0 || parent < 0,结束调整 。
注:根结点无需向上调整。
● 实现代码:
/**
* 入队
* @param val
*/
public void push(int val) {
//将入的元素放队列最后
data[usedSize++] = val;
//向上调整让其整体成为大根堆(从最后一个叶子节点开始向上调整)
//现在已经是局部大根堆,所以只需要跟父节点进行比较(父节点值一定大于两个孩子结点值)
adjustUp(usedSize - 1);
}
/**
* 向上调整
* @param child 堆中最后一个孩子结点
*/
private void adjustUp(int child) {
//满了进行扩容
if(isFull()) {
this.data = Arrays.copyOf(data,2*defaultSize);
return;
}
int parent = (child - 1) / 2;
while(child > 0) {
if(data[parent] > data[child]) {
break;
}else {
swap(data,child,parent);
child = parent;
parent = (child - 1) / 2;
}
}
}
private boolean isFull() {
return usedSize == defaultSize;
}
● 过程展示:
3)出队(已完成建大堆:88 77 25 55 66 10)
● 具体思路
① 将堆顶元素和放到最后一个元素交换,堆中元素个数-1。
② 将整个堆进行向下调整,调整区间为[0,size)。
● 实现代码:
/**
* 出队:每次删除的都是优先级高的元素(删除后仍要保持是大根堆)
*/
public void poll() {
if(isEmpty()) {
System.out.println("优先级队列为空,无法删除元素");
return;
}
swap(data,0,usedSize - 1);
usedSize--;
adjustDown(0,usedSize);
}
private boolean isEmpty() {
return usedSize == 0;
}
● 过程展示:
4)取出堆顶元素(已完成建大堆:88 77 25 55 66 10)
● 具体思路
0下标位置就是堆顶元素值
● 实现代码:
/**
* 获取堆顶元素
* @return
*/
public int peek() {
return data[0];
}
3.Java中自带的优先级队列的使用
3.1 默认的常用基本方法
所需要使用的类为PriorityQueue,其默认建小堆(也可以传比较器或者实现了Comparable接口的类):
PriorityQueue<Integer> queue = new PriorityQueue<>();
注:堆中存的是自定义类型的对象时,自定义类型需要实现Comparable接口,按照需要的比较方式重写compareTo方法,或者单独重新实现一个比较器,重写compare方法,将比较器作为参数传给堆。这样在对堆进行操作时才方便比较。
● 常用操作:
① offer() : 入队(添加元素)
② poll() : 出队(删除元素)
③ peek() : 查看堆顶元素
3.2 将默认的小根堆改成大根堆
如果要建大堆,需要自己实现一个比较器,重写compare方法:
//Java自带优先级队列的使用
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.offer(11);
queue.offer(2);
System.out.println(queue.peek());//默认小堆,这时候取堆顶为2
//建立大根堆(实现一个匿名比较器重写其compare方法)
PriorityQueue<Integer> queue2 = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//未传比较器,那么默认比较是返回o1 - o2(o1 > o2返回正数,相等返回0,o1 < o2返回负数)
return o2-o1; //修改为o2 - o1(o2 > o1返回正数,相等返回0,o2 < o1返回负数)
}
});
queue2.offer(11);
queue2.offer(2);
System.out.println(queue2.peek());//修改成大堆,这时候取堆顶为11
输出:
注:如果堆中存储的数据的类型为自定义类型,也可以让自定义类型实现Comparable接口,重写compareTo方法,将原比较顺序(this.属性名 - o.属性名(整型)) 更改为o.属性名 - this.属性名,如:
class Student implements Comparable<Student>{
int id;
String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public int compareTo(Student o) {
return o.id - this.id; //只能以id进行比较
}
}
PriorityQueue<Student> queue3 = new PriorityQueue<>();
queue3.offer(new Student(11,"rose"));
queue3.offer(new Student(2,"jack"));
assert queue3.peek() != null;
System.out.println(queue3.peek().id+ " " +queue3.peek().name);
这样也能将默认小堆修改为大堆,但是这样做有一定的局限性,Student类只能以id进行比较,不太灵活,所以我们如果想灵活一点,就可以另外实现多个比较器,当想以什么比较时,将对应的比较器传入堆中就好了。
4.拓展Java中常涉及到的比较方式
4.1 比较运算符
>, < , = ,>=,<=,!= 等等,适用于基本类型比较。
4.2 equals
主要比较两个字符串是否相等(包括字符串的长度和每个对应的字符),要实现两个相同自定义类型的对象的比较,需要让自定义类型重写其对应的equals方法。
4.3 实现Comparable接口
implements Comparable<比较的类型>,并重写compareTo方法。
4.4 实现自定义的比较器(实现Comparator接口)
//实现一个自定义比较器
class IdComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o2.name.compareTo(o1.name); //以姓名比较
}
}
PriorityQueue<Student> queue4 = new PriorityQueue<>(new NameComparator()); //以姓名比较
queue4.offer(new Student(11,"rose"));
queue4.offer(new Student(2,"jack"));
assert queue4.peek() != null;
System.out.println(queue4.peek().id+ " " +queue4.peek().name);
分享完毕~