1.树概念及结构
1.1树的概念
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
1.2 树的相关概念
1.3 树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。
我们这里就简单的了解其中最常用的孩子兄弟表示法。
1.4 树在实际中的运用(表示文件系统的目录树结构)
2.二叉树概念及结构
2.1概念
一棵二叉树(度最大为2)是结点的一个有限集合,该集合:
- 1. 或者为空
- 2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
从上图可以看出:
- 1. 二叉树不存在度大于2的结点
- 2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
注意:对于任意的二叉树都是由以下几种情况复合而成的:
2.2现实中的二叉树:
2.3 特殊的二叉树:
1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
2.4二叉树的性质
2.5 二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1. 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2. 链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构如红黑树等会用到三叉链。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{struct BinTreeNode* _pLeft; // 指向当前节点左孩子 struct BinTreeNode* _pRight; // 指向当前节点右孩子BTDataType _data; // 当前节点值域
}// 三叉链
struct BinaryTreeNode
{struct BinTreeNode* _pParent; // 指向当前节点的双亲 struct BinTreeNode* _pLeft; // 指向当前节点左孩子 struct BinTreeNode* _pRight; // 指向当前节点右孩子BTDataType _data; // 当前节点值域
};
3.二叉树的顺序结构及实现
3.1 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
3.2 堆的概念及结构 (完全二叉树)
大堆 :树中一个树及子树中,任何一个父亲都大于等于孩子
小堆 :树中一个树及子树中,任何一个父亲都小于等于孩子
堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
3.3 堆的实现
3.3.1 堆向下调整
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。
向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
//堆的向下调整(小堆)
void AdjustDown(int* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){// 选出左右孩子中小的那一个if (child + 1 < n && a[child + 1] < a[child]){++child;}// 如果小的孩子小于父亲,则交换,并继续向下调整if (a[child] < a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}
3.3.2 堆向上调整
//堆的向上调整(小堆)
void AdjustUp(int* 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;}else{break;}}
}
3.3.2堆的创建
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
3.3.3 建堆时间复杂度
3.3.4 堆的插入
先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。
void HeapPush(HP* hp, HPDataType x)
{assert(hp);HeapCheckCapacity(hp);hp->a[hp->size] = x;hp->size++;AdjustUp(hp->a, hp->size - 1);
}
3.3.5 堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
3.3.6 堆的代码实现
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>typedef int HPDataType;typedef struct Heap
{HPDataType* a;int size;int capacity;
}HP;// 堆的初始化
void HeapInit(HP* hp);
// 堆的销毁
void HeapDestroy(HP* hp);
// 堆的插入
void HeapPush(HP* hp, HPDataType x);
// 堆的删除
void HeapPop(HP* hp);
// 取堆顶的数据
HPDataType HeapTop(HP* hp);
// 堆的数据个数
int HeapSize(HP* hp);
// 堆的判空
bool HeapEmpty(HP* hp);
// 堆的打印
void HeapPrint(HP* hp);
// 堆的扩容
void HeapCheckCapacity(HP* hp);
#define _CRT_SECURE_NO_WARNINGS 1#include"Heap.h"void Swap(HPDataType* px, HPDataType* py)
{HPDataType tmp = *px;*px = *py;*py = tmp;
}void HeapInit(HP* hp)
{assert(hp);hp->a = NULL;hp->size = hp->capacity = 0;
}void HeapDestroy(HP* hp)
{assert(hp);free(hp->a);hp->capacity = hp->size = 0;
}void HeapCheckCapacity(HP* hp)
{assert(hp);if (hp->size == hp->capacity){size_t newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;HPDataType* tmp = realloc(hp->a, sizeof(HPDataType) * newCapacity);if (tmp == NULL){printf("realloc fail\n");exit(-1);}hp->a = tmp;hp->capacity = newCapacity;}
}void HeapPush(HP* hp, HPDataType x)
{assert(hp);HeapCheckCapacity(hp);hp->a[hp->size] = x;hp->size++;AdjustUp(hp->a, hp->size - 1);
}bool HeapEmpty(HP* hp)
{assert(hp);return hp->size == 0;
}int HeapSize(HP* hp)
{assert(hp);return hp->size;
}HPDataType HeapTop(HP* hp)
{assert(hp);assert(!HeapEmpty(hp));return hp->a[0];
}void AdjustDown(int* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){// 选出左右孩子中小的那一个if (child + 1 < n && a[child + 1] < a[child]){++child;}// 如果小的孩子小于父亲,则交换,并继续向下调整if (a[child] > a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}void HeapPop(HP* hp)
{assert(hp);assert(!HeapEmpty(hp));Swap(&hp->a[0], &hp->a[hp->size - 1]);hp->size--;AdjustDown(hp->a, hp->size, 0);
}
void HeapPrint(HP* hp)
{for (int i = 0; i < hp->size; ++i){printf("%d ", hp->a[i]);}printf("\n");
}
#include "Heap.h"int main()
{int a[] = { 70, 56, 30, 25, 15, 10, 75 };HP hp;HeapInit(&hp);for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i){HeapPush(&hp, a[i]);}HeapPrint(&hp);return 0;
}
3.4 堆的应用
3.4.1 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
- 升序:建大堆
- 降序:建小堆
2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
堆排序代码
Heap.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>typedef int HPDataType;typedef struct Heap
{HPDataType* a;int size;int capacity;
}HP;// 堆的初始化
void HeapInit(HP* hp);
// 堆的销毁
void HeapDestroy(HP* hp);
// 堆的插入
void HeapPush(HP* hp, HPDataType x);
// 堆的删除
void HeapPop(HP* hp);
// 取堆顶的数据
HPDataType HeapTop(HP* hp);
// 堆的数据个数
int HeapSize(HP* hp);
// 堆的判空
bool HeapEmpty(HP* hp);
// 堆的打印
void HeapPrint(HP* hp);
// 堆的扩容
void HeapCheckCapacity(HP* hp);// 堆的向上调整
void AdjustUp(int* a, int child);
// 堆的向下调整
void AdjustDown(int* a, int n, int parent);
// 交换函数
void Swap(HPDataType* px, HPDataType* py);
Heap.c
#define _CRT_SECURE_NO_WARNINGS 1#include"Heap.h"void Swap(HPDataType* px, HPDataType* py)
{HPDataType tmp = *px;*px = *py;*py = tmp;
}void HeapInit(HP* hp)
{assert(hp);hp->a = NULL;hp->size = hp->capacity = 0;
}void HeapDestroy(HP* hp)
{assert(hp);free(hp->a);hp->capacity = hp->size = 0;
}void HeapCheckCapacity(HP* hp)
{assert(hp);if (hp->size == hp->capacity){size_t newCapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;HPDataType* tmp = realloc(hp->a, sizeof(HPDataType) * newCapacity);if (tmp == NULL){printf("realloc fail\n");exit(-1);}hp->a = tmp;hp->capacity = newCapacity;}
}void AdjustUp(int* 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;}else{break;}}
}void HeapPush(HP* hp, HPDataType x)
{assert(hp);HeapCheckCapacity(hp);hp->a[hp->size] = x;hp->size++;AdjustUp(hp->a, hp->size - 1);
}bool HeapEmpty(HP* hp)
{assert(hp);return hp->size == 0;
}int HeapSize(HP* hp)
{assert(hp);return hp->size;
}HPDataType HeapTop(HP* hp)
{assert(hp);assert(!HeapEmpty(hp));return hp->a[0];
}void AdjustDown(int* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){// 选出左右孩子中小的那一个if (child + 1 < n && a[child + 1] < a[child]){++child;}// 如果小的孩子小于父亲,则交换,并继续向下调整if (a[child] < a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}void HeapPop(HP* hp)
{assert(hp);assert(!HeapEmpty(hp));Swap(&hp->a[0], &hp->a[hp->size - 1]);hp->size--;AdjustDown(hp->a, hp->size, 0);
}
void HeapPrint(HP* hp)
{for (int i = 0; i < hp->size; ++i){printf("%d ", hp->a[i]);}printf("\n");
}
test.c
#include "Heap.h"// 升序 空间复杂度是多少? O(N) 要求优化到O(1) -> 不能用Heap
//void HeapSort(int* a, int n)
//{
// HP hp;
// HeapInit(&hp);
// // 建议一个N个小堆
// for (int i = 0; i < n; ++i)
// {
// HeapPush(&hp, a[i]);
// }
//
// // Pop N 次
// for (int i = 0; i < n; ++i)
// {
// a[i] = HeapTop(&hp);
// HeapPop(&hp);
// }
//
// HeapDestroy(&hp);
//}// 升序
void HeapSort(int* a, int n)
{// 把a构建成小堆// 方法1:/*for (int i = 1; i < n; ++i){AdjustUp(a, i);}*/// 方法2:// O(N)for (int i = (n - 1 - 1) / 2; i >= 0; --i){//n-1是最后一个节点,(n-1-1)/2是最后一个节点的父节点AdjustDown(a, n, i);//小堆}
//// 依次选数,调堆// O(N*logN)for (int end = n - 1; end > 0; --end){Swap(&a[end], &a[0]);// 再调堆,选出次小的数AdjustDown(a, end, 0);}
}int main()
{//TestTopk();int a[] = { 70, 56, 30, 25, 15, 10, 75, 33, 50, 69 };for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i){printf("%d ", a[i]);}printf("\n");HeapSort(a, sizeof(a) / sizeof(a[0]));for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i){printf("%d ", a[i]);}printf("\n");return 0;
}
3.4.2 TOP-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能 数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
- 前k个最大的元素,则建小堆
- 前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
首先思考下建立大堆还是小堆?
建小堆,因为如果建大堆,最大的数可能挡在头的位置,其他9个次大的数就进不来。
思路:
1、用前K个数建立一个K个数的小堆。
2、剩下的N-K个数,依次跟堆顶的数据进行比较,如果比堆顶数据大,就替换堆顶的数据,再向下调整
3、最后堆里面K个数就是最大的K个数
#include "Heap.h"// 在N个数找出最大的前K个 or 在N个数找出最小的前K个
void PrintTopK(int* a, int n, int k)
{
// 1. 建堆--用a中前k个元素建堆
// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换HP hp;HeapInit(&hp);// 创建一个K个数的小堆for (int i = 0; i < k; ++i){HeapPush(&hp, a[i]);}// 剩下的N-K个数跟堆顶的数据比较,比他大,就替换他进堆for (int i = k; i < n; ++i){if (a[i] > HeapTop(&hp)){//HeapPop(&hp);//HeapPush(&hp, a[i]);hp.a[0] = a[i];AdjustDown(hp.a, hp.size, 0);}}HeapPrint(&hp);HeapDestroy(&hp);
}void TestTopk()
{int n = 1000000;int* a = (int*)malloc(sizeof(int)*n);srand(time(0));for (size_t i = 0; i < n; ++i){a[i] = rand() % 1000000;}// 再去设置10个比100w大的数a[5] = 1000000 + 1;a[1231] = 1000000 + 2;a[5355] = 1000000 + 3;a[51] = 1000000 + 4;a[15] = 1000000 + 5;a[2335] = 1000000 + 6;a[9999] = 1000000 + 7;a[76] = 1000000 + 8;a[423] = 1000000 + 9;a[3144] = 1000000 + 10;PrintTopK(a, n, 10);
}void TestHeap()
{int a[] = { 70, 56, 30, 25, 15, 10, 75 };HP hp;HeapInit(&hp);for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i){HeapPush(&hp, a[i]);}HeapPrint(&hp);HeapPop(&hp);HeapPrint(&hp);HeapPop(&hp);HeapPrint(&hp);HeapPop(&hp);HeapPrint(&hp);HeapPop(&hp);HeapPrint(&hp);HeapDestroy(&hp);
}// 升序 空间复杂度是多少? 要求优化到O(1) -> 不能用Heap
//void HeapSort(int* a, int n)
//{
// HP hp;
// HeapInit(&hp);
// // 建议一个N个小堆
// for (int i = 0; i < n; ++i)
// {
// HeapPush(&hp, a[i]);
// }
//
// // Pop N 次
// for (int i = 0; i < n; ++i)
// {
// a[i] = HeapTop(&hp);
// HeapPop(&hp);
// }
//
// HeapDestroy(&hp);
//}int main()
{//TestTopk();int a[] = { 70, 56, 30, 25, 15, 10, 75 };for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i){printf("%d ", a[i]);}printf("\n");HeapSort(a, sizeof(a) / sizeof(a[0]));for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i){printf("%d ", a[i]);}printf("\n");return 0;
}
4.二叉树链式结构及其实现
4.1 前置说明
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。
由于现在大家对二叉树结构掌握还不够深入,为了降低大家学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。
typedef char BTDataType;
typedef struct BinaryTreeNode
{struct BinaryTreeNode* left;struct BinaryTreeNode* right;BTDataType data;
}BTNode;BTNode* CreatBinaryTree()
{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;
}
注意:上述代码并不是创建二叉树的方式,真正创建二叉树方式后序详解重点讲解。
再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:
1. 空树
2. 非空:根节点,根节点的左子树、根节点的右子树组成的。
从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
4.2二叉树的遍历
4.2.1 前序、中序以及后序遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉 树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
- 1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
- 2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
- 3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
前序遍历:
中序遍历:
后序遍历:
// 二叉树前序遍历
void PreOrder(BTNode* root);
// 二叉树中序遍历
void InOrder(BTNode* root);
// 二叉树后序遍历
void PostOrder(BTNode* root);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);
}
前序遍历递归图解: