目录
1.树的概念
1.1树的概念
我们之前学习了线性表,链表,栈以及队列。这些数据结构类型。总结一下这些结构都属于线性的结构,但是只有这些结构管理起来数据却还远远不够,就比如我们想要做一个文件结构,我们都知道的文件结构是点开一个里面还有一个。
比如上面这样的,因此树状结构就在这里体现的淋漓尽致了。
首先树状结构不是线性的,它是一个由n个节点(n>0)个有限节点组成的一个具有层次关系的集合,之所以叫它树状结构是因为它长的像一个倒挂着的树。
- 树有一个特殊节点,就是第一个节点,称为树的根这个节点没有前驱节点。
- 除了根以外的节点都称作树的子节点,而子节点的前驱节点也只能有一个。但是可以有好多后继节点。
注意:在树的结构中,子节点之间是不能有相交的。否则就不是树状结构了
1.2树的相关概念
1.3树的表示
树的表示方法也有好几种,分别有双亲表示法,孩子兄弟表示法,这里先介绍一下孩子兄弟表示法。
typedef int DataType; struct Node { struct Node* _firstChild1; // 第一个孩子结点 struct Node* _pNextBrother; // 指向其下一个兄弟结点 DataType _data; // 结点中的数据域 };
这里表示为孩子兄弟表示法。
2.二叉树概念及其结构
2.1概念
一棵二叉树是节点的有限集合:
- 该集合可以为空
- 由一个根节点和两个子节点构成,可以少于两个子节点。但是不能多于两个。
由此可以看出二叉树的每个节点至多有两个节点。不存在度大于2的节点。而且每个节点有左右之分次序不能颠倒。
2.2特殊的树
- 满二叉树:一个二叉树,如果每层的节点数都到最大值,则这个数就是一个满二叉树。它的层数为k那么它的节点数就为2^k-1。
- 完全二叉树:完全二叉树是一个效率很高的数据结构。可以用一些不书面的语言来讲,为偶数节点的二叉树成为完全二叉树。
2.3二叉树的储存结构
二叉树可以有两种储存形式,第一种是顺序结构,第二种是链式结构。
- 顺序储存
顺序储存就是用数组来存,一般只适合用来表示完全二叉树。因为不是完全二叉树会有空间浪费的。一会下面的大小堆的问题就是用顺序表来表示的,功能非常强大。
还有一种链式储存完全二叉树的结构,这里先不提到。把大小堆问题解决之后再来整。
2.4堆的概念及其结构
堆可以说堆及是金字塔类型的,一种是从上面到最低下那一层都是按从大到小排列的,称为大堆,相反的则称为小堆
那么我们已经知道什么是大小堆了,那么它们的用途会在哪里呢?这里我们可以模拟实现一下大堆的生成。然后插入数据
2.5堆的实现
我们将一组乱序数组插入到堆中,但是我们要保证它是大堆的结构,下面是代码实例和图解,主要是调整的代码:
void AdjustUp(int* hp,Sty child)
{
//我们再插入数据之后就需要对堆进行排序了,根据之前的
//我们知道插入的位置都是孩子的位置,我们需要根据孩子
//的位置算出父亲节点的位置,就是下面这个公式。
int parent = (child - 1) / 2;
//结束条件是一直把孩子往上调,直到孩子到达根节点。
while (child!=0)
{
if (hp[parent] <hp[child])
{
Swap(&hp[parent], &hp[child]);
child = parent;
parent = (child - 1) / 2;
}
else break;
}
}
我们需要插入的数据就这样会被放到非常妥当的位置啦!
小堆的实现和大堆的一样的。那么我们知道了无论大堆还是小堆都可以进行排序,好像也只能做这么些东西了么。其实堆的应用主要还是用于Topk问题,如果给你N个乱序数你怎么找到这N个中前K个大的数呢?我们可能会想到冒泡或者快排(申明一下我目前还不直到快排,只知道有这个东西。太菜了)。但是用冒泡的话,时间复杂度会到o(n^2)的,无疑是大大消耗了时间。这里我们的大小堆会非常完美的解决这个问题
2.6Topk-问题
关键点来了啊啊啊啊啊啊,
- 用数据集合中前k个元素来建立一个堆
- 找前k个大的则建一个元素为k的小堆。
- 找前k个小的则建一个元素为k的大堆。
void test3()
{
int n = 10000;
int* a = (int*)malloc(sizeof(int) * n);
srand(time(NULL));
for (size_t i = 0; i < n; ++i)
{
a[i] = rand() % 1000000;
}
//设置10个比1000000大的数字,然后Top。
a[1] = 1000000 + 1;
a[1231] = 1000000 + 2;
a[6839] = 1000000 + 3;
a[194] = 1000000 + 4;
a[289] = 1000000 + 5;
a[57] = 1000000 + 6;
a[327] = 1000000 + 7;
a[99] = 1000000 + 8;
a[2984] = 1000000 + 9;
a[3598] = 1000000 + 10;
//找出10000个数中前10个大的数!
PrintTopk(a, n, 10);
}
void PrintTopk(int* a, int n, int k)
{
Hp h1;
HeapInit(&h1);
for (size_t i = 0; i < k; i++)
{
HeapPushmin(&h1, a[i]);
}
for (size_t i = k; i < n; i++)
{
if (a[i] > Top(&h1))
{
//这里这个两种方法来替换元素都是可以的,
/*MinPop(&h1);
HeapPushmin(&h1, a[i]);*/
h1.a[0] = a[i];
AdjustDownMin(h1.a, h1.size, 0);
}
}
print(&h1);
}
这样下来我们的Topk问题就得到了完美的解决了。
3.二叉树链式结构的实现
3.1 前置说明
首先我们上面的堆的结构并不属于真正的二叉树的实现,我们只不过使用了顺序表来模拟了叉树中大小堆的实现。链式二叉树才是真正的二叉树的入门结构。下面的代码就是创建链式二叉树的简单结构。
typedef char DataType;
typedef struct BinaryTreeNode
{
//树的左子树
struct BinaryTreeNode* left;
//树的右子树
struct BinaryTreeNode* right;
//数据域
DataType data;
}BTNode;
BTNode* Buynode(DataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
node->left = node->right = NULL;
node->data = x;
return node;
}
//二叉树的每个节点
BTNode* CreatTree()
{
BTNode* nodeA = Buynode('A');
BTNode* nodeB = Buynode('B');
BTNode* nodeC = Buynode('C');
BTNode* nodeD = Buynode('D');
BTNode* nodeE = Buynode('E');
BTNode* nodeF = Buynode('F');
//每个节点存的左右子树
nodeA->left = nodeB;
nodeA->right = nodeC;
nodeB->left = nodeD;
nodeC->left = nodeE;
nodeC->right = nodeF;
return nodeA;
}
以上就是简单二叉链的创建,这里最主要的实现二叉链的遍历。
3.2二叉链的遍历
我们每创建一个对象,需要它实现自己的功能,然后就是对这个结构的访问,之前的结构都属于线性的非常方便访问,那么这个树总分分叉我们该怎么遍历它呢?我们先来看一下链式二叉树的逻辑图吧
其中每个字母都是里面的节点,看到这里我们好像发现了什么,说的不好访问和遍历并不代表不可以实现aaa
- 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
- 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
- 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后
下面是三种遍历代码,具体方法是用递归实现的,
- 前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%c ", root->data);
PreOrder(root->left);
PreOrder(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 BinaryTreeNodeSize(BTNode* root, int* count)
{
if (root == NULL)
{
return;
}
//节点不为空就将计数器++,计数需要注意定义一个外部的变量。
(*count)++;
//然后遍历左右子树
BinaryTreeNodeSize(root->left, count);
BinaryTreeNodeSize(root->right, count);
}
二叉树叶子节点个数
int BinaryTreeLefSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
//考虑递归的终止条件,叶子节点左右子树都为空。则这个就为叶子节点。
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return BinaryTreeLefSize(root->left) + BinaryTreeLefSize(root->right);
}
二叉第k层节点个数
int BinaryTreeLevelSize(BTNode* root, int k)
{
if (root==NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
//如果树不为空,并且k!=1,说明这棵树第k层节点在子树里面,转换为求它左右子树,并且k-1的节点。
return BinaryTreeLevelSize(root->left, k - 1) + BinaryTreeLevelSize(root->right, k - 1);
}
我们这个求它第三层的节点个数,相当于求根节点左右子树第二层的节点个数。左右子树第二层节点个数,相当于求它左右子树的左右子树第一层节点个数~~~
二叉树的最大深度
int BinaryTreeDepheSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int leftDepth = BinaryTreeDepheSize(root->left);
int rightDepth = BinaryTreeDepheSize(root->right);
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
先找终止条件为左右子树做根节点为空了就终止,不然就左右子树大的那个+1~~~~~