定义
🦄二叉树是由树发展过来的,即度最大为2的树,且子树有左右之分,可以这么理解,二叉树是空结点跟左右子树的结合体。
🦄下面这张图可能更好理解一点,任何二叉树都是下列几种情况复合而成的。因此只要这个树的度超过 2 ,那么它就不是二叉树。
满二叉树
🦄满二叉树是一种特殊的二叉树,即每一层结点都到达最大值。举个简单的例子,假设这个二叉树根结点在 1 层且一共有 i 层,若结点总数为(2^i) -1 个那么这个二叉树就是满二叉树。
完全二叉树
🦄可以说满二叉树也是一种特殊的完全二叉树,完全二叉树的底层的结点的排序从左到右都是连续的。若假设下面这张图里的F结点还有一个左结点,那么这个二叉树就不是完全二叉树了。
性质
1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2 ^ (i - 1) 个结点。
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是(2 ^ h) - 1。
3. 对任何一棵二叉树, 如果度为 0 其叶结点个数为 n0 , 度为 2 的分支结点个数为 n2 ,则有 n0= n2+1 。
4. 若规定根节点的层数为 1 ,具有 n 个结点的满二叉树的深度,h = log (n + 1) 。(ps: 是 log 以2为底,n+1 为对数)
5. 对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从 0 开始编号,则对于序号为 i 的结点有:
1. 若 i>0 ,i 位置节点的双亲为:( i - 1 ) / 2 。
2. 若 2i + 1 < n ,左孩子为:2i+1 ,2i + 1 >= n否则无左孩子。
3. 若2i + 2 < n,右孩子为:2i + 2,2i + 2 >= n否则无右孩子。
应用
🦄与堆不同,二叉树是多个结点链接起来的链式结构,因此对应其特性,每一个根节点都指向左右两个结点。
typedef int BTdatatype; typedef struct BinaryTreeNode { BTdatatype data; //数据 struct BinaryTreeNode* left; //左结点 struct BinaryTreeNode* right; //右结点 }BTNode;
🦄而这里二叉树的实现主要依靠的是递归,为的便是能够在访问完一个子树后还能返回到它原来的根结点。
计算二叉树结点个数
🦄这里开始递归就初见雏形,需要一次次递归遍历整个二叉树来计算结点个数,通过返回值的方式将数值统计起来。(若使用全局变量,在多次调用该函数的时候就会出现全局变量未初始化的情况而导致结果出错)
int BinaryTreeSize(BTNode* root) { if (root == NULL) { return 0; //遇到空结点就返回0(代表0个结点) } return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1; //左右子树结点加上自己的总个数 }
计算叶子结点的个数
🦄这题跟上一题类似,但还需要加上一个条件筛选叶子结点。
// 二叉树叶子节点个数 int BinaryTreeLeafSize(BTNode* root) { if (root == NULL) { return 0; } if (root->left == NULL && root->right == NULL) //判断是否为叶子结点 { return 1; } return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right); //两边叶子结点的个数 }
第 k 层结点的个数
🦄这题的关键在于对 k 层的检索,既要找到 k 层的结点又要在 k 层前遇到空返回,则需要两个判断。举个例子而言,根节点距离 k 层的长度为 k-1 ,即第 2 层的结点距离 k 层的长度为 k-2 ,因此每次进一层的时候就把 k-- 当 k==1 的时候该结点就是 k 层的结点,因此只需要统计这些结点的个数就可以了。
// 二叉树第k层节点个数 int BinaryTreeLevelKSize(BTNode* root, int k) { if (root == NULL) //空结点不计数 { return 0; } if (k == 1 && root != NULL) //到k层且结点不为空则计数 { return 1; } return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1); //统合数量 }
查找值为x的节点
🦄这题要注意的是对结点的值的返回,我们都知道一次的结果并不一定会影响到最终返回的结果,因此处理好一找到点就立刻返回的方法非常重要。这里就通过对返回值的判断,只有找到目标结点时才会直接层层返回。
// 二叉树查找值为x的节点 BTNode* BinaryTreeFind(BTNode* root, BTdatatype x) { if (root == NULL) { return 0; } if (root->data == x) //找到值为x的结点返回 { return root; } BTNode* ret1 = BinaryTreeFind(root->left, x); //往左找x if (ret1) //有返回值则继续返回 { return ret1; } BTNode* ret2 = BinaryTreeFind(root->right, x); //往右找x if(ret2) //有返回值继续返回 { return ret2; } }
遍历
🦄遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。根据规则的不同,二叉树的遍历可以分作四种,分别是前序、中序、后序以及层序遍历。
前序遍历
传送门:144.二叉树的前序遍历
🦄口诀:根 左 右,即先访问根结点后再依次访问左右结点,倒也像一个人从根结点绕着外围跑遍了整个二叉树。
// 二叉树前序遍历 void BinaryTreePrevOrder(BTNode* root) { if (root == NULL) //为空返回 { printf("NULL "); return 0; } printf("%d ", root->data); //先打印根节点的数据 BinaryTreePrevOrder(root->left); //访问左子树 BinaryTreePrevOrder(root->right); //访问右子树 }
中序遍历
传送门:94.二叉树的中序遍历
🦄口诀:左 根 右,中序遍历则需要先访问左子树之后再访问根结点,最后才是右子树。简单地看就是从左到右依次把结点拿下来并访问。
// 二叉树中序遍历 void BinaryTreeInOrder(BTNode* root) { if (root == NULL) //为空返回 { printf("NULL "); return 0; } BinaryTreeInOrder(root->left); //先访问左子树 printf("%d ", root->data); //打印根结点 BinaryTreeInOrder(root->right); //再访问右子树 }
后序遍历
传送门:145.二叉树的后序遍历
🦄口诀:左 右 根 ,后序遍历则是先遍历两个子树之后才是访问根结点。即要访问这个结点它的左右子树都必须先遍历一遍。可以看成一个结点必须是叶结点才能对其访问,若非叶结点就先访问并“删除”左右结点后让原结点变成叶结点之后再进行访问。
// 二叉树后序遍历 void BinaryTreePostOrder(BTNode* root) { if (root == NULL) //为空返回 { printf("NULL "); return 0; } BinaryTreePostOrder(root->left); //先遍历左子树 BinaryTreePostOrder(root->right); //再遍历右子树 printf("%d ", root->data); //最后打印根结点 }
层序遍历
传送门:102. 二叉树的层序遍历
🦄正如其名,层序遍历就是以层为单位进行遍历。若要实现层序遍历的话,所需的思想与上面的遍历就完全不同。因为之前的遍历是以深度优先进行的,而层序遍历需要的是广度优先的遍历。
🦄为了实现依层遍历的功能,我们需要使用队列来辅助实现。第一次传入的是根结点,每次访问完根结点之后把队头删除,再把队头对应的左右子树对应的指针传入队列之中,根据队列先进先出的性质,所以后传入的结点就会较后地被访问。
typedef BTNode* Qdatatype; typedef struct Qnode { Qdatatype data; //数据 struct Queue* next; //指向下个结点 }Qnode; typedef struct Queue { Qnode* head; //队列的头 Qnode* tail; //队列的尾 int size; //大小 }Queue; void Queueinit(Queue* p) { p->head = NULL; //头尾结点制空 p->tail = NULL; p->size = 0; //数据量为0 } bool QueueEmpty(Queue* p) { assert(p); return p->head == NULL || p->tail == NULL; //二者一个为空则队列为空 } void Queuepush(Queue* p, Qdatatype x) { assert(p); //断言 Qnode* newnode = (Qnode*)malloc(sizeof(Qnode)); //开辟新结点 if (newnode == NULL) //开辟失败返回 { perror(malloc); exit(-1); } newnode->data = x; //赋值 newnode->next = NULL; if (p->head == NULL) //队列为空的情况 { p->head = p->tail = newnode; } else { p->tail->next = newnode; //链接 p->tail = newnode; } p->size++; //对size进行处理 } void Queuepop(Queue* p) { assert(p); //断言p不为空 assert(!QueueEmpty(p)); //断言队列不为空 Qnode* next = p->head->next; //存储下一结点 free(p->head); //释放当先头结点 p->head = next; //下一结点变成头结点 p->size--; //对size进行处理 } Qdatatype Queuefront(Queue* p) { assert(p); assert(!QueueEmpty(p)); //断言不为空 return p->head->data; //头结点存储的就是队头的数据 } void QueueDestroy(Queue* p) { assert(p); Qnode* cur = p->head; while (cur) { Qnode* next = cur->next; free(cur); cur = next; } p->head = p->tail = NULL; //头尾结点制空 p->size = 0; //大小归0 } // 层序遍历 void BinaryTreeLevelOrder(BTNode* root) { assert(root); Queue q; Queueinit(&q); //构建队列 Queuepush(&q, root); //根结点入队 while (!QueueEmpty(&q)) //队列为空遍历完成 { BTNode* front = Queuefront(&q); //取队头 printf("%d ",front->data); Queuepop(&q); //删队头 if (front->left) { Queuepush(&q, front->left); //插入队头的左结点 } if (front->right) { Queuepush(&q, front->right); //插入队头的右结点 } } printf("\n"); QueueDestroy(&q); //销毁队列 }
判断是否为完全二叉树
🦄这个问题是在层序遍历的基础之上实现的,我们知道判断是否为完全二叉树的关键在于最后一层前都为满,最后一层的结点必须连续。因此我们可以这样想,只要层序遍历直到遇到空指针就停止,之后判断队列里面还有没有非空结点。若队列里都是空指针则说明这棵树为完全二叉树。
// 判断二叉树是否是完全二叉树 int BinaryTreeComplete(BTNode* root) { assert(root); Queue q; Queueinit(&q); Queuepush(&q, root); while (!QueueEmpty(&q)) { BTNode* front = Queuefront(&q); //取队头 if (front == NULL) //队头不为空则继续循环 { break; } Queuepop(&q); Queuepush(&q, front->left); //插入左右结点 Queuepush(&q, front->right); } while (!QueueEmpty(&q)) { BTNode* front = Queuefront(&q); if (front != NULL) //只要判断队列之后还有没有非空结点 { QueueDestroy(&q); return -1; } Queuepop(&q); } QueueDestroy(&q); return 1; }
🦙那么,今天二叉树的基本操作与遍历就告一段落,如果这篇文章对你有帮助的话,不妨留下三连支持以下博主,谢谢大家!!!