由 树 的 节点数(对应位置)范围,求解出对应位置所在 二叉树的 深度

二叉树的遍历

线索树 (除了笔试,没任何用处) 核心就是 将二叉树的空余节点 合理利用

没有 左孩子 就指向 遍历顺序 前驱,没有 右孩子 就指向 遍历顺序 后继

本题的 中序遍历:

的 前驱 和 后继就是

前序遍历 和 后序遍历 可以 构造 唯一二叉树,除非 前序 第一个子树根节点 不在后序中间位置时(在 父节点的邻接左边)

普通树的遍历

对于 普通树 来说遍历只有 前序 和 后序,中序 你不知道 根 应该放在哪里(不唯一)

树和森林的转换(Forest 和 Tree)

只存父节点

并查集

用于寻找最近公共祖先

邻接表

此处 每一个 非叶节点 对应的 孩子,都被存在一个邻接表中

后期上机题的 各类 图算法 中,存储节点用的都是 邻接表(经常用到)

左儿子右兄弟 (二叉树存储)

一个 左儿子右兄弟 表示的 二叉树, 其左儿子的右链(包括左儿子)就是该点邻接表内容

左儿子右兄弟存储的是 有序多叉树,孩子之间只有顺序(第1个、第2个…)没有左右之分

上机考的非常少

[做题总结]

  1. 原树中叶子节点数 = 转换后的树中有右儿子的节点数 + 1

使用数学归纳法(删除法:分情况讨论)得证

  1. F的前序遍历就是T的前序遍历

  1. F的后序遍历就是T的中序遍历

二叉搜索树

Binary Search Tree 特征是 中序遍历有序 的一种 动态维护 数据结构,

如果 二叉树 中 有相同的节点,有三种处理办法

  1. 数据结构中开一个 计数器

  2. 等于的节点 放到 左子树

  3. 等于的节点 放到 右子树

BST 总共三种操作: 插入 删除 查找

每个操作的 时间复杂度 都和 树高 成 线性关系

插入

struct TreeNode  {
 
	int val;
	TreeNode *left , *right;
	
	TreeNode(int _val): val(_val), left(NULL), right(NULL) {}
	
}*root;
 
 
void insert(TreeNode* &root, int x) {
 
	if (!root) root = new TreeNode(x);
	
	else if (x < root->val) insert(root->left, x);
	
	else insert(root->right, x);
 
}

删除

void remove(TreeNode* &root, int x)
{
    if (!root) return;
    
    if (x < root->val)  remove(root->left, x);
    
    else if (x > root->val)  remove(root->right, x);
    
    else  //  x == root->val
    {
        if (!root->left && !root->right)  root = NULL;
        
        else if (!root->left)  root = root->right;
        
        else if (!root->right) root = root->left;
 
        else 
        {
            auto p = root->left;
            
            while(p->right) p = p->right;
            
            root->val = p->val;
            
// p = NULL;
// 这一行不起任何作用,只是把局部变量消除,要消除 root 上的节点必须用 root 才行
            
// remove(root, p->val);  
// 使用 root 也不对,因为 当前 root 节点的值就是 p-val,会陷入死循环
            
            remove(root->left, p->val);  
        }
    }
}

查找

int get_pre(TreeNode* &root, int x)     // 找前驱 即 val < x
{
    if (!root) return -INF;
    
    if (root->val >= x) return get_pre(root->left, x);
    
    return max(root->val, get_pre(root->right, x));    
}
 
 
 
 
int get_suc(TreeNode* &root, int x)   // 找后继 即 val > x  
{
    if (!root) return INF;
    
    if (root->val <= x) return get_suc(root->right, x); 
    
    // 一直迭代, 直到找到一个 可能的 答案
     
    return min(root->val, get_suc(root->left, x));   
    
    // 如果 root->val > x  右子树肯定不是该节点的对手   查找左子树中是否有遗漏的后继
}

平衡树(算法复杂度——树高)

动态维持 二叉排序树 中序遍历( 保证树高是

左旋 右旋 不改变中序遍历的结果

最小不平衡子树

平衡因子 平衡因子是 任意节点的固有属性

如何构造?

“找、拆、插”,直击痛点

从下往上,找距离最近的一个 平衡因子 大于 1 的 子树(三个节点)

将该子树上 每个节点 挂载的子树 拆开

平衡完三个节点,将拆下来的子树 根据大小 重新挂回去


表达式树

递归构造

前缀编码

编码总长只和各编码长度 有关 和 其所在位置无关

解码(从前往后,一旦匹配就可以译码)是 唯一的,所以前缀编码是 无损压缩数据的

所有编码均对应叶节点,因此 一组编码 就是 一颗二叉树

带权路径长度

前缀编码可以表示为一棵二叉树:

  • 字符对应叶节点
  • 叶节点深度等于编码长度
  • 左右边分别标记 01

可以通过计算二叉树的总权值替代计算编码的

哈夫曼编码

定义: 根据符号出现频率构造哈夫曼树,生成平均码长尽可能短的前缀编码

哈夫曼编码 ⊂ 前缀编码

构造

  1. 将每个字符及其频率作为一个叶节点;

  2. 每次选取频率最小的两个节点合并成新节点,新节点频率为二者之和;

  3. 重复该操作,直到只剩一个根节点,形成哈夫曼树;

  4. 从根到叶的边分别标记为 01,所得路径就是对应字符的哈夫曼编码。

哈夫曼树节点度数一定不为 1

存在一个平均码长最短的解,其中最小的两个节点互为兄弟节点

任意节点的两条边 标记不用遵循 左零右一 的规则,左右标记 不同即可

证明

采用 闫式集合分析法(适用于贪心,dp)可以证明 哈夫曼编码 求出 是最小的

  1. 贪心选择

    设当前权值最小的两个节点为

    在一棵最优树中,可以将权值较小的节点交换到更深的位置, 不会增大。因此,一定存在一个最优解,使 位于最深层且互为兄弟节点。

  1. 问题转化

    合并为一个权值为 的新节点 ,则原来的 个节点问题转化为 个节点的问题。

    设合并前后的树分别为 的深度为 ,插入 后 , 对应深度为 ,则:

  2. 递归求解

    由前两步可知,原问题一定存在一个最优树 ,使得 互为兄弟。将 合并为 后, 唯一对应一棵 个节点的树 ,且:

    下面用反证法证明 必须是 个节点问题的最优解。

    假设 不是最优解,则存在另一棵树 ,使得:

    中的节点 展开为 ,得到原问题的一棵树 ,则:

    这与 是原问题的最优解矛盾,故 必须是 个节点问题的最优解。

    因此,只需递归求解 个节点的问题,再展开 ,即可得到原问题的最优树。不断合并权值最小的两个节点,最终得到的哈夫曼树具有最小

K 进制编码

构造方式同哈夫曼编码,每次选最小的 个树求和,再将原 个树删除,加上新生成的树

,则有 补充零节点 的问题,因为要满足 完全 叉树的定义

补充零节点

完全 叉树中,设叶节点数为 ,内部节点数为 ,对边数计数:

化简得:

因此, 必须能被 整除(其中 ),即:


  1. 不能整除,需补充权值为 0 个虚节点,直到满足:
while ((heap.size() - 1) % (k - 1)) heap.push({0, 0});
  1. 权值为 0 的节点对 WPL 无贡献,因此补零不影响最终编码长度。

  2. 为使最长编码尽量短,零节点应尽量放在最深层,即最先参与合并。由于零是最小权值,堆会自动优先取出零节点,无需额外处理。

应用

合并果子 如何搬运可以最省体力?

代码:

// Problem: 合并果子
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/150/
// Memory Limit: 64 MB
// Time Limit: 1000 ms
 
#include <bits/stdc++.h>
using namespace std;
 
typedef long long LL;
typedef pair<int, int> PII;
const int N = 1e6 + 10;
 
 
signed main() {
    ios::sync_with_stdio(false),  cin.tie(0),  cout.tie(0);
	int n;
	cin >> n;
	
	// 	维护小根堆
	priority_queue <int, vector<int>, greater<int>> pq;
	
	// 输入加排序
	for (int i = 0; i < n; i ++ )
	{
		int ai;
		cin >> ai;
		pq.push(ai);
	}
	LL res = 0;		// LL 必须定义大小
	while (pq.size() - 1)		// 果堆数大于 1 合并
	{
		int first = pq.top();
		pq.pop();
		int second = pq.top();
		pq.pop();
		int merge = first + second;
		res += merge;
		pq.push(merge);	
	}
	cout << res;
    return 0;
}

荷马史诗 进制编码,如何取得 最短码长 (同时 求出最短树高)

每个单词构成一个节点:
(weight, height) = (wi, 0)
 
如果不满足满 k 叉树条件:
    加入若干个 (0, 0)
 
for 循环(重复直到只剩一个节点):
    取出 k 个最小节点
    sum = 这些节点的权值之和
    height = 1 + 这些节点高度的最大值
    答案加上 sum
    将 (sum, height) 放回堆中
 
最终:
    第一个答案 = 最小总码权 WPL
    根节点 height = 所有解中 `最短的` 最长码长

代码:

// Problem: 荷马史诗
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/description/151/
// Memory Limit: 64 MB
// Time Limit: 1000 ms
 
#include <bits/stdc++.h>
using namespace std;
 
typedef long long LL;
typedef pair<LL, int> PLI;
const int N = 1e6 + 10;
 
int n, k;
 
signed main() {
	priority_queue<PLI, vector<PLI>, greater<PLI>> heap; 
	// 小根堆 维护 字典树
	scanf("%d%d", &n, &k);
	
	while (n --) {
		LL w; // 权值大小w 超过 int 可表示范围
		scanf("%lld", &w);
		heap.push({w, 0});
	}
	
	while ((heap.size() - 1) % (k - 1)) heap.push({0, 0}); 
	
	// 补充:权值为 0 的节点
	
	LL ans = 0;
	
	while (heap.size() > 1) {
	    LL sum = 0;
	    int height = 0;
	
	    for (int i = 0; i < k; i++) {
	        auto [weight, h] = heap.top();
	        heap.pop();
	
	        sum += weight;
	        height = max(height, h);
	    }
	
	    ans += sum;
	    heap.push({sum, height + 1});
	}
	
	printf("%lld\n%d", ans, heap.top().second);
	
	return 0;
}

普通哈夫曼只需要比较

这道 叉哈夫曼题需要按以下顺序比较:

因此堆中的节点需要保存 ,并按 进行排序。