hi~上期我们学习数组,链表,队列和栈,散列表,其实这些都是链式存储。但是仅仅是链式存储已经无法满足我们实际开发的需求了。比如,我们在学习散列表以hashmap举例时。hashMap在java8后做了优化,当链表长度到达8以后转为红黑树结构以便增加查询与修改的效率。
1、树
1.1 什么是树
在生活与软件开发中,层次化的数据之间可能有的祖先—后代、上级—下属、整体—部分以及其他类似的关系都可以用它来表示。例如,家族的血统关系(家族树中的关系为父子关系)、一个地区或一个单位(部门)的组织机构(树中的关系为上下级关系)、软件工程中的模块化技术(树中的关系为部分与整体的关系)、磁盘上信息组织的目录结构(树中的关系为包含或所属关系)等。
比如一个企业:
他们总是从同一个“根” 衍生出许多“枝干” ,再从每一个“枝干” 衍生出许多更小的“枝干” ,最后衍生出更多的“叶子” 。
所以在数据结构中,树的定义如下。
树(tree) 是n(n≥0) 个节点的有限集。 当n=0时, 称为空树。 在任意一个非
空树中, 有如下特点。
1).有且仅有一个特定的称为根的节点。
2).当n>1时, 其余节点可分为m(m>0) 个互不相交的有限集, 每一个集合本身又是一个树, 并称为根的子树。
如下图就是标准的树结构。
在上图中,节点1是根节点(root) ;节点5、 6、 7、 8是树的末端,没有“孩子” ,被称为叶子节点(leaf) 。 图中的虚线部分,是根节点1的其中一个子树。
同时,树的结构从根节点到叶子节点,分为不同的层级。 从一个节点的角度来看,它的上下级和同级节点关系如下。
在上图中,节点4的上一级节点,是节点4的父节点(parent) ;从节点4衍生出来的节点,是节点4的孩子节点(child) ;和节点4同级,由同一个父节点衍生出来的节点,是节点4的兄弟节点(sibling) 。
树的最大层级数,被称为树的高度或深度。 显然,上图这个树的高度是4。这里就需要记一下,父节点,子节点,兄弟节点,叶子节点,根节点,深度,看了图也好记对吧。
下面我们来认识一下二叉树。
2、啥子是二叉树
2.1 概念:
1)、二叉树(binary tree)是树的一种特殊形式。 二叉,顾名思义,这种树的每个节点最多有2个孩子节点。 注意,这里是最多有2个,也可能只有1个,或者没有孩子节点。如下图:
注:其中5、6、7、8没有子节点,所以称为叶子节点。
2)、二叉树节点的两个孩子节点,一个被称为左孩子(left child) ,一个被称为右孩子(right child) 。 这两个孩子节点的顺序是固定的,就像人的左手就是左手,右手就是右手,不能够颠倒或混淆。
其中有些性质:
- 若规定根结点的层数为0,则一棵非空二叉树的第i层上最多有(i≥0)个结点。
归纳法证明:
i)当层数i=0时,二叉树在根结点只有一个结点,=1,结论成立;
ii)假设层数i=k时,结论成立,即第k层上最多有个结点;
iii)当层数i=k+1时,根据二叉树的定义,第k层上的每个结点最多有2个子结点,所以第k+1层上最多有×2=个结点。
注意:对于这种稀疏的二叉树,数组的空间利用率是非常小的。那什么样的二叉树最适合用数组表示呢?我们后面学到二叉堆,一种特殊的完全二叉树,就是用数组来存储的。
- 若规定只有根结点的二叉树的深度为0,则深度为k的二叉树的最大结点数是-1(k≥-1)个。
其实就是等比数列求和,还挺有意思的,手写证明下~哈 如下图,字丑勿喷~
证明如下:
- 具有n个结点的完全二叉树的深度k为大于或等于 -1的最小整数。
证明也简单:
为简洁起见,k为大于或等于 -1的最小整数,可简写为k=[ -1]。例如,[3.0]=3,[2.1]=3。
此外,二叉树还有两种特殊形式,一个叫作满二叉树,另一个叫作完全二叉树。
2.2 满二叉树
1)、概念:一个二叉树的所有非叶子节点都存在左右孩子, 并且所有叶子节点都在同一层级上, 那么这个树就是满二叉树。
这个概念也很好理解,好比每一个父亲都有一个儿子(左节点)女儿(右节点)。就很棒~
如下图:
这就是满二叉树的例子。那啥是完全二叉树呢?下面我们继续学习
2.3 完全二叉树
对一个有n个节点的二叉树, 按层级顺序编号, 则所有节点的编号为从1到n。 如果这个树所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同, 则这个二叉树为完全二叉树。如下图:
由图可知:在满叉树的基础上,我在最底层从右往左删去若干节点,得到的都是完全二叉树。
推论:满二叉树一定是完全二叉树,但是完全二叉树不一定是满二叉树
好理解吧。满二叉树要求所有分支都是满的;而完全二叉树只需保证最后一个节点之前的节点都齐全即可。
下面我们来学习二叉树是如何存储的?
3、存储结构
二叉树可以用哪些物理存储结构来表达呢?
i) 链式存储结构。
ii) 数组。
3.1 链式存储结构
链式存储是最直观的存储方式。
其每一个节点的结构为:
- Data 存储当前节点的数据
- Left 指向左孩子的left指针
- Right指向右孩子的RIght指针
再看看数组是如何存储的:
使用数组存储时,会按照层级顺序把二叉树的节点放到数组中对应的位置上。如果某一个节点的左孩子或右孩子空缺,则数组的相应位置也空出来。
问题:为啥要这么去设计呢?
因为这样可以更方便地在数组中定位二叉树的孩子节点和父节点。
由图对比可知:1->数组下标0,2->下标1,3->2....
结论:
- 一直一个父节点的下标为fatherIndex 可以推出,其节点的左右孩子节点下标为:2*fatherIndex +1,2*fatherIndex +2。
3.2 二叉树的应用
二叉树包含许多特殊的形式,每一种形式都有自己的作用,但是其最主要的应用还在于进行查找操作和维持相对顺序这两个方面。
3.2.1 查找
二叉树的树形结构使它很适合扮演索引的角色。(mysql索引就有一种是BTree)
这里我们介绍一种特殊的二叉树:二叉查找树(binary search tree) 。 光看名字就可以知道,这种二叉树的主要作用就是进行查找操作。
二叉查找树在二叉树的基础上增加了以下几个条件。
- 如果左子树不为空, 则左子树上所有节点的值均小于根节点的值
- 如果右子树不为空, 则右子树上所有节点的值均大于根节点的值
- 左、 右子树也都是二叉查找树
下图就是一个标准的二叉查找树
二叉查找树的这些条件有什么用呢?当然是为了查找方便。
比如我要查询值为9 所在的位置
1)9>6(根节点)-到右节点,9>8-到8的右节点-找到,结束。
对于一个节点分布相对均衡的二叉查找树来说,如果节点总数是n,那么搜索节点的时间复杂度就是O(logn),和树的深度是一样的。(算法中log级别的时间复杂度都是由于使用了分治思想,这个底数直接由分治的复杂度决定。)
这种依靠比较大小来逐步查找的方式,和二分查找算法非常相似。
3.2.2 维持相对顺序
这一点仍然要从二叉查找树说起。 二叉查找树要求左子树小于父节点,右子树大于父节点,正是这样保证了二叉树的有序性。
因此二叉查找树还有另一个名字——二叉排序树(binary sort tree) 。新插入的节点,同样要遵循二叉排序树的原则。 例如插入新元素5,由于5<6,5>3,5>4,所以5最终会插入到节点4的右孩子位置。
但是也有极端情况产生:
不只是外观看起来变得怪异了, 查询节点的时间复杂度也退化成了O(n)。怎么解决这个问题呢?这就涉及二叉树的自平衡了。 二叉树自平衡的方式有多种,如红黑树、 AVL树、 树堆等。红黑树我们在将散列表的时候提到过吧,他会进行自平衡,也可以说成自旋。
4、二叉树的遍历
4.1为什么需研究遍历
二叉树,是典型的非线性数据结构,遍历时需要把非线性关联的节点转化成一个线性的序列,以不同的方式来遍历,遍历出的序列顺序也不同。
那么,二叉树都有哪些遍历方式呢?
从节点之间位置关系的角度来看,二叉树的遍历分为4种。
- 前序遍历
- 中序遍历
- 后序遍历
- 层序遍历
从更宏观的角度来看,二叉树的遍历归结为两大类。
1). 深度优先遍历(前序遍历、 中序遍历、 后序遍历)。
2). 广度优先遍历(层序遍历)。
4.2 深度优先遍历
深度优先和广度优先这两个概念不止局限于二叉树,它们更是一种抽象的算法思想,决定了访问某些复杂数据结构的顺序。 在访问树、 图,或其他一些复杂数据结构时,这两个概念常常被使用到。
所谓深度优先,顾名思义,就是偏向于纵深,“一头扎到底” 的访问方式。 可能这种说法有些抽象,下面就通过二叉树的前序遍历、 中序遍历、 后序遍历,来看一看深度优先是怎么回事吧。
4.2.1. 前序遍历
二叉树的前序遍历,输出顺序是根节点、 左子树、 右子树。如下图:
详细步骤如下:
- 首先输出的是根节点1
- 由于根节点1存在左孩子,输出左孩子节点2。
- 由于节点2也存在左孩子,输出左孩子节点4。
- 节点4既没有左孩子,也没有右孩子,那么回到节点2,输出节点2的右孩子节点5。
- 节点5既没有左孩子,也没有右孩子,那么回到节点1,输出节点1的右孩子节点3。
- 节点3没有左孩子,但是有右孩子,因此输出节点3的右孩子节点6。
到此为止,所有的节点都遍历输出完毕。
4.2.2. 中序遍历
二叉树的中序遍历,输出顺序是左子树、 根节点、 右子树如下图
上图就是一个二叉树的中序遍历,每个节点左侧的序号代表该节点的输出顺序,详细步骤如下。
- 首先访问根节点的左孩子,如果这个左孩子还拥有左孩子,则继续深入访问下去,一直找到不再有左孩子的节点,并输出该节点。 显然,第一个没有左孩子的节点是节点4。
- 依照中序遍历的次序,接下来输出节点4的父节点2。
- 再输出节点2的右孩子节点5。
- 以节点2为根的左右节点已经输出完毕,将2当成1的左节点,这时再输出2的父节点1(回到整个二叉树的根节点)
- 由于节点3没有左孩子,所以直接输出根节点1的右孩子节点3。
- 最后输出节点3的右孩子节点6。
到此为止,所有的节点都遍历输出完毕(不太明白可以后面看代码加深理解)。
4.2.3 后序遍历
二叉树的后序遍历,输出顺序是左子树、 右子树、 根节点。
步骤依照前面两种就可以推出来啦。这里就不做介绍了,我们可以看代码理解,好下面我们看看代码是如何实现上面三种遍历方式的。
假设存在这样的二叉树
这里按照前序遍历顺序存储在链表里:
{15,9,7,6,null,null,8,null,null,14,null,12,null,null,20,19,18,null,null,null,24,22}
我们按照前三种遍历依次输出
- 前序遍历 15,9,7,6,8,14,12,20,19,18,24,22
- 中序遍历 6,7,8,9,14,12,15,18,19,20,22,24
- 后序遍历 6,8,7,12,14,9,18,19,22,24,20,15
代码如下:
public class MxyTree {
/**
* 构建二叉树
*
* @Pram TreeLinkList
*/
public static MxyTreeNode createBinaryTree(LinkedList<Integer> inputList) {
if (inputList == null || inputList.isEmpty()) {
return null;
}
MxyTreeNode node = null;
//每次进来弹出一个元素
Integer data = inputList.removeFirst();
if (data != null) {
//将弹出的元素放入到data里
node = new MxyTreeNode(data);
//递归弹出的元素放入左节点的data并赋值给node的左指针
node.leftChild = createBinaryTree(inputList);
//递归弹出的元素放入右节点的data并赋值给node的右指针
node.rightChid = createBinaryTree(inputList);
}
return node;
}
/**
* 前序遍历 根左右
*
* @param node
*/
public static void preOrderTraveral(MxyTreeNode node) {
if (node == null) {
return;
}
//输出当前node的data值
System.out.println(node.data);
//一直遍历该节点的左节点知道没有左节点存在了
preOrderTraveral(node.leftChild);
//如上相反
preOrderTraveral(node.rightChid);
}
/**
* 中序遍历
* 左 根 右
*
* @param
*/
public static void inOrderTraveral(MxyTreeNode node) {
if (node == null) {
return;
}
inOrderTraveral(node.leftChild);
System.out.println(node.data);
inOrderTraveral(node.rightChid);
}
/**
* 后续遍历
* 左 右 根
*/
public static void postOrderTraveral(MxyTreeNode node) {
if (node == null) {
return;
}
postOrderTraveral(node.leftChild);
postOrderTraveral(node.rightChid);
System.out.println(node.data);
}
static class MxyTreeNode {
int data;
MxyTreeNode leftChild;
MxyTreeNode rightChid;
public MxyTreeNode(int data) {
this.data = data;
}
}
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(new Integer[]{15, 9, 7, 6, null, null, 8, null, null, 14, null, 12, null, null,
20, 19, 18, null, null, null, 24, 22});
LinkedList<Integer> linkedList = new LinkedList<>(integers);
MxyTreeNode binaryTree = createBinaryTree(linkedList);
// preOrderTraveral(binaryTree);
// inOrderTraveral(binaryTree);
postOrderTraveral(binaryTree);
}
}
最好可以自己敲一遍哦~加深印象
绝大多数可以用递归解决的问题,其实都可以用另一种数据结构来解决,这种数据结构就是我们之前学习的栈。 因为递归和栈都有回溯的特性。
如何借助栈来实现二叉树的非递归遍历呢?下面以二叉树的前序遍历为例,看一看具体过程。
1. 首先遍历二叉树的根节点1,放入栈中。
2. 遍历根节点1的左孩子节点2,放入栈中。
3. 遍历节点2的左孩子节点4,放入栈中。
4. 节点4既没有左孩子,也没有右孩子,我们需要回溯到上一个节点2。 可是现在并不是做递归操作,怎么回溯呢?
别担心,栈已经存储了刚才遍历的路径。 让旧的栈顶元素4出栈,就可以重新访问节点2,得到节点2的右孩子节点5。
此时节点2已经没有利用价值(已经访问过左孩子和右孩子),节点2出栈,节点5入栈。
5. 节点5既没有左孩子,也没有右孩子,我们需要再次回溯,一直回溯到节点1。 所以让节点5出栈。根节点1的右孩子是节点3,节点1出栈,节点3入栈。
6. 节点3的右孩子是节点6,节点3出栈,节点6入栈。
7. 节点6既没有左孩子,也没有右孩子,所以节点6出栈。 此时栈为空,遍历结束。
那代码该如何写呢?别着急,下面我们看看代码如何实现:
/**
* 二叉树非递归前序遍历
*/
public static void preOrderTraveralWithStack(MxyTreeNode root){
Stack<MxyTreeNode> treeNodes = new Stack<>();
//指针思想,mxyTreeNode指向当前MxyTreeNode,不断变化
MxyTreeNode mxyTreeNode=root;
while (mxyTreeNode!=null||!treeNodes.isEmpty()){
//迭代访问MxyTreeNode的左节点并入栈
while (mxyTreeNode!=null){
System.out.println(mxyTreeNode.data);
//入栈
treeNodes.push(mxyTreeNode);
//指向下一个左节点
mxyTreeNode=mxyTreeNode.leftChild;
}
//如果没有左节点,就弹栈,访问右节点
if (!treeNodes.isEmpty()){
mxyTreeNode = treeNodes.pop();
//指向右节点
mxyTreeNode=mxyTreeNode.rightChid;
}
}
}
至于二叉树的中序、 后序遍历的非递归实现,思路和前序遍历差不太多,都是利用栈来进行回溯。这里就不再演示。
4.3 广度优先遍历
如果说深度优先遍历是在一个方向上“一头扎到底” ,那么广度优先遍历则恰恰相反:先在各个方向上各走出1步,再在各个方向上走出第2步、 第3步……一直到各个方向全部走完。 听起来有些抽象,下面让我们通过二叉树的层序遍历,来看一看广度优先是怎么回事。
层序遍历,顾名思义,就是二叉树按照从根节点到叶子节点的层次关系,一层一层横向遍历各个节点(从左向右)。如下图:
上图就是一个二叉树的层序遍历,每个节点左侧的序号代表该节点的输出顺序。
可是,二叉树同一层次的节点之间是没有直接关联的,如何实现这种层序遍历呢?
我们有了上面使用栈的经验,这里我们来分析一下。
- 根据上图遍历顺序,从左至右 1->2->3->4->5->6
- 解析:1(整体树的根节点)2(1的左节点)3(1的右节点)4(2的左节点)5(2的右节点)6(3的右节点)
- 建立联系:现在我们发现,前三个都可以和1建立联系,但是456便断开了怎么办,如果遍历4之前将1去除,2就可以和45建立联系,遍历6的时候,将2去除,3就可以和6建立联系。这是不是就是队列的性质,先进先出。
为了更方便理解,请看下图:
1. 根节点1进入队列。
2. 节点1出队,输出节点1,并得到节点1的左孩子节点2、 右孩子节点3。 让节点2和节点3入队。
3. 节点2出队,输出节点2,并得到节点2的左孩子节点4、 右孩子节点5。 让节点4和节点5入队。
4. 节点3出队,输出节点3,并得到节点3的右孩子节点6。 让节点6入队。
5. 节点4出队,输出节点4,由于节点4没有孩子节点,所以没有新节点入队。
6. 节点5出队,输出节点5,由于节点5同样没有孩子节点,所以没有新节点入队。
7. 节点6出队,输出节点6,节点6没有孩子节点,没有新节点入队。
到此为止,所有的节点都遍历输出完毕。有意思吧,我们前面学习的数据结构也用上了。那代码如何写呢,莫慌,代码如下:
/**
* 层序遍历
* @Parm root
*/
public static void levelOrderTraversal(MxyTreeNode root){
Queue<MxyTreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()){
//出栈并输出当前节点的data
MxyTreeNode node = queue.poll();
System.out.println(node.data);
if (node.leftChild!=null){
//入队
queue.offer(node.leftChild);
}
if (node.rightChid!=null){
queue.offer(node.rightChid);
}
}
}
5、啥子是二叉堆
5.1 初识二叉堆
二叉堆本质上是一种完全二叉树,它分为两个类型
- 最大堆
- 最小堆
什么是最大堆呢?最大堆的任何一个父节点的值,都大于或等于它左、 右孩子节点的值。如下图
什么是最小堆呢?最小堆的任何一个父节点的值,都小于或等于它左、 右孩子节点的值。如下图:
二叉堆的根节点叫作堆顶。所以通过最大堆和最小堆可以知道一个最值(最大堆的堆顶是整个堆中的最大元素;最小堆
的堆顶是整个堆中的最小元素)。那如何去构建一个二叉堆,我们学了上面的知识,知道这个二叉堆会在添加节点的时候会不断的自我调整。
5.2 二叉堆的自我调整
对于二叉堆,有如下几种操作。
- 插入节点
- 删除节点
- 构建二叉堆
这几种操作都基于堆的自我调整。 所谓堆的自我调整,就是把一个不符合堆性质的完全二叉树,调整成一个堆。 下面让我们以最小堆为例,看一看二叉堆是如何进行自我调整的。
5.2.1调整步骤
1)插入节点:当二叉堆插入节点时,插入位置是完全二叉树的最后一个位置。 例如插入一个新节点,值是 0。
这时,新节点的父节点5比0大,显然不符合最小堆的性质。 于是让新节点“上浮” ,和父节点交换位置。
继续用节点0和父节点3做比较,因为0小于3,则让新节点继续“上浮” 。
继续比较,最终新节点0“上浮” 到了堆顶位置。
2) 删除节点
二叉堆删除节点的过程和插入节点的过程正好相反,所删除的是处于堆顶的节点。 例如删除最小堆的堆顶节点1。
这时,为了继续维持完全二叉树的结构,我们把堆的最后一个节点10临时补到原本堆顶的位置。
接下来,让暂处堆顶位置的节点10和它的左、 右孩子进行比较,如果左、 右孩子节点中最小的一个(显然是节点2)比节点10小,那么让节点10“下沉” 。
继续让节点10和它的左、 右孩子做比较,左、 右孩子中最小的是节点7,由于10大于7,让节点10继续“下沉” 。
这样一来,二叉堆重新得到了调整。
3) 构建二叉堆
构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质就是让所有非叶子节点依次“下沉” 。
下面举一个无序完全二叉树的例子,如下图所示。
首先,从最后一个非叶子节点开始,也就是从节点10开始。 如果节点10大于它左、 右孩子节点中最小的一个,则节点10“下沉” 。
接下来轮到节点3,如果节点3大于它左、 右孩子节点中最小的一个,则节点3“下沉” 。
然后轮到节点1,如果节点1大于它左、 右孩子节点中最小的一个,则节点1“下沉” 。 事实上节点1小于它的左、 右孩子,所以不用改变。接下来轮到节点7,如果节点7大于它左、 右孩子节点中最小的一个,则节点7“下沉” 。
节点7继续比较,继续“下沉” 。
经过上述几轮比较和“下沉” 操作,最终每一节点都小于它的左、 右孩子节点,一个无序的完全二叉树就被构建成了一个最小堆。
堆的插入操作是单一节点的“上浮” , 堆的删除操作是单一节点的“下沉” , 这两个操作的平均交换次数都是堆高度的一半, 所
以时间复杂度是O(logn)。 构建堆的时间复杂度而是O(n)。
还记得上面学的知识么,在二叉树可以放在链表里,但是对于二叉堆虽然是一个完全二叉树,但它的存储方式并不是链式存储,而是顺序存储。 换句话说,二叉堆的所有节点都存储在数组中。
我们在学习完全二叉树的时候有个性质:
假设父节点的下标是parent,那么它的左孩子下标就是2×parent+1;右孩子下标就是2×parent+2。可以通过这个性质来确定左右孩子。
例如上面的例子中,节点6包含9和10两个孩子节点,节点6在数组中的下标是3,节点9在数组中的下标是7,节点10在数组中的下标是8。下面我们来看看二叉堆的代码实现:
public class MxyAdJust {
/**
* 构建堆
* @Pram array
*/
public static void buildHeap(int[] array) {
//从最后一个非叶子节点开始 依次下沉
//(array.length - 3) / 2: 可以知道最后一个非叶子节点右节点为 array.length-1 可以推出
for (int i = (array.length - 3) / 2; i >= 0; i--) {
downAdjust(array, i, array.length);
}
}
/**
* “下沉” 调整
*
* @param array 待调整的堆
* @param parentIndex 要“下沉” 的父节点
* @param length 堆的有效大小
*/
private static void downAdjust(int[] array, int parentIndex, int length) {
//temp 用于保存父节点的值,用于最后的赋值。
int temp = array[parentIndex];
//获取左节点
int childIndex = 2 * parentIndex + 1;
while (childIndex < length) {
// 如果有右孩子,且右孩子小于左孩子的值,则定位到右孩子
if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) {
childIndex++;
}
// 如果父节点小于任何一个孩子的值,则直接跳出
if (temp <= array[childIndex])
break;
//无须真正交换,单向赋值即可
array[parentIndex] = array[childIndex];
parentIndex = childIndex;
childIndex = 2 * childIndex + 1;
}
array[parentIndex] = temp;
}
/**
* “上浮” 调整
* @param array 待调整的堆
*/
public static void upAdjust(int[] array) {
int childIndex = array.length - 1;
int parentIndex = (childIndex - 1) / 2;
// temp 保存插入的叶子节点值,用于最后的赋值
int temp = array[childIndex];
while (childIndex > 0 && temp < array[parentIndex]) {
//无须真正交换,单向赋值即可
array[childIndex] = array[parentIndex];
childIndex = parentIndex;
parentIndex = (parentIndex - 1) / 2;
}
array[childIndex] = temp;
}
public static void main(String[] args) {
int[] array = new int[]{1, 3, 2, 6, 5, 7, 8, 9, 10, 0};
upAdjust(array);
System.out.println(Arrays.toString(array));
array = new int[]{7, 1, 3, 10, 5, 2, 8, 9, 6};
buildHeap(array);
System.out.println(Arrays.toString(array));
}
}
代码中有一个优化的点,就是在父节点和孩子节点做连续交换时,并不一定要真的交换,只需要先把交换一方的值存入temp变量,做单向覆盖,循环结束后,再把temp的值存入交换后的最终位置即可。其实二叉堆是优先队列和堆排序的基础。后面会慢慢说出。 下面我们看看优先队列。终于到优先队列了~
6、优先队列
6.1优先队列的特点
首先我们来熟悉一下普通队列的特点,普通队列,先进先出(FIFO)
优先队列不再遵循先入先出的原则,而是分为两种情况:
- 最大优先队列, 无论入队顺序如何, 都是当前最大的元素优先出队
- 最小优先队列, 无论入队顺序如何, 都是当前最小的元素优先出队
例如有一个最大优先队列,其中的最大元素是8,那么虽然8并不是队头元素,但出队时仍然让元素8首先出队。
要实现以上需求,利用线性数据结构并非不能实现,但是时间复杂度较高。所以我们前面学的二叉堆就派上用场了。
我们可以用最大堆来实现最大优先队列,这样的话,每一次入队操作就是堆的插入操作,每一次出队操作就是删除堆顶节点。
反之最小优先队列就可以用最小堆来实现了。
具体我们看下代码如何实现:
public class PriorityQueue {
private int size;
private int[] array;
public PriorityQueue() {
//队列的长度设置为32
array = new int[32];
}
/**
* 入队
*
* @Param key
*/
public void enQueue(int key) {
//队列长度超出范围,扩容
if (size >= array.length) {
resize();
}
array[size++] = key;
upAdjust();
}
/**
* 上浮调整
*/
private void upAdjust() {
//最后一个节点下标
int childIndex=size-1;
int parentIndex=(childIndex-1)/2;
// temp 保存插入的叶子节点值,用于最后的赋值
int temp=array[childIndex];
while (childIndex>0 && temp>array[parentIndex]){
//无须真正交换,单向赋值即可。
array[childIndex]=array[parentIndex];
childIndex=parentIndex;
parentIndex=childIndex/2;
}
array[childIndex]=temp;
}
/**
* 队列扩容
*/
private void resize() {
//队列容量翻倍
int newSize=size*2;
this.array = Arrays.copyOf(this.array, newSize);
}
/**
* 出队
* @return
* @throws Exception
*/
public int deQueue() throws Exception {
if (size<0){
throw new Exception("the queue is enpty");
}
//获取堆顶元素
int head = array[0];
//让最后一个元素移动到堆顶
array[0] = array[--size];
downAdjust();
return head;
}
/**
* 下沉
*/
private void downAdjust() {
// temp 保存父节点的值,用于最后的赋值
int parentIndex=0;
int temp=array[parentIndex];
int childIndex=1;
while (childIndex<size){
// 如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子
if (childIndex+1<size&&array[childIndex]<array[childIndex+1]){
childIndex++;
}
//如果 父节大于任何一个孩子节点,直接跳出。
if (temp>=array[childIndex]){
break;
}
//无须真正交换,单向复制就好
array[parentIndex]=array[childIndex];
parentIndex=childIndex;
childIndex=childIndex*2+1;
}
array[parentIndex]=temp;
}
public static void main(String[] args) throws Exception {
PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.enQueue(3);
priorityQueue.enQueue(5);
priorityQueue.enQueue(10);
priorityQueue.enQueue(2);
priorityQueue.enQueue(7);
System.out.println(" 出队元素:" + priorityQueue.deQueue());
System.out.println(" 出队元素:" + priorityQueue.deQueue());
}
}
上述代码采用数组来存储二叉堆的元素,因此当元素数量超过数组长度时,需要进行扩容来扩大数组长度。
好了关于树的就到这里,其实还有很多方面需要我们自己持续学习,不断完善。
我们这里总结一下,本篇学习的知识点:
- 什么是树
树是n个节点的有限集,有且仅有一个特定的称为根的节点。 当n>1时,其余节点可分为m个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。
- 什么是二叉树
二叉树是树的一种特殊形式,每一个节点最多有两个孩子节点。 二叉树包含完全二叉树和满二叉树两种特殊形式。二叉树的遍历方式有几种:根据遍历节点之间的关系,可以分为前序遍历、 中序遍历、 后序遍历、 层序遍历这4种方式;从更宏观的角度划分,可以划分为深度优先遍历和广度优先遍历两大类。
- 什么是二叉堆
二叉堆是一种特殊的完全二叉树,分为最大堆和最小堆。
在最大堆中,任何一个父节点的值,都大于或等于它左、 右孩子节点的值。
在最小堆中,任何一个父节点的值,都小于或等于它左、 右孩子节点的值。
- 什么是优先队列
优先队列分为最大优先队列和最小优先队列。
在最大优先队列中,无论入队顺序如何,当前最大的元素都会优先出队,这是基于最大堆实现的。
在最小优先队列中,无论入队顺序如何,当前最小的元素都会优先出队,这是基于最小堆实现的。