关于树的基本理论:
种类
- 满二叉树
- 完全二叉树:底层节点从左到右连续。
- 二叉搜索树
- 平衡二叉搜索树
二叉搜索树:对结构没什么要求,就是要保证节点是有顺序即可。具体的:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树。
平衡二叉搜索树:在二叉搜索树的基础上,左右子树的高度差不超过1。因此,平衡二叉搜索树的高度近似于log(n),这使得其在查找、插入、删除等操作的时间复杂度保持在**O(log n)**级别。
常见的平衡二叉搜索树包括:
- AVL树:AVL树是一种最早提出的平衡二叉搜索树,它通过旋转来维护树的平衡,以保证左右子树的高度差不超过1。在AVL树中,每个节点的平衡因子(左子树高度减去右子树高度)的绝对值不超过1。
- 红黑树:红黑树是另一种常见的平衡二叉搜索树,它也是通过旋转来维护树的平衡。在红黑树中,每个节点被标记为红色或黑色,同时满足以下性质:
- (1)根节点为黑色;
- (2)每个叶子节点(NIL节点)为黑色;
- (3)如果一个节点为红色,则其子节点必须为黑色;
- (4)从任意节点到其子树中每个叶子节点的路径上包含相同数量的黑色节点。
- B树/B+树:B树/B+树是一种多叉平衡树,它可以存储大量数据,并支持快速的查找、插入、删除等操作。在B树/B+树中,每个节点可以包含多个key-value键值对,同时满足以下性质:
- (1)每个节点最多有m个子节点;
- (2)除了根节点和叶子节点,每个节点至少有ceil(m/2)个子节点;
- (3)所有叶子节点都在同一层上。
存储方式
二叉树可以有多种存储方式,下面介绍两种较为常见的存储方式:
-
链式存储方式
链式存储方式是一种通过指针连接节点的方式来存储二叉树的结构。每个节点包括左右子节点指针和数据域,如果节点没有左或右子节点,相应的指针就为NULL。
链式存储方式可以轻松地实现二叉树的遍历操作,但是它需要额外的内存空间来存储指针,同时因为指针的跳跃,也会对缓存命中率造成影响。 -
数组存储方式
数组存储方式是一种使用数组来存储二叉树的结构,一般按照层序遍历的方式将二叉树中的节点按顺序存储在数组中。
对于一个节点的索引为i,则它的左子节点为2i,右子节点为2i+1。如果节点索引从1开始,则左子节点为2i,右子节点为2i+1,父节点为i/2。(如果从0开始就会变成坐姿节点 2 ∗ i + 1 2*i+1 2∗i+1,右子节点 2 ∗ i + 2 2*i+2 2∗i+2)
数组存储方式可以在一定程度上提高程序的效率,因为它可以避免指针跳跃造成的缓存命中率下降,但是在动态插入和删除节点时,可能需要重新调整数组的大小。
遍历方式
- 深度优先:先往深走,遇到叶子节点再往回走。
- 前中后序(递归、迭代):前中后区别,只看父节点的先后。
- 前:中左右
- 中:左中右
- 后:左右中
- 前中后序(递归、迭代):前中后区别,只看父节点的先后。
- 广度优先:一层一层的去遍历。
- 层次遍历(迭代)
深度优先
递归(简单)
递归三要素:
-
确定递归函数的参数和返回值:确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数,并且还要明确每次递归的返回值是什么,进而确定递归函数的返回类型。
-
确定终止条件:写完了递归算法,运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
-
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
前序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
// 注意这里返回的结果是List,所以我们要将排序好的结果放到list中去。
// 选择list,是因为他可以在后面添加元素。不像数组。
// 中左右
List<Integer> ans = new ArrayList<>();
preoder(root,ans);
return ans;
}
// 递归三部曲
public void preoder(TreeNode root, List<Integer> ans){
if(root == null){
return;
}
// 依次将每个节点放到ans里面
ans.add(root.val); // 中
preoder(root.left, ans); // 左
preoder(root.right, ans); // 右
}
}
c++代码有几点需要注意:指针和引用
class Solution {
public:
void preoder(TreeNode* node, vector<int>& ans){
if(node == NULL){
return ;
}
ans.push_back(node->val);
preoder(node->left, ans);
preoder(node->right, ans);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
preoder(root, ans);
return ans;
}
};
class Solution {
public:
void preoder(TreeNode* node, vector<int>* ans){
if(node == NULL){
return ;
}
ans->push_back(node->val);
preoder(node->left, ans);
preoder(node->right, ans);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
preoder(root, &ans);
return ans;
}
};
vector<int>& ans
和 vector<int>* ans
-
vector<int>& ans
: 将ans看成一个变量ans.push_back(node->val)
- 这是一个引用参数。你可以认为它是原始
vector
的别名。当你对ans
进行任何操作时,你实际上是在修改原始vector
。 - 使用引用时,你可以像使用普通变量一样使用它,无需进行解引用。
- 这是一个引用参数。你可以认为它是原始
-
vector<int>* ans
:ans是一个指针了。必须ans->push_back(value)
- 这是一个指针参数,指向
vector<int>
。要访问它指向的vector
,你需要进行解引用。 - 通常你会使用
->
运算符(如ans->push_back(value)
)或(*ans).
形式来操作指针指向的对象。
- 这是一个指针参数,指向
preoder(root, &ans);
不可以写成 preoder(root, *ans);
这两种方式的差异在于它们传递的参数及其意义。
-
preoder(root, &ans)
:- 这里你传递的是
ans
的地址,即一个指向vector<int>
的指针。这是预期的行为,因为preoder
函数接受一个指向vector<int>
的指针作为其参数。
- 这里你传递的是
-
preoder(root, *ans)
:- 你在这里传递的是
ans
指向的对象。但是,*ans
的类型是vector<int>
,而不是vector<int>*
。因此,这是不正确的,因为preoder
函数期望的是一个指针,而不是一个对象。
- 你在这里传递的是
当你希望传递一个对象的地址给函数时,你应该使用 &
运算符来获取该地址,而不是 *
运算符来解引用地址。
中序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<Integer>();
inorder(root, ans);
return ans;
}
public void inorder(TreeNode root, List<Integer> ans){
if(root == null){
return;
}
inorder(root.left, ans);
ans.add(root.val);
inorder(root.right, ans);
}
}
后序遍历
/**
* Def
* inition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<Integer>();
postorder(root, ans);
return ans;
}
public void postorder(TreeNode root, List<Integer> ans){
if(root == null){
return;
}
// 变化在这里,在进入递归之后的第一步,是先进入到左子节点,然后右子结点,最后父节点
postorder(root.left, ans);
postorder(root.right, ans);
ans.add(root.val);
}
}
迭代法
前序遍历
用栈来实现迭代,同时要注意先将右节点放到栈里面。因为栈是先进后出,这样才可以保证左节点在右节点前面出来
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
// 迭代法
// 通过一个栈来实现
Stack<TreeNode> stack = new Stack<>();
List<Integer> ans = new ArrayList<Integer>();
if(root==null){
return ans;
}
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop(); // 这里新建一个node 不然一直用root会出错
ans.add(node.val);
if(node.right!=null){ // 先right
stack.push(node.right);
}
if(node.left != null){ // 再left,
stack.push(node.left);
}
}
return ans;
}
}
后序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
// 将前序遍历的中左右--> 左右中
// 现在left添加到stack中,然后得到中右左,在反转ans。得到左右中。
List<Integer> ans = new ArrayList<Integer>();
Stack<TreeNode> stack = new Stack<>();
if(root == null){
return ans;
}
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
ans.add(node.val);
if(node.left!=null){
stack.push(node.left);
}
if(node.right!=null){
stack.push(node.right);
}
}
Collections.reverse(ans); // 在Java中的反转链表、数组
return ans;
}
}
中序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
// 与前序后序的区别在于,需要另外一个指针
// 指针用来遍历,ans用的是栈里的元素。两个元素不一样。
List<Integer> ans = new ArrayList<Integer>();
Stack<TreeNode> stack = new Stack<>();
if(root == null){
return ans;
}
TreeNode cur = root;
while(cur!=null || !stack.isEmpty()){
// 核心代码区。
if(cur != null){
stack.push(cur);
cur = cur.left;
}
else{
cur = stack.pop();
ans.add(cur.val);
cur = cur.right;
}
}
return ans;
}
}
定义方式
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}