二叉树算法之1-计算二叉树第k层节点个数

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二叉树算法之1-计算二叉树第k层节点个数相关的知识,希望对你有一定的参考价值。

参考技术A 算法思想:递归

算法解析:把k作为计数器通过参数递归传递,递归的过程中不断减1,直到k==0时说明找到一条从根节点到该k(k从0开始)层节点的路径,计作1条路径,也就是一个节点,不断展开左树和右树,最后就是k层节点数的和,即第一层计算结果由第二层得到,第二层计算结果由第三层得到,依次类推,直到第k层计算出最终结果。需要注意的是递归终止条件node==null,说明该条路径深度小于k。

二叉树 详解

二叉树

树 概念及结构

树的概念

树是一种非线性 的数据结构,它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一颗倒挂的树,也就是说它是根朝上,而叶朝下。

  • 有一个特殊的节点,称为根节点,根节点没有前驱节点
  • 除根节点外,其余节点被分为M(M>0)个互不相交的集合T1、T2、…、Tm,其中每一个集合Ti(1<=i<=m) 又是一棵与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继
  • 因此,树 是递归定义的

注意:树形结构中,子树之间不能有交集,否则就不是树形结构

树的相关概念


节点的度: 一个节点含有的子树的个数称为该节点的度;如上图:F点的度为3
叶节点或终端节点: 度为0的节点称为叶节点;如上图:B、C、H、I、P、Q…都是叶节点
非终端节点或分支节点: 度不为0的节点;如上图J、D、E…
双亲节点或父节点: 若一个节点含有子节点,则称这个节点为其子节点的父节点
孩子节点或子节点: 一个节点含有的子树根节点称为该节点的子节点;如图:B是A的孩子节点
兄弟节点: 具有相同父节点的节点互称为兄弟节点;如图:B、C、D、E、F、G是兄弟节点
树的度: 一棵树中最大节点的度称为树的度;如上图:树的度为6
节点的层次: 从根开始定义起,根为第一层,根的子节点为第二层,以此类推;
树的高度或深度: 树中节点的最大层次;如上图:树的高度为4.
堂兄弟节点: 双亲在同一层的节点互为堂兄弟节点,如图:H、I互为堂兄弟节点
节点的祖先: 从根到该节点所经分支上的所有节点;如图:A是所有节点的祖先
子孙: 以某节点为根的子树中的任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林: 由m(m>0)棵互补相交的树的集合称为森林;

树的表示

树结构相对线性表就比较复杂了,要储存表示起来比较麻烦,既要保存值域,也要保存节点和节点之间的关系 这里介绍一下孩子兄弟表示法


typedef int DataType;
struct Node
{
	struct Node* _fistchild1;   //第一个孩子的节点
	struct Node* _NextBrother;   //指向其下一个兄弟的节点
	DataType _data;               //节点中的数据域
};

二叉树概念及结构

概念

一棵二叉树是节点的一个有限集合,该集合:

  1. 或者为空
  2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成

    从上可以发现:
  3. 二叉树不存在度大于2的节点
  4. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

注意:二叉树都是由一下几种情况组合而成:

特殊的二叉树

  1. 满二叉树: 一个二叉树,如果每一层的节点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且节点总数为2^k - 1 ,则他就是满二叉树
  2. 完全二叉树: 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个节点的二叉树,当且仅当其每一个节点都与深度为K的满二叉树中编号从1到n的节点一一对应时称为完全二叉树。注意满二叉树是一种特殊的完全二叉树。

二叉树的性质

  1. 若规定根节点的层数为1,则一棵非空二叉树的的第i层上最多有2^(i-1) 个结点
  2. 若规定根节点的层数为1,则深度为h的二叉树的最大节点数是2^h -1
  3. 对任何一棵二叉树,如果度为0的叶节点个数为x,度为2的节点个数为y,则有 x=y+1
  4. 对一个完全二叉树来说,他的度为1的节点只有两种肯能 1或0 ,结合上面的条件可以推出
  • 当完全二叉树有偶数个节点(2n)的时候,这时候必然有一个度为1的节点,且度为0的节点的个数是所有节点的一半(n)
  • 当完全二叉树有奇数个节点(2n-1)的时候,这时候必然有0个度为1的节点,且度为0的节点的个数是n
  1. 对于具有n个节点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的节点有:
  1. 若i>0,则i位置节点的双亲序号:(i-1)/2 ;i=0,i为根节点编号
  2. 若2i+1<n,左孩子序号为:2*i+1 ,否则就没有左孩子
  3. 若2i+2<n,右孩子序号为:2*i+2 ,否则就没有右孩子

二叉树的存储结构

二叉树的存储结构可以使用两种结构存储:顺序存储结构 和 链式存储结构

  1. 顺序存储

顺序存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中只有 才会使用数组来存储。
关于 的博客可以戳这里->数据结构:堆 的详解

  1. 链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个节点由三个域组成,数据域左右指针域 左右指针分别用来给出该节点左孩子和右孩子所在的链结点的存储地址。链式结构又分为二叉链和三叉链。

二叉树链式结构的实现

因为堆二叉树的了解还不够深入所以这里用最简单的方法创建一个二叉树


typedef int DataType;
typedef struct BinaryTree
{
	DataType x;
	struct BinaryTree* _left;
	struct BinaryTree* _right;
}BT;

BT* BinaryTreeCreate1(BT* root)
{
	BT* n1 = BuyNode(3);
	BT* n2 = BuyNode(5);
	BT* n3 = BuyNode(4);
	BT* n4 = BuyNode(1);
	BT* n5 = BuyNode(2);
	BT* n6 = BuyNode(0);


	n1->_left = n3;
	n1->_right = n2;
	n3->_left = n4;
	n3->_right = n5;
	n5->_left = n6;

	return n1;
}

二叉树的遍历

二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次

前序遍历(先序遍历)

访问根节点的操作发生在遍历其左右子树之前

前序遍历实际上也就是按 根 左 右 的顺序来进行遍历,但要注意的是这里的左和右 是左子树和右子树,而不是左节点和右节点

进行前序遍历先访问根节点的值,然后以左节点为新的根节点,左子树为新的树按照根 左的顺序遍历直到根节点为NULL(如果根节点为NULL则说明该子树已经被遍历完了),然后返回上一级遍历 遍历右子树 按照 ** 根 左**,周而复始。
注意我们这里都是判断根节点是否为空来确定时还要递归下去

leetcode-二叉树的前序遍历

以leetcode上的前序遍历为例,这里要注意一下给的函数的参数的意义a是一个数组里面存储的是二叉树里的值,而returnsize是一个输出型参数,也就是传入一个参数returnsize的地址在函数内部修改使函数结束的时候,返回a数组的大小

一这颗子树为例:


如上十二步就可以把ABC全部存入数组了

void Pre_order(struct TreeNode*root,int *a,int *returnSize)
{
    if(root!=NULL)
    {
        a[*returnSize]=root->val;
        (*returnSize)++;
    }
    else
    return;
    Pre_order(root->left,a,returnSize); //左子树进行递归
    Pre_order(root->right,a,returnSize); //右子树进行递归
}


int* preorderTraversal(struct TreeNode* root, int* returnSize){
    int *p=(int *)malloc(2000*sizeof(int));
    *returnSize=0;
    Pre_order(root,p,returnSize);
    return p;
}

中序遍历

访问根节点的操作发生在遍历其左子树和右子树之间
中序遍历实际上就是先将左子树递归至空(NULL)然后再方位根节点和右子树

leetcode-中序遍历


 void postOrder(struct TreeNode*root,int *returnSize,int *a)
{
    if(root!=NULL)
    {
        postOrder(root->left,returnSize,a); //先将左子树遍历至NULL
    }
    else
    {
        return;
    }
    a[*returnSize]=root->val; // 存入根节点的值
    (*returnSize)++;
    postOrder(root->right,returnSize,a);  //遍历右子树 依然按照“左 根 右”
}
int* inorderTraversal(struct TreeNode* root, int* returnSize){
    int *p=(int *)malloc(2000*sizeof(int));
    int k=0;
    postOrder(root,&k,p);
    *returnSize=k;
    return p;
}

后序遍历

访问根节点的操作发生在遍历其左右子树之中
和上面的思路相同,后续遍历是先递归左子树至空 然后再递归右子树为空,最后返回空节点
leetcode-后续遍历

void postOrder(struct TreeNode*root,int *returnSize,int *a)
{
    if(root!=NULL)
    {
        postOrder(root->left,returnSize,a);  //左子树递归
    }
    else
    {
        return;
    }
    postOrder(root->right,returnSize,a); //右子树递归
    a[*returnSize]=root->val;    //存储根节点
    (*returnSize)++;
}
int* postorderTraversal(struct TreeNode* root, int* returnSize){
    int *p=(int *)malloc(2000*sizeof(int));
    int k=0;
    postOrder(root,&k,p);
    *returnSize=k;
    return p;
}

层序遍历

层序遍历: 设二叉树的根节点所在层数为1,层数遍历就是从所在二叉树的根节点出发,首先访问第一层的节点,然后从左到右访问第二层上的节点,接着第三层的节点

leetcode-层序遍历

这个leetcode题用c语言写还是有点小复杂的,如果不是以数组的形式输出的话会简单不少,难点在于要以数组输出的话,你要控制二叉树每一层的数组大小,因为每一层的元素不一定是满的。
其实这题还有一个难点是看明白函数参数和返回值的意义:首先是返回值返回的是一个指针数组的数组名实际上是一个数组指针,然后** returnColumnSizes是一个输出型数组,分别存放的是 指针数组 每个 数组指针 指向数组的大小,而* returnSize是个输出型参数,存放的是返回的的指针数组的大小
如果要写main函数调用的话


int main()
{
	TreeNode* root;
	root = CreatBT();
	int* returnColumnSizes;
	int returnSize;
	int** b;
	b = levelOrder(root, &returnSize, &returnColumnSizes);
	for (int i = 0; i < returnSize; i++)                  //控制行数
	{
		for (int j = 0; j < returnColumnSizes[i]; j++)   // 控制每一行列的个数
		{
			printf("%d ", b[i][j]);
		}
		printf("\\n");
	}
}

这里实现的基本思路是:

  • 创建一个存储二叉树节点的队列,将二叉树的祖先先入队
  • 然后进入循环,将队头元素的左右节点入队(这里要做检验 如果是NULL就不要入队了,同时还要设置一个参数记录有多少个NULL,来计算下一个数组的大小)最后将头节点出队列,循环的结束条件是队列为空

#include<assert.h>
 
typedef struct TreeNode* Datatype;
typedef struct listNode
{
	Datatype x;
	struct listNode* next;
}ListNode;

typedef struct Queue
{
	ListNode* head;
	ListNode* tail;
}Queue;

void QueueInit(Queue* q)
{
	ListNode* First = (ListNode*)malloc(sizeof(ListNode));
	q->head = q->tail = First;
	q->tail->next = NULL;
}
int QueueEmpty(Queue* q)
{
	return q->head == q->tail;
}
void QueuePush(Queue* q, Datatype x)
{
	assert(q);
	q->tail->x = x;
	ListNode* NewNode = (ListNode*)malloc(sizeof(ListNode));
	q->tail->next = NewNode;
	q->tail = NewNode;
	q->tail->next = NULL;
}

void QueuePop(Queue* q)
{
	assert(q);
	assert(!QueueEmpty(q));
	if (!QueueEmpty(q))
	{
		ListNode* temp = q->head->next;
		free(q->head);
		q->head = temp;
	}

}

Datatype QueueFront(Queue* q)
{
	assert(q);
	assert(!QueueEmpty(q));
	return q->head->x;
}



void QueueDestroy(Queue* q)
{
	ListNode* cur = q->head;
	while (cur != q->tail)
	{
		ListNode* temp = cur->next;
		free(cur);
		cur = temp;
	}
	free(cur);
	cur = NULL;
	q->head = NULL;
	q->tail = NULL;

}
//前面的都是队列的操作的函数
int** levelOrder(struct TreeNode* root, int* returnSize, int** returnColumnSizes) {
	if (root == NULL)
    {
        *returnSize=0;
        *returnColumnSizes=NULL;
        return NULL;
    }
	int** p = (int**)malloc(sizeof(int*));
	*p = NULL;
	int* q = (int*)malloc(sizeof(int*));
	Queue Q1;
	QueueInit(&Q1);
	QueuePush(&Q1, root);
	int colsize = 0;          //数组的个数,也就是行数
	int index = 1;   //每次数组的理论大小
	while (!QueueEmpty(&Q1))
	{
		int m = 0; //每一个数组实际上存了几个数,因为是NULL是不会进入数组的
        int k=0;

		for (int i = 0; i < index; i++)
		{
			if (!QueueEmpty(&Q1)&&QueueFront(&Q1))
			{
				p[colsize] = (int*)realloc(p[colsize], (m + 1) * sizeof(int)); //为新的元素重新调整数组的大小
				p[colsize][m] = QueueFront(&Q1)->val;
				m++;
				struct TreeNode* temp = QueueFront(&Q1);
                if(temp->left)
				QueuePush(&Q1, temp->left);
                if(temp->right)
				QueuePush(&Q1, temp->right);
                if(temp->left==NULL)
                k++;
                if(temp->right==NULL)
                k++;
			}

			if (!QueueEmpty(&Q1))
				QueuePop(&Q1);
			else
				break;
		}
		q = (int*)realloc(q, (colsize + 1) * sizeof(int));  //把每一行的元素个数存入数组
		q[colsize] = m;
        index=index*2-k;  //数组的理论大小要减去这一层空指针的个数
		colsize++;
		p = (int**)realloc(p,(colsize+1)*sizeof(int*));  //开辟下一行
		p[colsize] = NULL;
	}
	QueueDestroy(&Q1);
	*returnSize = colsize;
	*returnColumnSizes = q;
	return p;
}

例一:
如果一个二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK 中序遍历:HFIEJKG请画出这个二叉树:


首先通过先序遍历找到根节点然后在中序遍历里就可以找出对应的左子树右子树序列,再在根节点里画出相应的左子树和右子树序列,这样下一个子树的根节点也就找到了

二叉树的应用

二叉树节点个数

这其实如果对二叉树的遍历比较了解的话很简单,这里提供两种写法


typedef int DataType;
typedef struct BinaryTree
{
	DataType x;
	struct BinaryTree* _left;
	struct BinaryTree* _right;
}BT;
//二叉树节点个数
void BinaryTreeSize1(BT* root, int* x) //使用输出型参数,但要注意使用指针
{
	if (root == NULL)
		return;
	else
		*x+=1;
	BinaryTreeSize1(root->_left, x);
	BinaryTreeSize1(root->_right, x);
}

int BinaryTreeSize2(BT* root)  //直接递归的方法
{
	if (root == NULL)
		return 0;
	return BinaryTreeSize2(root->_left) + BinaryTreeSize2(root->_right) + 1; 

}

二叉树叶子节点的个数

这里的思路是把叶节点的个数分为:左子树的叶节点个数+右子树的叶节点的个数


typedef int DataType;
typedef struct BinaryTree
{
	DataType x;
	struct BinaryTree* _left;
	struct BinaryTree* _right;
}以上是关于二叉树算法之1-计算二叉树第k层节点个数的主要内容,如果未能解决你的问题,请参考以下文章

数据结构之二叉树的实现

二叉树的相关操作

万字总结!java语言基础知识入门

kafka架构,BAT面试&高级进阶

二叉树 详解

Java开发面试问题,java项目常见问题