树:一种有一个前驱节点,但是有多个后继节点链表结构,或者说是包含有限节点的层次关系的集合。
a. 基本概念:
- 节点的种类:
- 根节点:一棵树只有一个,最重要,最初是的节点
- 中间节点:非根节点,同时拥有前驱节点和后继节点的节点
- 叶节点:只有前驱结点,没有后继节点的节点
- 节点的关系:
- 父子关系:如果两个节点直接相连,我们称更靠近根节点的节点为“父节点”,而称另一个节点为“子节点”
- 兄弟关系:如果两个节点不直接相连,但是拥有相同的父节点,则称为“兄弟关系”
- 祖孙关系:如果两个节点不直接相连,但一个节点是另一个节点的后继节点,则称为祖孙关系
- 节点的深度/高度:通常我们称根的高度/深度为1,其余节点的深度/高度为它到根节点的距离
- 节点的距离:两个节点之间的距离,也是二者到达最近公共祖先的距离的和
- 最近公共祖先:两个节点共有的,且距离最近的父节点
- 当一个节点a存在于另一个节点b的子树中时,b本身即为a和b的最近公共祖先
- 当一个节点a和一个节点b均不在彼此的子树中时,则他们共有的最近的父节点为最近公共祖先
- 一些树的基本性质:
- 一个有n个节点的树,必然有n-1个节点
- 每一个节点及其子节点,都构成了一棵子树
- 树是没有环的
b. 树的类型:
- 根据限定子节点的个数分为二叉树,三叉树,…,不加限制的话,得到的就是多叉树
- 二叉树:最常见,最常用的树类型,子节点不超过2个的树
- 满二叉树:对于一个高度为h的二叉树,最多可以有2^h - 1个节点,若这些节点全都存在,则这棵树为满二叉树。一棵满二叉树,有2^(h-1)个叶节点
- 完全二叉树:除了最后一层,每一层节点都是满的,且最后一层节点都是严格从左往右排列的树
c. 树的遍历:访问所有节点一次,根据访问根的先后,可以分为三种遍历方式:
-
先序遍历:按照根节点-左节点-右节点的顺序遍历树
-
递归的方式实现:
void dfs(TreeNode* root){ if(!root) return; cout<<root->val<<","; dfs(root->left); dfs(root->right); }
-
迭代的方式实现:
class Solution { public: vector<int> preorderTraversal(TreeNode* root) { vector<int> ans; if(!root) return ans; stack<TreeNode*> st; TreeNode* rt = root; while(!st.empty()||rt){ while(rt){ ans.push_back(rt->val); //优先访问子树的根节点值 if(rt->right) st.push(rt->right); //将右子树放入栈内候选 rt = rt->left; } if(!st.empty()){ rt = st.top(); st.pop(); } } return ans; } };
-
-
中序遍历:按照左节点-根节点-右节点的顺序遍历树
-
递归的方式实现:
void dfs(TreeNode* root){ if(!root) return; dfs(root->left); cout<<root->val<<","; dfs(root->right); }
-
迭代的方式实现:
class Solution { public: vector<int> inorderTraversal(TreeNode* root) { vector<int> ans; if(!root) return ans; stack<TreeNode*> st; TreeNode* rt = root; while(!st.empty()||rt){ while(rt){ st.push(rt); //将子树的根节点压入栈内候选 rt = rt->left; //一路向左 } if(!st.empty()){ rt = st.top(); ans.push_back(rt->val); st.pop(); rt = rt->right;如果右子树存在,则继续遍历右子树 } } return ans; } };
-
-
后序遍历:按照左节点-右节点-根节点的顺序遍历树
-
递归的方式实现:
void dfs(TreeNode* root){ if(!root) return; dfs(root->left); dfs(root->right); cout<<root->val<<","; }
-
迭代的方式实现:
class Solution { public: vector<int> postorderTraversal(TreeNode* root) { vector<int> ans; if(!root) return ans; stack<TreeNode*> st; TreeNode* rt = root; TreeNode* pre = nullptr; while(!st.empty()||rt){ while(rt){ st.push(rt); rt = rt->left; } rt = st.top(); st.pop(); //如果右子树不存在,或者已经遍历过右子树,则可以将当前节点输出 if(rt->right==nullptr||rt->right==pre){ ans.push_back(rt->val); pre = rt; rt = nullptr; } else{ //右子树存在时,不能将当前节点输出,故压入栈,并继续遍历右子树 st.push(rt); rt = rt->right; } } return ans; } };
-
取巧的方式实现:先序遍历树,并优先访问右子树,最后将得到结果倒置,就是后序遍历的结果
-
d. 二叉搜索树:父节点的值大于等于左子树所有节点的值,且父节点的值小于等于右子树上所有值的特殊二叉树
-
性质:
- 中序遍历得到的序列一定是有序的
- 一棵高度为h的二叉搜索树,查询某个值的时间复杂度为O(h)
-
基本操作:
-
查询:
- 最大值:当右子树非空时访问右子树,直至右子树不存在,当前节点的值即为最大值
- 最小值:当左子树非空时访问左子树,直至左子树不存在,当前节点的值即为最小值
- 特定值:与当前节点的值比较,小于节点值则向左遍历,大于节点值则向右遍历,等于则直接返回
- 存在重复值的特定值第一次和最后一次出现的位置:与查找特定值的过程类似,第一次找到目标时所在的节点即为第一次出现的位置,以该点为基础继续遍历左右子树中与target值相同的节点,直至节点的左右子树值均不等于target,则当前节点为最后出现的位置。
- 查询任意值的前驱/后继节点:中序遍历,得到有序的数列,则某个节点的前驱节点和后继节点分别对应其在中序遍历中的前一个和后一个节点
-
插入:与查询特定值的过程类似,直到找到一个有空位并满足二叉搜索树关系的节点,将新节点连接到找到的位置的对应子树位置
TreeNode* insertIntoBST(TreeNode* root, int val) { if(!root){ root = new TreeNode(val); } else{ if(root->val>val) root->left = insertIntoBST(root->left, val); else root->right = insertIntoBST(root->right,val); } return root; }
-
删除:需要分情况讨论
- 如果删除的是叶节点,则直接删除
- 如果删除的节点只有一个子节点,则直接将父节点与子节点相连,然后删除中间节点
- 如果删除的节点有两个子节点,则我们寻找删除节点的右子树中最小的节点,交换其与我们要删除的节点的值后将找到的右子树节点删除
TreeNode* deleteNode(TreeNode* root, int key) { if(!root) return root; //如果节点为空,则没有可以删除的节点在,直接返回 //如果要删除的就是子树根节点 if(root->val==key){ //且没有左子树和右子树,则直接删除根节点,返回空节点给父节点 if(!root->left && !root->right) return nullptr; //如果只有右子树,则将右子树返回给父节点 else if(!root->left && root->right) return root->right; //如果只有左子树,则将左子树返回给父节点 else if(!root->right && root->left) return root->left; else{ //如果右子树没有左节点,则右子树的根节点就是我们要找的节点 //将他与根节点交换值,并将右子树的右子树与根节点连接 if(!root->right->left){ root->val = root->right->val; root->right = root->right->right; } else{ //如果右子树有左节点,则寻找最左的那个节点 TreeNode* temp = root->right; TreeNode* pre = root; while(temp->left){ pre = temp; temp=temp->left; } //将找到的节点的值赋值给根节点 root->val = temp->val; //将找到的节点的右子树与其父节点连接 pre->left = temp->right; } } } else if(root->val<key) root->right = deleteNode(root->right,key); else root->left = deleteNode(root->left,key); return root; }
-
e. 堆:一种特殊的完全二叉树,其根节点一定是所有节点的最大值或最小值(由此分为大根堆和小根堆),堆的任意一棵子树也是一个堆
- 实现方式:
- 二叉树表示:用结构体和指针实现
- 一维数组表示:第i个节点的左右孩子下标分别为i*2+1, i*2+2
- 常用操作:
-
插入:我们将新的节点插入数组末尾,然后检查其与父节点的关系是否满足堆的定义
- 满足:无需更多操作
- 不满足:交换它与父节点,继续检查父节点与它的父节点是否满足堆的关系。重复这个过程直至到达根节点或遇到一个满足关系的父节点(shift-up)
void MaxHeapInsert(vector<int>& a, int val){ a.push_back(n); int p = a.size()-1; while(a[p]>a[(p-1)/2]){ swap(a[p],a[(p-1)/2]); p=(p-1)/2; } } void MinHeapInsert(vector<int>& a, int val){ a.push_back(n); int p = a.size()-1; while(a[p]<a[(p-1)/2]){ swap(a[p],a[(p-1)/2]); p=(p-1)/2; } }
-
删除:
- 删除堆顶:先将堆顶和堆尾元素进行交换,然后删除堆尾。之后检查根节点与左右子节点是否满足堆的关系
-
满足:无需更多操作
-
不满足:如果左节点不满足,则与左节点交换;如果右节点不满足,则与右节点交换。重复此过程,直至到达底层或到达一个满足关系的位置(shift-down)
void MaxHeapify(vector<int>& a, int root){ int left = root*2+1; int right = root*2+2; int maxn = root; if(left<a.size()&&a[left]>a[maxn]) maxn = left; if(right<a.size()&&a[right]>a[maxn]) maxn = right; if(maxn!=root){ swap(a[root],a[maxn]); MaxHeapify(a,maxn); } } void MinHeapify(vector<int>& a, int root){ int left = root*2+1; int right = root*2+2; int minn = root; if(left<a.size()&&a[left]<a[minn]) minn = left; if(right<a.size()&&a[right]<a[minn]) minn = right; if(minn!=root){ swap(a[root],a[minn]); MinHeapify(a,minn); } }
-
- 删除任意值:先找到对应元素的位置,将它与堆尾交换,删除堆尾。再检查对应元素的位置与其父节点和子节点是否满足堆的关系
- 如果和父节点不满足关系,则shift-up
- 如果和子节点不满足关系,则shift-down
- 不可能出现即需要shift-up又需要shift-down的情况
- 删除堆顶:先将堆顶和堆尾元素进行交换,然后删除堆尾。之后检查根节点与左右子节点是否满足堆的关系
-