看了这篇文章,不再怕关于树的面试题了

基础知识就像是一座大楼的地基,它决定了咱们技术的高度java

在面试中,关于树的问题是不少的,例如简单点的会问你关于树的前中后序的遍历顺序是怎样的?难点会让你手写关于树的算法题,又或是在Java后端面试中也会涉及到一些树的知识,例如在HashMap中产生哈希冲突生成的链表到必定条件下为何要转成红黑树?,为何要用红黑树而不用B+树呢?在Mysql中索引的存储为何用B+树而不用其余树等等。其实这些东西咱们在平常开发过程当中都会用到,其实每一个程序员都不甘心天天工做只是CRUD,那么这些数据结构其实就是内功,咱们学会了它在平常工做分析问题,选用什么集合,排序怎么排,这些问题中咱们会多一些选择。git

什么是树

树(英语:tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具备树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具备层次关系的集合。把它叫作“树”是由于它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。程序员

再完备的定义也没有图直观,咱们先来看一下图(这里借用数据结构与算法做者王争老师的图,下面看到相似风格的图都是如此)。github

咱们能够看到这里的树和咱们现实生活的树是十分相像的,其中每一个元素咱们称之为节点,而用直线相连的关系咱们称之为父子关系。那么什么才能称之为树呢?web

  • 每一个节点都只有有限个子节点或无子节点
  • 没有父节点的节点称为根节点
  • 每个非根节点有且只有一个父节点
  • 除了根节点外,每一个子节点能够分为多个不相交的子树
  • 树里面没有环路

树的分类有不少,可是咱们经常使用的仍是二叉树(每一个节点最多含有两个子树的树称为二叉树),下图中所示其实就是二叉树。面试

二叉树中也会有分类,其中上图中②是满二叉树(除了叶子节点之外,每一个节点都有左右两个子节点),上图③是彻底二叉树(其它各层的节点数目均已达最大值,最后的全部节点从左向右连续地紧密排列,这样的二叉树被称为彻底二叉树),这里关键点是从左到右排列。那么为何会有彻底二叉树呢?这里涉及到了树的两种存储方式,一种直观的感觉就是直接链表存储。Node节点设置左右子节点两个元素。代码以下。算法

1class TreeNode <T>{
2    public T data;
3    public TreeNode leftNode;
4    public TreeNode rightNode;
5
6    TreeNode(T data){
7        this.data = data;
8    }
9}
复制代码

另外一种方式就是基于数组顺序存储,咱们把树从根节点开始放入数组下标为1的索引处,接下来一次从左到右放入。sql

序号 左节点位置 右节点位置
1 2 3
2 4 5
3 6 7
…… …… ……
n 2n 2n-1

这里若是是将根节点放在索引为0的地方开始的话,那么左节点就是2n+1,右节点就是2n+2后端

依据上面的表格,咱们应该可以获得对于每一个节点n来讲,它的左节点位置就是2n,它的右节点位置就是2n+1。这样咱们就可以将彻底二叉树存储在数组的结构中去了。若是是非彻底二叉树也用数组存储的话,那么数组的中间会产生许多的空洞,形成内存的浪费。api

用数组存储数据的优势在于无需向链表那样存储左右子节点的指针,节省空间。

二叉树的遍历

上面咱们简单了解了树的定义,接下来咱们学习一下二叉树的遍历(前序遍历、中序遍历、后序遍历),这也是面试中常常被问到的点。

  • 前序遍历是指,对于树中的任意节点来讲,先打印这个节点,而后再打印它的左子树,最后打印它的右子树
  • 中序遍历是指,对于树中的任意节点来讲,先打印它的左子树,而后再打印它自己,最后打印它的右子树
  • 后序遍历是指,对于树中的任意节点来讲,先打印它的左子树,而后再打印它的右子树,最后打印这个节点自己

咱们用代码表示一下更加直观。

 1/**
2@Description: 前序遍历
3@Param: [treeNode]
4@return: void
5@Author: hu_pf
6@Date: 2019/11/26
7*/

8private static void frontOrder(TreeNode<String> treeNode){
9    if (treeNode == null){
10        return;
11    }
12    System.out.printf(treeNode.data);
13    frontOrder(treeNode.leftNode);
14    frontOrder(treeNode.rightNode);
15}
16
17/**
18@Description: 中序遍历
19@Param: [treeNode]
20@return: void
21@Author: hu_pf
22@Date: 2019/11/26
23*/

24private static void middleOrder(TreeNode<String> treeNode){
25    if (treeNode == null){
26        return;
27    }
28    middleOrder(treeNode.leftNode);
29    System.out.printf(treeNode.data);
30    middleOrder(treeNode.rightNode);
31}
32
33/**
34@Description: 后序遍历
35@Param: [treeNode]
36@return: void
37@Author: hu_pf
38@Date: 2019/11/26
39*/

40private static void afterOrder(TreeNode<String> treeNode){
41    if (treeNode == null){
42        return;
43    }
44    afterOrder(treeNode.leftNode);
45    afterOrder(treeNode.rightNode);
46    System.out.printf(treeNode.data);
47}
复制代码

二叉查找树

二叉查找树中,每一个节点的值都大于左子树节点的值,小于右子树节点的值

二叉查找树就是动态的支持数据的增删改查,并且仍是自然有序的,咱们只要经过中序遍历那么的到的数据就是有序的数据了。

二叉查找树的插入

咱们只须要从根节点开始,依次比较要插入的数据和节点的关系便可。若是插入的数据比当前节点大,而且其右节点没有数据则放到其右节点上去,若是其右节点有数据,那么久再次遍历其右子树。若是插入数据比当前数据小的话过程相似。

用代码表示以下。

 1private void insertTree(int data){
2    if (this.rootNode == null){
3        rootNode = new TreeNode(data);
4        return;
5    }
6    TreeNode<Integer> p = rootNode;
7    while (p!=null){
8        Integer pData = p.data;
9        if (data>=pData){
10            if (p.rightNode == null){
11                p.rightNode = new TreeNode(data);
12                break;
13            }
14            p = p.rightNode;
15        }else {
16            if (p.leftNode == null){
17                p.leftNode = new TreeNode(data);
18                break;
19            }
20            p = p.leftNode;
21        }
22    }
23}
复制代码

二叉查找树的查找

二叉树的查找的话就比较简单了,拿要找的数据和根节点相比较,若是查找的数据比它小则在左子树进行查找,若是查找的数据比它大则在右子树进行查找。

 1private TreeNode findTreeNode(int data){
2
3    if (this.rootNode == null){
4        return null;
5    }
6
7    TreeNode<Integer> p = rootNode;
8    while (p != null){
9        if (p.data == datareturn p;
10        if (data >= p.data) p = p.rightNode;
11        else p = p.leftNode;
12    }
13    return null;
14}
复制代码

二叉查找树的删除

二叉查找树的删除操做比较复杂,须要考虑三种状况。

  • 删除的节点无子节点:直接删除便可
  • 删除的节点只有一个节点:父节点的引用换成其子节点便可
  • 删除的节点有两个节点:那么咱们须要想了,左子节点数据<父节点数据<右子节点数据,此时咱们若是删除了父节点的话,那么就须要从左节点或者右节点找到一个节点移动过来占据此位置,而且移动完之后还要保持一样的大小关系,那么移动哪一个呢?移动左子节点最大的节点,或者右子节点最小的节点。这里你们须要细细品味一下。为何要这样移动。

要找到右子节点最小的节点,只要找到右子节点哪一个没有左节点就表明那个节点是最小的,相反的若是要找到左子节点最大的节点,只要找到左子节点哪一个没有右节点就表明那个节点是最大的。

接下来咱们看一下删除的代码

 1private void deleteTreeNode(int data){
2
3    if (this.rootNode == null ) return;
4
5    TreeNode<Integer> treeNode = rootNode;
6    TreeNode<Integer> treeNodeParent = null;
7    while (treeNode != null){
8        if (treeNode.data == databreak;
9        treeNodeParent = treeNode;
10        if (data >= treeNode.data) treeNode = treeNode.rightNode;
11        else treeNode = treeNode.leftNode;
12    }
13
14    // 没有找到节点
15    if (treeNode == nullreturn;
16
17    TreeNode<Integer> childNode = null;
18    // 1. 删除节点没有子节点
19    if (treeNode.leftNode == null && treeNode.rightNode == null){
20        childNode = null;
21        if (treeNodeParent.leftNode == treeNode) treeNodeParent.leftNode = childNode;
22        else treeNodeParent.rightNode = childNode;
23        return;
24    }
25
26    // 2. 删除节点只有一个节点
27    if ((treeNode.leftNode !=null && treeNode.rightNode==null)||(treeNode.leftNode ==null && treeNode.rightNode!=null)){
28        // 若是此节点是左节点
29        if (treeNode.leftNode !=null)  childNode = treeNode.leftNode;
30        // 若是此节点是右节点
31        else childNode = treeNode.rightNode;
32        if (treeNodeParent.leftNode == treeNode) treeNodeParent.leftNode = childNode;
33        else treeNodeParent.rightNode = childNode;
34        return;
35    }
36
37
38    // 3. 删除的节点有两个子节点都有,这里咱们演示的是找到右子节点最小的节点
39    if (treeNode.leftNode !=null && treeNode.rightNode!=null){
40        TreeNode<Integer> minNode = treeNode.rightNode;
41        TreeNode<Integer> minNodeParent = treeNode;
42        while (minNode.leftNode!=null){
43            minNodeParent = minNode;
44            minNode = minNode.leftNode;
45        }
46        treeNode.data = minNode.data;
47        if (minNodeParent.rightNode != minNode) minNodeParent.leftNode = minNode.rightNode;
48        else minNodeParent.rightNode = minNode.rightNode;
49    }
50}
复制代码

删除的代码比较复杂,这里还有一个简单的作法,就是将其标记为删除。这样也就无需移动数据,在插入数据的时候判断是否删除便可。可是会比较占用内存。

平衡二叉树——红黑树

平衡二叉树(Balanced Binary Tree)具备如下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,而且左右两个子树都是一棵平衡二叉树。

上面咱们讲了什么是查找二叉树,那么查找二叉树在一些极端状况下有可能变成一个链表,因此再进行查找的话效率就会变低,而平衡二叉树就是为了解决这个问题出现的,即让整棵树看起来比较均匀,左右子节点的数量大体相同。这样就不会再极端状况下变成链表了。那么一般状况下咱们经常使用的平衡二叉树就是红黑树

这里我不讲解红黑树实现的代码,由于实在是太麻烦了,大概说一下他在Java后端的应用场景。Java的HashMap的结构实际上是数组加链表的结构,若是一个槽的链表过多的话也会影响性,因此当链表长度为8的时候就会自动转换为红黑树,以增长查询性能。接下来我会结合我自身面试的状况给你们说一下红黑树在面试中常常被问到的点。

  • HashMap何时后会将链表转换为红黑树?链表长度为8的状况
  • 在什么状况下红黑树会转换为链表?红黑树的节点为6的时候
  • 为何链表要转换为红黑树?答出链表查询性能很差便可
  • 为何不用其余的树?关键点,平衡树,答出红黑树的优势便可
  • 为何不用B+树?关键点,磁盘,内存。红黑树多用在内部排序,即全放在内存中的。B+树多用于外存上时,B+树也被称之为一个磁盘友好的数据结构

正常状况在面试中面试官是不会让你手写红黑树之类的,我在面试中大概碰到的就上面几个,只要答出红黑树的优势以及应用场景差很少就够了。

关于树的排序

是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似彻底二叉树的结构,并同时知足堆积的性质:即子节点的键值或索引老是小于(或者大于)它的父节点。

在面试中其实也会常常碰到一些排序的算法,例如堆排序或者堆排序的一些应用例如前求TopN的数据。其实堆的结构也是树。堆是具备如下性质的彻底二叉树:每一个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每一个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

其中第 1 个和第 2 个是大顶堆,第 3 个是小顶堆,第 4 个不是堆。既然堆本质上是一个彻底二叉树,那么咱们彻底能够用数组来存堆的数据。那么当前节点是n的话,其左子节点就是2n,其右子节点就是2n+1。

 1class Heap{
2    // 存放堆中的数据,用数组来装
3    private int [] a;
4
5    // 堆中能存放的最大数
6    private int n;
7
8    // 堆中已经存放的数量
9    private int count;
10
11    public Heap(int capacity){
12        a = new int [capacity + 1];
13        this.n = capacity;
14        count = 0;
15    }
16}
复制代码

如何在堆中插入数据

接下来咱们如何动态的将数据插入到堆中呢?

例如咱们上图中的在堆中插入元素22,那么此时应该怎么作来保持他还是一个堆呢?此时咱们上图中演示的大顶堆,那么就要保证大顶堆的概念要求,每一个节点的值都大于等于其左右子节点的值。那么咱们将其放入到数组最后一个位置,而后和它的父节点(n/2就是他的父节点)进行比较,若是大于它的父节点,就和父节点调换位置,继续和其父节点比较,直到它小于其父节点就中止。代码实现以下。

 1public void insert(int data){
2    if (count >= n) return;
3
4    count++;
5    // 把数据放到数组中
6    a[count] = data;
7
8    int index = count;
9    // 开始进行比较,先判断跳出条件
10    // 1. 首先能想到的是插入的数据知足了大(小)顶堆的数据要求
11    // 2. 加上极值条件,及是一颗空树状况
12    while (index/2>0 && a[index]>a[index/2]){
13        swap(a,index,index/2);
14        index = index/2;
15    }
16}
17
18private void swap(int [] a, int i , int j){
19    int swap = a[i];
20    a[i] = a[j];
21    a[j] = swap;
22}
复制代码

如何删除堆顶元素

咱们直到大小顶堆的根节点值是最大的或者最小的。那么若是删除了堆顶元素,接下来就要从左右子节点选个最大或者最小的放入到根节点,而后以此类推。那么咱们按照咱们刚才分析的若是进行移动的话,可能会产生数组的空洞。

咱们能够将根节点移除之后,而后再将数组最后的值给移到根节点,而后再进行依次比较换位置,这样就不会产生数组空洞了。

代码以下

 1public void removeMax(){
2
3    if (count == 0return;
4
5    // 将最后的元素移动到堆顶
6    a[1] = a[count];
7
8    count--;
9
10    heapify(a,count,1);
11}
12
13private void heapify(int [] a,int n,int i){
14    // 定义何时结束,当前节点与其左右子节点进行比对,若是当前节点是最大的则跳出循环(表明当前节点已是最大的)
15    while (true){
16        int maxIndex = i;
17        // 找到三个节点中最大节点的索引值
18        if (2*i<= n && a[i]<a[2*i]) maxIndex = 2*i; // 判断当前节点,是否小于左节点
19        if (2*i+1<= n && a[maxIndex]<a[2*i+1]) maxIndex = 2*i+1;// 判断最大节点是否小于右节点
20        // 若是当前节点已是最大节点就中止交换并中止循环
21        if (maxIndex == i )break;
22        // 找到中最大值的位置,并交换位置
23        swap(a,i,maxIndex);
24        i = maxIndex;
25    }
26}
复制代码

堆排序

咱们对于传进来一个无序的数组如何利用堆来进行排序呢。那么既然是利用堆来排序,那么咱们第一步确定是先将此数组变成一个堆结构。

建堆

如何将一个无序的数组变成一个堆结构呢?第一种咱们很容易就能想到的就是依次调用咱们上面的插入的方法,可是这样所用的内存会加倍,即咱们会新建一个内存进行存储这个堆。那么若是咱们想无需新增内存直接再原有的数组将其变成堆,该如何作呢?

这里数组是否是堆,那么如何将其变成堆呢?这里咱们采用从下往上建堆的方法,什么意思呢?其实就是递归的思想,咱们先将下面的建好堆,而后依次往上传递。咱们先堆化标号为4的树,而后再堆化标号为3的,而后堆化标号为2的,而后堆化标号为1的。依次进行。到最后咱们就获得了一个堆。下图中画圆圈表明了其堆化的树的范围。代码以下

 1public void buildHeap(int[] a,int n){
2    for (int i =n/2;i>=1;i--){
3        heapify(a,n,i);
4    }
5}
6
7private void heapify(int [] a,int n,int i){
8// 定义何时结束,当前节点与其左右子节点进行比对,若是当前节点是最大的则跳出循环(表明当前节点已是最大的)
9while (true){
10    int maxIndex = i;
11    // 找到三个节点中最大节点的索引值
12    if (2*i<= n && a[i]<a[2*i]) maxIndex = 2*i; // 判断当前节点,是否小于左节点
13    if (2*i+1<= n && a[maxIndex]<a[2*i+1]) maxIndex = 2*i+1;// 判断最大节点是否小于右节点
14    // 若是当前节点已是最大节点就中止交换并中止循环
15    if (maxIndex == i )break;
16    // 找到中最大值的位置,并交换位置
17    swap(a,i,maxIndex);
18    i = maxIndex;
19}
20}
复制代码

排序

排序的话就简单了,由于堆的根节点是最大或者最小的元素,因此第一种咱们依然可以想到的是直接将其根节点的值拿出来后放到另外一个数组,而后删掉堆顶元素,而后再拿掉堆顶元素,而后再删除依次类推。这样有一个缺点仍是占用内存,由于咱们又从新定义了一个数组,那么有没有办法不占用内存,直接在原有的数组中进行排序呢?

第二种办法就是不须要借助另外一个数组,怎么作呢?这个过程优势相似于删除堆顶元素,不一样的是删除堆顶元素咱们直接堆顶元素丢弃了,而排序的话咱们须要将堆顶元素和最后一个元素进行互换,互换完之后而后再将其堆化,而后再将堆顶元素与最后一个元素的前一个元素互换,而后再堆化,以此类推。

如何求前TopN的数据

相信微博你们都用,那么其中的前十热搜是如何实时的显现呢?其实求前TopN的数据用到的基本上都是堆,咱们能够创建一个大小为N的小顶堆,若是是静态数据的话,那么就依次从数组中取出元素与小顶堆的堆顶元素进行对比,若是比堆顶元素大,那么就丢弃堆顶元素,而后将数组中元素放入到堆顶,而后堆化。依次进行,最后获得的就是TopN大的数据了。若是是动态数据的话和静态数据也同样,也是进行不断的进行比对。最后若是想要TopN大的数据直接将此堆返回便可。

接下来我用动图演示一下是怎么求得的TopN数据。

数组为: 1 2 3 9 6 5 4 3 2 10 15
求得Top5

本文代码地址

有感兴趣的能够关注一下我新建的公众号,搜索[程序猿的百宝袋]。或者直接扫下面的码也行。

参考

相关文章
相关标签/搜索