优先级队列:队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队 列时,可能需要优先级高的元素先出队列
优先级队列的模拟实现:堆,他就是在二叉树的基础上,堆数据的操作进行了一些调整(让执行的数据有了优先级)。
堆:底层是一个数组(物理结构),它是一个顺序存储的二叉树(逻辑结构),并且必须是一颗完全二叉树。(从上到下,从左到右,节点是依次排列的,不能有空着的节点。)
大根堆:每棵树的根节点最大,大于两个孩子节点。
小根堆:每棵树的根节点最小,小于两个孩子节点。
因为堆是一颗完全二叉树,所以用顺序存储更好一些,因为数组一是块连续的空间,完全二叉树是没有空节点的,所以如果是一颗非完全二叉树,这棵树的节点可能是空的,那么在存储的时候就要存储一个null,所以就浪费了空间。对于非完全二叉树来说,显然是用链式存储更好一些。
求parent(父亲节点)下标和child(孩子节点)下标:
虽然数据在数组中进行存储,我们还是可以根据二叉树的一些性质来还原这颗二叉树。
如果下标i是0,就代表的是父亲节点,否则父亲节点就是;(i-1)/ 2
如果已知父亲节点,求左右孩子节点就是 i*2+1
堆的创建:(创建大根堆和小根堆)
向下调整建堆算法:首先思路就是从当前二叉树的最后一颗子树开始比较,比较的是孩子节点和当前子树的父亲节点,根据大根堆和小根堆的性质来决定是否要交换父亲节点和孩子节点。
创建大根堆: 用向下调正算法(是对于父亲节点来说的,每一棵子树都要从父亲节点向下调正), 只讲一下代码的实现: 首先要有一个父亲节点和孩子节点的下标,有数组的当前长度,从最后一棵树的子树开始,先让左右孩子进行比较,如果左孩子大,就左孩子和父亲节点交换,右孩子大,就右孩子和父亲节点进行交换,但是首先一个前提是要有左孩子和右孩子,也就是算出的孩子节点下标后< 当前数组的长度,否则数组越界。
然后判断左右孩子谁大,如果有孩子大,就让child下标走到右孩子,然后去判断child节点和parent节点谁大,如果>父亲节点,就交换位置,否则就呆在原来的位置就可以,不用交换,所以是break,然后在创建树的方法中调用向下调整算法就可以了。
如果是创建小根堆也是一样的,从最后一棵树的子树开始进行比较,先比较左右孩子的最小值,然后再去和父亲节点比较,根据小根堆的性质决定是否交换。
注:先让child标记左孩子节点,因为当前的子树没有右孩子,所以在child++的时候,要保证它有右孩子节点,同样也是< 数组的长度。
(建立大根堆或者小根堆)时间复杂度:
向下调正的时间复杂度是O(n),
建堆的时候要考虑调正节点的个数和调整的高度,最后一层是不需要向下调整的,从第一层开始,有一个节点,要向下调整h-1层,(最坏就是调整到最后一层),依次类推,时间复杂度是调整的节点个数 * 调整的高度。(因为每一层调整的高度是不一样的,每一层调整的节点个数也是不一样的)。
向上调整算法时间复杂度:O(n * logn)
一般向上调整算法用在堆的插入操作:先插入到最后一个节点,然后再向上调整,要保证调整完之后依然是大(小)根堆,这个时候只需要插入的元素和父亲节点去比较即可,因为此时父亲节点已经是最大的(/最小的)。
堆的删除:
首先要删除的一定是堆顶元素
1.让堆顶元素和最后一个节点进行交换
2.交换之后有效数据减少一个
3.进行向下调整
TopK问题:前十名,500强......
要比较前k个数据,如果是前k个最大的数据,要建立小根堆,如果是前k个最小的数据,要建立大根堆,因为topk问题都是比较大的数据,在有很多的数据的时候要进行排序时间复杂度很低,所以以建堆的方式去比较,首先遍历数组中的前k个元素,拿这前k个数据建立堆,例如:比较的是前k个最大的数据,就建立小根堆,再去遍历剩下数组中的元素,如果谁比堆顶数据大,就让堆顶元素和数组中的那个元素交换位置,(因为此时的堆顶元素是最小的,如果数组中有比这个大的数据,那么堆顶元素一定不是前k个最大的),同样的,建立大根堆也是一样的。
Java提供的PriorityQueue(优先级队列)是一个小根堆,所以只能比较前k个最大的元素,要比较前k个最小的元素,就需要把小根堆变成大根堆。
对象的比较:
有三种方法: 1.equals方法,
比如我们已经创建了一个学生类,这个类中并没有equals方法,但是它是继承于object类的,所以在比较两个对象的时候会去调用object的equals方法,但是这个方法也是用==去比较的,可以看源码;
所以这个时候我们需要去重写equals方法,让这个比较是按照我们想要的方式去比较,
就像上述代码,他是按照这个学生类中的age相等并且name相等去判定的。
2.comparable中的comparaeTo方法,实现comparable接口然后重写这个接口中的comparaeTo方法,只是这种比较的方法堆类的侵入性比较强,一旦重写了comparaeTo方法,以后就只能按照重写的方法中的方式去比较。
可以看到上边,重写compareTo方法之后,调用这个方法的时候就只能是用age去比较,this代表当前对象的引用,o是传过来的参数,如果this > o,会返回一个正数,否则返回一个负数,相等就返回0。
3.比较器:写一个类,实现comparator接口,重写接口中的compare方法,这个比较方法也就是我们说的要把小根堆转化为大根堆,因为已经是一个大根堆了,所以我们可以传一个比较器,
在compare方法中写我们想要的方式去比较,我们可以看一下源码,优先级队列中是支持传带一个参数的构造方法的,他会判断这个比较器空不空,如果传过去不为空,就回去调用我们自己写的比较器,这个时候如果把参数换位置,
此时在进行比较的时候就 变成了大根堆,因为只有o1 > o2 的时候返回的是一个负数,此时就把优先级队列中的小根堆换成了大根堆。