树、二叉树、特殊的二叉树(堆)、霍夫曼树

一.树

1.为什么需要树这种数据结构

1.数组存储方式的分析

优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。

缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低

2.链式存储方式的分析

优点:在一定程度上对数组存储方式有优化(如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)

缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)

3.树存储方式的分析

能提高数据存储,读取的效率, 比如二叉排序树,既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。

2.树的常用术语

节点

根节点

父节点

子节点

叶子节点 (没有子节点的节点)

节点的权(节点值)

路径(从 root 节点找到该节点的路线)

子树

树的高度(最大层数)

森林 :多颗子树构成森林

3.二叉树

树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树

二叉树的子节点分为左节点和右节点

如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树

如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二 层的叶子节点在右边连续,我们称为完全二叉树

4.二叉树的遍历

使用前序,中序和后序对下面的二叉树进行遍历

1.前序遍历

先输出父节点,再遍历左子树和右子树

2.中序遍历

先遍历左子树,再输出父节点,再遍历右子树

3.后序遍历

后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点

5.顺序储存二叉树

从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组

对数组表示的二叉树进行遍历(前序,中序,后序)

思路同普通二叉树

第 n 个元素的左子节点为 2 * n + 1

第 n 个元素的右子节点为 2 * n + 2

顺序存储二叉树的特点:

顺序二叉树通常只考虑完全二叉树

第 n 个元素的左子节点为 2 * n + 1

第 n 个元素的右子节点为 2 * n + 2

第 n 个元素的父节点为 (n-1) / 2

n : 表示二叉树中的第几个元素(按 0 开始编号如图所示

6.线索化二叉树

  • n 个结点的二叉链表中含有 n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域,存放指向 该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")
  • 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。根据线索性质 的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
  • 一个结点的前一个结点,称为前驱结点
  • 一个结点的后一个结点,称为后继结点

说明

中序遍历的结果:{8, 3, 10, 1, 14, 6}

当线索化二叉树后,Node 节点的 属性 left 和 right ,有如下情况:

left 指向左子树,也可能是指向前驱节点,比如 ① 节点 left 指向的左子树, 而 ⑩节点的 left 指向的就是前驱节点(leftType)

right 指向右子树,也可能是指向后继节点,比如①节点 right 指向的是右子树,而⑩节点的 right 指向的是后继节点(rightType)

   public void threadedNodes(node) {
        if(node == null) {
            return;
        }
        threadedNodes(node.getLeft());		//先线索化左子树
       //线索化当前结点
        if(node.getLeft() == null) {		 //处理前驱节点
            node.setLeft(pre);			//让当前结点的左指针指向前驱结点
            node.setLeftType(1);   		//修改当前结点的左指针的类型,指向前驱结点
        }
        if (pre != null && pre.getRight() == null) {	//处理后继结点
            pre.setRight(node);			//让前驱结点的右指针指向当前结点
            pre.setRightType(1); 		//修改前驱结点的右指针类型
        }
            pre = node;				 	//移动到下一节点
            threadedNodes(node.getRight());		//线索化右子树
	}

线索二叉树的遍历

因为线索化后,各个结点指向有变化,因此原来的遍历方式不能使用,这时需要使用新的方式遍历线索化二叉树,各个节点可以通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。 遍历的次序应当和中序遍历保持一致

二.堆(特殊的二叉树)

堆基本介绍

  • 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系
  • 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
  • 大顶堆举例说明
  • 小顶堆举例说明
  • 一般升序采用大顶堆降序采用小顶堆

三.霍夫曼树(特殊的二叉树)

1.基本介绍

  • 给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的带权路径长度(WPL)达到最小,称这样的二叉树为最优二叉树,也称为霍夫曼树
  • 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近

2.重点概念

1.路径和路径长度

  • 在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。路径中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1

2.结点的权

  • 若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权

3.带权路径长度

结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积

树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL

3.构成霍夫曼树的步骤

  1. 从小到大进行排序,将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
  2. 取出根节点权值最小的两颗二叉树
  3. 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
  4. 再将这颗新的二叉树,以根节点的权值大小再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗霍夫曼树

给定数组构建霍夫曼树

4.霍夫曼编码

1.基本介绍

  • 霍夫曼编码,是一种编码方式,属于一种程序算法
  • 霍夫曼编码广泛地用于数据文件压缩。其压缩率通常在 20%~90%之间

2.实现

根据不同字符出现的频率给定其权值,构建霍夫曼树

根据得到的霍夫曼树,给各个字符确定霍夫曼编码,想做的路径为0,向右的路径为1

根据霍夫曼编码将原字符串转换为只包含0和1的序列

3.注意事项

  • 这个霍夫曼树根据排序方法不同,也可能不太一样,这样对应的霍夫曼编码也不完全一样,但是 wpl 是 一样的,都是最小的, 最后生成的霍夫曼编码的长度是一样

10.二叉排序树(特殊的二叉树)

1.二叉排序树的介绍

对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当 前节点的值小,右子节点的值比当前节点的值大

2.二叉排序树的创建和遍历

中序遍历二叉排序树的创建

删除

分三种情况

  • 叶子节点
  • 只有一个子树
  • 有两个子树,找到右子树最小值,清除最小值位置,并将最小值放到目标位置
遍历

按对应方法遍历可直接得到排序后的数组

11.平衡二叉树(特殊的二叉树)

1.基本介绍

平衡二叉树也叫平衡二叉搜索树,又被称为 AVL 树, 可以保证查询效率较高

具有以下特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树

2.创建

1.左旋转

    private void leftRotate() {
        //创建新的结点,以当前根结点的值
        Node newNode = new Node(value);
        //把新的结点的左子树设置成当前结点的左子树
        newNode.left = left;
        //把新的结点的右子树设置成带你过去结点的右子树的左子树
        newNode.right = right.left;
        //把当前结点的值替换成右子结点的值
        value = right.value;
        //把当前结点的右子树设置成当前结点右子树的右子树
        right = right.right;
        //把当前结点的左子树(左子结点)设置成新的结点
        left = newNode;
    }

2.右旋转

    private void rightRotate() {
        Node newNode = new Node(value);
        newNode.right = right;
        newNode.left = left.right;
        value = left.value;
        left = left.left;
        right = newNode;
    }

3.双旋转

在某些情况下,单旋转不能完成平衡二叉树的转换

解决思路

  1. 当满足右旋转条件时
  2. 如果该节点左子树的右子树高度大于左子树的左子树高度,先对该节点的左子节点进行左旋转
  3. 再对当前节点进行右旋转
    //当添加完一个结点后,如果: (右子树的高度-左子树的高度) > 1 , 左旋转
    if(rightHeight() - leftHeight() > 1) {
        //如果它的右子树的左子树的高度大于它的右子树的右子树的高度
        if(right != null && right.leftHeight() > right.rightHeight()) {
            right.rightRotate();	//先对右子结点进行右旋转
            leftRotate(); 			//然后在对当前结点进行左旋转
        } else {
            leftRotate();		//直接进行左旋转即可
        }
        return ; 	//添加完毕,退出
    }
    //当添加完一个结点后,如果 (左子树的高度 - 右子树的高度) > 1, 右旋转
    if(leftHeight() - rightHeight() > 1) {
        //如果它的左子树的右子树高度大于它的左子树的高度
        if(left != null && left.rightHeight() > left.leftHeight()) {
            left.leftRotate(); 		//先对当前结点的左结点(左子树)->左旋转
            rightRotate();			//再对当前结点进行右旋转
        } else {
            rightRotate();			//直接进行右旋转即可
        }
    }		//代码结束

12.多路查找树(特殊的二叉树)

1.多叉树

如果允许每个节点可以有更多的数据项和更多的子节点(>2), 就是多叉树(multiway tree)

2-3 树,2-3-4 树就是多叉树,多叉树通过重新组织节点减少树的高度,能对二叉树进行优化。

2. B 树的应用

B 树通过重新组织节点,降低树的高度,并且减少 i/o 读写次数提升效率

  • 如图 B 树通过重新组织节点, 降低了树的高度
  • 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为 4k), 这样每个节点只需要一次 I/O 就可以完全载入
  • 将树的度 M 设置为 1024,在 600 亿个元素中最多只需要 4 次 I/O 操作就可以读取到想要的元素, B 树(B+)广泛 应用于文件存储系统以及数据库系统

3.2-3树

2-3树是最简单的B树结构

1.特点

  • 2-3 树的所有叶子节点都在同一层(只要是 B 树都满足这个条件)
  • 两个子节点的节点二节点,二节点要么没有子节点,要么有两个子节点
  • 有三个子节点的节点三节点,三节点要么没有子节点,要么有三个子节点
  • 2-3 树是由二节点和三节点构成的树

2.应用

满足二叉排序树条件,所有节点必须是二节点或三节点,所有叶子节点都在同一层

当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层, 拆后仍然需要满足上面 3 个条件

3. B 树

B树,B即 balance 平衡

利用B树作为索引

  • B 树的:节点的最多子节点个数(比如 2-3 树的阶是 3,2-3-4 树的阶是4)
  • B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
  • 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据
  • 搜索有可能在非叶子结点结束
  • 其搜索性能等价于在关键字全集内做一次二分查找

4. B+ 树

B+树是 B 树的变体,也是一种多路搜索树

利用B+树作为索引

  • B+树的搜索与 B 树也基本相同,区别是 B+树只有达到叶子结点才命中(B 树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
  • 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据) 恰好是有序
  • 不可能在非叶子结点命中
  • 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
  • 适合文件索引系统

6. B* 树

B*树是 B+树的变体,在 B+树的非根和非叶子结点再增加指向兄弟的指针

利用B+树作为索引

B_ 树定义了非叶子结点__关键字个数至少_**(2/3) ** M,即块的最低使用率为 2/3(而 B+树的块的最低使用率为的 1/2)

从第 1 个特点我们可以看出,**B_树分配新结点的概率比 B+树要低,空间使用率更高_

红黑树

13.图

1.介绍

图是一种数据结构,其中结点可以具有零个或多个相邻元素两个结点之间的连接称为结点也可以称为顶点

2.常用概念

  • 顶点(vertex)
  • 边(edge)
  • 路径
  • 无向图

  • 有向图

  • 带权图

3.图的表示方法

二维数组表示(邻接矩阵);链表表示(邻接表

1.邻接矩阵

邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于 n 个顶点的图而言,矩阵是的 row 和 col 表示的是 1…n 个点

2.邻接表

邻接矩阵需要为每个顶点都分配 n 个边的空间,其实有很多边都是不存在,会造成空间的一定损失

邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费邻接表由数组+链表组成

4.简单实现思路

存储顶点String,使用ArrayList(2)保存矩阵(边)

5.深度优先搜索

1.基本思想

  • 深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点, 可以这样理解: 每次都在访问完当前结点后首先访问当前结点的第一个邻接结点
  • 我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问
  • 显然,深度优先搜索是一个递归的过程

2.算法实现

	//深度优先遍历算法
    //i 第一次就是 0
    private void dfs(boolean[] isVisited, int i) {
        //首先我们访问该结点,输出
        System.out.print(getValueByIndex(i) + "->");
        //将结点设置为已经访问
        isVisited[i] = true;
        //查找结点 i 的第一个邻接结点 w
        int w = getFirstNeighbor(i);
        while(w != -1) {//说明结点w 存在
            if(!isVisited[w]) {
            	dfs(isVisited, w);
        	}
        	//如果 w 结点已经被访问过
            w = getNextNeighbor(i, w);
    	}
    }

	//对 dfs 进行一个重载, 遍历我们所有的结点,并进行 dfs
	public void dfs() {
        isVisited = new boolean[vertexList.size()];
        //遍历所有的结点,进行 dfs[回溯]
        for(int i = 0; i < getNumOfVertex(); i++) {
        if(!isVisited[i]) {
            dfs(isVisited, i);
            }
        }
	}

6.广度优先算法

1.基本思想

类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点

2.算法实现

	//广度优先遍历算法
	private void bfs(boolean[] isVisited, int i) {
        int w ; // 邻接结点 w
        //记录结点访问的顺序的队列
        LinkedList queue = new LinkedList();
        //访问结点,输出结点信息
        System.out.print(getValueByIndex(i) + "=>")
    	//标记为已访问
        isVisited[i] = true;
        //生成队列
  		w=getFirstNeighbor(i);
        while(w!=-1){
            //如果该结点被访问过,则跳过该结点
            if(!isVisited[w]) queue.addLast(w);
   			//将w指向下一个邻接结点
            w == getNextNeighbor(i,w);
        }
        //按队列中顺序进行遍历
        while(!queue.isEmpty()){
            int u=(Integer)queue.removeFirst();
           	//递归
            bfs(isVisited,u);
        }
    }

	//对 bfs 进行一个重载, 遍历我们所有的结点,并进行 bfs
	public void bfs() {
        isVisited = new boolean[vertexList.size()];
        //遍历所有的结点,进行 dfs[回溯]
        for(int i = 0; i < getNumOfVertex(); i++) {
        if(!isVisited[i]) {
            bfs(isVisited,temp, i);
            }
        }
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值