【算法】02_树的应用

1 树基础

1.1 基本概念

1.1.1 树

树是非线性存储,而链表,数组是线形存储。

  • 节点的高度
    节点到叶子结点的最长路径(边数)
  • 节点深度
    根节点到这个节点所经历的节点的个数(数据结构与算法之美中的定义是边的个数)
  • 节点层数
    节点深度+1
  • 树的高度
    根节点的高度
                          高度     深度      层
            1              3        1       1
         /     \         
      2           3        2        2       2
    /   \       /   \      
  4       5   6       7    1        3       3
 / \                 /   
8   9               10     0        4       4

1.1.2 二叉树

满二叉树

  • 叶子结点全部在最底层
  • 除叶子结点外,每个节点均有左右子节点
            1            
         /     \         
      2           3      
    /   \       /   \    
  4       5   6       7   

完全二叉树

  • 叶子结点在最底下两层
  • 倒数第二层的节点均有左右子节点
  • 最下面一层的叶子结点靠左边排列
            1            
         /     \         
      2           3      
    /   \       /   \    
  4       5   6       7  
 / \     /               
8   9   10   

二叉搜索树

  • 树中任意节点,其左子树不为空,则左子树中每个节点的值小于当前节点
  • 树中任意节点,其右子树不为空,则右子树中每个节点的值大于当前节点

平衡二叉查找树

  • 二叉树中任意节点的左右子树的高度不能大于1

1.1.3二叉树的性质

  1. 二叉树的i层,节点数最多是2^(i-1)i>=1
  2. 二叉树的深度是k,二叉树节点数最多是2^k - 1k>=1
  3. 具有n个节点的完全二叉树的深度最大是log(n) + 1
  4. 满二叉树的深度是k,节点总数是2^k - 1

1.1.4 霍夫曼树

1.1.4.1 概念

给定n个权值作为n个叶子结点,如果带权路径最小,这样的书就是霍夫曼树。如下例子所示,树B就是霍夫曼树。
例如给定4个数字:7,5,2,4

树A,带权路径值:2*7 +2*5+2*2+2*4=36

            0            
         /     \         
       0         0      
    /   \       /   \    
   7      5    2     4    

树B,带权路径值:1*7 +2*5+3*2+3*4=35

            0            
         /     \         
       7         0      
                 / \    
                5   0    
                   /  \
                  2    4
1.1.4.2 建造步骤
  1. 在 n 个权值中选出两个最小的权值
  2. 对应的两个结点组成一个新的二叉树,且新二叉树的根结点的权值为左右孩子权值的和
  3. 在原有的 n 个权值中删除那两个最小的权值,同时将新的权值加入到 n–2 个权值的行列中
  4. 重复 1,2,3 ,直到所以的结点构建成了一棵二叉树为止,这棵树就是哈夫曼树。

1.1.5 B树

每个节点最多包含k个子节点,k就是B树的阶。
B树有以下特点:

  1. 根结点至少有两个子女。
  2. 每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
  3. 每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
  4. 所有的叶子结点都位于同一层。
  5. 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

下图是3阶的B树示意图,可以参照B树特点加深理解。

在这里插入图片描述

1.1.6 B+树

一个m阶的B+树具有如下几个特征:

  1. 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点
  2. 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
  3. 每一个父节点的元素都会出现在子节点,是子节点的最大/最小元素。

在这里插入图片描述

B+树与B树区别

B树B+树
根节点与子节点均存储数据,叶子结点也会存储数据根节点与子节点存储的是数据的地址,叶子结点存储所有的数据
区间查找需要对树中序遍历区间查找就是遍历有序链表
叶子节点之间无连接叶子结点之间是链表链接
区间查找,最好的情况是存在于根节点,最坏的情况是存在于叶子结点,不稳定。稳定
B树的IO次数会更多中间节点不存储数据,只存储指针,同样大小的磁盘页可以容纳更多的节点元素。相同数据量,B+更加矮胖,IO次数也会减少。
IO次数更少:由于B+树在内部节点上不包含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子节点上关联的数据也具有更好的缓存命中率。
遍历更加方便:B+树的叶子结点都是相链的,因此对整棵树的遍历只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
  1. B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。

  2. B+树的查询效率更加稳定:由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

  3. B+树更便于遍历:由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。

  4. B+树更适合基于范围的查询:B树在提高了IO性能的同时并没有解决元素遍历的我效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。

1.2 树的遍历

树的结构如下:

      A                  
    /   \    
  B       C  
 / \     / \ 
D   E   F   G

1.2.1深度优先搜索(DFS)

前序遍历
对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最 后打印它的右子树。

中序遍历
对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后 打印它的右子树。

后序遍历
对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树, 最后打印这个节点本身。

遍历方式
前序遍历:A->B->D->E->C->F->G
中序遍历:D->B->E->A->F->C->G
后序遍历:D->E->B->F->G->C->A

1.2.2 广度优先遍历(BFS)

  • 也叫层次遍历
  • 会先访问离根节点最近的节点
  • 从左到右,从上到下的方法遍历
      A                  
    /   \    
  B       C  
 / \     / \ 
D   E   F   G

广度优先遍历:A->B->C->D->E->F->G

1.2 复杂度分析

O(n)
待续

2 应用场景

深度优先遍历-栈
广度优先遍历-队列

            1            
         /     \         
      2           3      
    /   \       /   \    
  4       5   6       7  
 / \     /               
8   9   10   

1、节点1,插入队列【12、取出节点1,插入1的子节点23 ,节点2在队列的前端【233、取出节点2,插入2的子节点45,节点3在队列的最前端【3454、取出节点3,插入3的子节点67,节点4在队列的最前端【45675、取出节点4,插入3的子节点89,节点5在队列的最前端【567896、取出节点5,插入5的子节点10,节点6在队列的最前端【6789107、取出节点6,没有子节点,不插入,节点7在队列的最前端【789108、取出节点7,没有子节点,不插入,节点8在队列的最前端【89109、取出节点8,没有子节点,不插入,节点9在队列的最前端【91010、取出节点9,没有子节点,不插入,节点10在队列的最前端【1011、取出节点10,队列为空,算法结束

3 模版代码

3.1 遍历树

/**
*  中序遍历
*/
public void inorderTraversal(TreeNode root) {
            Stack<TreeNode> stack = new Stack<>();
            if (root == null) {
                return list;
            }
            stack.push(root);
            while (stack.size() > 0) {
                TreeNode node = stack.pop();
                if (node.left == null && node.right == null) {
                    // 叶子结点
                    // 处理叶子结点
                    // 找到叶子结点,还需要继续迭代,不能向下走,不可执行stack.push(node)
                    continue;
                }
                // 右子节点入栈->本节点入栈->左子节点入栈,出栈的顺序就是左中右
                if (node.right != null) {
                    stack.push(node.right);
                    node.right = null;
                }
                stack.push(node);
                if (node.left != null) {
                    stack.push(node.left);
                    node.left = null;
                }
            }
        }
/**
*
*  前序遍历
*/
public List<Integer> preorderTraversal(TreeNode root) {
            if (root == null) {
                return list;
            }
            Stack<TreeNode> stack = new Stack<>();
            stack.push(root);
            while (stack.size() > 0) {
                TreeNode node = stack.pop();
                // 处理节点数据
                if (node.right != null) {
                    stack.push(node.right);
                }
                if (node.left != null) {
                    stack.push(node.left);
                }
            }
            return list;
        }
/**
*  后续遍历
*  按照中-右-左顺序遍历,然后逆序输出,就变成左->右->中。这个不是严格的后续遍历,只是输出结果与后续遍历一致
*/
public List<Integer> postorderTraversal(TreeNode root) {
            ArrayList<Integer> list = new ArrayList<>();
            if (root == null) {
                return list;
            }
            Stack<TreeNode> stack = new Stack<>();
            stack.push(root);
            while (!stack.isEmpty()) {
                TreeNode node = stack.pop();
                if (node.left == null && node.right == null) {
                    // 叶子结点
                    // 处理叶子结点
                    list.add(node.val);
                    // 找到叶子结点,还需要继续迭代,不能向下走,不可执行stack.push(node)
                    continue;
                }
                if (node.left != null) {
                    stack.push(node.left);
                    node.left = null;
                }
                if (node.right != null) {
                    stack.push(node.right);
                    node.right = null;
                }
                stack.push(node);
            }
            // 按照中-右-左顺序遍历,然后逆序输出
            Collections.reverse(list);
            return list;
        }

引用

算法第四版

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于LSTM的财务因子预测选股模型LSTM (Long Short-Term Memory) 是一种特殊的循环神经网络(RNN)架构,用于处理具有长期依赖关系的序列数据。传统的RNN在处理长序列时往往会遇到梯度消失或梯度爆炸的问题,导致无法有效地捕捉长期依赖。LSTM通过引入门控机制(Gating Mechanism)和记忆单元(Memory Cell)来克服这些问题。 以下是LSTM的基本结构和主要组件: 记忆单元(Memory Cell):记忆单元是LSTM的核心,用于存储长期信息。它像一个传送带一样,在整个链上运行,只有一些小的线性交互。信息很容易地在其上保持不变。 输入门(Input Gate):输入门决定了哪些新的信息会被加入到记忆单元中。它由当前时刻的输入和上一时刻的隐藏状态共同决定。 遗忘门(Forget Gate):遗忘门决定了哪些信息会从记忆单元中被丢弃或遗忘。它也由当前时刻的输入和上一时刻的隐藏状态共同决定。 输出门(Output Gate):输出门决定了哪些信息会从记忆单元中输出到当前时刻的隐藏状态中。同样地,它也由当前时刻的输入和上一时刻的隐藏状态共同决定。 LSTM的计算过程可以大致描述为: 通过遗忘门决定从记忆单元中丢弃哪些信息。 通过输入门决定哪些新的信息会被加入到记忆单元中。 更新记忆单元的状态。 通过输出门决定哪些信息会从记忆单元中输出到当前时刻的隐藏状态中。 由于LSTM能够有效地处理长期依赖关系,它在许多序列建模任务中都取得了很好的效果,如语音识别、文本生成、机器翻译、时序预测等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值