前言:笔者最近正在学习树的结构,因此写这个笔记用来记录,之后学的更多,笔记会更加完善,希望大家批评指正。
目录
树的概念
树由几部分组成
- 根节点:根节点没有前驱节点
- 除根节点外,其余节点被分成是一个结构与树类似的子树。每颗子树的根节点有且只有一个前驱,可以由0或多个后继
- 节点的度:一个节点含有的子树的个数称为该节点的度;
- 叶节点:度为0的节点称为叶节点
- 非终端节点或分支节点:度不为0的节点
- 双亲节点或父节点:若一个节点含有的子节点,则这个节点成为其子节点的父节点
- 孩子节点或子节点:一个节点延伸的子树的根节点称为该节点的子节点
- 兄弟节点:具有相同父节点的节点互称为兄弟节点
- 树的度:一颗树中,最大的节点的度称为树的度
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,一次类推
- 树的高度或深度:树中节点的最大层次
- 堂兄弟节点:双亲在同一层的节点互为堂兄弟
- 子孙:以某节点为根的子树中任一节点都为该节点的子孙
- 森林:由m颗互不相交的树的集合称为森林、
树的表示
树有很多表示方式,如:双亲表示法,孩子表示法,孩子兄弟表示法等,常用的是孩子兄弟表示法。
二叉树
二叉树的概念
一颗二叉树是节点的一个有限集合,该集合或者为空,或者是有一个根节点加上两颗分别称为左子树和右子树的二叉树组成。注意:左右子树有次序之分,并且两个子树依然是二叉树。
二叉树的特点:
1.每个节点最多有两颗子树,即二叉树不存在度大于2的节点
2.二叉树的子树有左右之分,次序不能颠倒。
特殊的二叉树
1.满二叉树:一个二叉树,如果每层的节点数都达到最大值,则这个二叉树就是满二叉树。
2.完全二叉树:完全二叉树是查找效率很高的数据结构,只有最后一行的节点可以不是满节点,另外满二叉树是特殊的完全二叉树。
二叉树的存储结构
1.顺序结构存储
顺序结构存储就是使用数组来存储,一般这种方法只适用于完全二叉树,因为不是完全二叉树会存在空间的浪费,而现实中只有堆会使用数组来存储。
2.链式存储
即使用链表来表示一颗二叉树,即用链来指示元素的逻辑关系。通常的方法是链表的每个节点由三个域组成,数据域和左右指针域,左右指针分别用来给除该节点左右孩子所在链结点的存储地址。链式存储又分为二叉链和三叉链。
//二叉链
struct BinaryTreeNode
{
struct BinaryTreeNode* _pLeft
struct BinaryTreeNode* _pRight;
int data;
};
//三叉链
struct BinaryTreeNode
{
struct BinaryTreeNode* _pParent;
struct BinaryTreeNode* _pRight;
struct BinaryTreeNode* _Left;
int data;
};
二叉树的性质
堆的结构
堆的概念
堆就是将元素按照完全二叉树的顺序存储方式存储在一个一维数组中,而当满足父节点的元素小于其左右孩子时,称为小堆,特别的当根节点的元素最小时叫做最小堆;当父节点的元素大于其左右孩子时,称为大堆,特别的当根节点的元素最大时,叫做最大堆。
堆的性质
- 小(大)堆中的某个节点总是不大于或不小于其父节点的值;
- 堆总是一个完全二叉树,所以可以使用数组来顺序存储;
- 以i为下标,可以看出堆或者说完全二叉树的规律:
- i 的左子节点下标:2i + 1
- i 的右子节点下标:2i + 2
- i 的父节点:( i - 1 ) / 2;
堆的调整算法
向下/上调整:是让调整的节点与其孩子节点进行比较,若想将其调整为小堆,那么根节点的左右子树必须都为小堆,若想将其调整为大堆,那么根节点的左右子树必须都为大堆。
向下调整算法的基本思想(以最大堆为例):
1.从根节点处开始,选出左右孩子中值较大的那个
2.让大的孩子与其父亲进行比较
3.若大的孩子比父亲还大,则该孩子与其父亲的位置进行交换,继续向下进行调整直到调整到叶子节点为止。
4.若大的孩子比父亲小,就不需处理,调整完成,整个树就是大堆了。
/*向下调整当前的节点和子节点,将堆调整成最大堆*/
void adjustDown(Heap& heap, int index) //heap是引入的待调整的堆,index是当前待调整的父节点
{
int cur = heap.arr[index];
int parent, child;
/*判断是否存在大于当前节点的子节点,如果不存在,则堆本身就是平衡的,不需要调整;如果存在,则将最大的子节点与之交换,交换后,如果这个子节点还有子节点,则需要继续按照同样的步骤堆这个子节点进行调整*/
for (parent = index; (parent * 2 + 1) < heap.size; parent = child)
/*这个循环时因为如果父节点小于左节点,那么循环内会将child ++ 变为右节点
{
child = parent * 2 + 1; //左子节点
//取两个子节点中的最大的点
if ( ((child + 1) < heap.size) && (heap.arr[child] < heap.arr[child + 1]) )
child ++;
//判断最大子节点是否大于当前的父节点
if (cur >= heap.arr[child]) break; //不大于,不需要调整,跳出循环
else
{
heap.arr[parent] = heap.arr[child];
heap.arr[child] = cur;
/*循环到这里就结束了,没有再判断右节点,那么如果换完之后,右节点大于父节点怎么办?:循环没有结束,而是因为父节点和较大的子节点交换位置,所以之前子节点作为父节点时构建好的大堆可能被破坏,所以parent = child,子节点作为父节点重新进行可能的换位操作。 */
}
}
}
向上调整算法的基本思想:
1.将目标节点与其父节点比较
2.若目标节点的值比其父节点的值大,则交换目标节点与其父节点的位置
3.将原目标节点的父节点当作新的目标节点继续进行向上调整。若目标节点的值比其父节点的值小,则停止向上调整,此时树已经是最大堆了。
void adjustUp(Heap& heap, int index)
{
if (index < 0 || index >= size) return ; //下标小于0或下标大于堆的元素个数,越界直接返回
while (index > 0)
{
int temp = heap.arr[index];
int parent = (index - 1) / 2; //无论是左节点还是右节点,使用这个公式都能正确得到其父节点的下标
if (parent >= 0)
if (temp > heap.arr[parent])
{
heap.arr[index] = heap.arr[parent];
heap.arr[parent] = temp;
index = parent;
}
else break; /*如果已经比父亲小,直接结束循环,但如果另一个子节点比父亲大呢?向上调整算法应用于插入元素时,因为原本堆已是最大堆,所以如果插入的元素大于其父节点那么一定大于其兄弟节点。*/ else break; //parent < 0,结束循环
}
}
向下调整算法的使用情景:在从一维数组构建树时,从最后的父节点开始向上遍历每个父节点,进行向下调整
向上调整算法的使用情景:向堆中插入元素,使用向上调整算法保证原本的最大堆在插入元素后依然是最大堆。
可以看到:向下调整算法中的index是父节点,看父节点和最大子节点之间的大小关系;向上调整算法中的index是子节点,只需看该子节点和父节点之间的大小关系,而不需要看兄弟节点。
建立最大堆的实现
#define DEFAULT_CAPACITY 128 //宏定义一个规定的容量
typedef struct Heap
{
int* arr; //存储堆元素的数组,二叉树的顺序存储
int size; //当前已存储的元素个数
int capacity; //当前存储的容量
}Heap;
/*构造堆*/
void buildHeap(Heap& heap)
{
for (int i = heap.size / 2 - 1; i >= 0; i -- )
{
adjustDown(head, i);
/*从最后一个父节点开始((heap.size) - 1 )/ 2,逐个调整所有的父节点(直到根节点),确保每一个父节点都是最大堆,嘴都整体上形成一个最大堆*/
}
}
/*初始化堆*/
bool initHeap(Heap& heap, int* orginal, int size)
{
int capacity = DEFAULT_CAPACITY > size ? DEFAULT_CAPACITY : size;
heap.arr = new int[capacity];
if (!heap.arr) return false;
heap.capacity = capacity;
heap.size = 0;
//如果传入的数组中有元素
if (size)
{
memcpy(heap.arr, orginal, size* sizeof(int)); //将原始数据复制到堆中
heap.size = size;
buildHeap(heap);
}
else heap.size = 0;
return true;
}
插入元素
例子:将99加入到原始堆中,对应数组为:{95, 93, 87, 92, 86, 82}
首先将新进的元素插入到大顶堆的尾部,此时最大堆已被破坏,需要重新调整。
代码实现
bool insertHeap(Heap& heap, int value)
{
if (heap.size == heap.capacity)
{
cout << "栈空间耗尽!" << endl;
return false;
}
int index = heap.size;
heap.arr[heap.size ++] = value;
adjustUp(heap, index);
}
弹出最大元素
如果将堆顶的元素弹出,那么顶部有一个空的节点,该怎么处理呢?
当插入节点时,我们将新的值插入到数组的尾部。现在来做相反的事情:取出数组的最后一个元素,将它放到堆的顶部,然后修复堆的属性(最大堆/最小堆)
bool popMax(Heap& heap, int& value);
bool popMax(Heap& heap, int& value)
{
if (heap.size < 1)return false;
value = heap.arr[0]; //这个是为了可能后续要将最大值输出
//将size - 1的位置值赋值给根节点,size本身再--
/*相当于
heap.arr[0] = heap.arr[ heap.size -- ];
heap.size --;
*/
heap.arr[0] = heap.arr[-- size];
adjustDown(heap, 0); //向下执行堆调整
return true;
}
堆排序 Heapsort()
堆排序利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种,可以利用数组的特点快速定位到指定索引的元素。
选择排序原理:第一次从待排序的元素中选出最小(最大)的元素,存放在序列的起始位置,然后从剩余未排序的元素中寻找到最小(大)的元素,然后放在已排序序列的末尾,以此类推,直到待排序的元素个数为0。
堆排序实现
void HeapSort(Heap& heap)
{
if (heap.size < 1) return 0;
while (heap.size > 0)
{
int tmp = heap.arr[0];
heap.arr[0] = heap.arr[heap.size - 1];
heap.arr[size - 1] = tmp;
heap.size --;
adjustDown(heap, 0);
}
}
判断堆是否为空
bool isEmpty(Heap& heap)
{
if (heap.size < 1) return true;
return false;
}
遍历打印堆
打印堆中的数据,可以有两种打印格式。第一种是按照堆的物理结构进行打印,即打印一排连续的数字;第二种打印格式是按照堆的逻辑结构进行打印,即打印成树形结构。
第一种方法使用正常的数组遍历就可以实现,这里展示如何使用树形结构打印出堆中元素
//求节点数为n的二叉树的深度
int depth(int n)
{
if (n > 0)
{
int m = 2;
int hight = 1;
while (m < n + 1)
{
m *= 2;
hight ++;
}
return hight;
else
return 0;
}
//打印堆
void HeapPrint(Heap& heap)
{
//按照物理结构进行打印
for (int i = 0; i < heap.size; i ++ )
printf("%d ", heap.arr[i]);
//按照树形结构进行打印
int h = depth(heap.size);
int N = pow(2, h) - 1; //与该二叉树深度相同的满二叉树的节点总数
int space = N - 1; //记录每一行前面的空格数
int row = 1; //当前打印的行数
int pos = 0; //待打印的数据的下标
while (1)
{
//打印前面的空格
for (int i = 0; i < space; i ++ )
cout << " ";
//打印数据和间距
int count = pow(2, row - 1); //每一行的数字个数
while (count --) //打印一行
{
printf("%02d", heap.arr[pos ++]);
int distance = (space + 1) * 2; //两个数之间的空格数
while(distance --)
{
cout << " " ;
}
if (pos >= heap.size)
{
cout << endl;
return;
}
}
cout << endl;
row ++;
space = space / 2 - 1;
}
}
销毁堆
void destroy(Heap& heap)
{
if (heap.arr) delete[] heap.arr;
heap.arr = NULL; //及时置空
heap.size = 0; //元素个数置0
heap.capacity = 0; //容量置0
}
堆的实际开发应用
优先队列
操作系统内核作业调度是优先队列的一个应用实例,它根据优先级的高低而不是先到先服务的方式来进行调度;
如果最小键值元素拥有最高的优先级。那么这种优先队列叫做升序优先队列(即总是先删除最小的元素),类似的,如果最大键值元素拥有最高的优先级,那么这种优先队列叫做降序优先队列(即总是先删除最大的元素)
参考文献:
结语:
笔记到这里结束了,在之后的学习中笔者会不断补充增加自己的知识,努力做的更好,大家共勉。
附录
总测试代码
#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <cmath>
using namespace std;
#define DEFAULT_CAPACITY 128
typedef struct Heap
{
int* arr;
int size;
int capacity;
}Heap;
bool initHeap(Heap& heap, int* orginal, int size);
void buildHeap(Heap& heap);
void adjustDown(Heap& heap, int index);
void adjustUp(Heap& heap, int index);
bool insertHeap(Heap& heap, int value);
bool popMax(Heap& heap, int& value);
bool isEmpty(Heap& heap);
int depth(int n);
void HeapPrint(Heap& heap);
void destroy(Heap& heap);
void HeapSort(Heap& heap);
//初始化堆
bool initHeap(Heap& heap, int* orignArray, int size)
{
int capacity = DEFAULT_CAPACITY > size ? DEFAULT_CAPACITY : size;
heap.arr = new int[capacity];
if (!heap.arr)
{
cout << "内存分配失败 " << endl;
return false;
}
heap.capacity = capacity;
heap.size = 0;
if (size)
{
memcpy(heap.arr, orignArray, size * sizeof(int));
heap.size = size;
buildHeap(heap);
}
return true;
}
void buildHeap(Heap& heap)
{
for (int i = (heap.size - 1) / 2; i >= 0; i --)
adjustDown(heap, i);
}
void adjustDown(Heap& heap, int index)
{
int cur = heap.arr[index];
int parent, child;
for (parent = index; (parent * 2 + 1) < heap.size; parent = child)
{
child = parent * 2 + 1;
if ((child + 1) < heap.size && (heap.arr[child + 1] > heap.arr[child]))
child ++;
if (cur >= heap.arr[child]) break;
else
{
heap.arr[parent] = heap.arr[child];
heap.arr[child] = cur;
}
}
}
void adjustUp(Heap& heap, int index)
{
if (index < 0 || index >= heap.size) return ; //索引值越界,返回
int temp = heap.arr[index]; //temp为插入的值
while (index > 0)
{
int parent = (index - 1) / 2;
if (parent >= 0)
{
if (heap.arr[parent] < temp)
{
heap.arr[index] = heap.arr[parent];
heap.arr[parent] = temp;
index = parent;
}
else break; //比父节点小,则不需要调整
}
else break; //越界,返回
}
}
bool insertHeap(Heap& heap, int value)
{
if (heap.size == heap.capacity)
{
cout << "栈空间耗尽!" << endl;
return false;
}
int index = heap.size; //插入元素的索引
heap.arr[heap.size ++] = value;
adjustUp(heap, index);
return true;
}
bool popMax(Heap& heap, int& value)
{
if (heap.size < 1) return false;
value = heap.arr[0];
heap.arr[0] = heap.arr[-- heap.size];
adjustDown(heap, 0); //向下调整
return true;
}
bool isEmpty(Heap& heap)
{
if (heap.size < 1) return true;
return false;
}
int depth(int n)
{
if (n > 0)
{
int m = 2;
int hight = 1;
while (m < n + 1)
{
m *= 2;
hight ++;
}
return hight;
}
else return 0;
}
void HeapPrint(Heap& heap)
{
//按照树形结构进行打印,这种space 和 distance 的计算方式适用于域宽为2的数字
int h = depth(heap.size);
int N = pow(2, h) - 1;
int space = N - 1;
int row = 1;
int pos = 0;
while (1)
{
for (int i = 0; i < space; i ++ )
cout << " " ;
int count = pow(2, row - 1);
while (count -- )
{
printf("%02d", heap.arr[pos ++]); //设置域宽为2
if (pos >= heap.size)
{
cout << endl;
return;
}
int distance = (space + 1) * 2; //上一行的space等于下一行的distance,第一行的space等于N - 1,
while (distance --)
{
cout << " " ;
}
}
cout << endl;
row ++;
space = space / 2 - 1;
}
}
void destroy(Heap& heap)
{
if (heap.arr) delete[] heap.arr;
heap.arr = NULL;
heap.size = 0;
heap.capacity = 0;
}
void HeapSort(Heap& heap)
{
if (heap.size < 1) return ;
while (heap.size > 0)
{
int temp = heap.arr[0];
heap.arr[0] = heap.arr[-- heap.size];
heap.arr[heap.size] = temp;
adjustDown(heap, 0);
}
}
int main()
{
Heap heap;
int orignArray[] = {82,95,85,96,86,94,96,95};
if (!initHeap(heap, orignArray, sizeof(orignArray) / sizeof(orignArray[0])))
{
cout << "初始化失败。" << endl;
exit(-1);
}
for (int i = 0; i < heap.size; i ++ )
printf("第%d个数为: %d\n",i, heap.arr[i]);
cout << "分割线--------------------" << endl;
printf("在堆中插入新的元素99,插入结果:\n");
insertHeap(heap, 99);
for (int i = 0; i < heap.size; i ++ )
printf("第%d个数为:%d\n", i, heap.arr[i]);
// int value;
// while (popMax(heap, value))
// cout << "依次列出最大元素:" << value << endl;
HeapPrint(heap);
return 0;
}