目录
树
概念
树节点每个节点名称编辑
节点
度
二叉树
概念
要求
特殊二叉树
满二叉树
完全二叉树
二叉树的顺序结构
堆
堆的实现
堆的定义及初始化
堆的销毁
堆的插入
堆的删除
取堆顶的数据
堆的数据个数
堆的判空
堆的应用
堆排序
向下建堆更高效
实现堆排序
TOP-K问题
Top-K解决
创建数据
筛选数据
二叉树的链式结构
定义链式二叉树
链式二叉树的遍历
前序遍历
中序遍历
后序遍历
层序遍历
二叉树节点的个数
叶子节点的个数
第k层节点的个数
查找值为k的节点
二叉树的销毁
树
概念
树是一种非线性的数据结构,其能够用来表示多个节点之间的关系。其叫树因为其逻辑结构像一颗树一样,最上面的是树干,而下面是它的枝叶(像一颗倒挂的树)。
在画树的时候,上面的3个都是错误示范。
1)不能越界连接,即上面的只能连接其下面的一行,不能越过下面一列,如1-1;
2)同一行之间的不能连接,如1-2;
3)每一个节点往上只能找到一个与其相连,如1-3:H向上有两个与其相连;
树节点每个节点名称
节点
子节点(孩子节点)和父节点(双亲节点):这个名称是相对而言的,每个节点都可能是父节点,一个节点的上一级有节点,其上一级就叫他的父节点,他就叫做上一级的子节点;eg:A是B,C,D...的父节点,J是E的子节点。
兄弟节点和堂兄弟节点:其两者要求都是:这两个节点要在同一级,如果这两个节点的父节点相同就是兄弟节点,如果父亲不同就是堂兄弟节点;eg:B,C是兄弟节点,H,I是堂兄弟节点。
祖先节点和子孙节点:这两个节点也是相对而言的,一个节点沿着线往上走都是它的祖先,沿着线往下走都是它的子孙;eg:J的祖先有:E和A;J的子孙有P和Q;
度
节点的度:一个节点含有的子节点的数量;eg:A节点的度是6;
叶节点或终端节点:度为0的节点;eg:H,I,P....
分支节点或非终端节点:度不是0的节点;
树的度:最大的节点的度;eg:图中节点的度最大是6‘;
树的深度:最上面是第一层,往下加一;eg:图中树的深度是4;
二叉树
概念
二叉树可以看成3个部分组成:根节点,左子树,右子树。
要求
二叉树的每一个节点的度都要小于等于2;
二叉树有左右之分,左右子树的指向是不同的;
特殊二叉树
满二叉树
除了叶节点每一个节点的度都是2;
每一行节点数量是:1,2,4,8...根据递推深度为h的满二叉树,节点的个数是2的h-1次方。
完全二叉树
与满二叉树相比,完全二叉树的倒数第二层节点的度可以不是2,倒数第二层往上节点的度都要是2。
高度为h的完全二叉树,节点个数范围是2的h-2次方到2的h-1次方-1;
树性质:对于任何一个二叉树而言,度为0的节点个数总是比度2的节点个数多一个;
二叉树可以有两种储存的方式:顺序结构,链式结构。
二叉树的顺序结构
二叉树的顺序结构就是用数组来储存二叉树;一般情况下,完全二叉树更适合用顺序结构储存,因为其结构紧密,在父节点和子节点之间可以将联系。二叉树的顺序储存,其在物理结构上是数组,但在逻辑结构上是二叉树。
数组下标从0开始,可以通过下标找到一个节点对应的父节点和子节点。此处用parent和child来代表父节点和子节点的下标;parent=(child-1)/2;child=parent*2+1(左)或parent*2+2(右)
堆
现实生活中我们不直接用顺序结构来存储二叉树,而是用顺序结构存储堆。此处我们只讲解堆的定义和实现方式,根据堆的性质还可以实现堆排序,其效率远超冒泡排序,以及在大量数据中快速找到前K的top-K问题。
堆实际上属于完全二叉树的;但是与完全二叉树相比,多了一个要求。堆有两种。
大堆:父节点总是比子节点大。
小堆:父节点总是比子节点小。
typedef int HDateType;
typedef struct Heap
{HDateType* a;int size;int capacity;
}Heap;
特点:可以看到整个树是一个大堆(小堆),但是如果只看他们的左右子树,可以看出其左右子树实际上也是大堆(小堆)。
堆的实现
堆的定义及初始化
堆是用数组实现的,所以其结构体与动态顺序表相同。
typedef int HDateType;
typedef struct Heap
{HDateType* a;int size;int capacity;
}Heap;//堆的初始化
void HeapInit(Heap* php)
{php->capacity = 4;php->a = (HDateType*)malloc(sizeof(HDateType) * (php->capacity));if (php->a == NULL)perror("malloc failed!");php->size = 0;
}
堆的销毁
堆的销毁就是顺序表的销毁;
//堆的销毁
void HeapDestory(Heap* php)
{free(php->a);php->a = NULL;php->capacity = php->size = 0;}
堆的插入
在向一个堆中插入元素的时候,肯定是插在数组尾部;但是这样就会破坏堆的原本结构,可能导致子节点不一定比父节点小了(大了);所以在插入节点后要对堆进行调整;
此处以小堆为例;
当插入元素之后,将元素与其父节点进行比较,如果比父节点小就向上交换,直到比父节点小或到最上面时停止。
//交换
void Swap(int* x, int* y)
{int tmp = *x;*x = *y;*y = tmp;
}//向上调整,建小堆
void AdjustUp(HDateType* a,int child)
{assert(a);int parent = (child - 1) / 2;while (child > 0){if (a[child] < a[parent]){//交换Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}elsebreak;}
}//堆的插入
void HeapPush(Heap* php, HDateType x)
{assert(php);//空间够不够if (php->capacity == php->size){php->capacity *= 2;HDateType* new = (HDateType*)realloc(php->a,sizeof(HDateType) * (php->capacity));if (new == NULL)perror("realloc failed!");php->a = new;}php->a[php->size] = x;php->size++;//向上调整AdjustUp(php,php->size-1);
}
堆的删除
堆的删除是删除堆顶的元素,也就是数组中下标为0的元素。
直接删除数组中下标为0的元素,再让其他元素向前移动会导致堆的形态被破坏。所以我们使用间接方法删除堆顶元素,将数组尾部元素与堆顶元素进行交换,再让size减减,此时最后一个元素就是无效元素了,完成了删除;但是交换之后堆顶元素要进行向下调整,找到大于它的子节点。
//向下调整
void AdjustDown(HDateType* a, int size, int parent)
{assert(a);int child = parent * 2 + 1;while (child < size){//看两个子节点哪一个大一些if (child+1<size&&a[child] < a[child + 1])child++;if (a[child] < a[parent]){//交换Swap(&a[child], &a[parent]);parent = child;child = 2 * parent + 1;}elsebreak;}
}//堆的删除
void HeapPop(Heap* php)
{assert(php);HDateType* a = php->a;//交换Swap(&a[0], &a[php->size - 1]);php->size--;//向下调整AdjustDown(a, php->size, 0);//此处传的0是指从下标为0的位置开始向下调整
}
取堆顶的数据
//取堆顶的数据
HDateType HeapTop(Heap* php)
{assert(php);assert(php->size);return php->a[php->size - 1];
}
堆的数据个数
//堆的数据个数
int HeapSize(Heap* php)
{assert(php);return php->size;
}
堆的判空
//堆的判空
bool IsHeapEmpty(Heap* php)
{assert(php);if (php->size == 0)return true;return false;
}
堆的应用
堆排序
运用数据结构堆实现排序。
思路一:建大堆,创建一个新的数组,通过HeapPush向上调整的方法是建堆,将所有元素放入到堆中,通过将大堆的方式得到的堆顶就是最大值,再将最大值与堆尾进行交换,将队尾数据不再看作堆中的数据,再向下调整,重复每次都可以得到堆中最大的元素,直到堆只有一个数据时停止。
思路二:建堆,与方法一不同的是,不创建新数组,利用向下调整的方法,从后向前建堆,从尾节点的父节点开始依次向前进行向下台调整,后续步骤与方法一相同。
注意:思路二的向下建堆的方式要比思路一更高效,下面进行解释;
向下建堆更高效
此处来计算方法一和方法二建堆的时间复杂度;
注意:在进行向上或向下调整的时候要求开始的节点位置的所有子树必须是堆。
通过计算,可以看到在时间复杂度上,向下建堆更高效。接下来我们用方法二实现堆排序。
实现堆排序
//堆排序
void HeapSort(int* a,int size)
{//先进行建堆for (int i = (size - 1) / 2; i >= 0; i--){//向下调整AdjustDown(a, size, i);}//此时数组第一个元素就是最大元素//将其反倒尾部,就相当于堆的删除int num = size;for (int i = 1; i < size; i++){Swap(&a[0], &a[num-1]);num--;//向下调整AdjustDown(a, num, 0);}
}
TOP-K问题
关于TOP-K问题:从大量和数据中快速的获得最大的前K个数据,其运用于游戏,商铺好评等排序上面,下面我们通过堆这一数据结构来解决TOP-K问题。
Top-K解决
此处我们用50000个小于10000的数据进行筛选排序。
创建数据
此处创建50000个数据到date.txt文件与我们讲的二叉树无关就不过多讲解了。
void CreatDate(void)
{FILE* pf = fopen("date.txt", "w");srand((unsigned int)time(NULL));for (int i = 0; i < 50000; i++){int a = rand() % 10000;//将数据写入文件中fprintf(pf, "%d\n", a);}fclose(pf);
}
筛选数据
先放入K个数据到数组中去,建小堆,此时堆顶就是堆内的最小数据;再遍历文件中数据,将大于堆顶的元素交换,在进行向下调整。
注意:此处我们只是筛选最大的K个数据,但是没有对齐进行排序,想要排序可以再调用堆排序函数。
void GetTopK(int* arr, int k)
{FILE* pf = fopen("date.txt", "r");//先取出k个数据建小堆for (int i = 0; i < k; i++){fscanf(pf, "%d", &arr[i]);}for (int i = (k - 1) / 2; i >= 0; i--){AdjustDown(arr, k, i);}//此时小堆已经建好了//堆顶是最小的元素,遍历文件中的数据,若文件中的数据大,交换到堆顶int cur = 0;int ret = fscanf(pf, "%d", &cur);while(ret!=EOF){if (cur > arr[0]){Swap(&cur, &arr[0]);//向下调整AdjustDown(arr, k, 0);}ret = fscanf(pf, "%d", &cur);}for (int i = 0; i < k; i++)printf("%d ", arr[i]);
}
当我们K传20的时候。
二叉树的链式结构
对于一般二叉树,即非满二叉树和完全二叉树,我们一般选择链式结构来实现其功能。
定义链式二叉树
二叉树的链式结构包含三部分:节点储存的数据,节点的左子树地址,节点的右子树地址。
//定义普通二叉树
typedef int BTDateType;typedef struct BinaryTreeNode
{BTDateType date;struct BinaryTreeNode* left;struct BinaryTreeNode* right;
}BTNode;
链式二叉树的遍历
链式二叉树的遍历包含四种:前序遍历,中序遍历,后序遍历和层序遍历。
前序遍历
前序遍历的顺序是:根,左子树,右子树。访问根节点的数据在访问左右子树之前。
下面演示用前序遍历打印上面二叉树中的数据,将空间节点打印成NULL,非空打印数据。
//前序遍历
void PreOrder(BTNode* root)
{if (root == NULL)printf("NULL ");else{printf("%d ", root->date);PreOrder(root->left);PreOrder(root->right);}
}
中序遍历
中序遍历:左子树,根,右子树。先一直找左子树,直到左子树是空才打印这个节点储存的数据。对上面的树进行中序遍历。
//中序遍历
void InOrder(BTNode* root)
{if (root == NULL)printf("NULL ");else{InOrder(root->left);printf("%d ", root->date);InOrder(root->right);}
}
后序遍历
后续遍历:左子树,右子树,根。
//后序遍历
void PostOrder(BTNode* root)
{if (root == NULL)printf("NULL ");else{PostOrder(root->left);PostOrder(root->right);printf("%d ", root->date);}
}
层序遍历
层序遍历就是一层一层向下遍历,每层从左向右遍历。
关于层序遍历无法直接实现,要使用队列的数据结构间接实现。
当先让树顶元素入队列,在让其出队列打印,同时让其左右子树入队列,再出队列入队列循环往复直到队列为空。
栈和队列(C语言)-CSDN博客文章浏览阅读495次,点赞12次,收藏23次。帮助C语言初学者掌握栈和队列数据结构,并且能够通过栈和队列的数据结构完成习题应用https://blog.csdn.net/2401_87944878/article/details/145253954https://blog.csdn.net/2401_87944878/article/details/145253954此时队列中的元素不再是int整形,而是结构体BTNode;
//层序遍历
void LevelOrder(BTNode* root)
{assert(root);//定义队列Queue q;QueueInit(&q);QueuePush(&q, root);while (!IsQueueEmpty(&q)){BTNode* cur = QueTop(&q);printf("%d ", cur->date);QueuePop(&q);//左右树入队列if(cur->left)QueuePush(&q, cur->left);if(cur->right)QueuePush(&q, cur->right);}
}
二叉树节点的个数
用前中后序都可以计算二叉树节点的个数,此处我们一前序为例。
int countNode = 0;
//计算节点个数
int TreeSize(BTNode* root)
{if(root){countNode++;TreeSize(root->left);TreeSize(root->right);}return countNode;
}
注意:count是全局变量,如果是局部变量则函数要多加一个int*的指针参数,否则每个函数中count都是不同的。
叶子节点的个数
//二叉树叶子节点的个数
int LeafSize(BTNode* root)
{if (root == NULL)return 0;if (root->left == NULL && root->right == NULL)return 1;else{int left = LeafSize(root->left);int right = LeafSize(root->right);return left + right;}
}
第k层节点的个数
第k层节点的个数,就是左右树的第k-1层的节点个数。
//二叉树第k层节点的个数
int TreeSizeK(BTNode* root,int k)
{if (root == NULL)return 0;if (k == 1)return 1;return TreeSizeK(root->left, k - 1) + TreeSizeK(root->right, k - 1);
}
查找值为k的节点
//查找节点值为k的节点
BTNode* FindK(BTNode* root, int k)
{if (root == NULL)return NULL;if (root->date == k)return root;BTNode* left = FindK(root->left, k);BTNode* right = FindK(root->right, k);if (left)return left;if (right)return right;return NULL;
}
二叉树的销毁
为了方便不建立新节点,直接利用后序遍历来销毁。
//二叉树的销毁
void TreeDestory(BTNode* root)
{if (root){TreeDestory(root->left);TreeDestory(root->right);free(root);root = NULL;}
}