B站左神算法课学习笔记(P5+P6):二叉树

🥳🥳PS:此篇开头的链表习题补充在了P4的末尾!!🥳🥳

目录

一、二叉树的节点结构:

二、树的递归序以及递归遍历

三、二叉树的非递归遍历

1. 先序遍历的非递归实现:

2. 后序遍历的非递归实现:

3. 中序遍历的非递归实现:

例:

四、层序遍历(宽度遍历)

五、经典问题

1、如何判断搜索二叉树?

2、如何判断完全二叉树?

3、如何判断满二叉树?

六*、二叉树递归套路(树形DP)

七、其他题型

1、最低公共祖先

2、后继节点

3、序列化与反序列化

4、折纸面试题(微软)


一、二叉树的节点结构:

// 定义二叉树节点结构
typedef struct TreeNode {
    int value; // 节点值
    struct TreeNode* left; // 指向左子节点的指针
    struct TreeNode* right; // 指向右子节点的指针
} TreeNode;

Q:

1、用递归和非递归的方式实现二叉树的先序、中序、后续遍历

2、如何直观地打印一颗二叉树

3、如何完成二叉树的宽度优先搜索(常见题目:求一颗二叉树的宽度)

二、树的递归序以及递归遍历

如下图所示,在递归遍历中,二叉树会有如下的1、2、3次机会回到自身的函数中。我们可以在其中穿插打印/求和等一系列操作以实现对于树结构的使用。

 按照“递归序”来理解前序、中序、后序遍历:

三、二叉树的非递归遍历

本质上说,递归函数自动实行了压栈、弹栈功能,如果自己实现,即可达到同样的效果,这样更有利于我们理解递归的过程:

1. 先序遍历的非递归实现:

2. 后序遍历的非递归实现:

3. 中序遍历的非递归实现:

网上的非递归实现二叉树的动画讲解很多,大家可以看看这两个:
二分搜索树 前中后序(递归和非递归)和层序遍历(动图)
二叉树遍历-先序(非递归)【图解+代码】_哔哩哔哩_bilibili

下面我们再来看一个例子,动手理解下面树的非递归中序遍历:

按照上述方法,对下面的树,画图实现非递归中序遍历:

大家可以参考我绘制的过程:

四、层序遍历(宽度遍历)

步骤:

1)维护一个队列;

2)对于一颗树,先将头节点放入;

3)弹出队列中节点,并将其左节点、右节点依次放入(若有);

4)重复2~3,直到队列为空;

代码实现(c++):

// 层序遍历函数
void levelOrderTraversal(TreeNode* root) {
    if (root == nullptr) return;  // 如果树为空,直接返回

    std::queue<TreeNode*> q;  // 创建一个队列
    q.push(root);             // 将根节点入队

    while (!q.empty()) {
        TreeNode* current = q.front(); // 访问队头节点
        q.pop();                      // 出队
        std::cout << current->value << " "; // 输出节点值

        // 将当前节点的左子节点和右子节点入队
        if (current->left != nullptr) {
            q.push(current->left);
        }
        if (current->right != nullptr) {
            q.push(current->right);
        }
    }
}

变体例题:求一颗二叉树的宽度?

tips:这里并未按照原视频设置那么多变量,而是采用“BFS+队列长度“来对于二叉树进行判断!

  • 时间复杂度:O(n),其中 n 是二叉树的节点数。我们遍历每个节点一次。
  • 空间复杂度:O(n),队列中最多会存储二叉树的一层节点,即最宽的一层的节点数。在最坏的情况下,队列的最大容量为树的宽度。
struct TreeNode {
    int value;
    TreeNode* left;
    TreeNode* right;

    TreeNode(int val) : value(val), left(nullptr), right(nullptr) {}
};
// 计算二叉树的宽度
int calculateTreeWidth(TreeNode* root) {
    if (root == nullptr) return 0;  // 如果树为空,宽度为 0

    std::queue<TreeNode*> q;  // 使用队列进行层序遍历
    q.push(root);  // 将根节点入队

    int maxWidth = 0;  // 记录最大宽度

    // 层序遍历
    while (!q.empty()) {
        int levelSize = q.size();  // 当前层的节点数
        maxWidth = std::max(maxWidth, levelSize);  // 更新最大宽度

        // 遍历当前层的所有节点
        for (int i = 0; i < levelSize; ++i) {
            TreeNode* current = q.front();
            q.pop();

            // 将左子节点和右子节点入队
            if (current->left != nullptr) {
                q.push(current->left);
            }
            if (current->right != nullptr) {
                q.push(current->right);
            }
        }
    }

    return maxWidth;  // 返回最大宽度
}
int main() {
    // 创建一个简单的二叉树
    TreeNode* root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->left = new TreeNode(4);
    root->left->right = new TreeNode(5);
    root->right->right = new TreeNode(6);
    // 计算并输出二叉树的最大宽度
    std::cout << "Maximum width of the tree: " << calculateTreeWidth(root) << std::endl;
    return 0;
}

重要!看代码注意用纸笔画图理解!硬看需要很多经验,新手最好一步一个脚印!

五、经典问题

1、如何判断搜索二叉树?

搜索二叉树:左子树的值总有 << 右子树;

判断:联想到中序遍历,输出中序遍历结果,若是升序,则是搜索二叉树!

// 中序遍历判断是否是搜索二叉树
bool inOrderTraversal(TreeNode* root, int& prev) {
    if (root == nullptr) return true;

    // 先遍历左子树
    if (!inOrderTraversal(root->left, prev)) {
        return false;
    }

    // 检查当前节点值是否大于前一个节点值
    if (root->value <= prev) {
        return false;
    }

    // 更新前一个节点的值为当前节点值
    prev = root->value;

    // 再遍历右子树
    return inOrderTraversal(root->right, prev);
}

// 检查树是否为搜索二叉树
bool isBinarySearchTree(TreeNode* root) {
    int prev = INT_MIN;  // 用于记录前一个节点的值
    return inOrderTraversal(root, prev);
}

2、如何判断完全二叉树?

完全二叉树:除最后一层,其余层都是满的;且最后一层从左到右依次填充。

判断:基于BFS——(1)对任意节点,若其有右节点但无左节点,return false;

                                (2)在(1)的前提下,从遇到第一个左右节点不全的节点开始,后续只能是

                                         叶子节点;

完全二叉树的判断
// 检查是否为完全二叉树
bool isCompleteBinaryTree(TreeNode* root) {
    if (root == nullptr) return true;

    std::queue<TreeNode*> q;
    q.push(root);
    bool leaf = false;  // 标记出现叶子节点

    while (!q.empty()) {
        TreeNode* current = q.pop();
		l = current->left;
		r = current->right;
		
		if(
            // 左右节点不全且有孩子(违反条件2)
			(leaf && (l || r))
			||
			(!l && r)    // 无左节点,却有右节点(违反条件1)
		  ){
		  	return false;
		  }

        // 如果当前节点是非叶节点,继续入队
        if (l) {
            q.push(l);
        }
        if (r) {
            q.push(r);
        } 
        if(l || r){
        	leaf = true;
		}
    }

    return true;  // 遍历完整棵树且未违反条件
}

3、如何判断满二叉树?

满二叉树:若树的最大深度为L,节点数为N,则当且仅当满足 N = 2^L -1 时,为满二叉树。

思路:求树高和节点数,验证等式是否成立。 

// 计算树的高度
int calculateHeight(TreeNode* root) {
    int height = 0;
    while (root) {
        height++;
        root = root->left; // 一直走左子树,直到最深的叶子节点
    }
    return height;
}

// 计算树的节点数
int countNodes(TreeNode* root) {
    if (!root) return 0;
    queue<TreeNode*> q;
    q.push(root);
    int nodeCount = 0;
    
    while (!q.empty()) {
        TreeNode* current = q.front();
        q.pop();
        nodeCount++;
        
        if (current->left) q.push(current->left);
        if (current->right) q.push(current->right);
    }
    
    return nodeCount;
}

// 判断是否为满二叉树
bool isFullBinaryTree(TreeNode* root) {
    if (!root) return true;  // 空树也是满二叉树

    int height = calculateHeight(root);
    // 计算理论上的节点数 N = 2^L - 1
    int expectedNodes = (1 << height) - 1;
    int actualNodes = countNodes(root);

    // 判断节点数是否相等
    return expectedNodes == actualNodes;
}

六*、二叉树递归套路(树形DP)

当可以向左树右树要信息的时候,需要什么信息,可以达成目的?

平衡二叉树:对于任意子树,其左树和右树的高度差不超过1。

此处以平衡二叉树为例,需同时满足以下三个条件:

(1)左树为平衡二叉树;

(2)右树为平衡二叉树;

(3)|h左 - h右| <= 1;

整理可得:左树需得到信息——是否平衡、高度是多少;右树也需得到信息——是否平衡、高度是多少。故可确定返回值的结构体为:是否是平衡二叉树 + 树高。

代码实现:

#include <bits/stdc++.h>
#include <iostream>
#include <cmath>
using namespace std;

struct Node{
	int val;
	Node* left;
	Node* right;
	//	记得写构造函数!	 
	Node(int value) : val(value), left(nullptr), right(nullptr) {}
};

struct ReturnType{
	bool isB;
	int ht;
};

ReturnType isBST(Node* root, int h){
	if(root == NULL){
		return ReturnType{true, 0};
	}
	
	ReturnType l = isBST(root->left, h);
	ReturnType r = isBST(root->right, h);
	
	bool isBalanced = l.isB && r.isB && (abs(l.ht - r.ht) <= 1);
	
	int ht=max(l.ht, r.ht)+1;
	
	return ReturnType{isBalanced, ht};
}


int main()
{
	// 创建一棵平衡二叉树
    Node* root = new Node(1);
    root->left = new Node(2);
    root->right = new Node(3);
    root->left->left = new Node(4);
    root->left->right = new Node(5);
    root->right->left = new Node(6);

    // 判断是否为平衡二叉树
    if (isBST(root, 0).isB) {
        cout << "The tree is balanced." << endl;
    } else {
        cout << "The tree is NOT balanced." << endl;
    }
}

搜索二叉树同理:从向左右子树要信息的角度思考,需满足如下条件:

  1. 左树是搜索二叉树;
  2. 右树是搜索二叉树;
  3. 左max < root;
  4. 右min > root;

整理可得(取并集),返回的值:是否是搜索二叉树 + min + max。

代码:

#include <bits/stdc++.h>
#include <cmath>
#include <iostream>
using namespace std;

struct Node{
	int val;
	Node* left;
	Node* right;
	Node(int v): val(v), left(nullptr), right(nullptr)	{}
};

struct ReturnType{
	bool isST;
	int min;
	int max;
};

ReturnType process(Node* root){
	if(root == NULL){
		return ReturnType{true, INT_MAX, INT_MIN};	// 注意叶节点初始化为无穷以免影响判断 
	}
	
	ReturnType l = process(root->left);
	ReturnType r = process(root->right);
	
	bool isSearchTree = l.isST && r.isST && (l.max < root->val) && (r.min > root->val);
	
	// 全局最大 / 最小值 	
	int min1 = min(root->val, l.min);
	min1 = min(min1, r.min);
	int max1 = max(root->val, l.max);
	max1 = max(max1, r.max);
	
	return ReturnType{isSearchTree, min1, max1};
}

int main()
{
	// 创建一棵二叉树
    Node* root = new Node(10);
    root->left = new Node(8);
    root->right = new Node(12);
    root->left->left = new Node(4);
    root->left->right = new Node(9);
    root->right->left = new Node(11);
    
    // 判断是否为搜索二叉树
    ReturnType result = process(root);
    cout << "Is it a search binary tree? " << (result.isST ? "Yes" : "No") << endl;
    
	return 0;
}

同理,使用套路解满二叉树的判断

需要满足的条件:

左右子树都是满二叉树;

返回值:树的深度,树的节点个数;

代码实现:

#include <bits/stdc++.h>
#include <iostream>
using namespace std;

struct Node{
	int val;
	Node* left;
	Node* right;
	Node(int v): val(v), left(nullptr), right(nullptr) {}
};

struct Info{
	int height;
	int nodeSum;
};

Info process(Node* root){
	if(root == NULL){
		return Info{0, 0};
	}
	
	Info inLeft = process(root->left);
	Info inRight = process(root->right);
	
	int h = max(inLeft.height, inRight.height)+1;
	int nodeSum = inLeft.nodeSum + inRight.nodeSum + 1;
	
	return Info{h, nodeSum};
}

int main()
{
	Node* root = new Node(10);
    root->left = new Node(8);
    root->right = new Node(12);
    root->left->left = new Node(4);
    root->left->right = new Node(9);
    root->right->left = new Node(11);
    root->right->right = new Node(2);
    
    Info result = process(root);
    cout<< (result.nodeSum == (1 << result.height)-1 ? "Yes":"no");
	return 0;
}

“套路”仅仅适用于大多数题目!

反例:求树上数据的中位数 -> 局部中位数无法推出全局中位数,故不可行!

七、其他题型

1、最低公共祖先

Q:给定两个在同一棵树上的二叉树节点node1和node2,找到他们的最低公共祖先;

最低公共祖先示例

思路

(1)定义集合set保存二叉树中所有的连接关系;

(2)接着定义集合set1,根据set中的信息,从o1节点往上找,找出从o1到根节点的路径;

(3)同理,让o2根据set中的信息往上找,当o2达到的节点包含在集合set1中时,该节点就是他们的最低公共祖先。

优化思路:先分类讨论,本题中共两种情况(分别为下图的黄色标记和蓝色标记):

                Case1:o1、o2在同一条路径上(o1是o2的lca或者o2是o1的lca);

                Case2:o1、o2不在同一条路径上(o1与o2不互为lca);

代码:

上述代码经过了多轮优化,所以直接看看不懂很正常!!

关键点:第二个 if 仅针对case2成立(汇聚点);按递归序扫描时,遇到o1或o2就会一直返回。

2、后继节点

Q:从结构上寻找后继节点(中序遍历中,某节点的下一个节点叫做后继节点);

 关键点:由于给出了parent指针,故需要根据结构进行优化;

分类讨论:1)该节点有右树;2)该节点无右树

代码实现:

注意,getSuccessorNode函数中,else分支中同时考虑了节点无右树的两种情况!

3、序列化与反序列化

序列化反序列化是将数据结构(在本例中是二叉树)转换为可存储或传输的格式的过程,并能够恢复其原始结构的技术。具体来说:

  1. 序列化:将二叉树转化为一个可以存储(如存入文件)或传输(如通过网络传输)的格式,通常是字符串或数组。【序列化后的数据结构应该包含足够的信息,以便在反序列化时能够恢复原来的二叉树结构。】

  2. 反序列化:从序列化的格式中恢复出原始的二叉树结构。

  • 序列化时,我们可以使用前序遍历(根-左-右)或后序遍历(左-右-根)等遍历方法来对二叉树进行遍历,并将遍历结果转化为字符串。例如,我们可以用“NULL”标识空节点,使用逗号分隔节点值。

  • 反序列化时,我们通过序列化时得到的字符串恢复出二叉树的结构。根据序列化时的规则逐步重建树的每个节点。

代码:

#include <bits/stdc++.h>
using namespace std;

// 定义二叉树节点结构体
struct Node {
    int val;
    Node* left;
    Node* right;
    Node(int v) : val(v), left(nullptr), right(nullptr) {}
};

// 序列化二叉树的函数
string serialize(Node* root) {
    if (root == nullptr) {
        return "NULL,";  // 使用"NULL"标记空节点
    }
    return to_string(root->val) + "," + serialize(root->left) + serialize(root->right);
}

// 反序列化二叉树的函数
Node* deserializeHelper(istringstream& ss) {
    string val;
    getline(ss, val, ',');  // 读取以逗号分隔的节点值

    if (val == "NULL") {
        return nullptr;  // 如果是空节点,返回nullptr
    }

    Node* node = new Node(stoi(val));  // 创建当前节点
    node->left = deserializeHelper(ss);  // 递归反序列化左子树
    node->right = deserializeHelper(ss);  // 递归反序列化右子树

    return node;
}

Node* deserialize(const string& data) {
    istringstream ss(data);
    return deserializeHelper(ss);
}

// 用于打印树的前序遍历
void preorder(Node* root) {
    if (root == nullptr) return;
    cout << root->val << " ";
    preorder(root->left);
    preorder(root->right);
}

int main() {
    // 构造一棵示例二叉树
    Node* root = new Node(10);
    root->left = new Node(8);
    root->right = new Node(12);
    root->left->left = new Node(4);
    root->left->right = new Node(9);
    root->right->left = new Node(11);
    root->right->right = new Node(14);
    
    cout << "Preorder traversal of the tree: ";
    preorder(root);
    cout << endl;

    // 序列化二叉树
    string serialized = serialize(root);
    cout << "Serialized tree: " << serialized << endl;

    // 反序列化二叉树
    Node* deserializedRoot = deserialize(serialized);
    cout << "Preorder traversal of deserialized tree: ";
    preorder(deserializedRoot);
    cout << endl;

    return 0;
}

4、折纸面试题(微软)

动手实践后可知,可以抽象为二叉树结构:

代码实现:(二叉树模拟+中序遍历)

上述代码空间复杂度 O(N) 级别,直接计算长度为 2^(N-1) 为 O(2^N) 级别!节省了很多的空间!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值