【数据结构】堆和二叉树详解

一、树的概念和相关定义

1.树的概念

  树是一种非线性结构。由于树的逻辑结构像一棵倒立的树而得名。树是由根节点以及它的子树组成,子树也是树,所以树是递归定义的

  不论哪一个节点,其子树不能有交集每个节点有且仅有一个父节点(根节点除外,根节点没有前驱节点。

2.树的相关定义

【数据结构】堆和二叉树详解_第1张图片

节点的度:节点的子树的个数。例如A的度为6。

树的度:树中所有节点的度的最大值。例如上面的树的度为6.

叶节点(终端节点):度为0的节点。例如P是叶节点。

分支节点(非终端节点): 度不为0的节点。除了叶节点之外都是分支节点。

父节点(双亲结点): 所有结点都是其子树的根节点的父节点。例如A是B的父节点。

子节点(孩子节点):所有节点都是父节点的子节点。例如B是A的子节点。

兄弟节点:有相同父节点的节点。例如B,C,D,E是兄弟节点。

唐兄弟节点:在树的同一层且父节点不同的节点。例如H,I是唐兄弟节点。

节点的祖先:从根节点开始到这个节点要“经历”的所有结点都是该节点的祖先(包括根结点本身)

节点的层次:根节点的层次为1,依此类推。例如P的层次是4。

树的高度(深度):数中节点层次的最大值。例如上面的树的高度为4。

子孙:以某节点为根节点的树的除根节点外的所有节点都是根节点的子孙。如上树中所有节点都是A的子孙(除A外)。

森林:由m(m>0)棵互不相交的树组成。

二、二叉树

1.二叉树

二叉树是一种特殊的树,每个节点最多只有两棵子树。为空树或者由根节点和左子树右子树组成。

【数据结构】堆和二叉树详解_第2张图片

 二叉树节点的度都在[0,2]这个区间内。二叉树的左子树和右子树的顺序不可交换,故二叉树是有序树。

2.完全二叉树

  完全二叉树是一种特殊的二叉树。高度为h的完全二叉树从层数为1到层数为h-1的结点都是满的,即每一层的上一层节点都有左子树和右子树。第h层的节点从左到右必须是连续的,即第一个为空的节点的前面所有节点都不为空,其后所有的节点都为空。

【数据结构】堆和二叉树详解_第3张图片

   满二叉树是特殊的完全二叉树,满二叉树的每一层节点的父节点都有且仅有两棵子树,即左子树和右子树。高度为h的满二叉树的节点为2^h - 1个。

三、二叉树的特点

(1)二叉树的性质

1.规定根节点所在层为第一层,则第i层节点最多有2^(i-1)个。

2.高度为h 的二叉树的节点数最多为2^h - 1个。

3.节点数为N的满二叉树的高度为log以2为底的(N+1)的对数。

4.设二叉树度为0,1,2的根节点数分别为n0,n1,n2。则n0 = n2+1

5.对一个N个节点的完全二叉树,按照从上到下从左到右的顺序给节点从0开始编号。

则该节点的父节点的编号为(i-1)/2,根节点没有父节点;

左右孩子节点的编号分别是(2*i+1)和(2*i+2),若(2*i+1)>=N,则该节点没有子树,若(2*i+2)>=N,则该节点有左子树但是没有右子树。

(2)二叉树的结构

1.顺序存储(完全二叉树)

    二叉树的逻辑结构是树状结构。顺序存储的二叉树物理结构是一维数组.按从上到下从左到右的顺序存储,数组的大小为N(二叉树节点个数)。一般数组只适合存储完全二叉树。

例如:

【数据结构】堆和二叉树详解_第4张图片

2.链式存储

   二叉树的链式结构是指用链表表示二叉树。链表的节点表示二叉树的节点,每个节点存储三个数据,即该节点的值,左孩子和右孩子的地址(二叉链,二叉树用该种链式存储,后面要学到的红黑树等高阶数据结构使用三叉链)。

  二叉链节点的数据部分包括该节点的值和左右孩子节点的地址;三叉链的数据部分包括该节点的值和父节点以及左右孩子节点的地址。

二叉链节点:

【数据结构】堆和二叉树详解_第5张图片

 三叉链节点:

【数据结构】堆和二叉树详解_第6张图片

四、二叉树的实现

1.顺序存储结构二叉树的实现

1.1堆的存储

  一般只用数组来存储完全二叉树,因为其他类型的树会导致数组的空间浪费。而实际运用一般只用数组来存储堆

  堆(一种特殊的二叉树)可以分为最大堆和最小堆,也叫大根堆和小根堆,最大堆是指所有父节点的值都比孩子节点要大的二叉树,最小堆是指所有父节点的值都比孩子节点小的二叉树。由此可知,任何一个数组都可以看作一棵完全二叉树,但不一定是堆;堆的逻辑结构是完全二叉树,堆的物理结构是数组。

1.2堆的实现

1.2.1堆的向下调整算法(时间复杂度: O(N))

   向下调整指的是父节点“向下”与孩子节点进行值比较,必要时进行值交换(调整)。向下调整是将根节点向下调整,最多可以调整到最下面的节点处(这里根节点是相对而言的,每一个被视为树的子结构的第一层的节点都成为其根节点)。

   堆的向下调整算法的条件是左子树和右子树都是标准的堆结构(同为大堆或小堆,这里以大堆举例说明)。若需将一个普通的完全二叉树调整为大堆,则需使用循环,从最右下方的子树的父节点开始,从右向左,从下到上依次进行调整。

 注意:如若向下调整时两个孩子节点均比父节点大,父节点需与较大的孩子节点进行交换。

举例:

【数据结构】堆和二叉树详解_第7张图片

 代码:

//向下调整(以大堆为例)
void AdjustDown(HPDataType* a, int n, int parent)
{
	assert(a);
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n)
		{
			child = a[child] > a[child + 1] ? child : child + 1;
		}
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

  

1.2.2堆的向上调整算法(时间复杂度:O(NlogN))

  条件:从第二个结点开始插入。

  若需将一个完全二叉树调整为堆结构,则需从第二个节点开始,每个节点都需先插入后向上调整。

  综上,想要实现堆的创建,首先要有一个完全二叉树(可用拷贝数组的方法实现),然后在对其进行向下调整或者向上调整,从而达到建堆的目的。

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

  由于二叉树是递归定义的,且链式存储时,链表的节点存储了节点的值和左右孩子节点的地址,故二叉树的定义和实现可以用递归来实现。

2.1二叉树的遍历

  二叉树的遍历包括前/中/后序的递归遍历以及层序遍历。

前序遍历:又名先根遍历,遍历顺序为根-->左子树-->右子树

中序遍历:又名中根遍历,遍历顺序为左子树-->根-->右子树

后序遍历:又名后根遍历,遍历顺序为左子树-->右子树-->根

层序遍历:顾名思义,一层一层遍历,从上往下、从左往右遍历。

举例:

分别写出各种遍历方法遍历以下二叉树的得到的值序列。

【数据结构】堆和二叉树详解_第8张图片

前序:1->2->4->7->NULL->NULL->NULL->NULL->3->5->8->NULL->NULL->NULL->6->NULL

->NULL      (12473586)

中序:NULL->7->NULL->4->NULL->2->NULL->1->NULL->8->NULL->5->NULL->3->NULL->6

->NULL      (74218536)

后序:NULL->NULL->7->NULL->4->NULL->2->NULL->NULL->8->NULL->5->NULL->NULL->6

->3->1        (74285631)

层序:12345678

前序遍历递归图解:

【数据结构】堆和二叉树详解_第9张图片

代码(前序遍历):

//前序遍历
//中序和后续遍历与之大同小异,只需调整后三行代码的顺序即可
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	printf("%d ", root->data);
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}

层序遍历:

思路:借助队列,利用其先进先出的特点,将队列的节点数据类型改为二叉树的节点类型,并在删除一个节点后将其左右节点插入队列,如此反复,知道队列为空则遍历结束。删除的节点顺序即为层序遍历的结果。

代码:

void BinaryTreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if(root != NULL)
	    QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		printf("%c ", front->_data);

		QueuePop(&q);
		if (front->_left)
		{
			QueuePush(&q, front->_left);
		}
		if (front->_right)
		{
			QueuePush(&q, front->_right);
		}
	}
	printf("\n");
	QueueDestroy(&q);
}

五、二叉树接口函数的代码实现

1.堆

接口函数的实现:

//初始化堆
void HeapInit(Heap* php)
{
	assert(php);
	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}

//交换两个元素
void Swap(HPDataType* pa, HPDataType* pb)
{
	HPDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

//向上调整
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	assert(a);
	while (child > 0)
	{
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//向下调整
//小堆(为了TopK函数的实现)
void AdjustDown(HPDataType* a, int n, int parent)
{
	assert(a);
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n)
		{
			child = a[child] < a[child + 1] ? child : child + 1;
		}
		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

// 堆的构建
void HeapCreate(Heap* php, HPDataType* a, int n)
{
	assert(php);

	//拷贝数组
	php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (php->a == NULL)
	{
		perror("malloc failed!");
		return;
	}
	php->size = n;
	php->capacity = n;
	memcpy(php->a, a, sizeof(HPDataType) * n);

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

	//向上调整
	for (int i = 1;i < n;i++)
	{
		AdjustUp(php->a, i);
	}
}

//插入堆元素
void HeapPush(Heap* php, HPDataType x)
{
	assert(php);

	//扩容
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		assert(tmp);
		php->a = tmp;
		php->capacity = newcapacity;
	}

	php->a[php->size] = x;
	php->size++;

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

//删除堆元素
void HeapPop(Heap* php)
{
	assert(php);

	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	
	//向下调整
	AdjustDown(php->a, php->size, 0);
}

//取栈顶元素
HPDataType HeapTop(Heap* php)
{
	assert(php);
	assert(php->size);

	return php->a[0];
}

//打印堆的元素
void HeapPrint(Heap* php)
{
	assert(php);

	for (int i = 0;i < php->size;i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

//堆的TopK问题
//N个数中最大的K个数(或者最小的K个数)
//以最大为例
//N是一个很大的数
void PrintTopK(int* a, int n, int k)
{
	//建立一个有k个元素的小堆
	//以N个数中的前k个为初始值
	int* MaxHeap = (int*)malloc(sizeof(int) * k);
	if (MaxHeap == NULL)
	{
		perror("malloc failed!");
		return;
	}
	for (int i = 0;i < k;i++)
	{
		MaxHeap[i] = a[i];
	}
	for (int i = (k - 1 - 1) / 2;i >= 0;i--)
	{
		AdjustDown(MaxHeap, k, i);
	}
	for (int i = k;i < n;i++)
	{
		if (a[i] > MaxHeap[0])
		{
			MaxHeap[0] = a[i];
			AdjustDown(MaxHeap, k, 0);
		}
	}
	for (int i = 0;i < k;i++)
	{
		printf("%d ", MaxHeap[i]);
	}
	printf("\n");
}

//堆的数组个数
int HeapSize(Heap* php)
{
	assert(php);

	return php->size;
}

// 堆的判空
bool HeapEmpty(Heap* php)
{
	assert(php);

	return php->size == 0;
}

//释放堆的空间
void HeapDestroy(Heap* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}

2.普通二叉树

接口函数的实现:

1.二叉树的节点个数:利用二叉树的节点个数=左子树节点个数+右子树节点个数+1的等式,用递归的方法来计算,递归的出口为,当根节点为NULL时,返回0。

2.二叉树的叶子节点个数:利用二叉树的叶子节点个数=左子树叶子节点个数+右子树叶子节点个数的等式,用递归的方法来计算,递归的出口为,当根节点为NULL时,返回0;当根节点不为空但无孩子节点时返回1。

3.二叉树第k层节点个数:利用二叉树第k层节点个数=左子树第k-1层节点个数+右子树第k-1层节点个数,用递归的方法来计算,递归的出口为,根节点为空时,返回0;当k=1时,返回1。

4.二叉树查找值为x的节点:先判断根节点是否为空,为空则返回NULL;若根节点的值为x,则返回根节点的地址;若不相等,则先后继续在其左子树和右子树进行查找。

5.层序遍历和判断是否为完全二叉树都需要用到队列这一数据结构,层序遍历在上文已经解释过这里不再赘述。判断二叉树是否为完全二叉树,可通过将节点按层序放入队列中,如若队列元素出现了NULL,且之后不再出现非空的节点,则为完全二叉树,反之则不是完全二叉树。

//BinaryTreeNode.c
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{
	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}
		
	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	root->_data = a[(*pi)++];
	root->_left = BinaryTreeCreate(a,pi);
	root->_right = BinaryTreeCreate(a,pi);
	return root;
}

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	printf("%c ", root->_data);
	BinaryTreePrevOrder(root->_left);
	BinaryTreePrevOrder(root->_right);
}

// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	BinaryTreeInOrder(root->_left);
	printf("%c ", root->_data);
	BinaryTreeInOrder(root->_right);
}

// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}

	BinaryTreePostOrder(root->_left);
	BinaryTreePostOrder(root->_right);
	printf("%c ", root->_data);
}

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 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层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;

	return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);
}

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->_data == x)
		return root;

	BTNode* p1 = BinaryTreeFind(root->_left, x);
	if(p1)
		return p1;
	
	BTNode* p2 = BinaryTreeFind(root->_right, x);
	if(p2)
		return p2;

	return NULL;
}

// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if(root != NULL)
	    QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		printf("%c ", front->_data);

		QueuePop(&q);
		if (front->_left)
		{
			QueuePush(&q, front->_left);
		}
		if (front->_right)
		{
			QueuePush(&q, front->_right);
		}
	}
	printf("\n");
	QueueDestroy(&q);
}

// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if(root)
	    QueuePush(&q, root);

	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front)
		{
			QueuePush(&q, front->_left);
			QueuePush(&q, front->_right);
		}
		else
		{
			break;
		}
	}
	//这个while循环只有一个可能,就是在队列元素中出现了NULL;
	//如果该元素之后的所有元素均为NULL,则该树为完全二叉树;
	//否则就不是完全二叉树
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front != NULL)
		{
			QueueDestroy(&q);
			return false;
		}
			
	}
	QueueDestroy(&q);
	return true;
}

// 二叉树销毁
//递归实现,按照左结点->根结点->右结点的顺序释放结点
void BinaryTreeDestory(BTNode** root)
{
	if (*root == NULL)
		return;

	BinaryTreeDestory(&((*root)->_left));
	BinaryTreeDestory(&((*root)->_right));
	free(*root);
	*root = NULL;
}

你可能感兴趣的:(数据结构,c语言)