一、⼆叉树的种类
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