树是一种非线性的数据结构,是由n个有限结点组成的一个具有层次关系的集合。
之所以称其为树,是因为它像一颗倒挂的树
形状:根朝上,叶朝下
**子树之间不能有交集**
节点的度:一个节点所含有的子树的个数 如上图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棵互不相交的树所组成的集合称为森林
树结构既要保存数值,也要保存节点和节点之间的关系
树的最常见的表示方式为
左孩子右兄弟表示法:
父亲指向左边第一个孩子
孩子之间用兄弟指针链接起来
typedef int Datatype;
struct Node
{
struct Node* child;//第一个孩子节点
struct Node* brother;//指向其兄弟的节点
Datatype data;//节点中的数值
};
一颗二叉树是节点的一个有限集合
特点
任意二叉树都是由下面几种情况所组成的
二叉树一般使用两种结构进行存储:顺序结构,链式结构
顺序结构
顺序结构存储就是使用数组进行存储,一般数组只适合表示完全二叉树,否则会有空间的浪费。二叉树顺序结构的存储在物理上是一个数组,在逻辑上是一颗二叉树。
数组下标计算父子关系公式
leftchild = parent*2+1 奇数
rightchild = parent*2+2 偶数
链式存储
二叉树的链式存储结构:用链表来表示一颗二叉树,即用链表来指示元素的逻辑关系。一般的方法是链表中每个结点都由三部分组成:数据和左右指针,左右指针分别用来给出该节点左孩子和右孩子所在链表的节点的存储地址。
一般的二叉树不适合用数组来存储,可能会存在大量的空间浪费。完全二叉树比较适合使用顺序结构存储。
堆可以被看作一棵完全二叉树的数组对象
堆是一个一维数组,把它所有元素(a0,a1,a2…an-1)按完全二叉树的顺序存储方式存储在堆中,并满足:ai<=a2i+1 且 ai<=a2i+2 (或 ai>=a2i+1 且 ai>=a2i+2) i=0,i=1…,则称为小堆(或大堆)。
将根节点最大的堆称作大根堆,根节点最小的堆称作小根堆。
根的性质
给定某个数组,逻辑上看作一个完全二叉树。通过从根节点开始的向下调整算法可以把它调整为大堆。
左右子树必须是同样的堆(大根堆或者小根堆),才能调整
int a = {9,3,2,6,5,10};
向下调整
给定一个数组,在逻辑结构上可以看作一颗完全二叉树(还不是堆),通过算法,将其构建成堆。
思路:从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点,直到符合堆的特点
因为堆是完全二叉树,满二叉树也是完全二叉树,为了方便,使用满二叉树进行计算时间复杂度
向下调整算法
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)
假设插入一个2到数组的末尾,再进行向上调整算法,直到构建成堆
删除堆是删除堆顶的数据,将堆顶的数据和最后一个数据进行交换,然后删除数组最后一个数据,再进行向下调整。
定义类型和结构体
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;
}
堆排序便是利用堆的特点进行排序,分为两个步骤
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;
}
降序的思路与升序的思路类似,这里就不加赘叙
求得数据集合中前K个最大或者最小的元素
方法:
当数据集合特别大时,方法1便不可行,数据太大不能在堆上存储数据,只能在磁盘上存储数据。
所以采取方法2
方法2也有两种方式去实现
代码实现 建小堆
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;
}
二叉树
二叉树遍历是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。
二叉树的遍历有三种方式:前序/中序/后序递归遍历
前序遍历:最先访问根节点,其次访问左子树,右子树
中序遍历:最先访问左子树,其次访问根,右子树
后序遍历:最先访问左子树,其次访问右子树,根
前序遍历
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);
}
对上面的二叉树进行后序遍历
运行结果与上述分析一致
层序遍历:假设二叉树的根节点所在的层数为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);
}
计算第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;
}
通过前序遍历数组
“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;
}
返回结果