【数据结构】二叉树

目录

1. 树概念及结构 

1.1 树的概念

1.2 树的表示

2. 二叉树概念及结构

2.1 二叉树概念

2.2 特殊的二叉树

2.3 二叉树的存储结构

2.3.1 顺序存储

2.3.2 链式存储

3. 堆的概念及结构

3.1 堆的实现

3.1.1 堆向下调整算法

3.1.2 堆排序

3.1.3 堆的实现

3.1.4 TopK问题

4. 二叉树链式结构的实现

4.1 二叉树链式结构的遍历

4.2 二叉树的基础面试题

4.2.1 二叉树最大深度

4.2.2 单值二叉树

4.2.3 二叉树的前序遍历

4.2.4 相同的树

4.2.5 翻转二叉树

4.2.6 对称二叉树

4.2.7 另一棵树的字树

4.2.8 平衡二叉树

4.2.9 二叉树的构建和遍历


1. 树概念及结构 

1.1 树的概念

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

树是递归定义的,每一棵树都是由 根+多个子树 构成,每棵子树也是一样的构成

注意:

树中的子树是不相交的

除了根节点外,每个节点有且仅有一个父节点

一颗N个节点的树有N-1条边

树中的基本概念:

节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6

叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I...等节点为叶节点

非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G...等节点为分支节点

双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点

孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点

兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点

树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6

节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;

树的高度或深度:树中节点的最大层次; 如上图:树的高度为4

堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点

节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先

子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙

森林:由m(m>0)棵互不相交的树的集合称为森林;

1.2 树的表示

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,实际中树有很多种表示方式,如:双亲表示法,孩子表示法、孩子兄弟表示法等等。这里就简单的了解其中最常用的孩子兄弟表示法

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

这种方式的好处在于:无论树中的节点有多少个孩子,都可以表示。因为这个节点只会指向自己的第一个孩子,剩下的孩子,让孩子之间用兄弟指针串联起来,需要访问时也可以访问。

双亲表示法:由一个数组把所有节点及其双亲的下标存起来(不实用,因为不适合插入删除数据)

如上图的双亲表示为:

2. 二叉树概念及结构

2.1 二叉树概念

一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。

二叉树的特点

① 每个结点最多有两棵子树,即二叉树不存在度大于2的结点。

② 二叉树的子树有左右之分,其子树的次序不能颠倒。

二叉树的性质:

1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多2^(i-1) 个结点

2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数2^h- 1

3. 对任何一棵二叉树, 如果度为0其叶结点个数为 n0, 度为2的分支结点个数为 n2,则有n0=n2+1

4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log2(n+1). (ps:log2(n+1)是log以2为底,n+1为对数)

5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:

① 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点

② 若2i+1=n否则无左孩子

③ 若2i+2=n否则无右孩子

2.2 特殊的二叉树

① 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。

② 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时(即前K-1层都是满的,最后一层可以不满但从左到右必须连续,且度为1的节点最多1个)称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树

2.3 二叉树的存储结构

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构

2.3.1 顺序存储

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树

2.3.2 链式存储

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

3. 堆的概念及结构

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki <= K2i+2(Ki >= K2i+1 且Ki >= K2i+2), 则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆(父亲大于等于孩子),根节点最小的堆叫做最小堆或小根堆(父亲小于等于孩子)

堆的性质:

① 堆中某个节点的值总是不大于或不小于其父节点的值

② 堆总是一棵完全二叉树。

③ 假设父亲的下标是parent,左孩子下标:leftchild = parent*2 + 1,右孩子下标:rightchild = parent*2 + 2;父亲下标parent = (child - 1)/ 2(向下取整)

 

3.1 堆的实现

3.1.1 堆向下调整算法

现在我们给出一个数组,逻辑上看做一棵完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

int a[] = {27,15,19,18,28,34,65,49,25,37};

该完全二叉树的特点在于:根节点的左子树和右子树都是小根堆在满足以上特点的情况下,如何将整棵树调整成小根堆呢?我们使用堆向下调整算法

步骤:

① 选出左右孩子中小的节点(15)

② 用该节点和父亲比

a. 若该节点比父亲小,则和父亲交换,并且把该节点原来的位置当成父亲继续向下调整。直到父亲走到叶子节点

b. 如果小的孩子比父亲大,则不需要处理,调整完成,整棵树已经是小根堆

此时向下调整算法的实现(前提是根节点的左子树和右子树都是小根堆或大根堆):

void Swap(int *p1, int *p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向下调整算法
void AdjustDown(int* a, int n, int parent) //传参为:数组,数组元素个数,双亲节点的下标
{
	//找出左右孩子中小的节点
	int child = parent * 2 + 1; //默认小的节点是左孩子
	while (child < n)//当下标超出了数组范围,说明该节点不存在
	{
		//选出左右孩子中小的节点(这样写的好处在于,当该if语句执行以后child就是小的节点,不需要再讨论左孩子右孩子谁大谁小)
		//*需要注意当双亲节点只有左孩子没有右孩子时,存在越界问题,所以要在判断条件里加一句chile+1 < n
		if (child + 1 < n && a[child + 1] < a[child])
		{
			++child;
		}

		if (a[child] < a[parent])//如果孩子小于双亲,交换,并让孩子原来的位置变成双亲结点
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

当根节点左右子树不是小根堆的时候,又该怎么处理?

答:从倒数的第一个非叶子节点(最后一个节点的父亲,其下标可由最后一个节点下标推出(n-1-1)/2),从后往前(每次调整完就让该非叶子节点减一(因为非叶子节点是挨着的,每次都减1就可以访问所有非叶子节点)),按编号,以此作为子树向下调整

void Swap(int *p1, int *p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向下调整算法
void AdjustDown(int* a, int n, int parent) //传参为:数组,数组元素个数,双亲节点的下标
{
	//找出左右孩子中小的节点
	int child = parent * 2 + 1; //默认小的节点是左孩子
	while (child < n)//当下标超出了数组范围,说明该节点不存在
	{
		//选出左右孩子中小的节点(这样写的好处在于,当该if语句执行以后child就是小的节点,不需要再讨论左孩子右孩子谁大谁小)
		//*需要注意当双亲节点只有左孩子没有右孩子时,存在越界问题,所以要在判断条件里加一句chile+1 < n
		if (child + 1 < n && a[child + 1] < a[child])//如果要调成大根堆则选左右孩子中大的节点即可,特别注意要先判断 child + 1 < n
		{
			++child;
		}

		if (a[child] < a[parent])//如果孩子小于双亲,交换,并让孩子原来的位置变成双亲结点(若想调整为大根堆则改成a[child] > a[parent]即可)
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else//当child小于parent则结束循环,没有这一句,当child<parent时会一直进行while死循环
		{
			break;
		}
	}
}

int main()
{
	/*int a[] = { 27,15,19,18,28,34,65,49,25,37 };//根节点的左右子树均为小根堆的情况
	int n = sizeof(a) / sizeof(a[0]);
	AdjustDown(a, n, 0);*/

	//随机数组调整成小根堆
	int a[] = { 15, 18, 28, 34, 65, 19, 49, 25, 37, 27 };
	int n = sizeof(a) / sizeof(a[0]);

	//建堆
	int i = 0;
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	return 0;
}

3.1.2 堆排序

void Swap(int *p1, int *p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向下调整算法
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}

		if (a[child] > a[parent])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//堆排序(升序) 步骤:1.建堆O(N) 2.选数
//排升序,建大堆
//为什么升序不能建小堆? 
//建小堆选出了最小的数,需要O(N)的时间,紧接着如何选次小的数呢?
//剩下的N-1个数继续建堆找次小的(剩下的节点父子关系全乱了所以只能重新建堆才能选出次小的),以此类推,最终时间复杂度为O(N^2),效率太低
void HeapSort(int* a, int n)
{
	//建大堆 (时间复杂度为O(N))
	int i = 0;
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	//建大堆后,让数组第一个和最后一个元素互换(最大的到了最后),紧接着选次大的,此时不把最后一个元素看在堆里面,(此时对于根节点,左右子树都是大堆,则进行一次向下调整即可)向下调整就能选出次大的,以此类推(向下调整的次数为树的高度次 O(logN))
	//即用这种方法,调整后父子间的关系没变,不需要每一次调整后重新建堆
	int end = n - 1;
	while (end)
	{
		Swap(&a[0], &a[end]);
		//选出次大的
		AdjustDown(a, end, 0);
		end--;
	}
}

int main()
{
	int a[] = { 15, 18, 28, 34, 65, 19, 49, 25, 37, 27 };
	int n = sizeof(a) / sizeof(a[0]);

	HeapSort(a, n);

	return 0;
}

堆排序算法性能分析:

空间效率:仅使用了常数个辅助单元,所以空间复杂度为O(1)

时间效率:建堆时间为O(n),之后有 n-1 次向下调整操作,每次调整的时间复杂度为O(h),所以堆排序的时间复杂度为O(nlogn)

3.1.3 堆的实现

Heap.h

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
#include <string.h>

typedef int HPDataType;

//大堆
struct Heap
{
	HPDataType* a;
	int size;     //当前数组元素个数
	int capacity; //容量
};

typedef struct Heap HP;

//初始化
void HeapInit(HP* php, HPDataType* a, int n);/*也可以这样定义 HP* HeapInit(HPDataType* a, int n);*/
//销毁
void HeapDestroy(HP* php);
//插入
void HeapPush(HP* php, HPDataType x);
//删除(堆顶数据)
void HeapPop(HP* php);
//返回堆顶数据
HPDataType HeapTop(HP* php);
//堆中数据个数
int HeapSize(HP* php);
//判空
bool HeapEmpty(HP* php);
//打印堆
void HeapPrint(HP* php);

void Swap(int* p1, int* p2);
//向下调整算法
void AdjustDown(int* a, int n, int parent);
//向上调整算法
void AdjustUp(int* a, int child);
Heap.c

#include "Heap.h"

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向下调整算法
void AdjustDown(HPDataType* a, int n, int parent)
{
	HPDataType child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])//特别注意这里要先判断child+1<n
		{
			++child;
		}

		if (a[child] > a[parent])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//向上调整算法
//由于是大根堆,沿着路径,只要孩子比双亲结点大,就交换数据,直到比较到根节点终止
void AdjustUp(int* a, int child)
{
	HPDataType parent = (child - 1) / 2;
	while (child > 0) 
	//注意:判断条件写parent>=0勉强正确,因为当child = 0,parent = -1/2 = 0,下次进入循环时退出
	//但如果下列条件变成了a[child] >= a[parent]时交换,则会死循环,所以最好写child > 0
	{
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//初始化
void HeapInit(HP* php, HPDataType* a, int n)
{
	assert(php);
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	memcpy(php->a, a, sizeof(HPDataType) * n); //把数组的内容拷贝到堆里的数组中去
	php->size = n;
	php->capacity = n;

	//建堆
	for (int i = (php->size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(php->a, php->size, i);
	}

}

//销毁
void HeapDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

//打印堆
void HeapPrint(HP* php)
{
	int i = 0;
	for (i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
	
	//分层打印
	int num = 0;
	int j = 0;
	int levelSize = 1; //堆每层的元素个数
	for (j = 0; j < php->size; j++)
	{
		printf("%d ", php->a[j]);
		num++;
		if (num == levelSize)
		{
			printf("\n");
			levelSize *= 2;
			num = 0;
		}
	}
	printf("\n\n");
}

//插入
void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	//如果满了则需要增容
	if (php->size == php->capacity)
	{
		HPDataType* tmp = (HPDataType*)realloc(php->a, php->capacity * 2 * sizeof(HPDataType));
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity *= 2;
	}

	//插入(插入数据后还要保证仍是堆 —— 沿着插入节点到根的路径向上调整)
	//由于是大根堆,沿着路径,只要孩子比双亲结点大,就交换数据,直到比较到根节点终止
	
	//插入数据
	php->a[php->size] = x;
	php->size++;

	//向上调整
	AdjustUp(php->a, php->size-1);
}

//删除(堆顶数据)
//类似于堆排序的思路:把堆顶元素与最后一个元素互换,删除最后一个元素,再让堆顶进行一次向下调整
void HeapPop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	Swap(&php->a[0], &php->a[php->size - 1]);//交换
	php->size--;//删除换到最后的原堆顶数据
	AdjustDown(php->a, php->size, 0);//向下调整
}

//返回堆顶数据
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

//堆中数据个数
int HeapSize(HP* php)
{
	assert(php);

	return php->size;
}

//判空
bool HeapEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}
test.c

#include "Heap.h"

//测试堆的创建销毁、增删查改等
int main()
{
	int a[] = { 15, 18, 28, 34, 65, 19, 49, 25, 39, 27 };
	int n = sizeof(a) / sizeof(a[0]);

	HP hp;
	HeapInit(&hp, a, n);
	//HeapPrint(&hp); //65 39 49 27 19 28 25 15 18

	HeapPush(&hp, 8);
	//HeapPrint(&hp); //65 39 49 34 27 19 28 25 15 18 8

	HeapPush(&hp, 88);
	//HeapPrint(&hp); //88 39 65 34 27 49 28 25 15 18 8 19

	HeapPop(&hp);
	//HeapPrint(&hp);//65 39 49 34 27 19 28 25 15 18 8

	int ret = HeapTop(&hp);
	//printf("%d\n", ret); //65



	HeapDestroy(&hp);
	return 0;
}

3.1.4 TopK问题

题目描述:找出N个数里面最小的前K个 (如果需要找最大的前K个建大根堆就行了)

/*
//把N个数建成小根堆,每次返回堆顶元素再删除,执行k次
int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize)
{
	HP hp;
    //建一个小根堆
    HeapInit(&hp, arr, arrSize);//需要修改前面向下调整代码
    int* retArr = (int*)malloc(sizeof(int)*k);

    for(int i = 0;i < k; i++)
    {
        retArr[i] = HeapTop(&hp);
        HeapPop(&hp);
    }

    HeapDestroy(&hp);
    *returnSize = k;

    return retArr;
}
//时间复杂度:O(n+logN*k),空间复杂度:O(N)
//该算法存在的问题是,当arrSize很大k很小的时候,光是建堆就花了非常大的空间,空间效率很差
*/

//效率最高的方法:时间复杂度O(n*logk),空间复杂度O(k)
//先把数组中前k个数据建成大堆,剩下的n-k个数,跟堆顶数据比较,如果比堆顶数据小,则替换堆顶的数据再向下调整,最终堆里就是最小的K个数
//最小的k个数中的任意一个一定比堆顶小(因为是大堆),就可以进堆
int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize)
{
	if(k == 0)//如果k等于0,malloc的时候就会出问题
	{
		*returnSize = 0;
		return NULL;
	}

	int* arrRet = (int*)malloc(sizeof(int)*k);

	//前k个数建大堆
	for(int i = 0; i < k; i++)
	{
		arrRet[i] = arr[i];
	}
	for(int j = (k-1-1)/2; j >= 0; j--)
	{
		AdjustDown(arrRet, k, j);
	}

	//剩下的n-k个数,比堆顶元素小就替换堆顶数据,进堆,再向下调整
	for(int i = k;i < arrSize; i++)
	{
		if(arr[i] < arrRet[0])
		{
			arrRet[0] = arr[i];
			AdjustDown(arrRet, k, 0);
		}
	}
	*returnSize = k;

	return arrRet;
}

4. 二叉树链式结构的实现

4.1 二叉树链式结构的遍历

所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算的基础。

前序/中序/后序的递归结构遍历:(根据访问结点操作发生位置命名)

1. NLR:前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。

2. LNR:中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。

3. LRN:后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。

由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为 根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历

层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历

"Test.c"

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include "Queue.h"

typedef char BTDataType;

typedef struct BinaryTreeNode
{
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
	BTDataType data;
}BTNode;

//前序遍历
void PrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%c ", root->data);
	PrevOrder(root->left);
	PrevOrder(root->right);
}
//中序遍历
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	InOrder(root->left);
	printf("%c ", root->data);
	InOrder(root->right);
}
//后序遍历
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%c ", root->data);
}

//层序遍历
void TreeLevelOrder(BTNode* root)
{
	Queue q;
	//注:此时队列的data里放的是树节点的指针
	QueueInit(&q);
	if (root)
	{
		QueuePush(&q, root);
	}

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		printf("%c ", front->data);
		
		if (front->left)
		{
			QueuePush(&q, front->left);
		}
		if (front->right)
		{
			QueuePush(&q, front->right);
		}
	}
	printf("\n");

	QueueDestroy(&q);
}

//创建一个二叉树节点
BTNode* CreateTreeNode(BTDataType x)
{
	BTNode* node = malloc(sizeof(BTNode));
	node->data = x;
	node->left = NULL;
	node->right = NULL;
	return node;
}

//求树中节点个数
//思路一:遍历计数的方式 (为了保证每次递归使用的是同一个size所以此处传的是指针,且每次调用该函数前要先把size置0)
//void TreeSize(BTNode* root, int* psize)
//{
//	if (root == NULL)
//	{
//		return ;
//	}
//	++(*psize);
//	TreeSize(root->left, psize);
//	TreeSize(root->right, psize);
//}

//思路二: 子问题拆解(分治算法)
//1. 如果为空 0个节点
//2. 如果非空 左子树节点个数 + 右子树节点个数 + 1(自己)
int TreeSize(BTNode* root)
{
	return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}

//求树中叶子节点的个数 (分支思想)
//1. 空树           return 0
//2. 叶子           return 1
//3. 非空且不是叶子 return 左子树叶节点个数+右子树叶节点个数
int TreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
	
}

//求树中第k层结点个数(k > 0)
//可划分为求根节点左、右子树的第k-1层之和,以此类推,直到k = 1时,相当于只有一个根结点
//1. 空树                return 0
//2. k划分到了1          return 1
//3. 非空且k还未划分到1  return 左子树的第k-1层节点数 + 右子树的第k-1层节点数 
int TreeKLevelSize(BTNode* root, int k)
{
	if (root == NULL)
	{
		return 0;
	}

	if (k == 1)
	{
		return 1;
	}
	
	return TreeKLevelSize(root->left, k-1) + TreeKLevelSize(root->right, k-1);
}

//查找树中值为x的那个结点
//思路:
//1.root == NULL return NULL
//2.root节点不是要找的那个结点,先到左树去找,如果没有在再到树去找
//3.左右都没有,返回NULL
BTNode* TreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}

	if (root->data == x)
	{
		return root;
	}
	
	BTNode* lret = TreeFind(root->left, x);  //记录左子树查找操作,避免重复操作
	BTNode* rret = TreeFind(root->right, x); //记录右子树查找
	//如果在左子树中找到了,返回
	if (lret)
	{
		return lret;
	}
	//右子树找到了,返回
	if(rret)
	{
		return rret;
	}
	//左右都没找到,返回空
	return NULL;
}

//二叉树的销毁
//遍历然后挨个释放空间(用后序遍历)
//传入一级指针存在野指针问题,调用该函数后,需把root手动置空
//void BinaryTreeDstory(BTNode** pproot)//传二级指针是为了在最后把root置空,一级指针改变不了root,但为了保持接口一致性还是传一级指针
void BinaryTreeDstory(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}

	//递归销毁左树
	BinaryTreeDstory(root->left);
	//递归销毁右树
	BinaryTreeDstory(root->right);
	//销毁根
	free(root);
	//调用该函数后,需把root手动置空
}

//判断二叉树是否为完全二叉树
//回顾:完全二叉树前n-1层是满的,最后一层从左到右连续
//思路:1.借助层序遍历(区别在于,左右孩子如果是空结点也入队),按照层序遍历走,节点是连续的
//2.如果是完全二叉树,当队列出的节点是NULL时,队列里的剩余节点应全部为NULL
bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
	{
		QueuePush(&q, root);
	}

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		//队头出队
		QueuePop(&q);
		if (front == NULL)//如果此时队头为空,则跳出循环,对队列剩下节点进行检查
		{
			break;
		}
		//入队头的左右孩子
		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
	}
	//检查队列剩余结点
	while(!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front)//如果有非空元素,说明不是完全二叉树
			return false;
	}

	QueueDestroy(&q);
	return  true; //队内全是空,说明是完全二叉树
}

int main()
{
	BTNode* A = CreateTreeNode('A');
	BTNode* B = CreateTreeNode('B');
	BTNode* C = CreateTreeNode('C');
	BTNode* D = CreateTreeNode('D');
	BTNode* E = CreateTreeNode('E');
	BTNode* F = CreateTreeNode('F');

	A->left = B;
	A->right = C;
	B->left = D;
	C->left = E;
	C->right = F;

	PrevOrder(A);  //A B D NULL NULL NULL C E NULL NULL F NULL NULL
	printf("\n");
	InOrder(A);    //NULL D NULL B NULL A NULL E NULL C NULL F NULL
	printf("\n");
	PostOrder(A);  //NULL NULL D NULL B NULL NULL E NULL NULL F C A
	printf("\n");

	思路一的方式计算树中节点个数
	//int size = 0;
	//TreeSize(A, &size);
	//printf("TreeSize = %d\n", size);
	//size = 0;
	//TreeSize(A, &size);
	//printf("TreeSize = %d\n", size);

	//思路二的方式计算树中节点个数
	printf("TreeSize = %d\n", TreeSize(A));  //6
	printf("TreeSize = %d\n", TreeSize(A));  //6

	printf("TreeLeafSize = %d\n", TreeLeafSize(A)); //3

	printf("TreeKLevelSize = %d\n", TreeKLevelSize(A, 3)); //3

	//查找成功和查找失败
	printf("TreeFind:%p\n", TreeFind(A, 'E'));//00C6C5C8
	printf("TreeFind:%p\n", TreeFind(A, 'X'));//00000000

	//层序遍历
	TreeLevelOrder(A); //A B C D E F

	//判断完全二叉树
	printf("BinaryTreeComplete:%d\n", BinaryTreeComplete(A)); //BinaryTreeComplete:0

	BinaryTreeDstory(A);

	return 0;
}
"Queue.h"

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>

//前置声明
struct BinaryTreeNode;

typedef struct BinaryTreeNode* QDataType;

//队列中的一个结点
typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType data;
}QueueNode;

//队列(由于需要两个指针,所以用结构体定义)
typedef struct Queue
{
	QueueNode* head; //头指针
	QueueNode* tail; //尾指针
}Queue;

//初始化
void QueueInit(Queue* pq);
//销毁
void QueueDestroy(Queue* pq);
//入队
void QueuePush(Queue* pq, QDataType x);
//出队
void QueuePop(Queue* pq);
//取队头数据
QDataType QueueFront(Queue* pq);
//取队尾数据
QDataType QueueBack(Queue* pq);
//判空
bool QueueEmpty(Queue* pq);
//计算队列元素个数
int QueueSize(Queue* pq);
"Queue.c"

#include "Queue.h"


//初始化
void QueueInit(Queue* pq)
{
	assert(pq);
	//不带哨兵位
	pq->head = pq->tail = NULL;
}

//销毁
void QueueDestroy(Queue* pq)
{
	assert(pq);

	QueueNode* cur = pq->head;
	while (cur)
	{
		QueueNode* next = cur->next;
		free(cur);
		cur = next;
	}

	pq->head = pq->tail = NULL;
}

//判空
bool QueueEmpty(Queue* pq)
{
	assert(pq);

	return pq->head == NULL; //等于空就为真, 不为空就是假
}


//入队
void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);
	QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
	if (newnode == NULL)//申请空间失败
	{
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;

	if (pq->tail == NULL)
	{
		pq->head = pq->tail = newnode;
	}
	else
	{
		pq->tail->next = newnode;
		pq->tail = newnode;
	}

}

//出队
void QueuePop(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));//空队列也不能调用出队操作

	if (pq->head->next == NULL)//只有一个结点的情况(如果不单独考虑,那当只有一个结点时,tail会仍然指向曾经的队尾)
	{
		free(pq->head);
		pq->head = pq->tail = NULL;
	}
	else
	{
		QueueNode* next = pq->head->next;
		free(pq->head);
		pq->head = next;
	}
}

//取队头数据
QDataType QueueFront(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	return pq->head->data;
}

//取队尾数据
QDataType QueueBack(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	return pq->tail->data;
}

int QueueSize(Queue* pq)
{
	int size = 0;
	QueueNode* cur = pq->head;

	while (cur)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

4.2 二叉树的基础面试题

4.2.1 二叉树最大深度

题目描述:给定一个二叉树,找出其最大深度(二叉树的深度为根节点到最远叶子节点的最长路径上的节点数)。

//和分治求结点个数思路一致,可把问题分解为
//根节点左子树深度与右子树深度的最大值 + 1 
int maxDepth(struct TreeNode* root)
{
    if(root == NULL)
    {
        return 0;
    }
    int leftDepth = maxDepth(root->left);
    int rightDepth = maxDepth(root->right);
    return leftDepth > rightDepth? leftDepth + 1 : rightDepth + 1;
    
    /*return maxDepth(root->left) > maxDepth(root->right)? maxDepth(root->left)+1 : maxDepth(root->right)+1;*/
    //如果树高度很大,这种写法的效率会变得非常低(解决办法:用变量把递归的结果存起来,防止重复计算)
    //或者 return fmax( maxDepth(root->left), maxDepth(root->right) ) + 1;
}

4.2.2 单值二叉树

题目描述:如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树。

只有给定的树是单值二叉树时,才返回 true;否则返回 false

bool isUnivalTree(struct TreeNode* root)
{
    if(root == NULL)
        return true;

    if(root->left && root->left->val != root->val)
        return false;

    if(root->right && root->right->val != root->val)
        return false;
    
    //递归
    return isUnivalTree(root->left) && isUnivalTree(root->right);
}

4.2.3 二叉树的前序遍历

题目描述:给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

int TreeSize(struct TreeNode* root)
{
    return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}

//preorderTraversal的子函数,避免每次递归都重新申请空间
void _preorder(struct TreeNode* root, int* arr, int* pi)
{
    if(root == NULL)
        return;
    
    arr[(*pi)++] = root->val;
    //易错点:如果直接写arr[i++],那么每次递归调用的并不是同一个i,返回上一层的时候,i并不会继续++而是上一次的i的值
    
    //递归访问左右子树
    _preorder(root->left, arr, pi);
    _preorder(root->right, arr, pi);
}

int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
    *returnSize = TreeSize(root);
    int* arr = malloc(sizeof(int) * (*returnSize));
    int i = 0;
    _preorder(root, arr, &i);

    return arr;
}

4.2.4 相同的树

题目描述:给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
    //都为空
    if(p == NULL && q== NULL)
        return true;
    
    //到这一行,已经没有都为空的情况了,如果为真,说明一定有一个为空
    if(p == NULL || q == NULL)
        return false;
    
    //如果值不相等,返回false(相等不能返回true,因为还要继续判断该节点的左右子树)
    if(p->val != q->val)
        return false;

    return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
//*心得:当比较结果是相等,但并不能得出结论并返回true的时候,就判断不相等,返回false
//eg:当p->val == q->val(两个结点的值相等),也不能说明两棵树就是相同的,不能返回true,但如果结点值不相等,可以说明一定不是相同的树

4.2.5 翻转二叉树

 题目描述:给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

struct TreeNode* invertTree(struct TreeNode* root)
{
    if(root == NULL)
        return NULL;

    struct TreeNode* right = root->right;
    root->right = invertTree(root->left);
    root->left = invertTree(right);

    return root;
}

4.2.6 对称二叉树

题目描述:给你一个二叉树的根节点 root , 检查它是否轴对称。

//子函数,判断左右子树是否轴对称
bool _isSymmetric(struct TreeNode* left, struct TreeNode* right)
{
    if(left == NULL && right == NULL)
        return true;

    if(left == NULL || right == NULL)
        return false;

    if(left->val != right->val)
        return false;
    //走到这一步 left和right已经相等了
    //*最重要的一步,不是比较某个结点,而是对子树进行比较
    return _isSymmetric(left->left, right->right)
        && _isSymmetric(left->right, right->left);
}

bool isSymmetric(struct TreeNode* root)
{
    if(root == NULL)
        return true;
    
    return _isSymmetric(root->left, root->right);
}

注:本题还可用:翻转二叉树,然后判断两棵树是否相等来解决。

4.2.7 另一棵树的字树

题目描述:

给你两棵二叉树 root subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false

二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。

//判断两棵树是否相同
bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
    //都为空
    if(p == NULL && q== NULL)
        return true;
    
    //到这一行,已经没有都为空的情况了,如果为真,说明一定有一个为空
    if(p == NULL || q == NULL)
        return false;
    
    //如果值不相等,返回false(相等不能返回true,因为还要继续判断该节点的左右子树)
    if(p->val != q->val)
        return false;

    return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}

//如果subRoot是root的子树,那么只要只需要subRoot和root的某个子树相同即可
//也就是说,让subRoot和root的所有子树比较一遍,有相等即可
//这样,问题就变成了,如何找出root的所有子树?
//=>所有节点做根节点,都是root的子树 =>怎么拿到所有节点? => 遍历(前序好一点,因为前面的已经相等了,后面就不需要比较了)
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot)
{
    //遍历root这棵树的每一个节点,每个节点作为子树去和subRoot作比较
    if(root == NULL)
        return false;
    //每个节点都会作为子树的根在这行出现与subRoot比较
    if(isSameTree(root, subRoot))
        return true;
    return isSubtree(root->left, subRoot) 
    || isSubtree(root->right, subRoot);

}

4.2.8 平衡二叉树

题目描述:给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

/*方法一
//判断二叉树高度
int maxDepth(struct TreeNode* root)
{
    if(root == NULL)
    {
        return 0;
    }
    int leftDepth = maxDepth(root->left);
    int rightDepth = maxDepth(root->right);
    return leftDepth > rightDepth? leftDepth+1 : rightDepth + 1;
}

bool isBalanced(struct TreeNode* root)
{
    //先判断当前节点为根节点的树满不满足条件,然后遍历每一个节点的树
     if(root == NULL)
        return true;
    
    int leftDepth = maxDepth(root->left);
    int rightDepth = maxDepth(root->right);

    return abs(leftDepth - rightDepth) < 2 //当前的树
    && isBalanced(root->left)              //所有的子树
    && isBalanced(root->right);
}
//注: 这是前序遍历的方法,存在大量的重复计算。
//isBalanced递归了N次(节点个数),每次递归N+N/2+N/2+N/4+N/4...次(假设是满二叉树)
//时间复杂度为O(N^2)
*/


//而用后续遍历可以大大降低时间复杂度到O(N)
//方法二:
bool _isBalanced(struct TreeNode* root, int* ph)
{
    if(root == NULL)
    {
        *ph = 0;
        return true;
    }
 
    //判断左子树是否平衡
    int leftHight = 0;
    if(_isBalanced(root->left, &leftHight) == false)
        return false;

    //判断右子树是否平衡
    int rightHight = 0;
    if(_isBalanced(root->right, &rightHight) == false)
        return false;

    //**把高度带给树上一层的父亲
    *ph = fmax(leftHight, rightHight) + 1;
    
    //运行到这里说明左右都是平衡的,且有了左右子树的高度,对整棵树进行判读
    return abs(leftHight - rightHight) < 2;
}


bool isBalanced(struct TreeNode* root)
{
    int hight = 0;
    return _isBalanced(root, &hight);
}

4.2.9 二叉树的构建和遍历

题目描述:编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。 例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。

示例:

输入:(输入包括1行字符串,长度不超过100)

abc##de#g##f###

输出:

c b e g d f a 
#include <stdio.h>
#include <stdlib.h>

typedef struct TreeNode
{
    struct TreeNode* left;
    struct TreeNode* right;
    char val;
}TreeNode;

TreeNode* CreateTree(char* str, int* pi)
{
    //空树
    if(str[*pi] == '#')
    {
        (*pi)++;
        return NULL;
    }
    
    //不是#,构建根
    TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));
    root->val = str[*pi];
    (*pi)++;
    
    //递归构建左子树
    //在遇到#之前一直在建立节点,直到遇到#才return,当左右都return后由root->left接收,完成左边的“链接”
    root->left = CreateTree(str, pi);
    //递归构建右子树
    root->right = CreateTree(str, pi);
    
    //左右子树构建好后,return返回到上一个节点,完成与父亲的“链接”
    return root;
    
}

void Inorder(TreeNode* root)
{
    if(root == NULL)
        return;
    
    Inorder(root->left);
    printf("%c ", root->val);
    Inorder(root->right);
}

int main()
{
    char str[100];
    scanf("%s", str);
    
    int i = 0;
    TreeNode* root = CreateTree(str, &i);
    Inorder(root);
    printf("\n");
    
    return 0;
}
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

舟叶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值