二叉树的内容比较多,分开更新。
树
这里首先要弄清楚的就是树的各个概念,其实是比较简单的,重点记住节点的度,树的度,祖先等概念,其他的都可以一眼懂
第二点就是树的表示,这个也很好理解。
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
第三就是二叉树的概念,它就是树的一种分支,关于概念呢,这里有两部分需要掌握,第一个是满二叉树和完全二叉树,第二个就是二叉树的性质。
单说一下二叉树的性质吧,做选择题用得到。
1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个结点.
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^h-1.
3. 对任何一棵二叉树, 如果度为0其叶结点个数为a , 度为2的分支结点个数为b ,则有 a= b+1
接下来就将重点了
二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1. 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。
二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2. 链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。(这里只说二叉链)
接下来用顺序存储来实现堆,需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
堆的概念以及实现
先给出堆的常用接口函数
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void Swap(HPDataType* p1, HPDataType* p2);
// O(logN)
void AdjustDwon(HPDataType* a, int size, int parent);
void AdjustUp(HPDataType* a, int child);
void HeapPrint(HP* php);
void HeapInit(HP* php);
void HeapDestroy(HP* php);
void HeapPush(HP* php, HPDataType x);
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
bool HeapEmpty(HP* php);
int HeapSize(HP* php);
然后给出比较简单的一些接口函数的实现,就是不需要利用其他新知识的
//交换
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//打印堆
void HeapPrint(HP* php)
{
assert(php);
for (int i = 0; i < php->size; ++i)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
//初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
//销毁堆
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
//去堆顶元素,也就是根
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
//判空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//返回堆中元素个数
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
接下来就需要实现其他的接口函数
//向下调整
void AdjustDwon(HPDataType* a, int size, int parent);
//向上调整
void AdjustUp(HPDataType* a, int child);
//插入数据,并保持堆
void HeapPush(HP* php, HPDataType x);
//删除数据,并保持堆
void HeapPop(HP* php);
1.堆的向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
具体过程就是:
1、从根开始,不断往下调
2、选出左右孩子中较小的,跟父亲比较,如果比父亲小,跟父亲交换,以小的孩子位置继续往下调。如果比父亲大,则停止。
下图代码是用向下调整算法建小堆,当然也可以改变两个if语句的小于号,变为大于号,从而实现向下调整算法建大堆
void swap(int *p1,int *p2)
{
int temp=*p1;
*p1=*p2;
*p2=temp;
}
//用向下调整算法建小堆
void AdjustDown(int *a,int n,int parent)
{
int child=parent*2+1; //左孩子,左孩子+1即为右孩子
while(child<n)
{
//选择出左右孩子中较小/大的那个
if(child+1<n && a[child+1]<a[child])//右孩子存在(防止越界)且如果右孩子比左孩子小
{
child++;//那就下标来到右孩子
}
if(a[child]<a[parent])//孩子小于父亲就交换,继续调整
{
swap(&a[parent],&a[child]);
parent=child;
child=parent*2+1;
}
else//小的孩子比父亲大或相等,则不处理,调整结束
{
break;
}
}
}
2.堆的向上调整算法
逻辑和上面的正好相反,从子节点开始调整,去找当前节点的父亲,下面代码是利用向上调整算法建小堆
void swap(int *p1,int *p2)
{
int temp=*p1;
*p1=*p2;
*p2=temp;
}
void AdjustUp(HPDataType *a,int child)
{
int parent = (child-1)/2;
//while(parent>=0) 不对的,parent不会小于0
while(child > 0)
{
if(a[parent]<a[child])
{
swap(&a[parent],&a[child]);
child=parent;
parent=(child-1)/2;
}
else
{
break;
}
}
}
3.插入数据,并保持堆
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType)*newcapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
4.删除数据,并保持堆
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&(php->a[0]), &(php->a[php->size - 1]));
php->size--;
AdjustDwon(php->a, php->size, 0);
}
接下里可以利用这些接口函数来干点堆真正需要的功能——排序,同时以上函数功能全为实现小堆,想改大堆改符号即可。
5.堆排序
这里排序也分两类,第一类,这种排序明显是有劣势的,首先HP的数据结构,写的很麻烦,也就是本文之前的接口函数,其实光打印还好,如果要排序出来,就有了下面的第二条劣势,也就是重新开辟了空间去建堆,因为这里本来就有a了,还要开一个hp去建堆,然后两者数据倒一下,数据量大的话,这种方式是绝对不能用的。
//1、你得先写一个Hp数据结构,反而复杂
//2、有O(N)空间复杂度
void HeapSort(int* a, int n)
{
HP hp;
HeapInit(&hp);
for (int i = 0; i < n; ++i)
{
HeapPush(&hp, a[i]);
}
int i = 0;
while (!HeapEmpty(&hp))
{
a[i++] = HeapTop(&hp);
HeapPop(&hp);
}
HeapDestroy(&hp);
}
void TestHeapSort()
{
//这里是光打印
HP hp;
HeapInit(&hp);
int a[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
{
HeapPush(&hp, a[i]);
}
//至此建堆完成,是小堆
while (!HeapEmpty(&hp))
{
printf("%d ", HeapTop(&hp));//打印堆顶
HeapPop(&hp);//删除堆顶,并保持小堆结构不变(会将次小的数放到堆顶)
}
printf("\n");
//上面是光打印
int a[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
HeapSort(a, sizeof(a) / sizeof(int));
}
int main()
{
TestHeapSort();
HeapSort();
return 0;
}
第二类,这种方法的优势就在于要在本身的数组上去建堆并且排序,因为数组本身就要看作完全二叉树。首先来看建堆
void HeapSort(int* a, int n)
{
//建堆方式1:O(N*logN),这里可以证明
for (int i = 1; i < n; ++i)
{
AdjustUp(a, i);
}
// 建堆方式2:O(N),这里可以证明
for (int i = (n-1-1)/2; i >= 0; --i)//倒数第一个非叶节点
{
AdjustDown(a, n, i);
}
//至此已经建好堆了
// O(N*logN)
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
接下来看排序,如果我们想排升序,应该建大堆!而不是小堆!
如果建小堆,即使使用建堆方式2,第一次建好选出最小值后,要是想选次小值,那必须重新建堆选数,那么时间复杂度将会来到O(n^2)。这里就没有发挥堆的优势。
所以这里可以建大堆,将大堆建好之后,将堆顶数据和堆底数据互换,然后不把堆底数据(放着最大的数)看作堆的一部分,然后把此时的堆顶数据向下调整建大堆,就可以选出次大的数到堆顶,向下调整的时间复杂度是O(logN),所以总的时间复杂度就是O(N*logN),至此,堆排序写好了,这里的排序速度优势就体现出来了。