1.二叉树概念及结构
1.1概念
一棵二叉树是结点的一个有限集合,该集合:
1. 或者为空
2. 由一个根结点加上两棵别称为左子树和右子树的二叉树组成

从上图可以看出:
1. 二叉树不存在度大于2的结点
2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
注意:对于任意的二叉树都是由以下几种情况复合而成的:

1.2 特殊的二叉树:
1.
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是 说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
2.
完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对 应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
1.3 二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1.
顺序存储
顺序结构存储就是使用
数组来存储,一般使用数组
只适合表示完全二叉树,因为不是完全二叉树会有空 间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。
二叉树顺 序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2.
链式存储
通过左右孩子指针寻找结点。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* left; // 指向当前结点左孩子
struct BinTreeNode* right; // 指向当前结点右孩子
BTDataType data; // 当前结点值域
}
// 三叉链,其实就是二叉链多了一个寻找父亲的指针
struct BinaryTreeNode
{
struct BinTreeNode* parent; // 指向当前结点的双亲
struct BinTreeNode* left; // 指向当前结点左孩子
struct BinTreeNode* right; // 指向当前结点右孩子
BTDataType data; // 当前结点值域
};
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。
现实中我们通常把堆
(
一种二叉树
)
使用顺序结构的数组来存储。
堆是完全二叉树,是完全二叉树,是完全二叉树。物理结构是数组,逻辑结构被我们抽象成一棵完全二叉树。
注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。这两个只是同名而已,他们没有任何关系。
2.堆的实现的定义
附:完全二叉树孩子父亲关系(在数组中下标关系)
父亲×2+1=左孩子,左孩子+1=右孩子,
(孩子-1)/2=父亲。
2.1 堆向下调整算法
下面给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根结点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

但是现在还不是堆,这不就予盾了吗?所以我们可以以下向上,进行向下调整算法,也就是从第一个非叶子结点开始向上依次建堆。
//单次向下调整算法,传入第一个非叶子结点和数组大小,以小堆为例
void AdjustDown(HPDataType* 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[parent] > a[child])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
2.2堆的创建
下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算 法,把它构建成一个堆。根结点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子结点的 子树开始调整,一直调整到根结点的树,就可以调整成堆。
2.3 建堆时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的
就是近似值,多几个结点不影响最终结果):

因此:
建堆的时间复杂度为
O(N)
2.4 堆的插入
先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆
2.5 堆的删除
删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调
整算法。
2.6 堆的代码实现
typedef int HPDataType;
typedef struct Heap
{
HPDataType* arr;
int size;
int capacity;
}Hp;
void Swap(HPDataType* p1, HPDataType* p2);
void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);//n是数据个数
//堆的初始化
void HeapInit(Hp* php);
// 堆的销毁
void HeapDestory(Hp* php);
// 堆的插入(小堆)
void HeapPush(Hp* php, HPDataType x);
// 堆的删除(小堆)
void HeapPop(Hp* php);
// 取堆顶的数据
HPDataType HeapTop(Hp* php);
// 堆的数据个数
int HeapSize(Hp* php);
// 堆的判空
int HeapEmpty(Hp* php);
//初始化
void HeapInit(Hp* php)
{
assert(php);
php->arr = NULL;
php->capacity = php->size = 0;
}
//销毁
void HeapDestory(Hp* php)
{
assert(php);
free(php->arr);
//先释放arr,顺序不能乱,乱了就找不到arr了
php->arr = NULL;
php->capacity = php->size = 0;
php = NULL;
}
//交换元素函数
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//从根开始,如果父亲比孩子大就交换
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0)
while (child > 0)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//堆的插入
void HeapPush(Hp* php, HPDataType x)
{
assert(php);
判断内存够不够,不够则申请空间
if (php->capacity == php->size)
{
int newcapacity = (php->capacity==0 )? 4:2*php->capacity;
HPDataType* newarr = (HPDataType*)realloc(php->arr,sizeof(HPDataType)*newcapacity);
if (newarr == NULL)
{
perror("realloc fail!");
return;
}
php->arr = newarr;
php->capacity = newcapacity;
}
php->arr[php->size] = x;
//得到结点后用向下调整算法插入
AdjustUp(php->arr,php->size);
php->size++;
}
堆的删除指的是删除堆顶元素,但是并不能直接删除,因为直接删除会导致元素的关系错乱,兄弟并父亲等等,我们往往是通过交换堆顶和堆尾元素后,再将堆尾元素删除,再对堆顶元素进行向下调整算法
//堆删除
void HeapPop(Hp* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->arr[0], &php->arr[php->size - 1]);
php->size--;
AdjustDown(php->arr, php->size,0);
}
HPDataType HeapTop(Hp* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->arr[0];
}
void HeapPop(Hp* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->arr[0], &php->arr[php->size - 1]);
php->size--;
AdjustDown(php->arr, php->size,0);
}
HPDataType HeapTop(Hp* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->arr[0];
}
//堆的判空
int HeapEmpty(Hp* php)
{
assert(php);
return php->size == 0;
}
3 堆的应用
3.1 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1.
建堆
升序:建大堆
降序:建小堆
2.
利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序
堆排序是一种选择排序,他是通过堆顶选出最大或最小的值放在最后,然后让堆的大小减一,选出次小或次大的,依此递推,直到把所有元素都选出来。
void heapsort(int* a, int size)
{
for (int i = (size - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, size, i);
}
int end = size - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
3.2 TOP-K问题
TOP-K
问题:即求数据结合中前
K
个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1.
用数据集合中前
K
个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2.
用剩余的
N-K
个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素
//以在100个中找最小k个为例
//制造100个随机数,在文件中
void CreateNDate(int k)
{
srand((unsigned int)time(0));
FILE* pf = fopen("data.txt", "w");
for (int i = 0; i < k; i++)
{
fprintf(pf, "%d\n", rand() % 100 +i);
}
fclose(pf);
pf = NULL;
}
void PrintTopK(int* a, int n, int k)
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen error");
return;
}
//读取前k个数据建堆
for (int i = 0; i < k; i++)
{
fscanf(pf, "%d", &a[i]);
}
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, k, i);
}
int tmp = 0;
//读文件大于堆顶进堆
while (fscanf(pf, "%d", &tmp) > 0)
{
if (tmp > a[0])
{
a[0] = tmp;
AdjustDown(a, k, 0);
}
}
fclose(pf);
pf = NULL;
}
4.二叉树链式结构的实现
链式二叉树中最重要的思想是将树分为左子树和右子树,下面详细介绍这种思想。
4.1 前置说明
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。先手动造出一棵树如下
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType _data;
struct BinaryTreeNode* _left;
struct BinaryTreeNode* _right;
}BTNode;
BTNode* CreatBinaryTree()
{
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(3);
BTNode* node4 = BuyNode(4);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
node1->_left = node2;
node1->_right = node4;
node2->_left = node3;
node4->_left = node5;
node4->_right = node6;
return node1;
}
注意:上述代码并不是创建二叉树的方式,真正创建二叉树方式后序文章再来讲解
再看二叉树基本操作前,再回顾下二叉树的概念,
二叉树是:
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分别又称为先根遍历、中根遍历和后根遍历。
// 二叉树前序遍历
一棵二叉树可以分成左子树和右子树
以左子树为跟又能再分……
第一次访问跟结的的左子树,2作为根子点又访问它的左子树3,3又访问它的左子树空树打卬N开始返回,访问3(根),然后又访问3的右子树空树,到此为止2的左子树访问完毕,开始访问2(根),然后访问2的右子树空树,到此为止,1的左子树访问完毕,开始访问1(根),然后开始访问1的右子树……
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
// 二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
// 二叉树后序遍历
void PostOrder(BTNode* root);
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
前序遍历结果:
1 2 3 4 5 6
中序遍历结果:
3 2 1 5 4 6
后序遍历结果:
3 2 5 6 4 1
4.3 结点个数以及高度等
// 二叉树结点个数
int BinaryTreeSize(BTNode* root);
int TreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return TreeSize(root->left) + TreeSize(root->right) + 1;
}
// 二叉树叶子结点个数
int BinaryTreeLeafSize(BTNode* root);
int TreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if(root->right==NULL && root->left==NULL)
return 1;
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
// 二叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
int TreelevelkSize(BTNode* root, int k)
{
if (root == NULL)
return 0;
if (k == 1)
return 1;
return TreelevelkSize(root->left, k - 1) + TreelevelkSize(root->right, k - 1);
}
// 二叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
int TreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int left = TreeHeight(root->left);
int right = TreeHeight(root->right);
return left > right ? left+1 : right + 1;
}
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* ret1= TreeFind(root->left, x);
if (ret1)
return ret1;
BTNode* ret2 = TreeFind(root->right, x);
if (ret2)
return ret2;
return NULL;
}