剑指offer 学习笔记 树
树的宽度(广度)优先遍历:先访问树的第一层节点,再访问第二层节点,直到最后一层。同一层节点中,从左到右依次访问。
二叉搜索树中,左子节点总是小于等于根节点,右子节点总是大于等于根节点。我们可以平均在O(logn)的时间内根据节点值在二叉搜索树中找到一个节点。
二叉树的特例有堆和红黑树。堆分最大堆和最小堆,最大(小)堆中根节点的值最大(小),很多需要快速找到最值的问题可以用堆来解决。红黑树是把树中的节点定义为红、黑两种颜色,并通过规则确保从根节点到叶节点的最长路径的长度不超过最短路径长度的两倍。STL中,set、multiset、map、multimap等数据结构都是基于红黑树实现的。
面试题7:重建二叉树。输入某二叉树的前序遍历和中序遍历的结果,该二叉树没有重复的节点值,请重建该二叉树,输出头结点。二叉树的节点定义如下:
struct BinaryTreeNode{
int m_nValue;
BinaryTreeNode *m_pLeft;
BinaryTreeNode *m_pRight;
};
在二叉树的前序遍历中,第一个数字总是树的根节点的值。但在中序遍历序列中,根节点的值在序列中间,左子树的节点的值位于根节点的值的左边,而右子树的节点的值位于根节点的值的右边。这样我们就可以找到左右子树的前序遍历序列和中序遍历序列,我们可以用递归完成剩下的部分:
#include <iostream>
using namespace std;
struct BinaryTreeNode {
int m_nValue;
BinaryTreeNode* m_pLeft;
BinaryTreeNode* m_pRight;
};
BinaryTreeNode* ConstructCore(int* startPreorder, int* endPreorder, int* startInorder, int* endInorder) {
BinaryTreeNode* root = new BinaryTreeNode();
root->m_nValue = *startPreorder;
root->m_pLeft = root->m_pRight = nullptr;
if (startPreorder == endPreorder) { // 当递归到前序遍历只含一个元素时
if (startInorder == endInorder && *startPreorder == *startInorder) { // 此时若中序遍历也只含一个元素并且这个元素值与前序遍历的元素值相同时,递归到底成功返回
return root;
} else { // 否则当前序和中序遍历的个数不相等(前序遍历只含一个元素但中序遍历有多个元素)或值不相等(前序遍历和中序遍历元素数都为1但这两个值不等)时
throw exception("Invalid input."); // 说明输入的前序和中序遍历不匹配
}
}
int* rootInorder = startInorder;
while (rootInorder < endInorder && *rootInorder != *startPreorder) { // 遍历中序序列找到根节点
++rootInorder;
}
if (rootInorder == endInorder && *rootInorder != *startPreorder) { // 若以上循环结束仍未找到根节点,说明输入有误
throw exception("Invalid input");
}
int leftLength = rootInorder - startInorder; // 左子树长度
int* leftPreorderEnd = startPreorder + leftLength; // 左子树先序遍历尾边界
if (leftLength > 0) { // 当左子树仍存在时
root->m_pLeft = ConstructCore(startPreorder + 1, leftPreorderEnd, startInorder, rootInorder - 1); // 继续递归左子树
}
if (leftLength < endPreorder - startPreorder) { // 左子树长度小于当前遍历的树的节点数-1(去掉根节点)时,说明存在右子树
root->m_pRight = ConstructCore(startPreorder + leftLength + 1, endPreorder, rootInorder + 1, endInorder); // 继续递归右子树
}
return root;
}
BinaryTreeNode* Construct(int* preorder, int* inorder, int length) {
if (preorder == nullptr || inorder == nullptr || length <= 0) {
return nullptr;
}
return ConstructCore(preorder, preorder + length - 1, inorder, inorder + length - 1);
}
void PreorderPrint(BinaryTreeNode* root) { // 先序遍历打印树验证结果
if (root == nullptr) {
return;
}
cout << root->m_nValue << endl;
PreorderPrint(root->m_pLeft);
PreorderPrint(root->m_pRight);
return;
}
int main() {
int preorder[] = {1,2,4,7,3,5,6,8};
int inorder[] = { 4,7,2,1,5,3,8,6 };
BinaryTreeNode* root = Construct(preorder, inorder, 8);
PreorderPrint(root);
return 0;
}
在剑指offer原书中,这句代码有误:
while (rootInorder <= endInorder && rootInorder != startPreorder) { // 遍历中序序列找到根节点
++rootInorder;
}
if (rootInorder == endInorder && *rootInorder != *startPreorder) { // 若以上循环结束仍未找到根节点,说明输入有误
throw exception("Invalid input");
}
错误在于,当while循环条件部分符号为小于等于时,若中序遍历中未找到根节点,rootInorder会指向endInorder之后,造成下一句中的if条件部分永远为false。
面试题8:二叉树的下一个节点。给定一棵二叉树和其中一个节点,如何找出中序遍历序列的下一个节点?树的节点结构如下:
struct BinaryTreeNode{
char m_nValue;
BinaryTreeNode *m_pLeft;
BinaryTreeNode *m_pRight;
BinaryTreeNode *m_pParent;
};
如果一个节点有右子树,那么它中序遍历的下一个节点就是它右子树中的最左子节点,即,从右子树出发一直沿着指向左子节点的指针,就能找到它中序遍历的下一个节点。
如果一个节点没有右子树,并且这个节点是左节点,那么它的中序遍历的下一个节点是它的父节点。
如果一个节点没有右子树,并且这个节点是右节点,我们可以沿着指向父节点的指针向上遍历,直到找到一个左节点,这个左节点的父节点就是它中序遍历的下一个节点。
假如二叉树如下图所示(将a换为1,b换为2,以此类推):
完整代码如下:
#include <iostream>
using namespace std;
struct BinaryTreeNode {
int m_nValue;
BinaryTreeNode* m_pLeft;
BinaryTreeNode* m_pRight;
BinaryTreeNode* m_pParent;
};
BinaryTreeNode* ConstructCore(int* startPreorder, int* endPreorder, int* startInorder, int* endInorder) {
BinaryTreeNode* root = new BinaryTreeNode();
root->m_nValue = *startPreorder;
root->m_pLeft = root->m_pRight = nullptr;
if (startPreorder == endPreorder) { // 当递归到前序遍历只含一个元素时
if (startInorder == endInorder && *startPreorder == *startInorder) { // 此时若中序遍历也只含一个元素并且这个元素值与前序遍历的元素值相同时,递归到底成功返回
return root;
} else { // 否则当前序和中序遍历的个数不相等(前序遍历只含一个元素但中序遍历有多个元素)或值不相等(前序遍历和中序遍历元素数都为1但这两个值不等)时
throw exception("Invalid input."); // 说明输入的前序和中序遍历不匹配
}
}
int* rootInorder = startInorder;
while (rootInorder < endInorder && *rootInorder != *startPreorder) { // 遍历中序序列找到根节点
++rootInorder;
}
if (rootInorder == endInorder && *rootInorder != *startPreorder) { // 若以上循环结束仍未找到根节点,说明输入有误
throw exception("Invalid input");
}
int leftLength = rootInorder - startInorder; // 左子树长度
int* leftPreorderEnd = startPreorder + leftLength; // 左子树先序遍历尾边界
if (leftLength > 0) { // 当左子树仍存在时
root->m_pLeft = ConstructCore(startPreorder + 1, leftPreorderEnd, startInorder, rootInorder - 1); // 继续递归左子树
}
if (leftLength < endPreorder - startPreorder) { // 左子树长度小于当前遍历的树的节点数-1(去掉根节点)时,说明存在右子树
root->m_pRight = ConstructCore(startPreorder + leftLength + 1, endPreorder, rootInorder + 1, endInorder); // 继续递归右子树
}
return root;
}
BinaryTreeNode* Construct(int* preorder, int* inorder, int length) {
if (preorder == nullptr || inorder == nullptr || length <= 0) {
return nullptr;
}
return ConstructCore(preorder, preorder + length - 1, inorder, inorder + length - 1);
}
BinaryTreeNode* GetNextInorder(BinaryTreeNode* pNode) { // 找到中序遍历下一个节点,若输入的已经是中序遍历最后一个节点,返回空指针
if (pNode == nullptr) {
return nullptr;
}
BinaryTreeNode* pNext = nullptr; // 用来指向输入节点中序遍历的下一个节点
if (pNode->m_pRight != nullptr) { // 当输入节点有右子树,中序遍历下一节点即为输入节点右子树的最左子节点
BinaryTreeNode* pRNode = pNode->m_pRight;
while (pRNode->m_pLeft != nullptr) { // 遍历找到该节点的最左子节点
pRNode = pRNode->m_pLeft;
}
pNext = pRNode;
} else if (pNode->m_pParent != nullptr) { // 当输入节点没有右子树但有父节点时
BinaryTreeNode* pCurrent = pNode, *pCurrentParent = pCurrent->m_pParent;
while (pCurrentParent != nullptr && pCurrent != pCurrentParent->m_pLeft) { // 当pCurrent节点的父节点不为空且pCurrent不是一个左节点时
pCurrent = pCurrentParent;
pCurrentParent = pCurrentParent->m_pParent; // 向上迭代
}
pNext = pCurrentParent; // 当循环结束时,要么找到了一个是左节点的父节点(此处包含了当输入节点没有右
// 子树时,输入节点是左节点和输入节点是右节点两种情况),要么父节点为空,意味着输入节点沿着父节点向上的所有节点都是右节点,
// 而右节点是中序遍历的最后一个节点,即输入节点是整棵树中序遍历的最后一个节点
}
return pNext; // 输入节点既没有右节点,其父节点也不存在时,即输入节点是根节点,并且这棵树只含左子树。那么它就是中序遍历的最后一个节点
}
void ConnectParent(BinaryTreeNode* root) { // 添加二叉树的父指针
if (root == nullptr) {
return;
}
if (root->m_pLeft != nullptr) {
root->m_pLeft->m_pParent = root;
ConnectParent(root->m_pLeft);
}
if (root->m_pRight != nullptr) {
root->m_pRight->m_pParent = root;
ConnectParent(root->m_pRight);
}
}
int main() {
int inorder[] = { 4,2,8,5,9,1,6,3,7 }; // 上图二叉树的中序遍历
int preorder[] = {1,2,4,5,8,9,3,6,7}; // 上图二叉树的前序遍历
BinaryTreeNode* root = Construct(preorder, inorder, 9); // 构建二叉树,但不带父指针
ConnectParent(root); // 添加二叉树的父指针
if (BinaryTreeNode* pNode = GetNextInorder(root->m_pRight->m_pRight)) { // 输入任意节点,找到中序遍历顺序下的下一节点
cout << pNode->m_nValue << endl;
} else {
cout << "已经是中序遍历的最后一个节点。" << endl;
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)