前言
重点图解二叉树及建堆,建大堆,建小堆,堆插入,堆删除,堆排序等(重点剖析向上调整算法和向下调整算法)这两个算法后续基本上都会用到
一. 什么是二叉树
二叉树,作为一种重要的数据结构,由节点组成,每个节点可以有两个子节点,通常称为左子节点和右子节点。二叉树是有序的,树中包含的各个节点的度不能超过2,即只能是0、1或者2。
二叉树具有以下几个特性:
- 二叉树的第i层最多有2i-1个结点;
- 如果二叉树的深度为K,那么此二叉树最多有2K -1个结点;
- 在二叉树中,叶子节点(终端节点)数为n0,度为2的节点数为n2,则n0=n2+1。
此外,许多实际问题抽象出来的数据结构往往是二叉树形式,因为即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。由于其高效的搜索性能,每个节点最多有两个子节点,可以通过比较大小迅速确定搜索方向。
1.1二叉树介绍
二叉树,作为一种重要的数据结构,是一种特殊的树形结构。每个节点最多有两个子节点,分别称为左子节点和右子节点。除此之外,每个节点都有一个值,并且满足从根节点到叶子节点的所有路径上的值都是递增或递减的。
二叉树具有以下几个特性:
1.若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2^(i-1)个结点.
2.若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h- 1.
3.对任何一棵二叉树,如果度为0其叶结点个数为n0,度为2的分支结点个数为n2.则有n0= n2+ 1
1.2特殊二叉树
满二叉树
满二叉树,作为一种特别的二叉树,其定义是每个层的结点数都达到最大值,也就是说除了叶子节点外,每个节点都有两个子节点。具体来说,如果一棵二叉树的深度为K,那么它的满二叉树的结点总数就应该是 (2^k) -1。
完全二叉树
完全二叉树,作为一种效率很高的数据结构,是由满二叉树衍生出来的。一棵深度为K且有n个结点的二叉树,如果其每个节点都与深度为K的满二叉树中编号从1至n的节点一一对应,则这棵二叉树被称为完全二叉树。如图
二. 二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式存储。本文介绍顺序存储
顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树, 因为不是完全二叉树会有空间的浪费。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。链式结构又分为二叉链和三叉链。我们这没用到链式存储。
三.堆
堆是计算机科学中一类特殊的数据结构的统称,通常是可以被看做一棵树的数组对象。堆总是满足下列性质: 堆中某个节点的值总是不大于或不小于其父节点的值。
堆通常被用来作为优先级队列,是最高效的优先级队列。 除此之外,堆还可以用来作为排序,思路是每次都把堆顶的元素和堆尾的元素交换,然后把除了堆尾的那个元素组成的堆进行堆化(就是把堆顶的元素进行下沉),不断重复直至堆为空为止。
3.1大堆和小堆区别
大堆小堆都为完全二叉树,树中每个父亲都大于或小于孩子(左右孩子之间大小不用比较)
3.2初始化堆
先构建一个顺序表(也就是数组,用它来复制给定的数组)
typedef int HPDatatype;
typedef struct Heap {
HPDatatype* a;
int size;
int capacity;
}Heap;
3.3 复制数组内容
这里使用HeapInit()进行初始化,复制给定的数组 int a[],后面进行HeapPush(堆插入)操作时直接扩容就行(后面会介绍)
注:php->a,它指向使用malloc开辟的新空间,调用库函数memcpy把int a[]中的数据复制到这新开辟的内存中
int a[] = { 27,15,19,18,28,34,65,49,25,37 };
Heap hp;
HeadInit(&hp, a, sizeof(a) / sizeof(HPDatatype));
void HeadInit(Heap* php, HPDatatype* a, int n)
{
php->a = (HPDatatype*)malloc(sizeof(HPDatatype) * n);
memcpy(php->a, a, sizeof(HPDatatype) * n);
php->size = n;
php->capacity = n;
//构建堆
//for (int i = (n - 1 - 1) / 2; i >= 0; --i)
//{
// ADjustDowm(php->a, n, i);
//}
}
四.向下调整算法
数组建堆:主要依赖向下调整算法
4.1向下调整算法代码
//前提:左右子树都是小堆
void ADjustDowm(HPDatatype* a, int n, int root)
{
int parent = root;
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;
}
}
}
4.2调整算法详细图解
五.建堆
5.1建堆代码(以小堆为例)
void HeapShort(int* a, int n)
{
//建堆
//for(int i=n-1;i>0;++i)
//树的高度logN,时间复杂度:O(n)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
ADjustDowm(a, n, i);
}
}
5.2 建堆过程图解
5.3建堆时间复杂度图解
建堆的时间复杂度为O(n);证明过程如下图
六.排序
6.1排序代码
//排序
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
//再继续选次小的
ADjustDowm(a, end, 0);
--end;
}
6.2排序(降序,升序)代码
只需要在向下调整算法这里修改(两处)比较大小的方向,就是构建大堆或者小堆
if (child + 1 < n && a[child + 1] < a[child])
if (a[child] < a[parent])
//前提:左右子树都是大堆(这里建大堆)
void ADjustDowm(HPDatatype* a, int n, int root)
{
int parent = root;
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 ADjustDowm(HPDatatype* a, int n, int root)
{
int parent = root;
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;
}
}
}
6.3排序调HeapShort图解
注:每次都是把arr[0]与arr[end]交换,end往前面迭代,然后每次从堆顶(arr[0])使用向下调整算法ADjustDown(),end减减到0时停止。
最坏情况下需要交换n次,每次交换需要向下调整。向下调整时间复杂度为O(logn),所以堆排序的时间复杂度为O(n*logn)
6.4堆插入全部代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
typedef int HPDatatype;
typedef struct Heap {
HPDatatype* a;
int size;
int capacity;
}Heap;
void Swap(HPDatatype* p1, HPDatatype* p2)
{
HPDatatype* tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//前提:左右子树都是小堆(这里建小堆)
void ADjustDowm(HPDatatype* a, int n, int root)
{
int parent = root;
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 HeapShort(int* a, int n)//堆排序
{
//建堆(小堆)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
ADjustDowm(a, n, i);
}
//排序
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
//再继续选次小的
ADjustDowm(a, end, 0);
--end;
}
}
void printfArr1(int arr[], int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int a[] = { 17,25,39,18,28,64,65,49,25,37 };
printf("原数组:\n");
printfArr1(a, sizeof(a) / sizeof(a[0]));
HeapShort(a, sizeof(a) / sizeof(HPDatatype));
printf("排序后数组:\n");
printfArr1(a, sizeof(a) / sizeof(a[0]));
return 0;
}
6.5运行结果
降序升序结果比较
七.堆插入
7.1向上调整算法代码
void ADjustUp(HPDatatype* a, int n, int child)
{
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;
}
}
}
7.2向上调整算法图解
7.3堆插入代码
由于原来HeadInit()初始化堆的时候,是直接把给定的数组复制给堆(也就是初始化堆大小是固定的)现在我们进行堆插入HeapPush()时,需要先判断堆是否满了,若满就使用realloc函数进行堆扩容(现在堆的空间是原来的两倍)
注:目前堆中的元素个数由php->size决定。realloc开辟了一块原来2倍的空间,只能代表堆可存放的空间有这么多,并不是堆中的元素有这么多。
void HeapPush(Heap* php, HPDatatype* x)
{
assert(php);
if (php->size == php->capacity)
{
php->capacity *= 2;
HPDatatype* tmp = (HPDatatype*)realloc(php->a, sizeof(HPDatatype) * php->capacity);
php->a = tmp;
}
php->a[php->size++] = x;
ADjustUp(php->a, php->size, php->size - 1);
}
7.4堆插入全部代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
typedef int HPDatatype;
typedef struct Heap {
HPDatatype* a;
int size;
int capacity;
}Heap;
void Swap(HPDatatype* p1, HPDatatype* p2)
{
HPDatatype* tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//前提:左右子树都是小堆
void ADjustDowm(HPDatatype* a, int n, int root)
{
int parent = root;
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 HeadInit(Heap* php, HPDatatype* a, int n)
{
php->a = (HPDatatype*)malloc(sizeof(HPDatatype) * n);
memcpy(php->a, a, sizeof(HPDatatype) * n);
php->size = n;
php->capacity = n;
//构建堆
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
ADjustDowm(php->a, n, i);
}
}
void ADjustUp(HPDatatype* a, int n, int child)
{
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(Heap* php, HPDatatype* x)
{
assert(php);
if (php->size == php->capacity)
{
php->capacity *= 2;
HPDatatype* tmp = (HPDatatype*)realloc(php->a, sizeof(HPDatatype) * php->capacity);
php->a = tmp;
}
php->a[php->size++] = x;
ADjustUp(php->a, php->size, php->size - 1);
}
void printfArr(Heap* php)
{
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
printf("\n");
}
void printfArr1(int a[],int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", a[i]);
}
printf("\n");
printf("\n");
}
int main()
{
int a[] = { 17,25,39,18,28,64,65,49,25,37 };
printf("原数组:\n");
printfArr1(a, sizeof(a) / sizeof(a[0]));
Heap hp;
HeadInit(&hp, a, sizeof(a) / sizeof(HPDatatype));
printf("初始化,构建小堆:\n");
printfArr(&hp);
HeapPush(&hp, 13);
printf("堆插入:\n");
printfArr(&hp);
return 0;
}
运行结果
进行堆插入操作后,堆仍然是小堆
八.堆删除
(堆顶)删除代码
void HeapPop(Heap* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
ADjustDowm(php->a, php->size, 0);
}
堆删除细节图解
全部代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
typedef int HPDatatype;
typedef struct Heap {
HPDatatype* a;
int size;
int capacity;
}Heap;
void Swap(HPDatatype* p1, HPDatatype* p2)
{
HPDatatype* tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//前提:左右子树都是小堆
void ADjustDowm(HPDatatype* a, int n, int root)
{
int parent = root;
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 ADjustUp(HPDatatype* a, int n, int child)
{
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 HeadInit(Heap* php, HPDatatype* a, int n)
{
php->a = (HPDatatype*)malloc(sizeof(HPDatatype) * n);
memcpy(php->a, a, sizeof(HPDatatype) * n);
php->size = n;
php->capacity = n;
//构建堆
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
ADjustDowm(php->a, n, i);
}
}
void HeapDestory(Heap* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapPush(Heap* php, HPDatatype* x)
{
assert(php);
if (php->size == php->capacity)
{
php->capacity *= 2;
HPDatatype* tmp = (HPDatatype*)realloc(php->a, sizeof(HPDatatype) * php->capacity);
php->a = tmp;
}
php->a[php->size++] = x;
ADjustUp(php->a, php->size, php->size - 1);
}
void HeapPop(Heap* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
ADjustDowm(php->a, php->size, 0);
}
HPDatatype HeapTop(Heap* php)//取堆顶的数据
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
void HeapShort(int* a, int n)//堆排序
{
//建堆
//for(int i=n-1;i>0;++i)
//树的高度logN,时间复杂度:O(n)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
ADjustDowm(a, n, i);
}
//排序
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
//再继续选次小的
ADjustDowm(a, end, 0);
--end;
}
}
void printfArr(Heap* php)
{
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
void printfArr1(int arr[], int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
//int a[] = { 27,15,19,18,28,34,65,49,25,37 };
int a[] = { 17,25,39,18,28,64,65,49,25,37 };
printf("给定的数组a:\n");
printfArr1(a,sizeof(a)/sizeof(a[0]));
printf("\n");
Heap hp;
HeadInit(&hp, a, sizeof(a) / sizeof(HPDatatype));
printf("构建小堆 :\n");
printfArr(&hp);
printf("\n");
printf("小堆中插入数据:\n");
HeapPush(&hp, 13);
printfArr(&hp);
printf("\n");
printf("删除堆顶:\n");
HeapPop(&hp);
printfArr(&hp);
return 0;
}
运行结果
二叉树递归细节有些不清晰的,可以看看下面这篇文章。
二叉树递归代码及图解(c语言):(前序,中序,后续,节点与叶子个数)
以上就是本期内容,欢迎参考指正,如有不懂,欢迎评论或私信出下期!!!