常用数据结构——二叉树,堆栈,大小堆和堆排序
最近刷leetcode有一道题需要用到最大堆,最小堆。其中用到了一个黑科技,一直想不出来,关于最大堆任意元素的删除(并非堆顶的删除),直到在网上看到了一个算法,顺便回顾一下大小堆。
参考:
https://www.jianshu.com/p/6d3a12fe2d04
二叉树
在数据结构中,二叉树是每个结点最多有两个子树的树结构,其中每个结点保存的有结点存储的数据,以及指向两个子树的指针(也可以保存父结点的指针)。通常子树被称作“左子树”和“右子树”。
二叉树的每个结点至多只有两棵子树,且左右子树有顺序之分不能颠倒。
性质:
- 二叉树的第i层至多有 2 i − 1 2^{i-1} 2i−1个结点。
- 深度为k的二叉树至多有 2 k − 1 2^k-1 2k−1个结点。
- 对任何一棵二叉树,如果其末端结点数为 n 0 n_0 n0(末端结点:无子结点),度为2的结点数为 n 2 n_2 n2(度为2的结点:具有左右两个子结点),则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1。
注1:关于第三条的证明:任何一个度为1的结点都可以被看作只是连接的线,进而全部可以忽略不计,那么原始的树可以被退化为只有末端结点和度为2的结点。之后,由于每一个度为2的结点都有两个子结点,从末端结点的父结点开始,将其父结点和左子结点抵消掉,将右结点放到父结点上面,递归退化,最终退化完发现只剩一个末端结点,故末端结点个数比度为2的结点个数大1。
树和二叉树的主要差别:
- 树的结点个数至少有1个,二叉树的结点个数可以为0。
- 树中结点的最大度数没有限制,二叉树结点的最大度数为2。
- 树的结点无左右等顺序之分,二叉树有左右之分。
满二叉树
一棵深度为 k k k,且有 2 k − 1 2^k-1 2k−1个节点的树为满二叉树。(叶子结点都在最后一层,且除了叶子结点以外,每一个结点都有左右子结点)
所以满二叉树的外形是一个标准的三角形。
性质:
- 满二叉树的深度 h h h和总层数 k k k相同。
- 叶子数为 2 h − 1 2^{h-1} 2h−1
- 第 k k k层的结点数为 2 ( k − 1 ) 2^(k-1) 2(k−1)
- 总结点数为 2 h − 1 2^h-1 2h−1
- 总结点数为奇数
- 树高 h = l o g 2 ( n + 1 ) h=log_2(n+1) h=log2(n+1)
注1:关于树高的计算十分重要,因为很多算法的时间复杂度都与树高有关。
完全二叉树
完全二叉树是由满二叉树引申出来的(弱化版本)。对于深度为k的,有n个结点的二叉树。当且仅当每一个结点都与深度为k的满二叉树中编号从1到n的结点一一对应,则称其为完全二叉树。
(若设二叉树的深度为h,除第h层外,其他各层的结点数都达到了最大个数,且第h层所有的结点都连续集中在最左边,这就是完全二叉树。)
所以满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
性质:
- 深度为k的完全二叉树至少有 2 k − 1 2^{k-1} 2k−1个结点,至多有 2 k − 1 2^{k}-1 2k−1个结点
- 树高 h = ( i n t ) l o g 2 ( n ) + 1 h=(int)log_2(n)+1 h=(int)log2(n)+1
- 由于完全二叉树没有空间的稀疏,即每一个结点的位置都可以被下标 n n n唯一确定,所以储存时除了可以使用Node结点的结构来使用,也可以使用线性表来存储。
堆
此处的堆是数据结构中的堆,而不是内存模型中的堆。
内存模型中的堆:
一、堆栈空间分配区别:
1、栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
2、堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
二、堆栈缓存方式区别:
1、栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
2、堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
数据机构中的堆:
堆通常可以被看作一棵树,其满足性质如下:
- 堆中任意结点的值总是不大于(不小于)其子节点的值
- 堆总是一颗完全树
将任意节点不大于其子节点的堆叫做最小堆或小根堆,而将任意节点不小于其子节点的堆叫做最大堆或大根堆。常见的堆有二叉堆、左倾堆、斜堆、二项堆、斐波那契堆等等。
二叉堆
二叉堆是完全二叉树,其分为最大堆和最小堆。最大堆:父结点的值总是大于或等于任何一个子结点的值。(非常适合做优先队列,或者查找某一数组第k大的数)
通常二叉堆都是由线性表(数组)来存储的,如上图的存储为:
int[] val = new int[]{9,8,7,6,5,4,3,2,1};
最大堆一般是有三种操作,插入、删除和得到堆顶元素。
插入(上浮):
为了保持最大堆完全二叉树的性质,插入时一般放在最后一位,之后对其执行上浮操作。即比较其于父节点的大小,如果大于父节点,则交换其与父节点的位置。
删除(下沉):
为了保持最大堆的性质,最大堆删除的时候只能删除堆顶数据。之后将最后一位数据放在堆顶,执行下沉操作,即其小于左右子节点中某一个时,将子节点中较大的一个与其交换位置。
得到堆顶元素:
只用返回最大堆的第一个元素即可。
class maxHeap {
int[] val;
int size = 0;
maxHeap (int k) {
val = new int[k];
}
public void insert(int x) {
// 将新元素放在最后一位上
int i = size;
// 将元素于父结点进行对比执行上浮操作
while (i > 0 && x > val[(i-1)/2]) {
val[i] = val[(i-1)/2];
i = (i-1)/2;
}
val[i] = x;
size++;
}
public void delete() {
// 删除堆顶元素
int i = 0;
int tmp = val[size-1];
val[size-1] = 0;
// 堆的大小减1
size--;
// 将元素于左右子结点进行对比执行下沉操作
while ((i+1)*2 < size && (tmp < val[(i+1)*2-1] || tmp < val[(i+1)*2])) {
// 对左右子结点进行对比
if (val[(i+1)*2-1] > val[(i+1)*2]) {
val[i] = val[(i+1)*2-1];
i = (i+1)*2-1;
} else {
val[i] = val[(i+1)*2];
i = (i+1)*2;
}
}
// 考虑到子节点可能只有左结点存在
if ((i+1)*2-1 < size && tmp < val[(i+1)*2-1]) {
val[i] = val[(i+1)*2-1];
i = (i+1)*2-1;
}
val[i] = tmp;
}
public int get() {
return val[0];
}
}
删除特定的元素(黑科技):
假如说,我们要删除某一个特定元素,这个元素可能不在堆顶,但具体在哪里我们也不知道。考虑到时间复杂度,不能每次都重新删除元素后再构造新的最大堆,所以就有了如下黑科技。
在每次删除元素的时候,并不是在堆的数组中删除,而是重新构建一个Map用来存储已经被删除的元素,进而当得到堆顶元素时,查询Map,如果其已经被删除了,那么就执行堆的删除操作,再重新查询。直到堆顶元素不存在于Map中或者Map中不需要删除这个元素。
import java.util.*;
public class maxHeap {
private int[] val;
private int size = 0;
private Map<Integer, Integer> map = new HashMap<>();
public maxHeap (int k) {
val = new int[k];
}
public void insert(int x) {
// 将新元素放在最后一位上
int i = size;
// 将元素于父结点进行对比执行上浮操作
while (i > 0 && x > val[(i-1)/2]) {
val[i] = val[(i-1)/2];
i = (i-1)/2;
}
val[i] = x;
size++;
}
public int pop() {
int tmp = this.get();
this.erase(tmp);
return tmp;
}
private void delete() {
// 删除堆顶元素
int i = 0;
int tmp = val[size-1];
val[size-1] = 0;
// 堆的大小减1
size--;
// 将元素于左右子结点进行对比执行下沉操作
while ((i+1)*2 < size && (tmp < val[(i+1)*2-1] || tmp < val[(i+1)*2])) {
// 对左右子结点进行对比
if (val[(i+1)*2-1] > val[(i+1)*2]) {
val[i] = val[(i+1)*2-1];
i = (i+1)*2-1;
} else {
val[i] = val[(i+1)*2];
i = (i+1)*2;
}
}
// 考虑到子节点可能只有左结点存在
if ((i+1)*2-1 < size && tmp < val[(i+1)*2-1]) {
val[i] = val[(i+1)*2-1];
i = (i+1)*2-1;
}
val[i] = tmp;
}
public void erase(int x) {
// 将删除元素存入Map中
if (map.containsKey(x)) {
map.put(x, map.get(x)+1);
} else {
map.put(x, 1);
}
}
public int get() {
// 获得堆顶元素,并将其于已删除元素进行对比。
if (size == 0) {
throw new NegativeArraySizeException("堆已为空");
}
int tmp = val[0];
while (map.containsKey(tmp) && map.get(tmp) > 0) {
map.put(tmp, map.get(tmp)-1);
this.delete();
tmp = val[0];
}
return tmp;
}
}