二叉树
1.树的概念及结构
1.1树的概念
树是一种非线性的数据结构,是由n个有限结点组成的一个具有层次关系的集合。
之所以称其为树,是因为它像一颗倒挂的树
形状:根朝上,叶朝下
- 具有一个特殊的节点——根节点
- 剩余的结点被分为n个互不相交的集合,每个集合又是类似树的结构,称为子树
- 树是由递归定义的

**子树之间不能有交集**
1.2树的相关概念

节点的度:一个节点所含有的子树的个数 如上图D的为3
叶节点(终端节点):度为0的节点 如上图 F,G,H,I,J
分支节点(非终端节点):度不为0的节点 如上图 B,C,D,E
父节点(双亲节点):若某个节点含有至少一个子节点,则这个节点称作子节点的父节点 如上图 E是J的父节点
子节点(孩子节点):某个节点含有的子树的根节点称作该节点的子节点 如上图J是E的子节点
亲兄弟节点:具有相同父节点的节点互称为兄弟节点 如上图 E和F
堂兄弟节点:双亲在同一层的节点互称为堂兄弟 如上图I和J
树的度:一颗树中,最大的节点的度称为树的度 如上图中,树的度为3
节点的层次:从根开始,根为第一层,根的子节点为第二层,以此类推
树的高度(深度):树中节点的最大层次 如上图树的高度为4
节点的祖先:从根到该节点所经分支上的所有节点 如上图节点的祖先为A
子孙:以某节点为根的子树中任意节点都称为该节点的子孙 如上图 所有节点都是A的子孙
森林:由n棵互不相交的树所组成的集合称为森林
1.3树的表示
树结构既要保存数值,也要保存节点和节点之间的关系
树的最常见的表示方式为
左孩子右兄弟表示法:
父亲指向左边第一个孩子
孩子之间用兄弟指针链接起来
typedef int Datatype;
struct Node
{
struct Node* child;//第一个孩子节点
struct Node* brother;//指向其兄弟的节点
Datatype data;//节点中的数值
};

2.二叉树的概念及结构
2.1概念
一颗二叉树是节点的一个有限集合
- 由一个根节点加上两棵称为左子树和右子树组成二叉树
- 可能为空

特点
- 二叉树不存在度大于2的节点
- 二叉树的子树由左右之分,次序不能颠倒,有序
任意二叉树都是由下面几种情况所组成的

2.2特殊的二叉树
- 满二叉树:二叉树如果每层的节点数都达到最大值,则这个二叉树为满二叉树;如果满二叉树的的层数为K,则节点总数为2^K-1
- 完全二叉树:完全二叉树的效率很高,是由满二叉树引出来的;对于深度为K,具有n个节点的二叉树,当且仅当其每个节点都与深度为K的满二叉树中从编号1到n的节点一一对应时,则称之为完全二叉树。满二叉树是一种特殊的完全二叉树
前k-1层都是满的,最后一层满或者不满都可以,从左到右要求连续,节点数量范围 [2^(k-1) , 2^k-1]

2.3二叉树的性质
- 若根节点的层数为1,则一颗非空二叉树的第i层上最多有2^(i-1)个节点
- 若根节点的层数为1,则深度为h的二叉树的最大节点数为2^h-1(等比数列求和)
- 对任意一颗二叉树,如果度为0的叶节点个数为n0,度为2的分支节点个数为n2,则存在公式 n0 = n2 + 1
- 若根节点的层数为1,具有n个节点的满二叉树的深度为 h=log(n+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 无右孩子
2.4二叉树的存储结构
二叉树一般使用两种结构进行存储:顺序结构,链式结构
顺序结构
顺序结构存储就是使用数组进行存储,一般数组只适合表示完全二叉树,否则会有空间的浪费。二叉树顺序结构的存储在物理上是一个数组,在逻辑上是一颗二叉树。

数组下标计算父子关系公式
leftchild = parent*2+1 奇数
rightchild = parent*2+2 偶数
链式存储
二叉树的链式存储结构:用链表来表示一颗二叉树,即用链表来指示元素的逻辑关系。一般的方法是链表中每个结点都由三部分组成:数据和左右指针,左右指针分别用来给出该节点左孩子和右孩子所在链表的节点的存储地址。

3.二叉树的顺序结构及实现
3.1二叉树的顺序结构
一般的二叉树不适合用数组来存储,可能会存在大量的空间浪费。完全二叉树比较适合使用顺序结构存储。

3.2堆的概念及结构
堆可以被看作一棵完全二叉树的数组对象
堆是一个一维数组,把它所有元素(a0,a1,a2…an-1)按完全二叉树的顺序存储方式存储在堆中,并满足:ai<=a2i+1 且 ai<=a2i+2 (或 ai>=a2i+1 且 ai>=a2i+2) i=0,i=1…,则称为小堆(或大堆)。
将根节点最大的堆称作大根堆,根节点最小的堆称作小根堆。
根的性质
- 堆中某个节点的值总是大于或小于其父节点
- 堆总是一颗完全二叉树

3.3堆的实现
3.3.1堆向下调整算法
给定某个数组,逻辑上看作一个完全二叉树。通过从根节点开始的向下调整算法可以把它调整为大堆。
左右子树必须是同样的堆(大根堆或者小根堆),才能调整
int a = {9,3,2,6,5,10};

向下调整

3.3.2堆的创建
给定一个数组,在逻辑结构上可以看作一颗完全二叉树(还不是堆),通过算法,将其构建成堆。
思路:从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点,直到符合堆的特点

3.3.3建堆时间复杂度
因为堆是完全二叉树,满二叉树也是完全二叉树,为了方便,使用满二叉树进行计算时间复杂度
向下调整算法

需要移动节点的总次数
T(n)=2^0*(h-1) + 2^1*(h-2) + 2^2*(h-3) +…+ 2^(h-2)*1
计算可得
T(n) = 2^h -1 - h
n = 2^h - 1 h = log(n+1)
T(n) = n - log(n+1) = n
所以:建堆的时间复杂度为O(N)
3.3.4堆的插入
假设插入一个2到数组的末尾,再进行向上调整算法,直到构建成堆

- 将元素插入到堆的末尾,成为最后一个孩子
- 插入之后如果不满足堆的定义,将新插入的元素,顺着其双亲向上调整即可
3.3.5堆的删除
删除堆是删除堆顶的数据,将堆顶的数据和最后一个数据进行交换,然后删除数组最后一个数据,再进行向下调整。

思路
- 堆顶元素与堆最后一个元素进行交换
- 删除堆最后一个元素
- 将堆顶元素向下调整到适当位置,构建出新的堆
3.3.6堆的代码实现
定义类型和结构体
typedef int Datatype;
typedef struct Heap
{
Datatype* a;//创建数组存储元素
Datatype size;//计算元素个数
Datatype capacity;//堆的容量
}Heap;
初始化堆
//初始化堆
void Heapinit(Heap* hp);
void Heapinit(Heap* hp)
{
assert(hp);
hp->a = NULL;
hp->size = hp->capacity = 0;
}

销毁堆
//销毁堆
void Heapdestory(Heap* hp);
void Heapdestory(Heap* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
插入数据
由上面堆的插入可知,数据从数组的末尾插入,然后进行向上调整,直到构建新的堆
由于交换元素在堆的其他功能中也会使用,为了方便就将其独立为函数
交换元素
void Swap(Datatype* p1, Datatype* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//插入数据
void Heappush(Heap* hp, Datatype x);
void Adjustup(Datatype* a, Datatype child)
{
assert(a);
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[parent] > a[child])
{
Swap(&a[parent], &a[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void Heappush(Heap* hp, Datatype x)
{
assert(hp);
//判断容量
if (hp->size == hp->capacity)
{
int newcapacity = hp->capacity == 0 ? 4 : 2 * hp->capacity;
Datatype* tmp = (Datatype*)realloc(hp->a, newcapacity * sizeof(Datatype));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
hp->capacity = newcapacity;
hp->a = tmp;
}
hp->a[hp->size] = x;
hp->size++;
//向上调整
Adjustup(hp->a, hp->size - 1);
}
将数据 5,4,8,2,9,10,7 插入堆中,经过向上调整最终变成小根堆

小根堆

打印堆
//打印堆
void Heapprint(Heap* hp);
void Heapprint(Heap* hp)
{
assert(hp);
assert(!Heapempty(hp));
int i = 0;
for (i = 0; i < hp->size; i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}

读取堆顶数据
//读取堆顶数据
Datatype Heaptop(Heap* hp);
//读取堆顶数据
Datatype Heaptop(Heap* hp)
{
assert(hp);
assert(!Heapempty(hp));
return hp->a[0];
}

删除堆顶数据
//删除堆顶数据
void Heappop(Heap* hp);
void Adjustdown(Datatype* a, int n, int parent)
{
assert(a);
int minchild = parent * 2 + 1;
while (minchild < n)
{
if (minchild + 1 < n && a[minchild+1] < a[minchild])
{
minchild++;
}
if (a[minchild] < a[parent])
{
Swap(&a[minchild], &a[parent]);
parent = minchild;
minchild = parent * 2 + 1;
}
else
{
break;
}
}
}
void Heappop(Heap* hp)
{
assert(hp);
assert(!Heapempty(hp));
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
//向下调整
Adjustdown(hp->a, hp->size, 0);
}


判断堆是否为空
//判断堆是否为空
bool Heapempty(Heap* hp);
bool Heapempty(Heap* hp)
{
assert(hp);
return hp->size == 0;
}
计算堆中的元素的个数
//计算堆中的元素的个数
int Heapsize(Heap* hp);
int Heapsize(Heap* hp)
{
assert(hp);
return hp->size;
}
以上是通过将数组中的值传到堆中,再进行堆的调整,最后变为大根堆或小根堆
那么可不可以直接在数组中建堆,再进行调整,最终变为大根堆或小根堆呢?
答案是:当然可以
利用向上调整算法或者向下调整算法直接再数组中建堆,最终变为大根堆或者小根堆
代码实现如下
向上调整建堆
void HeapCreat(int* a, int n)
{
int i = 0;
//向上建堆
for (i = 1; i < n; i++)
{
Adjustup(a, i);
}
}
int main()
{
Heap hp;
int arr[] = { 5,4,8,2,9,10,7 };
int sz = sizeof(arr) / sizeof(arr[0]);
HeapCreat(arr, sz);
return 0;
}

向上建堆时间复杂度计算

调整次数=每层节点个数*每层向下调整的次数(最坏情况下,全部调整)
T(N)=2^1* 1 + 2^2 * 2+ 2^3* 3+…+2^(h-1) *(h-1)
2^h -1 = N h=log(N+1)
估算T(N) = N*log(N)
向下调整
根据上面介绍的思路,代码实现如下
void HeapCreat(int* a, int n)
{
int i = 0;
for (i = (n - 1 - 1) / 2; i >= 0; i++)
{
Adjustdown(a, i);
}
}
int main()
{
Heap hp;
int arr[] = { 5,4,8,2,9,10,7 };
int sz = sizeof(arr) / sizeof(arr[0]);
HeapCreat(arr, sz);
return 0;
}

3.4堆的应用
3.4.1堆排序
堆排序便是利用堆的特点进行排序,分为两个步骤
1.建堆
升序:建大堆
降序:建小堆
2.通过堆删除的思路进行排序
升序
- 建大堆
- 把最后一个节点和第一个节点交换,不把最后一个节点看作堆中。开始开始向下调整,选出次大的元素,依次类推
代码实现
void HeapCreat(int* a, int n)
{
int i = 0;
//向下建堆 建大堆
for (i = (n - 1 - 1) / 2; i >= 0; --i)
{
Adjustdown(a, n, i);
}
i = 1;
while (i < n)
{
交换第一个和最后一个节点
Swap(&a[0], &a[n - i]);
重新向下调整
Adjustdown(a, n-i, 0);
++i;
}
}
int main()
{
int arr[] = { 5,4,8,2,9,10,7 };
int sz = sizeof(arr) / sizeof(arr[0]);
HeapCreat(arr, sz);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}

降序的思路与升序的思路类似,这里就不加赘叙
3.4.2TOP-K问题
求得数据集合中前K个最大或者最小的元素
方法:
- 排序 时间复杂度 O(N*logN)
- 建堆->堆排序
当数据集合特别大时,方法1便不可行,数据太大不能在堆上存储数据,只能在磁盘上存储数据。
所以采取方法2
方法2也有两种方式去实现
- 建大堆 建N个数的大堆,再popK次即可
- 建小堆,建K个数的小堆,依次遍历后(N-K)个数,如果比堆顶的数据大,便进行交换,再向下调整即可
代码实现 建小堆
void Creatflie(const char* file, int N)
{
FILE* fin = fopen("test.txt", "w");
if (fin == NULL)
{
perror("fopen fail");
exit(-1);
}
srand((unsigned int)time(0));
int i = 0;
for (i = 0; i < N; i++)
{
fprintf(fin, "%d\n", rand() % 10);
}
fclose(fin);
}
void Printtopk(const char* file, int k)
{
assert(file);
FILE* fout = fopen("test.txt", "r");
if (fout == NULL)
{
perror("fopen fail");
exit(-1);
}
int* minHeap = (int*)malloc(k * sizeof(int));
if (minHeap == NULL)
{
perror("malloc fail");
exit(-1);
}
int i = 0;
//读取前k个元素
for (i = 0; i < k; i++)
{
fscanf(fout, "%d", &minHeap[i]);
}
//建k个数的小根堆
for (i = (k - 2) / 2; i < k; i++)
{
Adjustdown(minHeap, k, i);
}
//读取N-K个数
int val = 0;
while (fscanf(fout, "%d", &val) != EOF)
{
if (val > minHeap[0])
{
minHeap[0] = val;
Adjustdown(minHeap, k, 0);
}
}
for (i = 0; i < k; i++)
{
printf("%d ", minHeap[i]);
}
printf("\n");
free(minHeap);
fclose(fout);
}
int main()
{
const char* file = "test.txt";
int N = 10;
int k = 5;
//创建文件
Creatflie(file, N);
Printtopk(file, k);
return 0;
}

4.二叉树的链式结构及实现
4.1前置说明
二叉树
- 空树
- 非空:根节点,根节点的左子树,根节点的右子树组成
4.2二叉树的遍历
4.2.1前序,中序以及后序遍历
二叉树遍历是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。

二叉树的遍历有三种方式:前序/中序/后序递归遍历
前序遍历:最先访问根节点,其次访问左子树,右子树
中序遍历:最先访问左子树,其次访问根,右子树
后序遍历:最先访问左子树,其次访问右子树,根
前序遍历
void Preorder(BTnode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
assert(root);
printf("%d ", root->data);
Preorder(root->left);
Preorder(root->right);
}
对上面的二叉树进行前序遍历

运行结果与上述分析一致

中序遍历
void Inorder(BTnode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
assert(root);
Inorder(root->left);
printf("%d ", root->data);
Inorder(root->right);
}
对上面的二叉树进行中序遍历

运行结果与上述分析一致

后序遍历
void Postorder(BTnode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
assert(root);
Postorder(root->left);
Postorder(root->right);
printf("%d ", root->data);
}
对上面的二叉树进行后序遍历

运行结果与上述分析一致

4.2.2层序遍历
层序遍历:假设二叉树的根节点所在的层数为1,层序遍历便是从二叉树的根节点出发,先访问第一层的根节点,然后依次从左向右访问第二层上的节点,其次是第三层上的节点,以此类推,从上到下,从左到右逐层访问树的节点的过程就是层序遍历。

层序遍历采取队列的思想:先入先出
例如根节点先入队列,然后出队列时,通过左右指针将左右子树的根节点带入队列,重复此操作,直到将二叉树完全遍历。

代码实现
void Levelorder(BTnode* root)
{
QE q;
QEinit(&q, root);
if (root)
{
QEpush(&q, root);
}
while (!QEempty(&q))
{
BTnode* front = QEfront(&q);
QEpop(&q);
printf("%d ", front->data);
//下一层
if (front->left)
{
QEpush(&q, front->left);
}
if (front->right)
{
QEpush(&q, front->right);
}
}
printf("\n");
QEdestory(&q);
}

4.3节点个数以及高度
计算第k层叶子节点的总数
//计算第k层节点个数
int Treeksize(BTnode* root,int k)
{
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
//转化成计算第(k-1)层节点的个数
return Treeksize(root->left, k - 1) + Treeksize(root->right, k - 1);
}


计算总的节点个数
int Treesize(BTnode* root)
{
if (root == NULL)
{
return 0;
}
return Treesize(root->left) + Treesize(root->right) + 1;
}


计算叶子节点总数
int Treeleafsize(BTnode* root)
{
if (root == NULL)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return Treeleafsize(root->left) + Treeleafsize(root->right);
}


高度
后序思想
计算左子树,右子树高度
父亲高度=较高的子树+1
int Treeheight(BTnode* root)
{
if (root == NULL)
{
return 0;
}
int lret = Treeheight(root->left);
int rret = Treeheight(root->right);
return lret > rret ? lret + 1 : rret + 1;
}

求节点所在位置
//返回x所在的节点
BTnode* Treefind(BTnode* root, int x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
//找左子树
BTnode*lret = Treefind(root->left, x);
if (lret)
{
return lret;
}
//左子树没有找到,找右子树
BTnode* rret = Treefind(root->right, x);
if (rret)
{
return rret;
}
return NULL;
}
4.4二叉树的创建和销毁
通过前序遍历数组
“1 2 3 NULL NULL 4 NULL NULL 5 NULL 6 NULL NULL” 构建二叉树
BTnode* Binarytreecreate(BTdatatype* a, int* pi)
{
if (NULL == a[*pi])
{
(*pi)++;
return NULL;
}
BTnode* root = (BTnode*)malloc(sizeof(BTnode));
if (root == NULL)
{
perror("malloc fail");
return NULL;
}
root->data = a[*pi];
(*pi)++;
root->left = Binarytreecreate(a, pi);
root->right = Binarytreecreate(a, pi);
return root;
}
中序遍历二叉树并打印

二叉树的销毁
void Binarytreedestory(BTnode* root)
{
if (root == NULL)
{
return;
}
Binarytreedestory(root->left);
Binarytreedestory(root->right);
free(root);
}

判断二叉树是否为完全二叉树
int Binarytreecomplete(BTnode* root)
{
QE q;
QEinit(&q);
if (root)
{
QEpush(&q, root);
}
while (!QEempty(&q))
{
BTnode* front = QEfront(&q);
QEpop(&q);
if (front == NULL)
{
break;
}
QEpush(&q, front->left);
QEpush(&q, front->right);
}
//遇到NULL之后,从上面循环跳出
//如果后面全是空,则为完全二叉树
//如果后面存在非空,则不为完全二叉树
while (!QEempty(&q))
{
BTnode* front = QEfront(&q);
QEpop(&q);
if (front != NULL)
{
return false;
}
}
QEdestory(&q);
//如果整个程序走完,则说明是完全二叉树,返回true
return true;
}

返回结果

1328





