C语言最终文章-二叉树

2024-06-17 20:52
文章标签 语言 二叉树 文章 最终

本文主要是介绍C语言最终文章-二叉树,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言
  • 二叉树的性质
  • 二叉树的存储方式
    • 顺序存储
      • 堆及其应用
      • TopK问题
      • 堆排序
    • 链式存储
      • 二叉树的练习
        • 1.二叉树查找值为x的节点
        • 2.判断是否为完全二叉树
        • LC226.翻转二叉树
        • [LC572. 另一棵树的子树](https://leetcode.cn/problems/subtree-of-another-tree/description/)
        • 两道选择题
  • 结言

前言

  • 这篇文章大概就是我C语言学习的最后一篇文章了。

本篇文章主要内容:

  • 二叉树的几个性质,堆、堆排序和用堆实现TopK问题。
  • 向上建堆和向下建堆
  • 二叉树的练习

二叉树原图:

在这里插入图片描述

二叉树的性质

开始讲二叉树时,我们先来学一下二叉树的重要性质。

1.对于任意二叉树: n 0 = n 2 + 1 n0 = n2+1 n0=n2+1 (其中 n0代表度为0的个数,n2代表度为2的节点个数)
2. N − 1 = n 0 ∗ 0 + n 1 ∗ 1 + n 2 ∗ 2 + . . . . N-1 =n0*0+n1*1+n2*2+.... N1=n00+n11+n22+.... (N-1代表边的个数)
3. 对于满二叉树来说,二叉树节点个数 = 2 h − 1 2^h-1 2h1 (h为二叉树的高度)

先来看第一条 n 0 = = n 2 + 1 ? n0 ==n2+1? n0==n2+1

来让我们想一想二叉树是如何构建的呢?

首先我们要明白一件事: 首先我们要明白一件事: 首先我们要明白一件事: 度为1的是有度为0的节点增加一个分支得来的,度为2的是由度为1的节点增加一个分支得来的。

一开始我们只有一个根节点,那么我们的 n 0 = 1 n0=1 n0=1,当我们增加一个度为1的

节点时,那么此时我们的 n 0 还是 = 0 , n 1 = 1 n0还是 = 0,n1=1 n0还是=0n1=1,当我们增加一个度为2的时候
n 0 − > 2 , n 1 − > 0 , n 2 − > 1 n0->2,n1->0,n2->1 n0>2n1>0n2>1

过程如下图:
在这里插入图片描述
那我们不难发现,我们的 n 0 = n 2 + 1 n0=n2+1 n0=n2+1 这个条件始终成立,只要我们增加一个度为2的节点,那么n0肯定会增加一个。

再来看第二条,对与一棵树而言,子数肯定也是一棵树,两个兄弟节点之间也
不会有连线。那么 N 个节点就可以形成 N-1 条边。而 n0 有有0条边,n1有1条边…,那么总的边数 N-1 就可以得出来第二条的公式。

第三条

- 对于一个满二叉树来说
在这里插入图片描述

  • 第一层节点个数: 2 0 2^0 20
  • 第二层节点个数: 2 1 2^1 21
  • 第三层节点个数: 2 2 2^2 22
  • 第四层节点个数: 2 3 2^3 23

那么如果一个h层的满二叉树它的节点总数 T(N) = 2 0 + 2 1 + 2 2 + . . . + 2 h − 1 2^0+2^1+2^2+...+2^{h-1} 20+21+22+...+2h1

所以 T ( N ) = 2 h − 1 T(N) = 2^{h}-1 T(N)=2h1

二叉树的存储方式

顺序存储

顺序存储本质上就是用数组存。这里我们理解一下完全二叉树和满二叉树

  • 满二叉树:各层的节点都是满的状态-就如上面那个图一样如上图
  • 完全二叉树:前 h-1层都是满的,第h层从右向左连续去掉节点。这就相当于,在最后一层它的节点必须是连续的。
    在这里插入图片描述
    理解完这两个概念之后,如果我们把这些二叉树的节点都放入数组中会怎么样呢? 如图:
    在这里插入图片描述
    当我们把树的节点都放入数组中后,我们发现双亲节点和它的左孩子和右孩子之间存在这么一个关系。
  • l c h i l d i = 2 ∗ p a r e n t i + 1 , r c h i l d i = 2 ∗ p a r e n t i + 2 lchild_i = 2*parent_i+1,rchild_i = 2*parent_i+2 lchildi=2parenti+1rchildi=2parenti+2
  • p a r e n t i = ( ( l , r ) c h i l d i − 1 ) / 2 parent_{i} = ((l,r)child_i-1)/2 parenti=((l,r)childi1)/2

注意: 这个规则只对完全二叉树和满二叉树使用,当然其他的二叉树我们也可以这么存储,只不过途中我们要把位置空出来,这样比较浪费空间,所以我们会使用下面的链式存储。

堆及其应用

当我们对顺序存储有了以上的了解后,我们就可以来实现堆了。注意这里的堆跟操作系统的堆可不一样,我们这里只是一个数据结构。

堆的操作:

  • 入堆
  • 出堆顶元素
  • 取堆顶元素
  • 判空

堆可以用来做什么呢?
比如:排出中国最有钱的前10个人,堆排序等等。

这里先介绍两个概念:

  • 大堆:堆顶元素比其他元素大
  • 小堆:堆顶元素比其他元素小

为什么要有这两个堆呢?

同时注意:大堆和小堆并不意味着有序。

如下图:大堆和小堆,只能说明堆顶元素,一定比其他元素大,由于二叉树左子树和右子树是两个子树。子树和树具有相同的性质

  • 对于大堆而言: 左子树也是大堆,右子树也是大堆,那么左子树的左子树也是大堆…
    在这里插入图片描述
    如果你要排升序的话,利用堆排大堆还是小堆呢呢?

如果排小堆,那么每次堆顶就是我们的最小元素,但是然后怎么排呢?这样剩
下的元素就不一定是小堆了。所以当我们排升序的时候我们排大堆,直接把堆顶元素与最后一个位置的元素交换,注意这时我们下面的子树并没有受影响,所以我们直接再排一次大堆就可以了。然后不断的排,数组就有序了。

下面我们都以建大堆为例:

先来说入堆

入堆的话,我们需要用到向上调整算法

算法演示如下:
在这里插入图片描述
当我们插入一个新元素后,不断地去找双亲节点判断大小,如果比双亲的值大

那么就交换,否则就不交换。

代码:

void ADjustUp(HpDataType* a, int n)
{int child = n - 1;int parent = (n - 1 - 1) / 2;while (parent >= 0){if (a[child] > a[parent]) //大的往上调{swap(&a[parent], &a[child]);child = parent;parent = (child - 1) / 2;}elsebreak;}
}
void HpPush(Hp* hp, HpDataType x)
{assert(hp);check_capacity(hp); //检查容量hp->a[hp->size] = x;hp->size++;ADjustUp(hp->a,hp->size); // 开区间
}

出堆顶元素

这里我们就要用到 向下调整算法:,由于我们必须保证树是一个大堆,所以每次把堆顶元素Pop调后,要保证树还是一个大堆。

这里我们直接把堆顶 a[0] 与 最后一个元素交换。我们本质上还是数组。
然后再用一次向下调整算法即可。

向下调整算法过程如图:
在这里插入图片描述
代码:

void AdjustDown(HpDataType* a, int begin, int end)
{int parent = begin;int child = 2 * parent + 1;while (child < end){if (child + 1 < end && a[child] > a[child + 1]){child += 1;}if (a[parent] > a[child]){swap(&a[parent], &a[child]);parent = child;child = 2 * parent + 1;}elsebreak;}
}
//出堆
void HpPop(Hp* hp)
{assert(hp && hp->size > 0);swap(&hp->a[0],&hp->a[hp->size-1]);//此时这个位置就不能才排了hp->size--;AdjustDown(hp->a,0,hp->size); }

堆的总代码:

typedef int HpDataType;
typedef struct Heap
{HpDataType* a;int size;int capacity;
}Hp;//初始化堆
void HpInit(Hp* hp);
//销毁堆
void HpDestory(Hp* hp);
//入堆
void HpPush(Hp* hp, HpDataType x);
//出堆
void HpPop(Hp* hp);
//判空
bool HpEmpty(Hp* hp);
//堆顶元素
HpDataType HpTop(Hp* hp);
//向下
void AdjustDown(HpDataType* a, int begin, int end);void swap(HpDataType* e1, HpDataType* e2);void swap(HpDataType* e1, HpDataType* e2)
{HpDataType tmp = *e1;*e1 = *e2;*e2 = tmp;
}//初始化堆
void HpInit(Hp* hp)
{hp->size = 0;hp->capacity = 4;hp->a = (HpDataType*)malloc(sizeof(HpDataType) * (hp->capacity));if (hp->a == NULL){perror("malloc fail!\n");exit(-1);}
}
//销毁堆
void HpDestory(Hp* hp)
{hp->size = hp->capacity = 0;free(hp->a);
}
void check_capacity(Hp* hp)
{if (hp->capacity == hp->size){HpDataType* ptr = (HpDataType*)realloc(hp->a, sizeof(HpDataType) * (hp->capacity * 2));if (ptr == NULL){printf("realloc fail!\n");exit(-1);}hp->capacity *= 2;hp->a = ptr;}
}//排倒序 - 建大堆
void ADjustUp(HpDataType* a, int n)
{int child = n - 1;int parent = (n - 1 - 1) / 2;while (parent >= 0){if (a[child] > a[parent]) //大的往上调{swap(&a[parent], &a[child]);child = parent;parent = (child - 1) / 2;}elsebreak;}
}
//入堆
void HpPush(Hp* hp, HpDataType x)
{assert(hp);check_capacity(hp);hp->a[hp->size] = x;hp->size++;ADjustUp(hp->a,hp->size); // 开区间
}
//堆顶元素
HpDataType HpTop(Hp* hp)
{assert(hp && hp->size > 0);return hp->a[0];
}
//建大堆
void AdjustDown(HpDataType* a, int begin, int end)
{int parent = begin;int child = 2 * parent + 1;while (child < end){if (child + 1 < end && a[child] > a[child + 1]){child += 1;}if (a[parent] > a[child]){swap(&a[parent], &a[child]);parent = child;child = 2 * parent + 1;}elsebreak;}
}
//出堆
void HpPop(Hp* hp)
{assert(hp && hp->size > 0);swap(&hp->a[0],&hp->a[hp->size-1]);//此时这个位置就不能才排了hp->size--;AdjustDown(hp->a,0,hp->size); }
//判空
bool HpEmpty(Hp* hp)
{return hp->size == 0;
}

TopK问题

现在有一个问题

  • 你只有1KB的空间,如何把在4G的文件(假设都是整数)中找出前K(K > 0)大的数字
    既然我们学的是堆,肯定使用堆来解决,如果没有空间限制的话,我们可以把数据都读到内存中,然后排K次大堆即可。可关键我们只有1KB(1024byte)的空间,那如何做呢?

在这里插入图片描述
排成小堆后,堆顶元素就是最小值,每次读入一个比堆顶大的数据,读到最后就是前K个最大的数据了。

代码实现:

#define N 100000 //这里以 10万个数据为例子。
//创建数据
void CreatData()
{FILE* fin = fopen("data.txt", "w");if (fin == NULL){perror("fopen fail!\n");exit(-1);}int i = 0;srand(time(0));for (i = 0; i < N; i++){int x = (rand() + i) % 10000;fprintf(fin,"%d\n", x);}fclose(fin);
}//要求:只给你1KB空间,排出4G个整数的前K个最大值
void TopK()
{FILE* fout = fopen("data.txt","r");if (fout == NULL){perror("fopen fail!\n");exit(-1);}int k;printf("请输入k>: ");scanf("%d", &k);int i = 0;int* KminArr = (int*)malloc(sizeof(int) * k);//读取K个数建最小堆for (i = 0; i < k; i++){fscanf(fout,"%d",&KminArr[i]);}int x = 0;while (fscanf(fout, "%d", &x) > 0){if (x > KminArr[0]){KminArr[0] = x;AdjustDown(KminArr,0,k);}}HeapSort(KminArr,k);//前k个最大值printf("前%d个最大值为:", k);for (i = 0; i < k; i++){printf("%d ", KminArr[i]);}printf("\n");
}

堆排序

当我们了解了向上和向下两种算法的时候,我们就可以来实现啊堆排序了。

如果给你这样一个数组,如何排序呢?
在这里插入图片描述
思路:

建大堆,不断交换堆顶和数组最后一个位置的元素,然后再对剩下的数字进行向下调整算法。直到数组有序为止。

关键就是,我们如何去建大堆呢,前面我们讲堆的时候,那数组经过向上调整算法之后,本来就是一个大堆(ps:建大堆的前提就是源数据也是大堆)。那我们拿到一个随机的数组之后如何建大堆呢?

  • 以最后一个非叶子节点为开始,不断向下调整,直到达到堆顶,此时即为大堆。

在这里插入图片描述
建完大堆之后,不断交换堆顶和数组最后一个位置的元素,然后再对剩下的数字进行向下调整算法。

总代码:

void HeapSort(int* a, int n)
{int i = 0;for (i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(a,i,n);}int end = n - 1;while (end > 0){swap(&a[end],&a[0]);AdjustDown(a,0,end);end--;}
}void PrintArray(int* a, int n)
{for (int i = 0; i < n; i++){printf("%d ", a[i]);}printf("\n");
}
void TestHeapSort()
{int a[] = { 6,1,3,8,9,3,2,5,10,7 };int sz = sizeof(a) / sizeof(a[0]);HeapSort(a,sz);PrintArray(a,sz);
}

那么堆排序的时间复杂度是多少呢?

我们先来看一下建堆的复杂度:
在这里插入图片描述
建堆是 O(N),下面交换以及排大堆是 N*LOGN,按照大O渐进表示法,我们堆排序的时间复杂度为: O ( N ∗ L O G N ) O(N*LOGN) O(NLOGN)

再来看一下向上调整算法,建堆的复杂度:

在这里插入图片描述
这也是为什么我们不用向上调整算法的原因。

链式存储

先来看一下链式二叉树如何定义。
在这里插入图片描述
每一个节点我们都有数据和左孩子和右孩子。直接看遍历方式

链式二叉树有那么四种遍历方式,他们的访问方式为:

  • 前序遍历: 根 − > 左子树 − > 右子树 根->左子树->右子树 >左子树>右子树
  • 中序遍历: 左子树 − > 根 − > 右子树 左子树->根->右子树 左子树>>右子树
  • 后序遍历: 左子树 − > 右子树 − > 根 左子树->右子树->根 左子树>右子树>

很明显,这三种遍历方式很适合用递归来解决。
再来看一下层序遍历 (以上面大堆结果为例):

在这里插入图片描述
然后再挨个把队列的元素,Pop完就可以了,由于我们现在队列的元素是树的节点,所以最后队列里面全是 N U L L NULL NULL

  • 层序遍历的结果:就是按照层次把树中的元素打印出来。这里我们打印的就为 10 − > 9 − > 3 − > 8 − > 7 − > 3 − > 2 − > 5 − > 1 − > 6 10->9->3->8->7->3->2->5->1->6 10>9>3>8>7>3>2>5>1>6

代码实现:

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)
{if (root == NULL)return;printf("%c ", root->_data);BinaryTreePrevOrder(root->_left);BinaryTreePrevOrder(root->_right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{if (root == NULL)return;BinaryTreePrevOrder(root->_left);printf("%c ", root->_data);BinaryTreePrevOrder(root->_right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{if (root == NULL)return;BinaryTreePrevOrder(root->_left);BinaryTreePrevOrder(root->_right);printf("%c ", root->_data);
}
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{Queue q;QueueInit(&q);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);}}QueueDestroy(&q);}

二叉树的练习

1.二叉树查找值为x的节点

思路:

  • 如果此时我们这个节点的值 == 我们要找的值,直接返回即可。
  • 如果不等于取左右子树去找
  • 返回其中一个答案。

代码:

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{if (root == NULL)return NULL;if (root->_data == x)return root;BTNode* left = BinaryTreeFind(root->_left,x);BTNode* right = BinaryTreeFind(root->_right,x);return left == NULL ? right : left;
}
2.判断是否为完全二叉树

思路:

非完全二叉树我们测序遍历的时候跟完全二叉树有什么区别呢?

在这里插入图片描述
那就是我们会入空值进去,所以当我们取队头元素去到空值的时候,我们要判断如果队列中出现了非空的值,那就说明这时一个非完全二叉树。

代码:

// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{Queue q;QueueInit(&q);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);if (front != NULL)return false;QueuePop(&q);}QueueDestroy(&q);return true;
}
LC226.翻转二叉树

在这里插入图片描述
思路:

  • 翻转二叉树是把左子树和右子树互换,那么我们每一个子树都要互换。所以我们要不断往子树递归把子树的左子树和右子树交换。这就跟我们的层序遍历一样。

所以我们要递归左右子树,最后再把左右子树交换,然后再返回root。

代码:

struct TreeNode* invertTree(struct TreeNode* root) {if (root == NULL)return NULL;struct TreeNode* left = invertTree(root->left);struct TreeNode* right = invertTree(root->right);root->right = left;root->left = right;return root;
}

在这里插入图片描述

LC572. 另一棵树的子树
  • 建议先做一下这道题100. 相同的树

这题其实就是去找和 subRoot 相同的子树。

注意这个例子:我们要保证我们找的 root和 subRoot 左右子树都相同。
所以这时我们要遍历的root的左子树才能判断与subRoot相同。
在这里插入图片描述
思路:

  • 如果 r o o t 和 s u b R o o t 都为 N U L L ,直接返回 t r u e root和subRoot都为NULL,直接返回true rootsubRoot都为NULL,直接返回true,如果只有一个为空,返回false,如果此时两个树的val相同,判断此时这个位置是否相同,如果相同直接返回true,不想同去root的左右子树,继续去找。只要找到一个就返回true。

代码:

bool isSameTree(struct TreeNode* p, struct TreeNode* q) {if (p == NULL && q == NULL)return true;    if (p == NULL || q == NULL)return false;if (p->val != q->val)return false;return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){if (root == NULL && subRoot == NULL)return true;if (root == NULL || subRoot == NULL)return false;if (root->val == subRoot->val){if (isSameTree(root,subRoot))return true;}return isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);
}
两道选择题

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

结言

到这里本篇文章就结束了。

本篇文章的基本思路如下:
在这里插入图片描述
OK,二叉树部分就到这吧,学C++去了。
在这里插入图片描述

这篇关于C语言最终文章-二叉树的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1070469

相关文章

微信公众号脚本-获取热搜自动新建草稿并发布文章

《微信公众号脚本-获取热搜自动新建草稿并发布文章》本来想写一个自动化发布微信公众号的小绿书的脚本,但是微信公众号官网没有小绿书的接口,那就写一个获取热搜微信普通文章的脚本吧,:本文主要介绍微信公众... 目录介绍思路前期准备环境要求获取接口token获取热搜获取热搜数据下载热搜图片给图片加上标题文字上传图片

C语言中的数据类型强制转换

《C语言中的数据类型强制转换》:本文主要介绍C语言中的数据类型强制转换方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C语言数据类型强制转换自动转换强制转换类型总结C语言数据类型强制转换强制类型转换:是通过类型转换运算来实现的,主要的数据类型转换分为自动转换

利用Go语言开发文件操作工具轻松处理所有文件

《利用Go语言开发文件操作工具轻松处理所有文件》在后端开发中,文件操作是一个非常常见但又容易出错的场景,本文小编要向大家介绍一个强大的Go语言文件操作工具库,它能帮你轻松处理各种文件操作场景... 目录为什么需要这个工具?核心功能详解1. 文件/目录存javascript在性检查2. 批量创建目录3. 文件

C语言实现两个变量值交换的三种方式

《C语言实现两个变量值交换的三种方式》两个变量值的交换是编程中最常见的问题之一,以下将介绍三种变量的交换方式,其中第一种方式是最常用也是最实用的,后两种方式一般只在特殊限制下使用,需要的朋友可以参考下... 目录1.使用临时变量(推荐)2.相加和相减的方式(值较大时可能丢失数据)3.按位异或运算1.使用临时

使用C语言实现交换整数的奇数位和偶数位

《使用C语言实现交换整数的奇数位和偶数位》在C语言中,要交换一个整数的二进制位中的奇数位和偶数位,重点需要理解位操作,当我们谈论二进制位的奇数位和偶数位时,我们是指从右到左数的位置,本文给大家介绍了使... 目录一、问题描述二、解决思路三、函数实现四、宏实现五、总结一、问题描述使用C语言代码实现:将一个整

C语言字符函数和字符串函数示例详解

《C语言字符函数和字符串函数示例详解》本文详细介绍了C语言中字符分类函数、字符转换函数及字符串操作函数的使用方法,并通过示例代码展示了如何实现这些功能,通过这些内容,读者可以深入理解并掌握C语言中的字... 目录一、字符分类函数二、字符转换函数三、strlen的使用和模拟实现3.1strlen函数3.2st

Go语言中最便捷的http请求包resty的使用详解

《Go语言中最便捷的http请求包resty的使用详解》go语言虽然自身就有net/http包,但是说实话用起来没那么好用,resty包是go语言中一个非常受欢迎的http请求处理包,下面我们一起来学... 目录安装一、一个简单的get二、带查询参数三、设置请求头、body四、设置表单数据五、处理响应六、超

C语言中的浮点数存储详解

《C语言中的浮点数存储详解》:本文主要介绍C语言中的浮点数存储详解,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、首先明确一个概念2、接下来,讲解C语言中浮点型数存储的规则2.1、可以将上述公式分为两部分来看2.2、问:十进制小数0.5该如何存储?2.3 浮点

基于Python实现多语言朗读与单词选择测验

《基于Python实现多语言朗读与单词选择测验》在数字化教育日益普及的今天,开发一款能够支持多语言朗读和单词选择测验的程序,对于语言学习者来说无疑是一个巨大的福音,下面我们就来用Python实现一个这... 目录一、项目概述二、环境准备三、实现朗读功能四、实现单词选择测验五、创建图形用户界面六、运行程序七、

使用Go语言开发一个命令行文件管理工具

《使用Go语言开发一个命令行文件管理工具》这篇文章主要为大家详细介绍了如何使用Go语言开发一款命令行文件管理工具,支持批量重命名,删除,创建,移动文件,需要的小伙伴可以了解下... 目录一、工具功能一览二、核心代码解析1. 主程序结构2. 批量重命名3. 批量删除4. 创建文件/目录5. 批量移动三、如何安