1、二叉树的存储方式
使用数组保存二叉树结构,用层序遍历的方式放入数组中,只适合表示完全二叉树,下图数组下标是0~11
下标关系
已知双亲(parent)的下标:
- 左孩子(left)下标 = 2 ∗ p a r e n t + 1 2 * parent + 1 2∗parent+1
- 右孩子(left)下标 = 2 ∗ p a r e n t + 2 2 * parent + 2 2∗parent+2
已知孩子(child)下标,则:
- 双亲(parent)下标 = ( c h i l d − 1 ) / 2 (child - 1) / 2 (child−1)/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;
}
}