数据结构 day03 队列与二叉树

1. 队列

队列是一种先进先出的受限线性表,只能在线性表的一端进行删除,在线性表的另一端进行插入。队列的实现可以用顺序结构进行实现,也可以使用链式结构进行实现。这两种实现的方法分别是顺序队列和链式队列。

在队列中,我们需要实现以下的功能:

  • 初始化队列。
  • 入队操作。
  • 出队操作。
  • 判断队列是否为空。
  • 获取队列的大小。
  • 获取队头元素。
  • 获取队尾元素。
  • 销毁队列。

根据这些操作,我们设计出来了以下接口:

// 初始化队列
Queue initQueue(int size);

// 入队
void pushQueue(Queue queue, void * data);

// 出队
void popQueue(Queue queue);

// 队头元素
void *getFrontQueue(Queue queue);

// 队尾元素
void *getBackQueue(Queue queue);

// 队列大小
int getSizeQueue(Queue queue);

// 队列是否为空
int isEmptyQueue(Queue queue);

// 销毁队列
void destoryQueue(Queue queue);

1.1 顺序队列

顺序队列是使用顺序表进行实现的。在这里我们借用之前的动态数组模块进行实现。先将动态数组生成动态库,以便在此处进行代码的复用。在头文件中,我们对上述的接口进行申明,并在头文件中额外添加以下代码:

#pragma once

#include "DynamicArray.h"

#define MAX_SIZE 1024

#pragma comment(lib, "./DynamicArray.lib")

typedef void *Queue;

在这里我们定义了MAX_SIZE来限定队列的容量,使用了#pragma comment(lib, "./DynamicArray.lib")来进行动态数组动态库的链接。

在这里我们需要思考动态数组的低地址还是高地址作为队头好还是队尾好。实际上在这里无论是动态数组高地址的部分还是低地址的部分,他们作为队头队尾都是无所谓的。因为队列是在两端进行操作的,无论怎么样设计都需要移动大量的元素。由于具体的实现比较简单,此处直接给出关于上述各个模块接口实现的代码:

#include "SeqQueue.h"

// 初始化队列
Queue initQueue(int size)
{
	Queue *queue = (Queue *)initDynamicArray(size);

	return queue;
}

// 入队
void pushQueue(Queue queue, void * data)
{
	if (queue == NULL)
	{
		return;
	}

	if (data == NULL)
	{
		return;
	}

	if (getSizeQueue(queue) == MAX_SIZE)
	{
		return;
	}

	insertDynamicArray(queue, getSizeDynamicArray(queue), data);
}

// 出队
void popQueue(Queue queue)
{
	if (queue == NULL)
	{
		return;
	}

	if (getSizeQueue(queue) == 0)
	{
		return;
	}

	removeByPositionDynamicArray(queue, 0);
}

// 队头元素
void *getFrontQueue(Queue queue)
{
	if (queue == NULL)
	{
		return NULL;
	}

	if (getSizeQueue(queue) == 0)
	{
		return NULL;
	}

	return ((struct _DynamicArray *)queue)->pAddr[0];
}

// 队尾元素
void *getBackQueue(Queue queue)
{
	if (queue == NULL)
	{
		return NULL;
	}

	if (getSizeQueue(queue) == 0)
	{
		return NULL;
	}

	return ((struct _DynamicArray *)queue)->pAddr[getSizeQueue(queue) - 1];
}

// 队列大小
int getSizeQueue(Queue queue)
{
	if (queue == NULL)
	{
		return -1;
	}

	return getSizeDynamicArray(queue);
}

// 队列是否为空
int isEmptyQueue(Queue queue)
{
	if (queue == NULL)
	{
		return -1;
	}

	return getSizeQueue(queue) == 0;
}

// 销毁队列
void destoryQueue(Queue queue)
{
	if (queue == NULL)
	{
		return;
	}

	destroyDynamicArray(queue);
}

接下来对顺序队列进行测试。测试代码如下:

#if 1

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>

#include "SeqQueue.h"

struct Person
{
	void *node;

	char name[64];
	unsigned int age;
};

// 自定义输出方法
void printPerson(struct Person *person)
{
	printf("(%s, %u) ", person->name, person->age);
}


int main(int argc, char* argv[])
{
	struct Person p1 = { NULL, "张三", 65 };
	struct Person p2 = { NULL, "小叶", 46 };
	struct Person p3 = { NULL, "小长", 78 };
	struct Person p4 = { NULL, "小青", 43 };
	struct Person p5 = { NULL, "许仙", 42 };
	struct Person p6 = { NULL, "小白", 43 };

	Queue queue = initQueue(10);

	printf("入队前: \n");
	printf("顺序队列大小: %d\n", getSizeQueue(queue));
	printf("顺序队列是否为空: %s\n", isEmptyQueue(queue) ? "是" : "否");

	pushQueue(queue, &p1);
	pushQueue(queue, &p2);
	pushQueue(queue, &p3);
	pushQueue(queue, &p4);
	pushQueue(queue, &p5);
	pushQueue(queue, &p6);

	printf("入队后: \n");
	printf("顺序队列大小: %d\n", getSizeQueue(queue));
	printf("顺序队列是否为空: %s\n", isEmptyQueue(queue) ? "是" : "否");

	while (!isEmptyQueue(queue))
	{
		printf("队头元素为: ");
		printPerson(getFrontQueue(queue));

		printf("对尾元素为: ");
		printPerson(getBackQueue(queue));
		putchar('\n');

		popQueue(queue);
	}

	printf("出队后: \n");
	printf("顺序队列大小: %d\n", getSizeQueue(queue));
	printf("顺序队列是否为空: %s\n", isEmptyQueue(queue) ? "是" : "否");

	destoryQueue(queue);
	queue = NULL;

	system("pause");
	return 0;
}

#endif

可以正常的运行出来:

在这里插入图片描述

1.2 链式队列

链式队列是用链表进行实现的。在前面我们实现了普通版的单链表以及企业版的单链表。现在我们模仿企业版的单链表来进行链式队列进行实现。在链表中,由于我们使用指针,所以对各个元素的操作可以很灵活。此处我们可以定义一个尾指针以方便和头指针一起维护链表的两端,实现入队和出队操作。这样的实现可以避免每次出队或者入队操作的时候要进行大量元素的遍历。在这里我们规定我们在单链表头进行出队操作,在单链表尾进行入队操作。我们头文件中链式队列的定义如下:

#pragma once

struct _QueueNode
{
	struct _QueueNode *_next;
};

// 链式队列
struct _LinkQueue
{
	struct _QueueNode _head;
	struct _QueueNode *_pTail;
	int _size;
};

typedef void *Queue;

其余的关于模块接口均在头文件中进行申明。接下里需要写实现部分,实现部分的代码如下:

#include "LinkQueue.h"

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

// 初始化队列
Queue initQueue(int size)
{
	struct _LinkQueue *_pLinkQueue = (struct _LinkQueue *)malloc(sizeof(struct _LinkQueue));
	_pLinkQueue->_head._next = NULL;
	_pLinkQueue->_pTail = &_pLinkQueue->_head;
	_pLinkQueue->_size = 0;

	return _pLinkQueue;
}

// 入队
void pushQueue(Queue queue, void * data)
{
	if (queue == NULL)
	{
		return;
	}

	if (data == NULL)
	{
		return;
	}

	struct _LinkQueue *_pLinkQueue = (struct _LinkQueue *)queue;

	// 插入到表尾
	_pLinkQueue->_pTail->_next = data;
	
	// 更新尾结点 
	_pLinkQueue->_pTail = data;
	_pLinkQueue->_pTail->_next = NULL;
	_pLinkQueue->_size++;
}

// 出队
void popQueue(Queue queue)
{
	if (queue == NULL)
	{
		return;
	}

	struct _LinkQueue *_pLinkQueue = (struct _LinkQueue *)queue;

	// 判断队列是否为空
	if (_pLinkQueue->_size == 0)
	{
		return;
	}

	// 删除第一个结点
	struct _QueueNode *_pQueueNode = _pLinkQueue->_head._next;
	_pLinkQueue->_head._next = _pQueueNode->_next;
	_pLinkQueue->_size--;

	// 若链表只有一个结点,删除后表头指针指向空,更新尾结点位置
	if (_pLinkQueue->_head._next == NULL)
	{
		_pLinkQueue->_pTail = &_pLinkQueue->_head;
	}
}

// 队头元素
void *getFrontQueue(Queue queue)
{
	if (queue == NULL)
	{
		return NULL;
	}

	struct _LinkQueue *_pLinkQueue = (struct _LinkQueue *)queue;

	// 队列无元素
	if (_pLinkQueue->_size == 0)
	{
		return NULL;
	}

	return _pLinkQueue->_head._next;
}

// 队尾元素
void *getBackQueue(Queue queue)
{
	if (queue == NULL)
	{
		return NULL;
	}

	struct _LinkQueue *_pLinkQueue = (struct _LinkQueue *)queue;

	// 队列无元素
	if (_pLinkQueue->_size == 0)
	{
		return NULL;
	}

	return _pLinkQueue->_pTail;
}

// 队列大小
int getSizeQueue(Queue queue)
{
	if (queue == NULL)
	{
		return -1;
	}

	struct _LinkQueue *_pLinkQueue = (struct _LinkQueue *)queue;
	return _pLinkQueue->_size;
}

// 队列是否为空
int isEmptyQueue(Queue queue)
{
	if (queue == NULL)
	{
		return -1;
	}

	struct _LinkQueue *_pLinkQueue = (struct _LinkQueue *)queue;
	return _pLinkQueue->_size == 0;
}

// 销毁队列
void destoryQueue(Queue queue)
{
	if (queue == NULL)
	{
		return;
	}

	free(queue);
	queue = NULL;
}

该实现部分和企业版的单链表的实现一样,但是这里需要值得注意的是在我们进行出队操作的时候。但队里的元素的个数大于1的时候,我们直接在队头操作只操作队头指针是没有任何问题的,而当我们队列的元素有1个时候,我们的队尾指针就需要手动修改为头结点。接下来我们对链式队列的代码进行测试,由于我们的模块接口与顺序队列的是一模一样的,所以这里的测试代码与上面的代码一样,只是把输出中的“顺序队列”改成了“链式队列”而已,经测试改代码能正常执行。

在这里插入图片描述

2. 二叉树

2.1 树的一些基本概念

  • 树的定义:树(Tree)是n( n ≥ 0 n \geq 0 n0 )个结点的有限集。n=0时称为空树。在任意一棵非空树中:(1)有且仅有一个特定的称为根(Root)的结点;(2)当n ( n > 1 n > 1 n>1)时,其余结点可以分为m( m > 0 m>0 m>0)个互不相交的有限集 T 1 、 T 2 、 . . . 、 T m T_1、T_2、...、T_m T1T2...Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。

  • 树的结构特点:
    (1)树是非线性结构,有一个直接前驱,但可能有多个直接后继,也就是1:n的关系。在树中,根结点没有直接前驱,叶子没有直接后继。
    (2)树的定义具有递归性,树中还有树。
    (3)树可以为空,即结点个数为0。

  • 根:根节点,没有前驱。

  • 叶子:终端结点,没有后继。

  • 森林:指m棵不相交的树的集合。

  • 有序树:结点各个子树从左至右有序,不能互换。

  • 无序树:结点各子树可以互换位置。

  • 双亲:结点的直接前驱的那个结点。

  • 孩子:结点的直接后继结点。

  • 兄弟:与结点在同一层且同一双亲的结点。

  • 堂兄弟:与结点在同一层且非同一双亲的结点。

  • 祖先:从根节点到该结点所经分支的所有结点。

  • 子孙:该结点的下层子树的任意结点。

  • 结点:树的数据元素。

  • 结点的度:结点挂接的子树数目,有多少个直接后继就是多少度。

  • 结点的层次:从跟到该结点的层数。

  • 终端结点:度为0的结点,即叶子结点。

  • 分支结点:除树根以外的结点,也称内部结点。

  • 树的度:所有结点的度的最大值。

  • 树的深度:所有结点中最大的层数。

例如下面有一棵这样的树:

在这里插入图片描述
在这棵树中,结点1是根节点,其中该节点的度为2。这棵树的深度是6。结点12、结点13、结点16、结点15、结点11都是这棵树的叶子结点,这些结点没有直接后继。结点11的双亲结点为6,它的兄弟结点为10,而堂兄弟则是位于同一层的结点7、结点8和结点9,它的祖先结点为结点1、结点3和结点6。

在树的表示之中,我们一般使用左孩子右兄弟表示法。其中森林转化为树也是常用左孩子右兄弟表示法。例如上述的树根据左孩子右兄弟表示法可以表示为以下的树:

在这里插入图片描述

在上面的树中我们可以很快的看出来结点9是结点8的兄弟,结点13是结点8的孩子结点。

2.2 二叉树的基本概念

  • 二叉树的定义:n( n ≥ 0 n \geq 0 n0)个结点的有限集合,由一个根节点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成。
  • 二叉树的基本特征:
    (1)二叉树的每个结点最多只有两棵子树(不存在度大于2的结点)。
    (2)左子树和右子树的次序不能颠倒,二叉树是有序树。
  • 二叉树的性质:
    (1)在二叉树的第 i 层上至多有 2 i − 1 2^{i-1} 2i1个结点。
    (2)深度为 k 的二叉树至多有 2 k − 1 2^k-1 2k1个结点。
    (3)对于任意一棵二叉树,若度为2的结点树有 n 2 n_2 n2个,则叶子结点树 n 0 n_0 n0必定为 n 2 + 1 n_2 + 1 n2+1个,即 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
    (4)具有 n 个结点的完全二叉树的深度必为 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2 n \rfloor + 1 log2n+1
  • 满二叉树:一颗深度为 k 且有 2 k − 1 2^k-1 2k1个结点的二叉树。如下图就是一棵满二叉树。
    在这里插入图片描述
  • 完全二叉树:除最后一层外,每一层上的结点数均达到最大值,在最后一层上只缺少右边的若干结点。如下图就是一棵完全二叉树。

在这里插入图片描述

2.3 二叉树的递归遍历

遍历是指按照某条搜索路径去遍历每个结点且不重复。遍历是树结构插入、删除、修改、查找和排序等运算的前提,是二叉树运算的基础与核心。

二叉树的遍历主要有四层方式,分别为先序遍历、中序遍历、后序遍历、层序遍历。每一种遍历方法对于每个结点都是先左后右的。接下来均以下面二叉树为例:

在这里插入图片描述

先序遍历就是先遍历二叉树的根节点,再遍历二叉树的左子树,最后再遍历二叉树的右子树。以此递归下去就可以得到二叉树的先序遍历的序列。上述二叉树得到的先序遍历为:1 2 4 7 12 5 8 13 9 14 16 3 6 10 15 11。

中序遍历是先遍历二叉树的左子树,再遍历根节点,最后再遍历二叉树的右子树。上述二叉树的中序遍历为:7 12 4 2 13 8 5 9 14 16 1 3 10 15 6 11。

后序遍历为先遍历二叉树的左子树,再遍历二叉树的右子树,最后再遍历二叉树的根结点。上述二叉树的后续遍历为:12 7 4 13 8 16 14 9 5 2 15 10 11 6 3 1。

层序遍历就是根据每一层从左到右遍历即可。上述二叉树的层序遍历为:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16。

在上述的四种遍历方法之中,其中先序遍历、中序遍历和后序遍历是用递归的形式定义出来的。因此我们可以使用递归的方法进行程序的编写。以上述二叉树为例,先序遍历的代码如下:

#if 1

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>

struct BinaryNode
{
	int data;
	struct BinaryNode *lchild;
	struct BinaryNode *rchild;
};


// 二叉树的先序遍历
void recursion(struct BinaryNode *root)
{
	if (root == NULL)
	{
		return;
	}

	// 先遍历根节点
	printf("%d ", root->data);
	// 再递归遍历左子树
	recursion(root->lchild);
	// 最后递归遍历右子树
	recursion(root->rchild);
}

int main(int argc, char* argv[])
{
	struct BinaryNode node1 = { 1, NULL, NULL };
	struct BinaryNode node2 = { 2, NULL, NULL };
	struct BinaryNode node3 = { 3, NULL, NULL };
	struct BinaryNode node4 = { 4, NULL, NULL };
	struct BinaryNode node5 = { 5, NULL, NULL };
	struct BinaryNode node6 = { 6, NULL, NULL };
	struct BinaryNode node7 = { 7, NULL, NULL };
	struct BinaryNode node8 = { 8, NULL, NULL };
	struct BinaryNode node9 = { 9, NULL, NULL };
	struct BinaryNode node10 = { 10, NULL, NULL };
	struct BinaryNode node11 = { 11, NULL, NULL };
	struct BinaryNode node12 = { 12, NULL, NULL };
	struct BinaryNode node13 = { 13, NULL, NULL };
	struct BinaryNode node14 = { 14, NULL, NULL };
	struct BinaryNode node15 = { 15, NULL, NULL };
	struct BinaryNode node16 = { 16, NULL, NULL };

	// 建立二叉树结点间的关系
	node1.lchild = &node2;
	node1.rchild = &node3;
	node2.lchild = &node4;
	node2.rchild = &node5;
	node3.rchild = &node6;
	node4.lchild = &node7;
	node5.lchild = &node8;
	node5.rchild = &node9;
	node6.lchild = &node10;
	node6.rchild = &node11;
	node7.rchild = &node12;
	node8.lchild = &node13;
	node9.rchild = &node14;
	node10.rchild = &node15;
	node14.rchild = &node16;


	recursion(&node1);

	system("pause");
	return 0;
}

#endif

程序的运行结果如下:

在这里插入图片描述

2.3 二叉树的非递归遍历

二叉树的非递归遍历就是利用栈等数据结构进行遍历的操作。接下来说一下关于二叉树的先序非递归遍历。

在前面的例子中,我们树的结点只有数据域一个整数以及两个指针域分别表示左右孩子。而此处的二叉树的结点我们需要多设计一个标志位,其值分别为0和1,默认均设置为0。

接下来说一下非递归遍历的思路,首先定义一个栈,然后将根节点压入栈中。接下来对栈中的元素进行循环,每次首先弹出栈顶元素,判断该元素的标志位是否为1,若该元素的标志位为1则直接输出元素的值。若标志位为0,则分别将该结点的右孩子入栈,再让左孩子入栈,并且将该结点的标志位改为1再进行入栈。重复上面的操作,即可以得到先序遍历的结果。

在实现代码中,我们引入了之前的SeqStack的实现,并生成动态库链接到该程序中。同样以前面的二叉树为例,其实现的代码如下:

#if 1

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>

#include "SeqStack.h"

#pragma comment(lib, "./SeqStack.lib")

struct BinaryNode
{
	// 数据域
	int data;
	// 指针域
	struct BinaryNode *lchild;
	struct BinaryNode *rchild;
	// 标志位
	int flag;
};

void nonRecursion(struct BinaryNode *root)
{
	if (root == NULL)
	{
		return;
	}

	Stack stack = initStack();

	// 将树根压入栈
	pushStack(stack, root);

	while (!isEmptyStack(stack))
	{
		// 获取树根
		struct BinaryNode *node = getTopStack(stack);
		popStack(stack);

		// 若为真直接输出进行下一轮
		if (node->flag)
		{
			printf("%d ", node->data);
			continue;
		}

		// 修改标志位
		node->flag = 1;
		// 将右左子树以及自身入栈
		pushStack(stack, node->rchild);
		pushStack(stack, node->lchild);
		pushStack(stack, node);
	}

	// 销毁栈
	destroyStack(stack);
}


int main(int argc, char* argv[])
{
	struct BinaryNode node1 = { 1, NULL, NULL, 0 };
	struct BinaryNode node2 = { 2, NULL, NULL, 0 };
	struct BinaryNode node3 = { 3, NULL, NULL, 0 };
	struct BinaryNode node4 = { 4, NULL, NULL, 0 };
	struct BinaryNode node5 = { 5, NULL, NULL, 0 };
	struct BinaryNode node6 = { 6, NULL, NULL, 0 };
	struct BinaryNode node7 = { 7, NULL, NULL, 0 };
	struct BinaryNode node8 = { 8, NULL, NULL, 0 };
	struct BinaryNode node9 = { 9, NULL, NULL, 0 };
	struct BinaryNode node10 = { 10, NULL, NULL, 0 };
	struct BinaryNode node11 = { 11, NULL, NULL, 0 };
	struct BinaryNode node12 = { 12, NULL, NULL, 0 };
	struct BinaryNode node13 = { 13, NULL, NULL, 0 };
	struct BinaryNode node14 = { 14, NULL, NULL, 0 };
	struct BinaryNode node15 = { 15, NULL, NULL, 0 };
	struct BinaryNode node16 = { 16, NULL, NULL, 0 };

	// 建立二叉树结点间的关系
	node1.lchild = &node2;
	node1.rchild = &node3;
	node2.lchild = &node4;
	node2.rchild = &node5;
	node3.rchild = &node6;
	node4.lchild = &node7;
	node5.lchild = &node8;
	node5.rchild = &node9;
	node6.lchild = &node10;
	node6.rchild = &node11;
	node7.rchild = &node12;
	node8.lchild = &node13;
	node9.rchild = &node14;
	node10.rchild = &node15;
	node14.rchild = &node16;


	struct BinaryNode *tree = &node1;

	nonRecursion(tree);

	system("pause");
	return 0;
}

#endif

2.4 二叉树的编程案例

在这里,我们需要实现以下的功能:

  • 求二叉树的叶子结点数量
  • 求树的高度/深度
  • 二叉树的拷贝
  • 二叉树的释放

首先实现求二叉树叶子结点的数目。对于一棵空树来说,二叉树的叶子结点为0。叶子结点是左右子树均为空树的结点。只要结点的孩子不为空,则继续去看左子树与右子树的叶子结点。根据这个思想,我们可以设计出来二叉树求叶子结点数目的算法:

// 求二叉树叶子结点的数量
void getLeafOfNum(struct BinaryNode *root, int *leafNum)
{
	if (root == NULL)
	{
		return;
	}

	if (root->lchild == NULL && root->rchild == NULL)
	{
		(*leafNum)++;
	}

	getLeafOfNum(root->lchild, leafNum);
	getLeafOfNum(root->rchild, leafNum);
}

在上述代码中root表示树根节点,leafNum表示统计叶子结点数目的指针,在调用函数之前,需要将leafNum指向的内存的值清0。

接下里我们来统计以下二叉树的高度。空树的高度为0,对于任意一棵二叉树,则它的高度为左子树高度与右子树高度中最高的高度再加上自身一层的高度。则可以得出任意一个结点的高度=max(左子树的高度,右子树的高度) + 1。根据此可以得出以下的算法:

// 获取树的高度
int getHeightOfTree(struct BinaryNode *root)
{
	if (root == NULL)
	{
		return 0;
	}

	// 分别计算左子树与右子树的高度
	int lheight = getHeightOfTree(root->lchild);
	int rheight = getHeightOfTree(root->rchild);

	return lheight > rheight ? lheight + 1 : rheight + 1;
}

接下来需要对一棵二叉树进行拷贝。在拷贝的时候先想想该如何拷贝一棵树。我们在进行每次拷贝的时候,我们都需要先拷贝该二叉树的左子树,再拷贝二叉树的右子树,然后再拷贝该结点,并将该结点的左右孩子指针分别指向拷贝出来的左子树与右子树。因此我们可以得出来一下的算法:

// 拷贝二叉树
struct BinaryNode *copyTree(struct BinaryNode *root)
{
	if (root == NULL)
	{
		return NULL;
	}

	// 分别拷贝左右子树
	struct BinaryNode *lsubtree = copyTree(root->lchild);
	struct BinaryNode *rsubtree = copyTree(root->rchild);

	// 拷贝该结点
	struct BinaryNode *node = malloc(sizeof(struct BinaryNode));
	node->data = root->data;
	node->lchild = lsubtree;
	node->rchild = rsubtree;
}

最后说一下释放树中所有结点的操作。所谓释放树的结点的空间,其实就是该树的结点都是开辟在堆区上的,因为如果树位于栈上的话是由操作系统自动进行释放的。接下里说一下释放树的思路。在释放每一棵树的时候,我们需要先释放该树的左子树,再释放树的右子树,最后再释放树根结点。因此我们只需要递归地操作下去就可以释放整棵树的内存空间。算法的实现如下:

// 释放树
void freeTree(struct BinaryNode *root)
{
	if (root == NULL)
	{
		return;
	}

	// 分别释放左右子树
	freeTree(root->lchild);
	freeTree(root->rchild);

	free(root);
	root = NULL;
}

接下来我们需要对上述的所有算法进行测试,测试的二叉树与前面所用二叉树一致,代码如下:

#if 1

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>

struct BinaryNode
{
	int data;
	struct BinaryNode *lchild;
	struct BinaryNode *rchild;
};

// 求二叉树叶子结点的数量
void getLeafOfNum(struct BinaryNode *root, int *leafNum)
{
	if (root == NULL)
	{
		return;
	}

	if (root->lchild == NULL && root->rchild == NULL)
	{
		(*leafNum)++;
	}

	getLeafOfNum(root->lchild, leafNum);
	getLeafOfNum(root->rchild, leafNum);
}

// 获取树的高度
int getHeightOfTree(struct BinaryNode *root)
{
	if (root == NULL)
	{
		return 0;
	}

	// 分别计算左子树与右子树的高度
	int lheight = getHeightOfTree(root->lchild);
	int rheight = getHeightOfTree(root->rchild);

	return lheight > rheight ? lheight + 1 : rheight + 1;
}


// 拷贝二叉树
struct BinaryNode *copyTree(struct BinaryNode *root)
{
	if (root == NULL)
	{
		return NULL;
	}

	// 分别拷贝左右子树
	struct BinaryNode *lsubtree = copyTree(root->lchild);
	struct BinaryNode *rsubtree = copyTree(root->rchild);

	// 拷贝该结点
	struct BinaryNode *node = malloc(sizeof(struct BinaryNode));
	node->data = root->data;
	node->lchild = lsubtree;
	node->rchild = rsubtree;
}

// 二叉树的先序遍历
void recursion(struct BinaryNode *root)
{
	if (root == NULL)
	{
		return;
	}

	printf("%d ", root->data);
	recursion(root->lchild);
	recursion(root->rchild);
}

// 释放树
void freeTree(struct BinaryNode *root)
{
	if (root == NULL)
	{
		return;
	}

	// 分别释放左右子树
	freeTree(root->lchild);
	freeTree(root->rchild);

	free(root);
	root = NULL;
}

int main(int argc, char* argv[])
{
	struct BinaryNode node1 = { 1, NULL, NULL };
	struct BinaryNode node2 = { 2, NULL, NULL };
	struct BinaryNode node3 = { 3, NULL, NULL };
	struct BinaryNode node4 = { 4, NULL, NULL };
	struct BinaryNode node5 = { 5, NULL, NULL };
	struct BinaryNode node6 = { 6, NULL, NULL };
	struct BinaryNode node7 = { 7, NULL, NULL };
	struct BinaryNode node8 = { 8, NULL, NULL };
	struct BinaryNode node9 = { 9, NULL, NULL };
	struct BinaryNode node10 = { 10, NULL, NULL };
	struct BinaryNode node11 = { 11, NULL, NULL };
	struct BinaryNode node12 = { 12, NULL, NULL };
	struct BinaryNode node13 = { 13, NULL, NULL };
	struct BinaryNode node14 = { 14, NULL, NULL };
	struct BinaryNode node15 = { 15, NULL, NULL };
	struct BinaryNode node16 = { 16, NULL, NULL };

	// 建立二叉树结点间的关系
	node1.lchild = &node2;
	node1.rchild = &node3;
	node2.lchild = &node4;
	node2.rchild = &node5;
	node3.rchild = &node6;
	node4.lchild = &node7;
	node5.lchild = &node8;
	node5.rchild = &node9;
	node6.lchild = &node10;
	node6.rchild = &node11;
	node7.rchild = &node12;
	node8.lchild = &node13;
	node9.rchild = &node14;
	node10.rchild = &node15;
	node14.rchild = &node16;


	struct BinaryNode *tree = &node1;

	// 求叶子结点的数量
	int leafNum = 0;
	getLeafOfNum(tree, &leafNum);
	printf("二叉树的叶子结点有%d个\n", leafNum);


	// 树的高度或者深度
	int height = getHeightOfTree(tree);
	printf("二叉树的高度为 %d \n", height);

	// 二叉树的拷贝
	struct BinaryNode *newTree = copyTree(tree);
	printf("被拷贝的树: ");
	recursion(tree);

	printf("\n拷贝出的树: ");
	recursion(newTree);
	putchar('\n');

	// 释放树
	freeTree(newTree);

	system("pause");
	return 0;
}

#endif

测试结果如下:

在这里插入图片描述

3. 插入排序

插入排序是排序的一种方法。插入排序是将一个序列分为排序好的序列和未排序好的序列,每次将右边未排序好的元素插入到左边指定的位置之中,从而整体是一个有序的序列。

下面以一个数列为例,假设数列名为arr
在这里插入图片描述
我们左边的为已排好序的元素,右边为未排序好的元素。我们按照从小到大的序列进行排序,一个元素的情况下一定是已排序好的,接下里我们需要从第一个元素开始,看看后续有没有a[i] > a[i+1]的情况,最开始的时候i=1,则发现后续中元素2满足此情况,则需要将元素2的值拷贝出来:

在这里插入图片描述然后将前面凡是大于元素2的元素均往后移动一位,然后元素2就插入在该位置:

在这里插入图片描述在这里插入图片描述

此时就有两个元素就排序好。后续的操作也是类似的

在这里插入图片描述此时后面的元素不满足,所以后面的那一个元素与前面自动构成已排序好的,此时接线往后面移动即可:
在这里插入图片描述
此时到元素1,前面的元素均往后面移动一位,再将1插入首位置

在这里插入图片描述当遍历完之后则元素所有均以排好序:
在这里插入图片描述

根据上述思想,我们可以写出来以下的代码实现:

#if 1

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>

// 插入排序法
void insertSort(int arr[], int len)
{
	for (int i = 1; i < len; i++)
	{
		if (arr[i] < arr[i - 1])
		{
			// 执行插入操作
			int tmp = arr[i];
			int j = i - 1;
			while (j >= 0 && tmp < arr[j])
			{
				arr[j + 1] = arr[j];
				j--;
			}
			arr[j + 1] = tmp;
		}
	}
}

int main(int argc, char* argv[])
{
	int arr[] = { -4, 23, -4, 6, 34, -98, 7, 5, 3, -78, 4, 87, 6, 54, -7, 9, 6, 6, 5, -5 };
	int length = sizeof arr / sizeof(int);

	insertSort(arr, length);

	for (int i = 0; i < length; i++)
	{
		printf("%d ", arr[i]);
	}
	putchar('\n');

	system("pause");
	return 0;
}

#endif

运行结果如下:

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值