一,递归序和递归理解
#include<iostream>
//递归的基础样例,用来理解递归。
struct TreeNode
{
int value;
TreeNode* left;
TreeNode* right;
};
void f(TreeNode* root)
{
if (root == NULL) return;
//1
//当到达该节点时的操作。
f(root->left);
//去遍历根节点的左子树。
//2
//遍历完左子树后进行的操作
f(root->right);
//3
//遍历完右子树后进行的操作。
//每个节点会被访问三次,两次是返回时访问的。
//理解递归,调用函数本身,一定有某一时刻会返回调用处。
}
这是在B站左神的视频。
1,三种遍历树的方式
先序,中序和后序遍历。规律就是都是先遍历左子树,再遍历右子树。根据本身(中序)访问的次序分的类,如先,中,后,就叫作中序遍历。
也可以这样理解:根据第几次到达节点进行操作来分类,第一次到达就进行操作就叫作先序遍历。
二,二叉树的基础知识
二叉树(Binary Tree)是数据结构中的一种树形结构,其中每个节点最多有两个子节点,通常分别称为 左子节点(left child)和 右子节点(right child)。二叉树是一种非常重要且广泛应用的数据结构,在许多算法和问题的解决中都有重要作用。
1. 二叉树的基本概念
- 节点:二叉树的每一个元素都被称为节点(Node)。每个节点包含一个值(或数据),以及指向其左右子节点的指针。
- 根节点(Root):二叉树的最顶层节点,树的起点,通常是整个二叉树的访问起点。
- 子节点:根节点或其他节点的下级节点。
- 左子节点(Left Child):节点的左子树。
- 右子节点(Right Child):节点的右子树。
- 叶子节点(Leaf Node):没有子节点的节点,通常是树的末端。
- 父节点:每个节点的直接上级节点。
- 深度:节点到根节点的路径长度。
- 高度:从节点到叶子节点的最长路径长度。
2. 二叉树的类型
- 满二叉树(Full Binary Tree):每个节点要么是叶子节点,要么有两个子节点。
- 完全二叉树(Complete Binary Tree):除了最后一层外,其他层的节点都被填满,且最后一层的节点从左到右依次填充。
- 平衡二叉树(Balanced Binary Tree):左右子树的高度差不超过 1 的二叉树。例如 AVL 树、红黑树。
- 二叉搜索树(Binary Search Tree, BST):对于树中的每个节点,左子树所有节点的值小于当前节点的值,右子树所有节点的值大于当前节点的值。
3. 二叉树的遍历
二叉树遍历是访问树中所有节点的过程,常见的遍历方法有:
- 前序遍历(Pre-order Traversal):根节点 → 左子树 → 右子树
- 中序遍历(In-order Traversal):左子树 → 根节点 → 右子树
- 后序遍历(Post-order Traversal):左子树 → 右子树 → 根节点
- 层序遍历(Level-order Traversal):逐层从上到下,从左到右访问所有节点,通常使用队列实现。
4. 二叉树的性质
- 节点数:二叉树中,深度为
d
的完全二叉树最多有2^d - 1
个节点。 - 高度与节点数:一个二叉树的高度(或深度)与节点数有关系。例如,完全二叉树的高度
h
与节点数n
的关系是h = log2(n + 1)
。 - 二叉树的最大节点数:在给定高度
h
下,二叉树最多有2^h - 1
个节点。
5. 二叉树的应用
二叉树是许多复杂数据结构和算法的基础,具有广泛的应用,常见的应用包括:
- 二叉搜索树(BST)用来实现快速查找、插入和删除操作。
- 堆(Heap):一种完全二叉树,通常用于实现优先队列。
- 表达式树:用于表示数学表达式,支持高效的运算。
- 红黑树、AVL 树等平衡二叉树用于实现高效的排序和查找。
- 决策树:用于机器学习中的分类和回归问题。
6. 二叉树的实现
常见的二叉树通常使用 链式存储 方式来实现。每个节点通常包含:
- 数据(值)
- 左子节点指针
- 右子节点指针
一个简单的二叉树节点定义如下(用 C++ 实现):
struct TreeNode {
int value; // 节点值
TreeNode* left; // 左子节点指针
TreeNode* right; // 右子节点指针
TreeNode(int x) : value(x), left(nullptr), right(nullptr) {}
};
7. 复杂度分析
- 查找、插入、删除:在最坏情况下,如果树的高度接近
n
,这些操作的时间复杂度为O(n)
。在平衡二叉树中,最坏情况下的时间复杂度为O(log n)
。 - 遍历:所有遍历方法的时间复杂度都是
O(n)
,其中n
是树中的节点数。
总结
二叉树是计算机科学中的一种基础数据结构,广泛应用于各种算法和数据结构中。通过合理的遍历、搜索与更新操作,二叉树在查找、排序、表达式解析、优先队列等多种场景中都扮演着重要角色。
二叉树的插入和遍历
第一次尝试:
#include<iostream>
using namespace std;
struct TreeNode
{
int value;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : value(x),left(NULL),right(NULL){}
};
TreeNode* create_Node(int);
void Tree_insert(TreeNode*, int);
void preOrder_traverse(TreeNode*);
int main()
{
TreeNode* root = create_Node(0);
for (int i = 1; i < 5; i++)
{
int j = i * i;
int k = 2 * i;
Tree_insert(root, j);
Tree_insert(root, k);
}
preOrder_traverse(root);
return 0;
}
TreeNode* create_Node(int x)
{
TreeNode* node = new TreeNode(x);
return node;
}
void Tree_insert(TreeNode* root, int x)
{
TreeNode* current = root;
TreeNode* prev = root;
while (current != NULL)
{
if (x < current->value)
{
prev = current;
current = current->left;
}
else if (x = current->value);
else
{
prev = current;
current = current->right;
}
}
if (x < prev->value) prev->left = create_Node(x);
else prev->right = create_Node(x);
}
void preOrder_traverse(TreeNode* current)
{
//具体函数里面操作自己设置
if (current == NULL) return;
cout << current->value << " ";
preOrder_traverse(current->left);
preOrder_traverse(current->right);
//至于中序,后序就是将访问该节点的操作放到访问完左节点后面和右节点后面。
}
改进:
指针传递 vs 引用传递
void insert(TreeNode* root) {
root = new TreeNode(5); // 修改的是root的副本
}
int main() {
TreeNode* root = nullptr;
insert(root); // root没有被修改
// root仍然是nullptr
}
使用引用传递(按引用传递指针
void insert(TreeNode*& root) { // 使用引用传递
root = new TreeNode(5); // 修改的是root的引用,实际修改了外部root
}
int main() {
TreeNode* root = nullptr;
insert(root); // root被修改为新的节点
// root现在指向一个新创建的节点
}
为什么推荐使用引用传递:
- 正确修改根节点:在二叉树插入时,特别是当树为空时(根节点是
nullptr
),你希望在insert()
函数内部修改根节点的指向。通过引用传递,你可以直接修改根节点,并且修改会反映到外部。 - 避免副本问题:使用按值传递(指针的副本)会导致对指针的修改没有影响,从而无法正确操作树的结构。
结论:
- 按值传递指针:在函数内部修改指针时,不会影响外部的指针。对于需要修改外部指针(如根节点)的操作,按值传递不合适。
- 按引用传递指针:能够确保在函数内部修改指针时,修改会反映到外部,因此更加适用于需要修改根节点或其他外部指针的场景。
因此,在处理二叉树插入时,推荐使用引用传递来确保修改能够正确地反映到外部变量中。
补充:
TreeNode*& root
是 C++ 中对指针的引用传递的写法。这种语法结合了指针(*
)和引用(&
),它的含义是:传递一个指向 TreeNode
类型对象的指针,并且该指针是通过引用传递的。这意味着在函数内部可以修改指针本身的值,并且这种修改会影响到外部的变量。
1. *
(指针)与 &
(引用)的组合
-
*
表示指针,指针是一个变量,它保存的是另一个变量的地址。在TreeNode* root
中,root
是一个指向TreeNode
类型对象的指针。 -
&
表示引用,引用是一种别名,它让你能够直接操作原始变量而不是它的副本。通过引用传递时,函数内部对变量的修改会反映到外部。
2. 为什么 *
和 &
可以连用?
结合 *
和 &
使用时,意味着我们传递的是指针的引用。这两者结合起来,实际上是实现了按引用传递指针。具体来说,TreeNode*& root
表示:
root
是一个指向TreeNode
的指针(TreeNode*
)。&
表示引用,意味着我们将传递指针的引用。这样,在函数内部对root
的修改会直接影响外部的root
。
第二次
void Tree_insert(TreeNode*& root, int x)
{
TreeNode* current = root;
TreeNode* prev = root;
if (root == NULL)
{
root = new TreeNode(x);
return;
}
while (current != NULL)
{
if (x < current->value)
{
prev = current;
current = current->left;
}
else if (x == current->value) return;
else
{
prev = current;
current = current->right;
}
}
if (x < prev->value) prev->left = create_Node(x);
else prev->right = create_Node(x);
}