树
树是一组实体的集合(A collection of entities),这些实体被称为节点(Nodes),它们通过链接来模拟层次结构。
根结点/Root:没有父结点的结点。
子结点/Children
父结点/Parent
兄弟结点/Sibling:有相同父结点的结点。
叶结点/Leaf:没有子结点的结点。
若能通过单向链接从结点A走到结点B,则称:
A是B的祖先(A is ancestor of B)。
B是A的子孙(B is descendent of A)。
具有N个结点的树有N-1个边。
深度/depth:从根结点到结点X的路径长度,路径上的每条边都会为长度贡献一个单位。因此,我们也可以说深度是从根结点到结点X路径上的边数。
高度/height:结点X到叶结点的最长路径上的边数。
树的高度:根结点的高度。
二叉树
概念
二叉树(Binary tree)是一种特定性质的树,它的每个结点最多只能有两个子结点。
满二叉树(Strict/Proper binary tree):每个结点只能有0或2个子结点。
完全二叉树(Complete binary tree):除最后一层外所有层必须填满。最后一层可以不填满,但结点必须尽量靠左排列。
第 i 层能拥有结点最大数量为2^i。
完美二叉树(Perfect binary tree):所有层级都被填满的二叉树。
完美二叉树拥有高度为 h 的二叉树中结点的最大数量,具体为2 ^ 层数 - 1。
反过来,可求出拥有n个结点的完美二叉树的高度为。
平衡二叉树(Balanced binary tree):如果二叉树的每个结点的左右子树高度差都不超过k,则称这样的树是平衡二叉树。通常情况下,k取1。
空树(Empty tree):没有结点的树。空树的高度为-1。
二叉树的两种实现方式:
1.动态创建结点
2.使用数组
二叉搜索树
二叉搜索树(Binary search tree)是一种特殊的二叉树,对于二叉搜索树中每一个结点,左子树的所有结点的值都更小或相等,右子树的所有结点的值都更大。
使用BST存储数据,在搜索到每一个结点时,我们可以通过判断当前值和目标值的大小关系,决定下一步进入哪个子树。和二分查找类似,这样大大提高了搜索效率。
要保持搜索的高效,需要尽可能保持二叉搜索树是平衡的。因为极端条件下,如果每个结点都只有一个子结点,二叉树退化成类似链表的构造,搜索效率就退化成O(n)了。
C++实现二叉搜索树的搜索与插入
#include <iostream>
using namespace std;
struct BstNode{
int data;
BstNode* left;
BstNode* right;
};
BstNode* GetNewNode(int data)
{
BstNode* newNode = new BstNode;
newNode -> data=data;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
BstNode* Insert(BstNode* root,int data)
{
if(root == NULL)
{
root = GetNewNode(data);
}else if(data<=root->data)
{
root->left = Insert(root->left,data);
}else{
root->right = Insert(root->right,data);
}
return root;
}
bool Search(BstNode* root,int data)
{
if(root == NULL){
return false;
}
else if(data==root->data)
{
return true;
}else if(data<root->data)
{
return Search(root->left,data);
}else{
return Search(root->right,data);
}
}
int main()
{
BstNode* root = NULL;//to store address of root node
root = Insert(root,15);
root = Insert(root,10);
root = Insert(root,20);
root = Insert(root,25);
root = Insert(root,8);
root = Insert(root,12);
int num;
cout<<"Enter num to be searched"<<endl;
cin>>num;
if(Search(root,num)){
cout<<"found"<<endl;
}else{
cout<<"not found"<<endl;
}
}
找到二叉搜索树中的最大值和最小值
不断让临时结点向左或向右走就行。
用迭代实现:
int FindMin(BstNode* root){
if(root==NULL){
cout<<"tree is empty";
return -1;
}
while(root->left!=NULL){
root=root->left;
}
return root->data;
}
int FindMax(BstNode* root){
if(root==NULL){
cout<<"tree is empty";
return -1;
}
while(root->right!=NULL){
root=root->right;
}
return root->data;
}
用递归实现:
int FindMin(BstNode* root){
if(root==NULL){
cout<<"tree is empty";
return -1;
}else if(root->left==NULL){
return root->data;
}
return FindMin(root->left);
}
int FindMax(BstNode* root){
if(root==NULL){
cout<<"tree is empty";
return -1;
}else if(root->right==NULL){
return root->data;
}
return FindMax(root->right);
}
得到二叉树的高度
二叉树的高度即左子树和右子树高度中的较大值,可用递归求解。
int FindHeight(Node* root){
if(root==NULL){
return -1;//叶子结点高度为0
}
int leftHeight = FindHeight(root->left);
int rightHeight = FindHeight(root->right);
return max(leftHeight,rightHeight)+1;
}
二叉树的遍历
和线性表不同,在二叉树中,当我们指向某个结点时,可能会有多个可能的遍历方向。此外,在探索了某个方向后,我们还需要返回以探索不同方向。
树的遍历(Tree traversal):按照某个顺序访问树的某个结点且仅访问一次的过程。
"访问"意味着读取或处理结点中的顺序,在下面的示例中指打印。
遍历通常有两种方法:
广度优先遍历(Breadth-first traversal):先访问同一层级或深度的所有结点,再访问下一层的结点。
深度优先遍历(Depth-first traversal):访问一个结点,意味着要完整遍历路径上的所有子树。
深度优先遍历可分为三种:
前序遍历/DLR(Data Left Right):先访问根结点,再访问左子树,最后访问右子树。
中序遍历/LDR(Left Data Right):先访问左子树,再访问根结点,最后访问右子树。
后序遍历/LRD(Left Right Data):先访问左子树,再访问右子树,最后访问根结点。
层序遍历(Level-order traversal):
层序遍历意味着按照层级顺序访问结点,属于广度优先遍历的一种。
很显然,仅靠一个指针我们无法做到从左到右遍历同层级的结点,因为它们之间没有任何链接。
要实现层序遍历,我们需要使用队列。我们将已知的结点存入队列中,在访问它的时候把它的子节点加入队列,再将其本身移除。
这样做,我们不会丢失子结点的地址;而且由于队列先进先出的特性,可以保证我们先发现的结点会先被访问。
void LevelOrder(Node* root){
if(root==NULL){
return;
}
queue<Node*>Q;
Q.push(root);
while(!Q.empty()){
Node* current = Q.front();
cout<<current->data<<endl;
if(current->left!=NULL){
Q.push(current->left);
}
if(current->right!=NULL){
Q.push(current->right);
}
Q.pop();
}
}
时间复杂度:每个结点只会访问一遍,所以复杂度为O(n)。
空间复杂度:取决于最多进入队列的结点数目。最坏情况为O(n),此时为完美二叉树;最好情况为O(1),此时每层只有一个结点。
前序遍历(Preorder traversal):
即DLR,先访问根结点,然后左子树,然后右子树。
void Preorder(Node*root){
if(root==NULL)return;
cout<<root->data<<endl;
Preorder(root->left);
Preorder(root->right);
}
检查二叉树是否是二叉搜索树
思路:给出一个范围,判断每个根结点是否处在这个范围中,并不断更新这个范围。初始时为负无穷到正无穷,因为树的根结点取值没有要求。
bool IsBstUtil(Node* root,int minValue,int maxValue){
if(root==NULL){
return true;
}
if(root->data<minValue&&root->data>maxValue
&&IsBstUtil(root->left,minValue,root->data)
&&IsBstUtil(root->right,root->data,maxValue)){
return true;
}else{
return false;
}
}
bool IsBinarySearchTree(Node* root){
return IsBstUtil(root,INT_MAX,INT_MIN);
}
搜索二叉树结点的删除
要删除一个叶结点,首先,我们从其父结点中移除对它的引用,之后回收内存即可。
但对于有子结点的结点,直接断开其和父结点的链接会导致整棵树断掉。
对于只有一个子结点的结点,我们可以将其父结点与其的链接转移到子结点上,直接跳过它,之后回收内存即可。
对于有两个子结点的结点,我们可以用右子树的最小值替代该结点的值。由于树中最小的结点肯定没有左子结点,因为如果有则意味着存在更小的值,我们如此做就将问题简化为第二种情况。之后,我们按照第二种情况的做法,将原本存在的最小值结点删去即可。
Node* Delete(Node *root, int data) {
if(root == NULL) return root;
else if(data < root->data) root->left = Delete(root->left,data);
else if (data > root->data) root->right = Delete(root->right,data);
else {
// Case 1: No child
if(root->left == NULL && root->right == NULL) {
delete root;
root = NULL;
}
//Case 2: One child
else if(root->left == NULL) {
Node *temp = root;
root = root->right;
delete temp;
}
else if(root->right == NULL) {
Node *temp = root;
root = root->left;
delete temp;
}
// case 3: 2 children
else {
Node *temp = FindMin(root->right);
root->data = temp->data;
root->right = Delete(root->right,temp->data);
}
}
return root;
}