数据结构——二叉树

数据结构——二叉树

1.树

树是一种非线性的数据结构,它是由 n 个有限结点组成的一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树。

——有一个特殊的结点,称为根结点,根节点没有前驱结点。

——除了根结点之外,其余结点被分为M个互不相交的集合,其中每一个集合又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或者多个后继。因此,树是递归定义的。

——树形结构中,⼦树之间不能有交集,否则就不是树形结构。

 例如:

(注:该图片来自于百度)

1.1 各种概念

——父结点/双亲结点:若⼀个结点含有⼦结点,则这个结点称为其⼦结点的⽗结点。

——子结点/孩子结点:⼀个结点含有的⼦树的根结点称为该结点的⼦结点。

——结点的度:有几个孩子,就为几度。

——树的度:最大结点的度就为树的度。

——叶子结点/终端结点:度为0的结点就为叶子结点。

——分支结点/非终端结点:度不为0的结点。

——兄弟结点:具有相同的父结点。

——结点的层次:有几层层次就为多少。

——树的高度/深度:树的最大层次。

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

——路径:⼀条从树中任意节点出发,沿⽗节点-⼦节点连接,达到任意节点的序列。

——子孙:以某结点为根的⼦树中任⼀结点都称为该结点的⼦孙。

——森林:由多棵互不相交的树组成的。

1.2 树的表示

用孩子兄弟表示法:

struct TreeNode { struct Node* child; //左边开始的第一个孩子的结点 struct Node* brother; //指向其右边的下一个兄弟结点 int data; //结点中的数据域 };

用图片来进行分析:

(注:上述图片来自于比特就业课)

通过上述的图片我们可以比较清晰地了解该方法。

1.3 应用

⽂件系统是计算机存储和管理⽂件的⼀种⽅式,它利⽤树形结构来组织和管理⽂件和⽂件夹。在⽂件系统中,树结构被⼴泛应⽤,它通过⽗结点和⼦结点之间的关系来表⽰不同层级的⽂件和⽂件夹之间的关联。

2.二叉树

2.1 概念与结构

二叉树是树形结构的一种。

一棵二叉树是结点的一个有限集合,该集合由一个根结点加上两棵别称为左子树和右子树的二叉树组成。

例如:

(注:该图片来自于比特就业课)

二叉树的特点:

——二叉树不存在度大于2的结点

——二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

2.2 特殊的二叉树

2.2.1 满二叉树

每一层的结点都达到最大值,则这个二叉树就是满二叉树。结点的总数就是 2^k - 1

(注:图片来自于百度)

2.2.2 完全二叉树

完全⼆叉树是效率很⾼的数据结构,完全⼆叉树是由满⼆叉树⽽引出来的。要注意的是满⼆叉树是⼀种特殊的完全⼆叉树。例如:

(注:图片来自比特就业课)

则该结构的特点是:假设二叉树的层次为K层,则除了第K层外,每层结点的个数达到最大结点数,第K层不一定达到最大结点数。

且完全二叉树结点的顺序是从左到右顺序放置的。

2.3 二叉树的存储结构

二叉树一般可以使用两种结构存储:一种是顺序结构,一种是链式结构。

2.3.1 顺序结构

顺序结构是使用数组来进行存储的,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费,完全二叉树使用顺序结构进行存储。

2.3.2 链式结构

⼆叉树的链式存储结构是指,⽤链表来表⽰⼀棵⼆叉树,即⽤链来指⽰元素的逻辑关系。 通常的⽅法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别⽤来给出该结点左孩⼦和右孩⼦所在的链结点的存储地址 。链式结构⼜分为⼆叉链和三叉链,当前我们学习中⼀般都是⼆叉链。后⾯课程学到⾼阶数据结构如红⿊树等会⽤到三叉链。

3.实现顺序结构二叉树

堆是一种特殊的二叉树,具有二叉树的特性的同时,还具备其他的特性。

3.1 堆的概念与结构

分为小跟堆和大根堆,我们通过图片来进行了解:

堆具有以下的性质:

——堆中某个结点的值总是不大于或者不小于其父结点的值

——堆总是一棵完全二叉树

二叉树的性质:

对于具有n个结点的完全二叉树,如果按照从上至下、从左至右的数组顺序对所有结点从0开始编号(例如上述的图片),则对于序号为i的结点有:

 1)若i > 0 , i 位置结点的双亲序号为:( i - 1 ) / 2 ;i = 0, i为根结点编号,无双亲编号。

 2)若2i + 1 < n,左孩子序号:2i + 1,2i + 1 >= n,否则无左孩子。

 3)若2i + 2 < n,右孩子序号:2i + 2,2i + 2 >= n,否则无右孩子。

3.2 堆的实现

3.2.1 堆的初始化与销毁

与前几次实现基本相同,这里省略分析过程。最终的代码会汇合在一起。

3.2.2 堆数据的插入

 由于插入的数据是随机的,我们不确定插入数据的大小,而大堆和小堆之间的数据的顺序是有一定规律的,因此,我们要通过适当的方法来进行插入。用堆的向上调整方法

这里对于堆的向上调整算法不再做过多的介绍,直接上代码:

void Swap(int* x, int* y) { int tmp = *x; *x = *y; *y = tmp; } void AdustUp(HPDataType* arr, int child) { int parent = (child - 1) / 2; while (child > 0)//不需要等于0,child只要走到了根结点的位置,根结点不需要交换了 { if (arr[child] < arr[parent]) { Swap(&arr[child], &arr[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } void HPPush(HP* php, HPDataType x) { assert(php); //判断空间是否足够 if (php->capacity == php->size) { //扩容 int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity; HPDataType* tmp = (HPDataType*)realloc(php->arr, newCapacity * sizeof(HPDataType)); if (tmp == NULL) { perror("realloc fail!"); exit(1); } php->arr = tmp; php->capacity = newCapacity; } php->arr[php->size] = x; AdustUp(php->arr, php->size); ++php->size; }

3.2.3 堆数据的删除

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法

与之前不同的是,在堆的删除中,我们删除的是堆顶的数据。

代码如下:

void AdjustDown(HPDataType* arr, int parent, int n) { int child = parent * 2 + 1; while (child < n) { //找左右孩子中最小的那一个 if (child + 1 < n && arr[child] > arr[child + 1]) { child++; } if (arr[child] < arr[parent]) { Swap(&arr[child], &arr[parent]); parent = child; child = parent * 2 + 1; } else { break; } } } void HPPop(HP* php) { assert(php && php->size); Swap(&php->arr[0], &php->arr[php->size - 1]); --php->size; AdjustDown(php->arr, 0, php->size); }

3.3 堆的应用---TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

当数据量太大的时候,就不能用排序的方法,这时,我们可以用堆来解决问题。

思路:拿K个数据来建堆,然后让取到的数据分别与原始的K个数据进行比较,最后所得即为所求。时间复杂度为O(N),空间复杂度为O(K)。根据上述的分析可得,找最小的K个数据,建大堆,找最大的K个数据,建小堆。

代码实现如下:

void CreateNDate() { //造数据 int n = 100000; srand(time(0)); const char* file = "data.txt"; FILE* fin = fopen(file, "w"); if (fin == NULL) { perror("fopen error"); return; } for (int i = 0; i < n; i++) { int x = (rand() + i) % 1000000; fprintf(fin, "%d\n", x); } fclose(fin); } void TOPK() { int k = 0; printf("请输入K:"); scanf("%d", &k); //从文件中读取前K个数据 const char* file = "data.txt"; FILE* fout = fopen(file, "r"); if (fout == NULL) { perror("fopen fail!"); exit(1); } int* minHeap = (int*)malloc(k * sizeof(int)); if (minHeap == NULL) { perror("malloc fail!"); exit(0); } for (int i = 0; i < k; i++) { fscanf(fout, "%d", &minHeap[i]); } //建堆 for (int i = (k - 1 - 1) / 2; i >= 0; i--) { AdjustDown(minHeap, i, k); } int x = 0; while (fscanf(fout, "%d", &x) != EOF) { if (x > minHeap[0]) { minHeap[0] = x; AdjustDown(minHeap, 0, k); } } for (int i = 0; i < k; i++) { printf("%d ", minHeap[i]); } fclose(fout); } int main() { //test01(); CreateNDate(); TOPK(); return 0; }

4.实现链式结构二叉树

用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。

一般来说,我们用的是二叉链表的结构来实现的。

 4.1 前中后序遍历

1)前序遍历(Preorder Traversal 亦称先序遍历):访问根结点的操作发生在遍历其左右子树之前。
访问顺序为:根结点、左子树、右子树
2)中序遍历(Inorder Traversal):访问根结点的操作发生在遍历其左右子树之中(间)。
访问顺序为:左子树、根结点、右子树
3)后序遍历(Postorder Traversal):访问根结点的操作发生在遍历其左右子树之后。
访问顺序为:左子树、右子树、根结点

4.1.1 前序遍历

代码实现如下:

void PreOrder(BTNode* root) { if (root == NULL) { return; } printf("%d ", root->data); PreOrder(root->left); PreOrder(root->right); }

其本质是应用递归的方法。 

可以用图像的方法来进行验证,这里省略~

4.1.2 中序遍历

代码如下:

//中 void InOrder(BTNode* root) { if (root == NULL) { return; } InOrder(root->left); printf("%d ", root->data); InOrder(root->right); }

4.1.3 后序遍历

代码如下:

//后 void PostOrder(BTNode* root) { if (root == NULL) { return; } PostOrder(root->left); PostOrder(root->right); printf("%d ", root->data); }

4.2 结点个数以及高度等

实现下列问题:

// 二叉树结点个数 int BinaryTreeSize(BTNode* root); // 二叉树叶子结点个数 int BinaryTreeLeafSize(BTNode* root); // 二叉树第k层结点个数 int BinaryTreeLevelKSize(BTNode* root, int k); //二叉树的深度/高度 int BinaryTreeDepth(BTNode* root); // 二叉树查找值为x的结点 BTNode* BinaryTreeFind(BTNode* root, BTDataType x); // 二叉树销毁 void BinaryTreeDestory(BTNode** root);

这里省略各问题的分析(提示:均运用递归的方式使得解决问题的方法更加简便)

代码实现如下:

// 二叉树结点个数 int BinaryTreeSize(BTNode* root) { if (root == NULL) { return 0; } return (1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right)); } // 二叉树叶子结点个数,即没有孩子结点的个数 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) { //思路:左子树第K层结点的个数 + 右子树第K层结点的个数 if (root == NULL) { return 0; } if (k == 1) { return 1; } return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1); } //二叉树的深度/高度(即层数) int BinaryTreeDepth(BTNode* root) { //1 + 左 + 右 if (root == NULL) { return 0; } int leftdep = BinaryTreeDepth(root->left); int rightdep = BinaryTreeDepth(root->right); return leftdep > rightdep ? leftdep + 1 : rightdep + 1; } // 二叉树查找值为x的结点 BTNode* BinaryTreeFind(BTNode* root, BTDataType x) { if (root == NULL) { return NULL; } if (root->data == x) { return root; } BTNode* leftFind = BinaryTreeFind(root->left, x); if (leftFind) { return leftFind; } BTNode* rightFind = BinaryTreeFind(root->right, x); if (rightFind) { return rightFind; } return NULL; } // 二叉树销毁 void BinaryTreeDestory(BTNode** root) { //先左后右,最后根结点 if (*root == NULL) return; BinaryTreeDestory(&((*root)->left)); BinaryTreeDestory(&((*root)->right)); free(*root); *root == NULL; }

4.3 层序遍历

设二叉树的根结点所在层数为1,层序遍历就是从所在二叉树的根结点出发,首先访问第一层的树根结点,然后从左到右访问第2层上的结点,接着是第三层的结点,以此类推,自上而下,自左至右逐层访问树的结点的过就是层序遍历

我们借助队列来解决。(先进先出)

代码实现如下:

//层序遍历 void LevelOrder(BTNode* root) { Queue q; QueueInit(&q); QueuePush(&q, root); while (!QueueEmpty(&q)) { BTNode* top = QueueFront(&q); printf("%d ", top->data); //出队 QueuePop(&q); if (top->left) { QueuePush(&q, top->left); } if (top->right) { QueuePush(&q, top->right); } } QueueDestroy(&q); }

4.4 判断是否为完全二叉树

涉及到层,所以我们还采用队列的方法来进行运算。

代码实现如下:

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); QueuePop(&q); if (front != NULL) { QueueDestroy(&q); return false; } } QueueDestroy(&q); return true; }

Read more

深入解剖STL Stack/Queue:配接器模式的容器变奏与源码探秘

深入解剖STL Stack/Queue:配接器模式的容器变奏与源码探秘

❤️@燃于AC之乐 来自重庆 计算机专业的一枚大学生 ✨专注 C/C++ Linux 数据结构 算法竞赛 AI 🏞️志同道合的人会看见同一片风景! 👇点击进入作者专栏: 《算法画解》 ✅ 《linux系统编程》✅ 《C++》 ✅ 🌟《算法画解》算法相关题目点击即可进入实操🌟 感兴趣的可以先收藏起来,请多多支持,还有大家有相关问题都可以给我留言咨询,希望希望共同交流心得,一起进步,你我陪伴,学习路上不孤单! 文章目录 * 前言 * 1. stack 概述 * 2. stack 定义完整列表 * 3. stack 没有迭代器 * 4. 以 list 作为 stack 的底层容器 * 5. queue 概述 * 6. queue 定义完整列表 * 7. queue 没有迭代器

By Ne0inhk

C++ 设计模式概述及常用模式

C++ 设计模式概述 本文介绍了C++中23种设计模式的分类及实现示例,主要分为三大类: 创建型模式(5个):单例模式(常用)、工厂方法模式(常用)、抽象工厂模式(常用)、建造者模式和原型模式。这些模式专注于对象的创建机制。 结构型模式(7个):适配器模式(常用)、桥接模式、组合模式和装饰器模式(常用)等。这些模式处理类和对象的组合方式。 行为型模式:未完整列出,但包含观察者模式等(未展示完整代码)。 文章通过简洁的C++代码示例展示了常用设计模式的实现方法,如单例模式通过私有构造函数和静态方法确保唯一实例,工厂方法模式通过抽象工厂类创建产品等。这些模式为解决特定设计问题提供了可重用的解决方案。 C++ 设计模式概述及常用模式 设计模式可分为三大类:创建型、结构型、行为型。以下是23个设计模式的分类及代码示例: 一、创建型模式(5个) 1. 单例模式(Singleton)⭐ 常用 classSingleton{private:static

By Ne0inhk

STL:vector的常用接口使用及底层讲解和实现

目录 1.vector的介绍及使用 1.1 vector的介绍 1.2 vector的使用 1.2.1vector的构造 1.2.2 vector iterator 的使用 1.2.3vector的空间增长问题 1.2.3 vector 增删查改 1.2.4vector 迭代器失效问题。(重点) 2.vector深度剖析及模拟实现 2.2 动态二维数组理解 1.vector的介绍及使用 1.1 vector的介绍 vector的文档介绍:https://cplusplus.com/reference/vector/vector/ 1.2

By Ne0inhk
【数据结构】二叉搜索树 C++ 简单实现:增删查改全攻略

【数据结构】二叉搜索树 C++ 简单实现:增删查改全攻略

目录 前言: 1、什么是二叉搜索树? 2、二叉搜索树性能分析 3、key类型二叉搜索树的实现 节点结构 类结构 3.1、插入 3.2、中序遍历 3.3、查找 3.4、删除 4、key_value类型二叉搜索树的实现 节点结构 类结构 4.1、构造函数 4.1.1 默认构造 4.1.2 拷贝构造 4.2、赋值重载 4.3、析构 4.4、插入 总结 前言: 今天这篇,

By Ne0inhk