一、二叉树的顺序存储
-
存储方式
使用数组保存二叉树结构,方式即将二叉树用层序遍历方式放入数组中。
一般只适合表示完全二叉树,因为非完全二叉树会有空间的浪费。
这种方式的主要用法就是堆的表示 -
下标关系
已知双亲(parent)的下标,则:
左孩子(left)下标 = 2 * parent + 1
右孩子(right)下标 = 2 * parent + 2
已知孩子(不区分左右)(child)下标,则:
双亲(parent)下标 = (child - 1) / 2
二、堆 heap
- 堆逻辑上是一棵完全二叉树
- 堆物理上是保存在数组中
- 满足任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆
- 反之,则是小堆,或者小根堆,或者最小堆
- 堆的基本作用是,快速找集合中的最值
1、PriorityQueue 方法
默认是小堆
每次入队,需保证当前是大根堆或小根堆
每次弹出最小元素,依然是大根堆或小根堆
public class TestDemo {
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
// 每放一个元素 都得保证当前的堆 是大堆 或者是小堆
priorityQueue.offer(1);
priorityQueue.offer(2);
priorityQueue.offer(3);
System.out.println(priorityQueue.poll()); // 1 ->默认是小堆
System.out.println(priorityQueue.peek()); // 2
}
}
1.1、操作-调整为大根堆
从最后一颗子树出发,每棵子树都是向下调整的
public class TestHeap {
public int[] elem;
public int usedSize;
public TestHeap() {
this.elem = new int[10];
}
/**
* 向下调整
* @param parent 每棵树的根节点
* @param len 每棵树的调整的结束位置
*/
public void shiftDown(int parent, int len) {
int child = 2 * parent + 1;
// 判断有右孩子
while(child < len) {
if(child + 1 < len && this.elem[child] < this.elem[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;
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
public void createHeap(int[] array) {
for (int i = 0; i < array.length; i++) {
this.elem[i] = array[i];
usedSize++;
}
// 双亲(parent)下标 = (child - 1) / 2
for (int parent = (usedSize-1-1)/2; parent >= 0 ; parent--) {
// 调整
shiftDown(parent, usedSize);
}
}
}
public class TestDemo {
public static void main(String[] args) {
int[] array = {
27,15,19,18,28,34,65,49,25,37};
TestHeap testHeap = new TestHeap();
testHeap.createHeap(array);
}
}
时间复杂度分析:
T(n) = n - log(n + 1)
当 n 越来越大时,也就是T(n) = n
1.2、入队列-向上调整
private void shiftUp(int child) {
int parent = (child - 1) / 2;
while(child > 0) {
if(elem[child] > elem[parent]) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
child = parent;
parent = (child - 1) / 2;
} else {
break;
}
}
}
// 向上调整
public void offer(int val) {
if(isFull()) {
// 扩容
elem = Arrays.copyOf(elem, 2*elem.length);
}
this.elem[usedSize++] = val;
shiftUp(usedSize - 1);
}
public boolean isFull() {
return usedSize == this.elem.length;
}
1.3、出队列
每次出队列,都要保证出最大的或最小的
1、交换0下标和最后一个元素
2、调整0下标
public int pop() {
if(isEmpty()) {
throw new RuntimeException("优先级队列为空");
}
// 1、交换0下标和最后一个元素
int tmp = elem[0];
elem[0] = elem[usedSize - 1];
elem[usedSize - 1] = tmp;
// 2、调整0下标
shiftDown(0, usedSize);
return tmp;
}
public boolean isEmpty() {
return usedSize == 0;
}
1.4、获取队首元素
public int peek() {
if(isEmpty()) {
throw new RuntimeException("优先级队列为空");
}
return elem[0];
}
2、堆的其他应用
2.1、TopK 问题
方法一:整体排序
方法二:建一个大根堆 弹出堆顶 k 个元素
方法三:建小根堆 出较小的的堆顶元素 得前 k个元素
找前3个最大的元素:
1、把前3个元素,建小根堆
2、堆顶的元素是当前k个元素中的最小值
3、如果下一个X元素比堆顶元素大,出堆顶元素到最后,入这个X元素,然后再次调整堆顶元素为小根堆
优点:
堆的大小只有 k
时间复杂度:O(n*logk)
堆调整为堆的高度logk
总结:
- 如果要求前 k 个最大的元素,要建一个大根堆,堆内元素就是前k大
- 如果要求前 k 个最小的元素,要建一个小根堆,堆内元素就是前k小
堆顶元素比下一个大,出堆顶元素,调整 - 第 k 大的元素,建一个小堆,堆顶元素就是第 k 大的元素
- 第 k 小的元素,建一个大堆,堆顶元素就是第 k 小的元素
- 求数组当中的前K个最小的元素
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
public class TopK {
// n*logn
public static int[] topK(int[] array, int k) {
// 1、创建一个大小为 K 的大根堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1; // 大根堆
}
});
// 2、遍历数组当中的元素,前K个元素放到队列当中
for (int i = 0; i < array.length; i++) {
if(maxHeap.size() < k) {
maxHeap.offer(array[i]);
} else {
// 3、从第 k+1 个元素开始,每个元素和堆顶元素进行比较
int top = maxHeap.peek();
if(top > array[i]) {
// 3.1、先弹出
maxHeap.poll();
//3.2、后存入
maxHeap.offer(array[i]);
}
}
}
// 4、存储前 K 个元素,返回
int[] ret = new int[k];
for (int i = 0; i < k; i++) {
ret[i] = maxHeap.poll();
}
return ret;
}
public static void main(String[] args) {
int[] array = {
18,21,8,10,34,12};
int[] ret = topK(array, 3);
System.out.println(Arrays.toString(ret));
}
}
- LeetCode 373. 查找和最小的 K 对数字
class Solution {
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
// 创建大小为 K 的大根堆
PriorityQueue<List<Integer>> maxHeap = 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)); // 大根堆
}
});
// 加入 K 组序列,然后比较堆顶元素
for (int i = 0; i < Math.min(nums1.length, k); i++) {
for (int j = 0; j < Math.min(nums2.length, k); j++) {
if(maxHeap.size() < k) {
List<Integer> list = new ArrayList<>();
list.add(nums1[i]);
list.add(nums2[j]);
maxHeap.offer(list);
} else {
// 堆顶元素大 poll
int top = maxHeap.peek().get(0) + maxHeap.peek().get(1);
if(top > nums1[i] + nums2[j]) {
maxHeap.poll();
List<Integer> list = new ArrayList<>();
list.add(nums1[i]);
list.add(nums2[j]);
maxHeap.offer(list);
}
}
}
}
// 存储 k 组序列
List<List<Integer>> ret = new ArrayList<>();
for (int i = 0; i < k && !maxHeap.isEmpty(); i++) {
// 没有k组 空指针异常
ret.add(maxHeap.poll());
}
return ret;
}
}
2.2、堆排序
堆一组数据从小到大排序,建大根堆还是小根堆?
应该是小根堆,因为不是每次弹出堆顶元素,而是对此数组排序
/**
* 堆排序
* 1、0下标和最后一个未排序的小标交换
* 2、end--
*/
public void heapSort() {
for (int end = usedSize - 1; end > 0; end--) {
int tmp = elem[0];
elem[0] = elem[end];
elem[end] = tmp;
shiftDown(0, end);
}
}
TestHeap.java
import java.util.Arrays;
public class TestHeap {
public int[] elem;
public int usedSize;
public TestHeap() {
this.elem = new int[10];
}
/**
* 向下调整
* @param parent 每棵树的根节点
* @param len 每棵树的调整的结束位置
*/
public void shiftDown(int parent, int len) {
int child = 2 * parent + 1;
// 判断有右孩子
while(child < len) {
if(child + 1 < len && this.elem[child] < this.elem[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;
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
public void createHeap(int[] array) {
for (int i = 0; i < array.length; i++) {
this.elem[i] = array[i];
usedSize++;
}
// 双亲(parent)下标 = (child - 1) / 2
for (int parent = (usedSize-1-1)/2; parent >= 0 ; parent--) {
// 调整
shiftDown(parent, usedSize);
}
}
private void shiftUp(int child) {
int parent = (child - 1) / 2;
while(child > 0) {
if(elem[child] > elem[parent]) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
child = parent;
parent = (child - 1) / 2;
} else {
break;
}
}
}
// 入队列 向上调整
public void offer(int val) {
if(isFull()) {
// 扩容
elem = Arrays.copyOf(elem, 2*elem.length);
}
this.elem[usedSize++] = val;
shiftUp(usedSize - 1);
}
public boolean isFull() {
return usedSize == this.elem.length;
}
// 出队列
public int pop() {
if(isEmpty()) {
throw new RuntimeException("优先级队列为空");
}
// 1、交换0下标和最后一个元素
int tmp = elem[0];
elem[0] = elem[usedSize - 1];
elem[usedSize - 1] = tmp;
// 2、调整0下标
shiftDown(0, usedSize);
return tmp;
}
public boolean isEmpty() {
return usedSize == 0;
}
public int peek() {
if(isEmpty()) {
throw new RuntimeException("优先级队列为空");
}
return elem[0];
}
/**
* 堆排序
* 1、0下标和最后一个未排序的小标交换
* 2、end--
*/
public void heapSort() {
for (int end = usedSize - 1; end > 0; end--) {
int tmp = elem[0];
elem[0] = elem[end];
elem[end] = tmp;
shiftDown(0, end);
}
}
}
三、java 对象的比较
1、问题提出
优先级队列在插入元素时有个要求:插入的元素不能是null或者元素之间必须要能够进行比较,为了简单起见,我们只是插入了Integer类型,那优先级队列中能否插入自定义类型对象?
class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
}
public class TestPriorityQueue {
public static void TestPriorityQueue() {
PriorityQueue<Card> p = new PriorityQueue<>();
p.offer(new Card(1, "♠"));
p.offer(new Card(2, "♠"));
}
public static void main(String[] args) {
TestPriorityQueue();
}
}
优先级队列底层使用堆,而向堆中插入元素时,为了满足堆的性质,必须要进行元素的比较,而此时Card是没有办法直接进行比较的,因此抛出异常
2、覆写基类的equal
-
如果没有重写equals方法,默认调用的是object
-
重写equals方法:
public class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
@Override
public boolean equals(Object o) {
// 自己和自己比较
if (this == o) return true;
// o如果是null对象,或者o不是Card的子类
// getClass() != o.getClass()比较是不是同一个类型
if (o == null || getClass() != o.getClass()) return false;
// 强转为Card类型
Card c = (Card) o;
// 比较数值和花色是否一样
return rank == card.rank && Objects.equals(suit, card.suit);
}
@Override
public int hashCode() {
return Objects.hash(rank, suit);
}
}
基本类型可以直接比较,但引用类型最好调用其equal方法
注意: 一般覆写 equals 的套路就是上面演示的
- 如果指向同一个对象,返回 true
- 如果传入的为 null,返回 false
- 如果传入的对象类型不是 Card,返回 false
- 按照类的实现目标完成比较,例如这里只要花色和数值一样,就认为是相同的牌
- 注意下调用其他引用类型的比较也需要 equals,例如这里的 suit 的比较
覆写基类equal的方式虽然可以比较,但缺陷是:equal只能按照相等进行比较,不能按照大于、小于的方式进行比较。
3、基于Comparble接口类的比较
public interface Comparable<E> {
// 返回值:
// < 0: 表示 this 指向的对象小于 o 指向的对象
// == 0: 表示 this 指向的对象等于 o 指向的对象
// > 0: 表示 this 指向的对象等于 o 指向的对象
int compareTo(E o);
}
对用用户自定义类型,如果要想按照大小与方式进行比较时:在定义类时,实现Comparble接口即可,然后在类中重写compareTo方法
class Card implements Comparable<Card>{
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
@Override
public int compareTo(Card o) {
return this.rank - o.rank;
}
@Override
public String toString() {
return "Card{" +
"rank=" + rank +
", suit='" + suit + '\'' +
'}';
}
}
public class TestDemo {
public static void main(String[] args) {
// 默认是小根堆
PriorityQueue<Card> priorityQueue = new PriorityQueue<>();
priorityQueue.offer(new Card(1, "♥")); // 直接放到底层的queue数组的0下标
priorityQueue.offer(new Card(2, "♥"));
System.out.println(priorityQueue);
// 结果:[Card{rank=1, suit='♥'}, Card{rank=2, suit='♥'}]
// priorityQueue.offer(null); // err ->NullPointerException
}
}
public class TestDemo {
public static void main(String[] args) {
Card card1 = new Card(2, "♥");
Card card2 = new Card(1, "♦");
System.out.println(card1.compareTo(card2)); // 1 -> 第一张牌大
}
}
缺点:对类的侵入性太高,一旦写好根据哪种规则比较,就不能轻易修改
4、基于比较器比较
class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
}
class RankComparator implements Comparator<Card> {
@Override
public int compare(Card o1, Card o2) {
return o1.rank - o2.rank;
}
}
public class TestDemo {
public static void main(String[] args) {
Card card1 = new Card(1, "♥");
Card card2 = new Card(2, "♦");
RankComparator rankComparator = new RankComparator();
int ret = rankComparator.compare(card1, card2);
System.out.println(ret); // -1
}
}
比较队列:
class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
@Override
public String toString() {
return "Card{" +
"rank=" + rank +
", suit='" + suit + '\'' +
'}';
}
}
class RankComparator implements Comparator<Card> {
@Override
public int compare(Card o1, Card o2) {
return o1.rank - o2.rank;
}
}
public class TestDemo {
public static void main(String[] args) {
Card card1 = new Card(1, "♥");
Card card2 = new Card(2, "♦");
RankComparator rankComparator = new RankComparator();
PriorityQueue<Card> priorityQueue = new PriorityQueue<>(rankComparator);
priorityQueue.offer(card1);
priorityQueue.offer(card2);
System.out.println(priorityQueue);
// [Card{rank=1, suit='♥'}, Card{rank=2, suit='♦'}]
}
}
匿名内部类:
class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
@Override
public String toString() {
return "Card{" +
"rank=" + rank +
", suit='" + suit + '\'' +
'}';
}
}
public class TestDemo {
public static void main(String[] args) {
Card card1 = new Card(2, "♥");
Card card2 = new Card(1, "♦");
// 1、
PriorityQueue<Card> priorityQueue = new PriorityQueue<>(new Comparator<Card>() {
@Override
public int compare(Card o1, Card o2) {
return o1.rank - o2.rank;
}
});
// 2、lambda 表达式 -> 可读性非常差
// PriorityQueue<Card> priorityQueue = new PriorityQueue<>((x, y) -> {return x.rank - y.rank;});
priorityQueue.offer(card1);
priorityQueue.offer(card2);
System.out.println(priorityQueue); // [Card{rank=1, suit='♦'}, Card{rank=2, suit='♥'}]
}
}
5、三种方式对比
覆写的方法 | 说明 |
---|---|
Object.equals | 因为所有类都是继承自 Object 的,所以直接覆写即可,不过只能比较相等与 |
否 | |
Comparable.compareTo | 需要手动实现接口,侵入性比较强,但一旦实现,每次用该类都有顺序,属于 |
内部顺序 | |
Comparator.compare | 需要实现一个比较器对象,对待比较类的侵入性弱,但对算法代码实现侵入性 |
强 |