代码随想录中的二叉树

一、⼆叉树的种类

1、满⼆叉树:如果⼀棵⼆叉树只有度为0的结点和度为2的结点,并且度为0的结点在同⼀层上,则这棵⼆叉树为满⼆叉树。
2、完全⼆叉树:在完全⼆叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最⼤值,并且最下⾯⼀层的节点都集中在该层最左边的若⼲位置。若最底层为第 h 层,则该层包含 1~ 2^h -1个节点。

1、优先级队列其实是⼀个堆,堆就是⼀棵完全⼆叉树,同时保证⽗⼦节点的顺序关系。
最大(小)堆是指在树中,存在一个结点而且该结点有儿子结点,该结点的data域值都不小于(大于)其儿子结点的data域值,并且它是一个完全二叉树(不是满二叉树)。注意区分选择树,因为选择树(selection tree)概念和最小堆有些类似,他们都有一个特点是“树中的根结点都表示树中的最小元素结点”。同理最大堆的根结点是树中元素最大的。

2、完全⼆叉树只有两种情况,情况⼀:就是满⼆叉树,情况⼆:最后⼀层叶⼦节点没有满。

3、⼆叉搜索树:⼆叉搜索树是⼀个有序树
若它的左⼦树不空,则左⼦树上所有结点的值均⼩于它的根结点的值;
若它的右⼦树不空,则右⼦树上所有结点的值均⼤于它的根结点的值;
它的左、右⼦树也分别为⼆叉排序树
4、平衡⼆叉搜索树(AVL):它是⼀棵空树或它的左右两个⼦树的⾼度差的绝对值不超过1,并且左右两个⼦树都是⼀棵平衡⼆叉树

1、C++中map、set、multimap、multiset的底层实现都是平衡⼆叉搜索树,所以map、set的增删操作时间复杂度是 log(n) ,注意这⾥没有说unordered_map、unordered_set,unordered_map、unordered_map底层实现是哈希表。
2、二叉树节点的高度与深度:
⼆叉树节点的深度:指从根节点到该节点的最⻓简单路径边的条数
⼆叉树节点的⾼度:指从该节点到叶⼦节点的最⻓简单路径边的条数
3、注意:⼆叉搜索树中不能有重复元素。除非题目有明确指定,不然默认是不能有重复元素的。
4、中序遍历下,输出的⼆叉搜索树节点的数值是有序序列
5、C++中如果使⽤std::map或者std::multimap可以对key排序,但不能对value排序在这里插入图片描述

二、⼆叉树的存储⽅式

1、链式存储⽅式:链式存储通过指针把分布在散落在各个地址的节点串联⼀起。——⽤指针(左指针,右指针)

struct TreeNode{
	int val;
	TreeNode *left;
	TreeNode *right;
	TreeNode(int x):val(x),left(NULL),right(NULL){}	// 构造函数
};

2、顺序存储的⽅式:顺序存储的元素在内存是连续分布的。——⽤数组(如果⽗节点的数组下表是i,那么它的左孩⼦就是i * 2 + 1,右孩⼦就是 i * 2 + 2)。

⽤链式表示的⼆叉树,更有利于我们理解,所以⼀般我们都是⽤链式存储⼆叉树
所以⼤家要了解,⽤数组依然可以表示⼆叉树

三、⼆叉树的遍历⽅式

1、深度优先遍历:
前序遍历:中左右(递归法,迭代法
中序遍历:左中右(递归法,迭代法)
后序遍历:左右中(递归法,迭代法)
2、⼴度优先遍历:
层次遍历(迭代法)

(1)栈其实就是递归的⼀种实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使⽤⾮递归的⽅式来实现的。
(2)⼴度优先遍历的实现⼀般使⽤队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能⼀层⼀层的来遍历⼆叉树。

四、⼆叉树的遍历实现

1、递归的三要素:
(1)确定递归函数的参数返回值
确定哪些参数是递归的过程中需要处理的,那么就在递归函数⾥加上这个参数, 并且还要明确每次递归的返回值是什么进⽽确定递归函数的返回类型
(2) 确定终⽌条件
写完了递归算法,运⾏的时候,经常会遇到栈溢出的错误,就是没写终⽌条件或者终⽌条件写的不对,操作系统也是⽤⼀个栈的结构来保存每⼀层递归的信息,如果递归没有终⽌,操作系统的内存栈必然就会溢出。
(3)确定单层递归的逻辑
确定每⼀层递归需要处理的信息。在这⾥也就会重复调⽤⾃⼰来实现递归的过程。

递归的实现就是:每⼀次递归调⽤都会把函数的局部变量、参数值和返回地址等压⼊调⽤栈中,然后递归返回的时候,从栈顶弹出上⼀次递归的各项参数,所以这就是递归为什么可以返回上⼀层位置的原因。

2、三种方法实现二叉树的前、中、后序遍历
(1)递归法
前序遍历:

class Solution {
public:
	 void traversal(TreeNode* cur, vector<int>& vec) {
	 	// 终止条件
		if (cur == NULL) return;
		
		// 单层递归的逻辑
		vec.push_back(cur->val); 	// 中
		traversal(cur->left, vec); 	// 左
		traversal(cur->right, vec); // 右
	 }
	 vector<int> preorderTraversal(TreeNode* root) {
		 vector<int> result;
		 traversal(root, result);
		 return result;
	 }
};

中序遍历:

void traversal(TreeNode* cur, vector<int>& vec) {
	// 终止条件
	if (cur == NULL) return;
		
	// 单层递归的逻辑
	traversal(cur->left, vec); 	// 左
	vec.push_back(cur->val); 	// 中
	traversal(cur->right, vec); // 右 
}

后序遍历:

void traversal(TreeNode* cur, vector<int>& vec) {
	 // 终止条件
	 if (cur == NULL) return;
		
	// 单层递归的逻辑
	traversal(cur->left, vec); 	// 左
	traversal(cur->right, vec); // 右
	vec.push_back(cur->val); 	// 中 
 }

(2)非递归法(迭代法,代码风格不统一):利用栈stack。
注意:中序遍历时,访问节点(遍历节点)和处理节点(将元素放进结果集)不⼀致。
前序遍历:为什么要先加⼊ 右孩⼦,再加⼊左孩⼦呢? 因为这样出栈的时候才是中左右的顺序。

class Solution {
public:
	 vector<int> preorderTraversal(TreeNode* root) {
		 stack<TreeNode*> st;
		 vector<int> result;
		 if (root == NULL) return result;
		 st.push(root);
		 while (!st.empty()) {
			 TreeNode* node = st.top(); // 中
			 st.pop();
			 result.push_back(node->val);
			 if (node->right) st.push(node->right); // 右(空节点不⼊栈)
			 if (node->left) st.push(node->left); 	// 左(空节点不⼊栈)
		 }
		 return result;
	 }
};

中序遍历:

class Solution {
public:
	 vector<int> inorderTraversal(TreeNode* root) {
		 vector<int> result;
		 stack<TreeNode*> st;
		 TreeNode* cur = root;
		 while (cur != NULL || !st.empty()) {
			 if (cur != NULL) { // (1)指针来访问节点,访问到最底层
				 st.push(cur); 	// 将访问的节点放进栈
				 cur = cur->left; // 左
			 } 
			 else {
				 cur = st.top(); // (2)从栈⾥弹出的数据,就是要处理的数据(放进result数组⾥的数据)
				 st.pop();
				 result.push_back(cur->val); // 中
				 cur = cur->right; // 右
			 }
		 }
		 return result;
	 }
};

后序遍历:

class Solution {
public:
	 vector<int> postorderTraversal(TreeNode* root) {
		 stack<TreeNode*> st;
		 vector<int> result;
		 if (root == NULL) return result;
		 st.push(root);
		 while (!st.empty()) {
			 TreeNode* node = st.top();
			 st.pop();
			 result.push_back(node->val);
			 if (node->left) st.push(node->left); 	// 相对于前序遍历,这更改⼀下⼊栈顺序 (空节点不⼊栈)
			 if (node->right) st.push(node->right); // 空节点不⼊栈
		 }
		 reverse(result.begin(), result.end()); 	// 将结果反转之后就是左右中的顺序了
		 return result;
	 }
};

(3)非递归法(迭代法,代码风格统一):利用栈stack。

前序遍历与后序遍历的转换:
先序遍历是中左右,后续遍历是左右中,那么只需调整⼀下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图前序遍历与后续遍历的转换

前序遍历:

class Solution {
public:
	 vector<int> preorderTraversal(TreeNode* root) {
		 vector<int> result;
		 stack<TreeNode*> st;
		 if (root != NULL) st.push(root);
		 while (!st.empty()) {
			 TreeNode* node = st.top();
			 if (node != NULL) {
				 st.pop();
				 if (node->right) st.push(node->right); // 右
				 if (node->left) st.push(node->left); 	// 左
				 st.push(node); // 中
				 st.push(NULL);
			 } 
			 else {
				 st.pop();
				 node = st.top();
				 st.pop();
				 result.push_back(node->val);
			 }
		 }
		 return result;
	 }
};

中序遍历:

class Solution {
	public:
	 vector<int> inorderTraversal(TreeNode* root) {
		 vector<int> result;
		 stack<TreeNode*> st;
		 if (root != NULL) st.push(root);
		 while (!st.empty()) {
			 TreeNode* node = st.top();
			 if (node != NULL) {
				 st.pop(); // 将该节点弹出,避免重复操作,下⾯再将右中左节点添加到栈中
				 if (node->right) st.push(node->right); // 添加右节点(空节点不⼊栈)
				 st.push(node); // 添加中节点
				 st.push(NULL); // 中节点访问过,但是还没有处理,加⼊空节点做为标记。
				 if (node->left) st.push(node->left); // 添加左节点(空节点不⼊栈)
			 } 
			 else { // 只有遇到空节点的时候,才将下⼀个节点放进结果集
			 	 st.pop(); // 将空节点弹出
				 node = st.top(); // 重新取出栈中元素
				 st.pop();
				 result.push_back(node->val); // 加⼊到结果集
			 }
		 }
		 return result;
	 }
};

后序遍历:

class Solution {
public:
	 vector<int> postorderTraversal(TreeNode* root) {
		 vector<int> result;
		 stack<TreeNode*> st;
		 if (root != NULL) st.push(root);
		 while (!st.empty()) {
			 TreeNode* node = st.top();
			 if (node != NULL) {
				 st.pop();
				 st.push(node); // 中
				 st.push(NULL);
				 if (node->right) st.push(node->right); // 右
				 if (node->left) st.push(node->left); // 左
			 } 
			 else {
				 st.pop();
				 node = st.top();
				 st.pop();
				 result.push_back(node->val);
			 }
		 }
		 return result;
	 }
};

3、层序遍历的实现:利用队列queue。

class Solution {
public:
	 vector<vector<int>> levelOrder(TreeNode* root) {
		 queue<TreeNode*> que;
		 if (root != NULL) que.push(root);
		 vector<vector<int>> result;
		 while (!que.empty()) {
			 int size = que.size();
			 vector<int> vec;
			 // 这⾥⼀定要使⽤固定⼤⼩size,不要使⽤que.size(),因为que.size是不断变化的
			 for (int i = 0; i < size; i++) {
				 TreeNode* node = que.front();
				 que.pop();
				 vec.push_back(node->val);
				 if (node->left) que.push(node->left);
				 if (node->right) que.push(node->right);
			 }
			 result.push_back(vec);
		 }
		 return result;
	 }
};

五、不变量:二分法

1、思考为什么要写while(left <= right), 为什么要写right = middle - 1。即定义 target 是在一个在左闭右闭的区间里,「也就是[left, right] (这个很重要)」
注意:int mid = (left + right) / 2; 这么写其实有⼀个问题,就是数值越界,例如left和right都是最⼤int,这么操作就越界了,在⼆分法中尤其需要注意!

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0;
        int right = n - 1; // 定义target在左闭右闭的区间里,[left, right] 
        while (left <= right) { // 当left==right,区间[left, right]依然有效
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle;
            }
        }
        // 分别处理如下四种情况
        // 目标值在数组所有元素之前  [0, -1]
        // 目标值等于数组中某一个元素  return middle;
        // 目标值插入数组中的位置 [left, right],return  right + 1
        // 目标值在数组所有元素之后的情况 [left, right], return right + 1
        return right + 1;
    }
};

2、为什么要写while (left < right), 为什么要写right = middle,即定义 target 是在一个在左闭右开的区间里,也就是[left, right) 。

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0;
        int right = n; // 定义target在左闭右开的区间里,[left, right)  target
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在 [middle+1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值的情况,直接返回下标
            }
        }
        // 分别处理如下四种情况
        // 目标值在数组所有元素之前 [0,0)
        // 目标值等于数组中某一个元素 return middle
        // 目标值插入数组中的位置 [left, right) ,return right 即可
        // 目标值在数组所有元素之后的情况 [left, right),return right 即可
        return right;
    }
};

两种写法的时间复杂度都为:O(logn),空间复杂度都为:O(1)
链接:https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程。 训练营的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第14天的训练营,讲解了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论还分享了相关的博客文章和配图,帮助学员更好地理解和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15天的讨论,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16天的讨论,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地解决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值