一、堆
1.1、堆的原理精讲
堆(Heap)是计算机科学中的一种特殊的数据结构,通常用于实现优先队列。在堆中,每个节点都有一个键值(key),并且满足以下性质:
1. 二叉堆(Binary Heap):
最常见的堆是二叉堆,它是一种完全二叉树。
二叉堆可以是最大堆(Max Heap)或最小堆(Min Heap)。
在最大堆中,任何一个父节点的值都大于或等于其子节点的值。
在最小堆中,任何一个父节点的值都小于或等于其子节点的值。
2. 堆的性质:
堆的性质保证了在堆顶(树的根节点)的元素总是最大(最大堆)或最小(最小堆)。
这意味着可以非常快速地获取堆中的最大或最小元素,时间复杂度为O(1)。
3. 基本操作:
插入(Insert):向堆中插入一个新元素,并保持堆的性质。
删除(Delete):删除堆顶元素,并重新调整堆以保持其性质。
查找(Find):快速查找堆顶元素(最大或最小)。
4. 时间复杂度:
插入和删除操作的时间复杂度通常为O(log n),因为堆是一棵完全二叉树,所以从根节点到插入或删除的节点的最大深度是log n。
查找操作的时间复杂度为O(1),因为堆顶元素总是可立即访问的。
5. 实现:
堆通常使用数组来实现,因为数组可以提供快速的随机访问能力。
父节点和子节点之间的关系可以通过数组索引来计算,例如,对于索引为i的节点,其左子节点的索引为2i + 1,右子节点的索引为2i + 2。
6. 应用:
堆广泛应用于需要快速访问最大或最小元素的场景,如任务调度、数据压缩、网络流量控制等。
堆排序是一种基于堆的排序算法,通过构建最大堆或最小堆来实现。
下图就是一个最大堆:
最大堆特点:
- 每个节点最多可以有两个节点
- 根结点的键值是所有堆结点键值中最大者,且每个结点的值都比其孩子的值大。
- 除了根节点没有兄弟节点,最后一个左子节点可以没有兄弟节点其他节点必须有兄弟节点
堆是可以用数组来表示的树:
如何在数组中快速创建堆:
比如左边这个堆,如何快速转变成右边的最大堆?
- 首先我们需要找到最后一个结点的父结点如图 (a) ,我们找到的结点是87,然后找出该结点的最大子节点与自己比较,若该子节点比自身大,则将两个结点交换.
图 (a) 中,87比左子节点95小,则交换之.如图 (b) 所示:
- .我们移动到前一个父结点93,如图(c)所示.同理做第一步的比较操作,结果不需要交换
- 继续移动结点到前一个父结点82,如图 (d) 所示,82小于右子节点95,则82与95交换,如图 (e) 所示,82交换后,其值小于左子节点,不符合最大堆的特点,故需要继续向下调整,如图 (f) 所示
- 所有节点交换完毕,最大堆构建完成
1.2、堆的算法实现
1.2.1、堆数据结构定义
#define DEFAULT_CAPCITY 128
typedef struct _Heap {
int* arr; // 存储堆元素的数组
int size; // 当前已存储的元素个数
int capacity; // 当前存储的容量
}Heap;
1.2.2、最大堆建立
初始化堆:
// 初始化堆
bool initHeap(Heap& heap, int* orginal, int size) {
int capacity = DEFAULT_CAPCITY > size ? DEFAULT_CAPCITY : size; // size 比默认大小大,才取size,空间给足
heap.arr = new int[capacity]; // 类型我用的int 整型来测试
heap.size = 0;
// 如果原始数据存在才建堆
if (size > 0) {
memcpy(heap.arr, orginal, size * sizeof(int)); // 需要拷贝到的地址、拷贝的地址、所占的空间
heap.size = size;
buildHeap(heap); // 建堆
}
return true;
}
建成最大堆:
// 建堆(最大堆)
void buildHeap(Heap& heap) {
int i;
// 从非叶子结点的最后一个父节点开始
for (i = heap.size / 2 - 1; 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) {
// parent = 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;
}
}
}
建立最大堆的完整代码:
printf:
printf函数将格式化的输出发送到标准输出流stdout。通常情况下,stdout是指向终端或控制台的输出流。
使用printf时,输出的内容会显示在用户的屏幕上,或者被重定向到其他文件或设备。
函数原型:int printf(const char *format, …);
示例代码:printf(“Hello, World!\n”); 会将字符串"Hello, World!"输出到标准输出。
fprintf:
fprintf函数将格式化的输出发送到一个指定的文件流。这个文件流可以是任何有效的FILE *指针,包括stdout、stderr或其他通过fopen、freopen等函数打开的文件。
使用fprintf时,输出的内容可以被写入到文件中,或者输出到标准错误流stderr等。
函数原型:int fprintf(FILE *stream, const char *format, …);
示例代码:fprintf(stderr, “An error occurred!\n”); 会将错误信息"An error occurred!"输出到标准错误流。
#include <bits/stdc++.h>
using namespace std;
#define DEFAULT_CAPCITY 128
typedef struct _Heap {
int* arr; // 存储堆元素的数组
int size; // 当前已存储的元素个数
int capacity; // 当前存储的容量
}Heap;
bool initHeap(Heap& heap, int* orginal, int size); //初始堆
static void buildHeap(Heap& heap); // 建堆
static void adjustDown(Heap& heap, int index); // 上下调整(变成最大堆)
// 初始化堆
bool initHeap(Heap& heap, int* orginal, int size) {
int capacity = DEFAULT_CAPCITY > size ? DEFAULT_CAPCITY : size; // size 比默认大小大,才取size,空间给足
heap.arr = new int[capacity]; // 类型我用的int 整型来测试
heap.size = 0;
// 如果原始数据存在才建堆
if (size > 0) {
memcpy(heap.arr, orginal, size * sizeof(int)); // 需要拷贝到的地址、拷贝的地址、所占的空间
heap.size = size;
buildHeap(heap); // 建堆
}
return true;
}
// 建堆(最大堆)
void buildHeap(Heap& heap) {
int i;
// 从非叶子结点的最后一个父节点开始
for (i = heap.size / 2 - 1; 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) {
// parent = 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;
}
}
}
int main() {
Heap hp;
int origVals[] = { 1,2,3,87, 93,82,92,86,95 };
int i = 0 ;
if (!initHeap(hp, origVals, sizeof(origVals) / sizeof(origVals[0])) ){
fprintf(stderr, "初始化堆失败!\n"); //
exit(-1);
}
for (i = 0; i < hp.size; i++) {
printf("第 %d 元素为: %d \n", i, hp.arr[i]);
}
system("pause");
return 0;
}
测试结果:
1.2.3、插入元素(在最大堆基础上)
比如将数字99插入到上面大顶堆中的过程如下:
- 原始最大堆如下:
- 将新进的元素插入到大顶堆的尾部,如下图b所示
- 此时最大堆已经被破坏,需要重新调整,因加入的节点比父节点大,则新节点跟父节点调换即可,如图c所示;调整后,新节点如果比新的父节点小,则已经调整到位,如果比新的父节点大,则需要和父节点重新进行交换,如图d,至此,最大堆调整完成
在尾部插入:
// 最大堆尾部插入节点,同时保证最大堆的特性
bool insert(Heap& heap, int value) {
if (heap.size == heap.capacity) {
fprintf(stderr, "栈空间耗尽\n");
return false;
}
int index = heap.size;
heap.arr[heap.size] = value; // 尾部添加上数据
heap.size++; // 添加数据后 长度++
adjustUp(heap, index);
return true;
}
需要重新建立成最大堆,就需要将插入元素网上调整:
// 向上调整(因为插入是插在最底部,就需要网上调整)
void adjustUp(Heap& heap, int index) {
if (index < 0 || index >= heap.size) {
return;
}
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;
}
}
}
对于上文,我们初始化属于直接将整个数组 cpoy 的来的,先加入了insert算法,我们可以在初始化就成为最大堆。
// 初始化堆
bool initHeap(Heap& heap, int* orginal, int size) {
int capacity = DEFAULT_CAPCITY > size ? DEFAULT_CAPCITY : size; // size 比默认大小大,才取size,空间给足
heap.arr = new int[capacity]; // 类型我用的int 整型来测试
heap.size = 0;
// 如果原始数据存在才建堆
if (size > 0) {
法一:整个 copy
//memcpy(heap.arr, orginal, size * sizeof(int)); // 需要拷贝到的地址、拷贝的地址、所占的空间
//heap.size = size;
//buildHeap(heap); // 建堆
//方式二:一次插入一个
for (int i = 0; i < size; i++) {
insert(heap, orginal[i]);
}
}
return true;
}
1.2.4、堆顶元素出列
我们删除堆顶的时候,再用最后一个元素放到堆顶,然后用上文的向下排列方法,重新重塑为最大堆
// 出堆顶最大元素
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;
}
其实依次出顶,就属于一个排序,堆排序。
// 出堆顶元素
printf("依次打印堆顶元素: \n");
int value;
while(popMax(hp, value)) {
printf(" %d", value);
}
cout << endl;
结果如下:
1.2.5、堆的完整代码
#include <bits/stdc++.h>
using namespace std;
#define DEFAULT_CAPCITY 128
typedef struct _Heap {
int* arr; // 存储堆元素的数组
int size; // 当前已存储的元素个数
int capacity; // 当前存储的容量
}Heap;
bool initHeap(Heap& heap, int* orginal, int size); //初始堆
bool insert(Heap& Heap, int value); // 向堆里插入元素
bool popMax(Heap& heap, int& value); // 出堆顶最大元素
static void buildHeap(Heap& heap); // 建堆
static void adjustDown(Heap& heap, int index); // 向下调整(变成最大堆)
static void adjustUp(Heap& heap, int index); // 向上调整(因为插入是插在最底部,就需要网上调整)
// 初始化堆
bool initHeap(Heap& heap, int* orginal, int size) {
int capacity = DEFAULT_CAPCITY > size ? DEFAULT_CAPCITY : size; // size 比默认大小大,才取size,空间给足
heap.arr = new int[capacity]; // 类型我用的int 整型来测试
heap.size = 0;
// 如果原始数据存在才建堆
if (size > 0) {
法一:整个 copy
//memcpy(heap.arr, orginal, size * sizeof(int)); // 需要拷贝到的地址、拷贝的地址、所占的空间
//heap.size = size;
//buildHeap(heap); // 建堆
//方式二:一次插入一个
for (int i = 0; i < size; i++) {
insert(heap, orginal[i]);
}
}
return true;
}
// 建堆(最大堆)
void buildHeap(Heap& heap) {
int i;
// 从非叶子结点的最后一个父节点开始
for (i = heap.size / 2 - 1; 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) {
// parent = 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;
}
}
}
// 向上调整(因为插入是插在最底部,就需要网上调整)
void adjustUp(Heap& heap, int index) {
if (index < 0 || index >= heap.size) {
return;
}
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;
}
}
}
// 最大堆尾部插入节点,同时保证最大堆的特性
bool insert(Heap& heap, int value) {
if (heap.size == heap.capacity) {
fprintf(stderr, "栈空间耗尽\n");
return false;
}
int index = heap.size;
heap.arr[heap.size] = value; // 尾部添加上数据
heap.size++; // 添加数据后 长度++
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;
}
int main() {
Heap hp;
int origVals[] = { 1,2,3,87, 93,82,92,86,95 };
int i = 0 ;
if (!initHeap(hp, origVals, sizeof(origVals) / sizeof(origVals[0])) ){
fprintf(stderr, "初始化堆失败!\n"); //
exit(-1);
}
for (i = 0; i < hp.size; i++) {
printf("第 %d 元素为: %d \n", i, hp.arr[i]);
}
// 插入元素 99
insert(hp, 99);
printf("\n插入了新的元素, 99\n\n");
for (i = 0; i < hp.size; i++) {
printf("第 %d 元素为: %d \n", i, hp.arr[i]);
}
// 出堆顶元素
printf("依次打印堆顶元素: \n");
int value;
while(popMax(hp, value)) {
printf(" %d", value);
}
cout << endl;
system("pause");
return 0;
}
测试结果:
1.3、企业级项目实践
1.3.1、优先队列
操作系统内核作业调度是优先队列的一个应用实例,它根据优先级的高低而不是先到先服务的方
式来进行调度:
如果最小键值元素拥有最高的优先级,那么这种优先队列叫作升序优先队列(即总是先删除最小
的元素)。
类似的,如果最大键值元素拥有最高的优先级,那么这种优先队列叫作降序优先队列(即总是先删除最大的元素)。
由于这两种类型是完全对称的,所以只需要关注其中一种,比如升序优先队列。
完整代码如下:
#include <bits/stdc++.h>
using namespace std;
#define DEFAULT_CAPCITY 128
typedef struct _tsak {
int priotity; // 降序优先级队列
string name; // 名字
}Task;
typedef Task DataType;
// 这个宏的作用是生成一个表达式(a < b)
// 这个表达式用于比较两个值 a 和 b 是否满足小于关系
#define isLess(a,b) (a.priotity<b.priotity)
typedef struct _PriorityQueue {
DataType* arr; // 存储堆元素的数组
int size; // 当前已存储的元素个数
int capacity; // 当前存储的容量
}PriorityQueue;
bool init(PriorityQueue& pq, int* orginal, int size); //初始堆
bool push(PriorityQueue& pq, DataType value); // 向堆里插入元素
bool pop(PriorityQueue& pq, DataType& value); // 出堆顶最大元素
bool isEmpty(PriorityQueue& pq); // 是否为空
bool isFull(PriorityQueue& pq); // 是否已经满了
void destroy(PriorityQueue& pq); // 销毁
static void build(PriorityQueue& pq); // 建堆
static void adjustDown(PriorityQueue& pq, int index); // 向下调整(变成最大堆)
static void adjustUp(PriorityQueue& pq, int index);// 向上调整(因为插入是插在最底部,就需要网上调整)
/*初始化优先队列*/
bool init(PriorityQueue& pq, DataType* orginal, int size) {
int capacity = DEFAULT_CAPCITY > size ? DEFAULT_CAPCITY : size;
pq.arr = new DataType[capacity];
if (!pq.arr)return false;
pq.capacity = capacity;
pq.size = 0;
//如果存在原始数据则构建最大堆
if (size > 0) {
//方式一:直接调整所有元素
memcpy(pq.arr, orginal, size * sizeof(Task));
pq.size = size;
//建堆
build(pq);
}
return true;
}
/*销毁优先级队列*/
void destroy(PriorityQueue& pq) {
if (pq.arr)delete[]pq.arr;
}
/*优先队列是否为空*/
bool isEmpty(PriorityQueue& pq) {
if (pq.size < 1)return true;
return false;
}
/*优先队列是否为满*/
bool isFull(PriorityQueue& pq) {
if (pq.size < pq.capacity)return false;
return true;
}
int size(PriorityQueue& pq) {
return pq.size;
}
/*从最后一个父节点(size/2-1的位置)逐个往前调整所有父节点(直到根节
点),
确保每一个父节点都是一个最大堆,最后整体上形成一个最大堆*/
void build(PriorityQueue& pq) {
int i;
for (i = pq.size / 2 - 1; i >= 0; i--) {
adjustDown(pq, i);
}
}
/*将当前的节点和子节点调整成最大堆*/
void adjustDown(PriorityQueue& pq, int index)
{
DataType cur = pq.arr[index];//当前待调整的节点
int parent, child;
/*判断否存在大于当前节点子节点,如果不存在,则堆本身是平衡的,不
需要调整;
如果存在,则将最大的子节点与之交换,交换后,如果这个子节点还有子节点
点,则要继续
按照同样的步骤对这个子节点进行调整
*/
for (parent = index; (parent * 2 + 1) < pq.size; parent = child) {
child = parent * 2 + 1;
//取两个子节点中的最大的节点
if (((child + 1) < pq.size) && isLess(pq.arr[child], pq.arr[child+ 1])) {
child++;
}
//判断最大的节点是否大于当前的父节点
if (isLess(pq.arr[child], cur)) {//不大于,则不需要调整,跳出循环
break;
}
else {//大于当前的父节点,进行交换,然后从子节点位置继续向下调整
pq.arr[parent] = pq.arr[child];
pq.arr[child] = cur;
}
}
}
/*将当前的节点和父节点调整成最大堆*/
void adjustUp(PriorityQueue& pq, int index) {
if (index < 0 || index >= pq.size) {//大于堆的最大值直接return
return;
}
while (index > 0) {
DataType temp = pq.arr[index];
int parent = (index - 1) / 2;
if (parent >= 0) {//如果索引没有出界就执行想要的操作
if (isLess(pq.arr[parent], temp)) {
pq.arr[ index] = pq.arr[parent];
pq.arr[parent] = temp;
index = parent;
}
else {//如果已经比父亲小直接结束循环
break;
}
}
else {//越界结束循环
break;
}
}
}
/*删除优先队列中最大的节点,并获得节点的值*/
bool pop(PriorityQueue& pq, DataType& value) {
if (isEmpty(pq))return false;
value = pq.arr[0];
pq.arr[0] = pq.arr[--pq.size];
//heap.arr[0]=heap.arr[heap.size-1];
//heap.size--;
adjustDown(pq, 0);//向下执行堆调整
return true;
}
/*优先队列中插入节点*/
bool push(PriorityQueue& pq, DataType value) {
if (isFull(pq)) {
fprintf(stderr, "优先队列空间耗尽!\n");
return false;
}
int index = pq.size;
pq.arr[pq.size++] = value;
adjustUp(pq, index);
return true;
}
int main(void) {
PriorityQueue pq;
Task tasks[] = {
{9, "Task1"},
{5, "Task2"},
{7, "Task3"}
};
// int task[] = { 1,2,3,87,93, 82,92,86,95 };
int i = 0;
if (!init(pq, tasks, sizeof(tasks) / sizeof(tasks[0]))) {
fprintf(stderr, "初始化优先队列失败!\n");
exit(-1);
}
for (i = 0; i < pq.size; i++) {
printf("名字: %s 优先级: %d\n", pq.arr[i].name.c_str(), pq.arr[i].priotity);
}
//堆中插入优先级为88的任务
Task a = { 88,"ccb" };
push(pq, a);
//堆中元素出列
printf("按照优先级出列:\n");
DataType value;
while (pop(pq, value)) {
printf("%d %s\n", value.priotity,value.name.c_str());
}
destroy(pq);
system("pause");
return 0;
}
测试结果:
1.3.2、堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素.
(选择排序工作原理-第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零)
堆排序
// 实现堆排序
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 - 1]; // 堆低 放到 堆顶
heap.arr[heap.size - 1] = temp; // 堆顶 放到 堆尾
// 关键一部:把最后一个排除堆的范围
heap.size--;
// 向下调整,补全最大堆
adjustDown(heap, 0);
}
}
完整代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef struct _Heap {
int* arr; // 存储堆元素的数组
int size; // 当前已存储的元素个数
int capacity; // 当前存储的容量
}Heap;
bool initHeap(Heap& heap, int* orginal, int size); //初始堆
bool insert(Heap& Heap, int value); // 向堆里插入元素
void heapSort(Heap& heap); // 堆排序
bool popMax(Heap& heap, int& value); // 出堆顶最大元素
static void buildHeap(Heap& heap); // 建堆
static void adjustDown(Heap& heap, int index); // 向下调整(变成最大堆)
static void adjustUp(Heap& heap, int index); // 向上调整(因为插入是插在最底部,就需要网上调整)
// 初始化堆
bool initHeap(Heap& heap, int* orginal, int size) {
// heap.arr = new int[capacity]; // 类型我用的int 整型来测试
heap.arr = orginal;
heap.size = size;
// 如果原始数据存在才建堆
if (size > 0) {
buildHeap(heap); // 建堆
方式二:一次插入一个
//for (int i = 0; i < size; i++) {
// insert(heap, orginal[i]);
//}
}
return true;
}
// 建堆(最大堆)
void buildHeap(Heap& heap) {
int i;
// 从非叶子结点的最后一个父节点开始
for (i = heap.size / 2 - 1; 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) {
// parent = 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;
}
}
}
// 向上调整(因为插入是插在最底部,就需要网上调整)
void adjustUp(Heap& heap, int index) {
if (index < 0 || index >= heap.size) {
return;
}
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;
}
}
}
// 最大堆尾部插入节点,同时保证最大堆的特性
bool insert(Heap& heap, int value) {
if (heap.size == heap.capacity) {
fprintf(stderr, "栈空间耗尽\n");
return false;
}
int index = heap.size;
heap.arr[heap.size] = value; // 尾部添加上数据
heap.size++; // 添加数据后 长度++
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;
}
// 实现堆排序
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 - 1]; // 堆低 放到 堆顶
heap.arr[heap.size - 1] = temp; // 堆顶 放到 堆尾
// 关键一部:把最后一个排除堆的范围
heap.size--;
// 向下调整,补全最大堆
adjustDown(heap, 0);
}
}
int main() {
Heap hp;
int origVals[] = { 1,2,3,87, 93,82,92,86,95 };
int i = 0 ;
if (!initHeap(hp, origVals, sizeof(origVals) / sizeof(origVals[0])) ){
fprintf(stderr, "初始化堆失败!\n"); //
exit(-1);
}
for (i = 0; i < hp.size; i++) {
printf("第 %d 元素为: %d \n", i, hp.arr[i]);
}
// 堆排序
heapSort(hp);
printf("堆排序后结果:\n");
for (int i = 0; i < sizeof(origVals) / sizeof(origVals[0]); i++) {
printf(" %d", origVals[i]);
}
system("pause");
return 0;
}
测试结果:
二、栈
2.1、栈的原理精讲
进出的一端称为栈顶(top),另一端称为栈底(base)。栈可以用顺序存储,也可以用链式存储。我们先看顺序存储方式:
其中,base指向栈底,top指向栈顶。
注意: 栈只能在一端操作,后进先出,这是栈的关键特征,也就是说不允许在中间查找、取值、插入、删除等操作 ,我们掌握好顺序栈的初始化、入栈,出栈,取栈顶元素等操作即可。
2.2、顺序栈的算法实现
2.2.1、栈的结构体定义
#define MaxSize 128 // 预先分配空间
typedef int ElemType;
typedef struct _SqStack {
ElemType* base; // 栈底指针
ElemType* top; // 栈顶指针
}SqStack;
2.2.2、初始化
栈顶和占地都处在最后的位置
bool InitStack(SqStack& S) //构造一个空栈S
{
S.base = new int[MaxSize]; //为顺序栈分配一个最大容量为Maxsize的空间
if (!S.base) //空间分配失败
return false;
S.top = S.base; //top初始为base,空栈
return true;
}
2.2.3、入栈
入栈操作:判断是否栈满,如果栈已满,则入栈失败,否则将元素放入栈顶,栈顶指针向上移动一个空间(top++)。
// 入栈 插入元素
bool PushStack(SqStack& S, int e) {
if ((S.top - S.base) == MaxSize) { // 栈满了
return false;
}
*(S.top) = e; // 元素放入栈顶
S.top++;
return true;
}
2.2.4、出栈
出栈操作:和入栈相反,出栈前要判断是否栈空,如果栈是空的,则出栈失败,否则将栈顶元素暂存给一个变量,栈顶指针向下移动一个空间(top–)
// 删除栈顶元素, 暂存在变量e中
bool PopStack(SqStack& S, ElemType& e) {
if (S.base == S.top) { // 栈为空
return false;
}
e = *(S.top - 1);
S.top--; // 直接减减,不就是堆顶元素没了嘛
return true;
}
2.2.5、获取栈顶元素
取栈顶元素和出栈不同,取栈顶元素只是把栈顶元素复制一份,栈的元素个数不变,而出栈是指栈顶元素取出,栈内不再包含这个元素。
bool GetTop(SqStack& S, ElemType& e) { //返回S的栈顶元素,栈顶指针不变
if (S.base == S.top) { // 栈为空
return false;
}
else {
e = *(S.top - 1);
return e; // 返回栈顶元素
}
}
2.2.6、判断空栈
这种情况,栈就为空,此时此刻,top 和 base 相等
// 判断空栈
bool IsEmpty(SqStack& S) {
if (S.base == S.top) {
return true;
}
return false;
}
2.2.7、返回栈的个数
就是 top - base
int GetSize(SqStack& S) {//返回栈中元素个数
return(S.top - S.base);
}
2.2.8、销毁栈
从栈顶一层一层的 free
void DestoryStack(SqStack& S)
{
if (S.base) {
free(S.base);
S.base = NULL;
S.top = NULL;
}
}
2.2.9、栈的完整代码实现
#include <bits/stdc++.h>
using namespace std;
#define MaxSize 128 // 预先分配空间
typedef int ElemType;
typedef struct _SqStack {
ElemType* base; // 栈底指针
ElemType* top; // 栈顶指针
}SqStack;
bool InitStack(SqStack& S) //构造一个空栈S
{
S.base = new int[MaxSize]; //为顺序栈分配一个最大容量为Maxsize的空间
if (!S.base) //空间分配失败
return false;
S.top = S.base; //top初始为base,空栈
return true;
}
// 入栈 插入元素
bool PushStack(SqStack& S, int e) {
if ((S.top - S.base) == MaxSize) { // 栈满了
return false;
}
*(S.top) = e; // 元素放入栈顶
S.top++;
return true;
}
// 删除栈顶元素, 暂存在变量e中
bool PopStack(SqStack& S, ElemType& e) {
if (S.base == S.top) { // 栈为空
return false;
}
e = *(S.top - 1);
S.top--; // 直接减减,不就是堆顶元素没了嘛
return true;
}
bool GetTop(SqStack& S, ElemType& e) { //返回S的栈顶元素,栈顶指针不变
if (S.base == S.top) { // 栈为空
return false;
}
else {
e = *(S.top - 1);
return e; // 返回栈顶元素
}
}
// 判断空栈
bool IsEmpty(SqStack& S) {
if (S.base == S.top) {
return true;
}
return false;
}
int GetSize(SqStack& S) {//返回栈中元素个数
return(S.top - S.base);
}
void DestoryStack(SqStack& S)
{
if (S.base) {
free(S.base);
S.base = NULL;
S.top = NULL;
}
}
int main() {
int n, x;
SqStack S;
InitStack(S);//初始化一个顺序栈S
cout << "请输入元素个数n:" << endl;
cin >> n;
cout << "请依次输入n个元素,依次入栈:" << endl;
while (n--)
{
cin >> x;//输入元素
PushStack(S, x);
}
cout << "元素依次出栈:" << endl;
while (!IsEmpty(S))//如果栈不空,则依次出栈
{
int res;
GetTop(S, res);
cout << "栈顶元素" << res << "\t"; //输出栈顶元素
PopStack(S, x);//栈顶元素出栈
}
cout << endl;
DestoryStack(S);
system("pause");
return 0;
}
测试结果:
2.3、顺序栈修改
如果我们把栈的定义,修改如下如何写呢?
typedef struct _SqStack{
int top; //栈顶的位置
ElemType* base; //栈底指针
}SqStack
完整代码如下:
#include <iostream>
#include <string>
using namespace std;
#define MaxSize 128 // 预先分配空间
typedef int ElemType;
// 修改后的栈结构
typedef struct _SqStack {
int top; // 栈顶的位置
ElemType* base; // 栈底指针
} SqStack;
// 初始化栈
bool InitStack(SqStack& S) {
S.base = new ElemType[MaxSize];
if (!S.base) return false;
S.top = 0;
return true;
}
// 入栈
bool PushStack(SqStack& S, ElemType e) {
if (S.top >= MaxSize) return false;
S.base[S.top++] = e;
return true;
}
// 出栈
bool PopStack(SqStack& S, ElemType& e) {
if (S.top == 0) return false;
e = S.base[--S.top];
return true;
}
// 获取栈顶元素
bool GetTop(SqStack& S, ElemType& e) {
if (S.top == 0) return false;
e = S.base[S.top - 1];
return true;
}
// 判断栈是否为空
bool IsEmpty(SqStack& S) {
return S.top == 0;
}
// 获取栈的大小
int GetSize(SqStack& S) {
return S.top;
}
// 销毁栈
void DestroyStack(SqStack& S) {
delete[] S.base;
S.base = nullptr;
S.top = 0;
}
int main() {
int n, x;
SqStack S;
InitStack(S);
cout << "请输入元素个数n:" << endl;
cin >> n;
cout << "请依次输入n个元素,依次入栈:" << endl;
while (n--) {
cin >> x;
PushStack(S, x);
}
cout << "元素依次出栈:" << endl;
while (!IsEmpty(S)) {
ElemType res;
PopStack(S, res);
cout << "栈顶元素:" << res << "\t";
}
cout << endl;
DestroyStack(S);
system("pause");
return 0;
}
2.4、栈的链式结构
此处我直接给出完整代码:
#include <iostream>
using namespace std;
typedef int ElemType;
// 定义栈节点结构体
typedef struct StackNode {
ElemType data; // 数据域
StackNode* next; // 指向下一个节点的指针
} StackNode, * LinkStackPtr;
// 定义栈结构体
typedef struct LinkStack {
LinkStackPtr top; // 栈顶指针
int count; // 栈中元素数量
} LinkStack;
// 初始化链栈
bool InitStack(LinkStack& S) {
S.top = nullptr; // 初始化栈顶指针为空
S.count = 0; // 初始化元素数量为0
return true;
}
// 入栈操作
bool Push(LinkStack& S, ElemType e) {
StackNode* newNode = new StackNode; // 创建新节点
newNode->data = e; // 新节点数据域赋值
newNode->next = S.top; // 新节点的next指向当前的栈顶节点
S.top = newNode; // 栈顶指针指向新节点
S.count++; // 元素数量加1
return true;
}
// 出栈操作
bool Pop(LinkStack& S, ElemType& e) {
if (S.count == 0) return false; // 栈为空则出栈失败
StackNode* temp = S.top; // 暂存栈顶节点
e = temp->data; // 获取栈顶节点的数据
S.top = temp->next; // 栈顶指针指向下一个节点
delete temp; // 删除原栈顶节点
S.count--; // 元素数量减1
return true;
}
// 获取栈顶元素
bool GetTop(LinkStack& S, ElemType& e) {
if (S.count == 0) return false; // 栈为空则获取失败
e = S.top->data; // 获取栈顶节点的数据
return true;
}
// 销毁栈
void DestroyStack(LinkStack& S) {
while (S.top) {
StackNode* temp = S.top;
S.top = S.top->next;
delete temp;
}
S.count = 0;
}
int main() {
LinkStack S;
InitStack(S); // 初始化栈
// 入栈操作示例
cout << "链式结构入栈: 0 到 4" << endl;
for (int i = 0; i < 5; i++) {
Push(S, i);
}
// 出栈操作示例
ElemType e;
cout << "链式结构出栈:" << endl;
while (S.count > 0) {
Pop(S, e);
cout << e << " ";
}
cout << endl;
DestroyStack(S); // 销毁栈
system("pause");
return 0;
}
测试结果:
2.5、栈的企业级应用
找迷宫通路需要使用回溯法,找迷宫通路是对回溯法的一个很好的应用,实现回溯的过程用到数据结构—栈!
回溯法:对一个包括有很多个结点,每个结点有若干个搜索分支的问题,把原问题分解为若干个子问题求解的算法;当搜索到某个结点发现无法再继续搜索下去时,就让搜索过程回溯(回退)到该节点的前一个结点,继续搜索该节点外的其他尚未搜索的分支;如果发现该结点无法再搜索下去,就让搜索过程回溯到这个结点的前一结点继续这样的搜索过程;这样的搜索过程一直进行到搜索到问题的解或者搜索完了全部可搜索分支没有解存在为止。
完整代码如下:
该头文件主要是设计栈的一系列算法
// maze.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 100
typedef struct _Position{//迷宫坐标
int _x;
int _y;
}Position;
#define MaxSize 128 //预先分配空间,这个数值根据实际需要预估确定
typedef Position ElemType;
typedef struct _SqStack{
ElemType * base; //栈底指针
ElemType* top; //栈顶指针
}SqStack;
bool InitStack(SqStack& S)//构造一个空栈S
{
S.base = new ElemType[MaxSize];//为顺序栈分配一个最大容量为Maxsize的空间
if (!S.base)//空间分配失败
return false;
S.top = S.base;//top初始为base,空栈
return true;
}
bool PushStack(SqStack& S, ElemType e)//插入元素e为新的栈顶元素
{
if (S.top - S.base == MaxSize)//栈满
return false;
*(S.top++) = e;//元素e压入栈顶,然后栈顶指针加1,等价于*S.top=e;
S.top++;
return true;
}
bool PopStack(SqStack& S, ElemType& e)//删除S的栈顶元素,暂存在变量e中
{
if (S.base == S.top) {//栈空
return false;
}
e = *(--S.top);//栈顶指针减1,将栈顶元素赋给e
return true;
}
ElemType* GetTop(SqStack& S)//返回S的栈顶元素,栈顶指针不变
{
if (S.top != S.base) {//栈非空
return S.top - 1;//返回栈顶元素的值,栈顶指针不变
}
else {
return NULL;
}
}
int GetSize(SqStack& S) {//返回栈中元素个数
return(S.top - S.base);
}
bool IsEmpty(SqStack& S) {//判断栈是否为空
if (S.top == S.base) {
return true;
}
else {
return false;
}
}
void DestoryStack(SqStack& S) {//销毁栈
if (S.base){
free(S.base);
S.base = NULL;
S.top = NULL;
}
}
该 cpp 文件主要是用来迷宫计算
// maze.cpp
#include <bits/stdc++.h>
#include"maze.h"
using namespace std;
#define ROW 6
#define COL 6
typedef struct _Maze{
int map[ROW][COL];
}Maze;
// 迷宫的初始化
void InitMaze(Maze* m, int map[ROW][COL])
{
for (int i = 0; i < ROW; ++i)
{
for (int j = 0; j < COL; ++j)
{
m->map[i][j] = map[i][j];
}
}
}
// 打印迷宫
void PrintMaze(Maze* m)
{
for (int i = 0; i < ROW; ++i)
{
for (int j = 0; j < COL; ++j)
{
printf("%d ", m->map[i][j]);
}
printf("\n");
}
printf("\n");
}
// 判断是否是有效的入口
int IsValidEnter(Maze* m, Position cur)
{
assert(m);
if ((cur._x == 0 || cur._x == ROW - 1)
|| (cur._y == 0 || cur._y == COL - 1)
&& (m->map[cur._x][cur._y] == 1))
return 1;
else
return 0;
}
// 判断当前节点的下一个节点能否走通
int IsNextPass(Maze* m, Position cur, Position next)
{
assert(m);
//判断next节点是否为cur的下一节点
if (((next._x == cur._x) && ((next._y == cur._y + 1) || (next._y ==cur._y - 1))) //在同一行上并且相邻
|| ((next._y == cur._y) && ((next._x == cur._x + 1) || (next._x ==cur._x - 1)))) {//或在同一列上并且相邻
//判断下一个节点是否在迷宫里面
if (((next._x >= 0 && next._x < ROW) || (next._y >= 0 && next._y< COL))
&& (m->map[next._x][next._y] == 1)) {
return 1;
}
}
return 0;
}
// 判断当前节点是不是有效的迷宫出口
int IsValidExit(Maze* m, Position cur, Position enter)
{
assert(m);
//这里首先得保证该节点不是入口点,其次只要它处在迷宫的边界即可
if ((cur._x != enter._x || cur._y != enter._y)
&& ((cur._x == 0 || cur._x == ROW - 1)
|| (cur._y == 0 || cur._y == COL - 1)))
{
return 1;
}
else
return 0;
}
// 找迷宫通路
int PassMaze(Maze* m, Position enter, SqStack* s)
{
assert(m && IsValidEnter(m, enter) == 1); //对给的迷宫的入口进行合法性判断
Position cur = enter;
Position next;
PushStack(*s, cur); //首先将迷宫的入口压入栈中
m->map[cur._x][cur._y] = 2; //将入口值改为2 做标记
//PrintMaze(m);
while (!IsEmpty(*s)) {
cur = *GetTop(*s);
//printf("cur:%d%d\n",cur._x,cur._y);
if (IsValidExit(m, cur, enter) == 1) //判断当前位置是否出口
return 1;
//尝试向左一步:看当前节点的左一个节点能不能走通
next = cur;
next._y = cur._y - 1;
if (IsNextPass(m, cur, next) == 1)
{
PushStack(*s, next);
m->map[next._x][next._y] = m->map[cur._x][cur._y] + 1;
//PrintMaze(m);
continue;
}
//尝试向上一步:看当前节点的上一个节点能不能走通
next = cur;
next._x = cur._x - 1;
if (IsNextPass(m, cur, next) == 1) //next节点能够走通时,将其压入栈中
{
PushStack(*s,next);
m->map[next._x][next._y] = m->map[cur._x][cur._y] + 1; //将next节点的值等于cur节点的值加1
//PrintMaze(m);
continue;
}
//右:看当前节点的向右的一个节点能不能走通
next = cur;
next._y = cur._y + 1;
if (IsNextPass(m, cur, next) == 1)
{
PushStack(*s, next);
m->map[next._x][next._y] = m->map[cur._x][cur._y] + 1;
//PrintMaze(m);
continue;
}
//下:看当前节点的下一个节点能不能走通
next = cur;
next._x = cur._x + 1;
if (IsNextPass(m, cur, next) == 1)
{
PushStack(*s, next);
m->map[next._x][next._y] = m->map[cur._x][cur._y] + 1;
//PrintMaze(m);
continue;
}
//走到这里说明当前节点的四个方向都走不通,进行回溯,看前一个节点未被遍历的方向是否还能走通
Position tmp;
PopStack(*s, tmp);
}
return 0;
}
int main()
{
int map[ROW][COL] = { //用二维数组描绘迷宫:1代表通路,0代表墙
0,0,1,0,0,0,
0,0,1,1,1,0,
0,0,1,0,0,0,
0,1,1,1,1,0,
0,0,1,0,1,0,
0,0,0,0,1,0
};
Maze m;
Position enter; //迷宫入口
enter._x = 0;
enter._y = 2;
InitMaze(&m, map);
PrintMaze(&m);
//system("pause");
//exit(0);
SqStack s; //定义栈,保存已走过的坐标轨迹,便于回溯
InitStack(s); //栈的初始
int ret = PassMaze(&m, enter, &s);//使用栈和回溯法解开迷宫
if (ret) {
printf("恭喜你!终于找到了出口~\n");
}
else {
printf("不是我笨!实在没有出口~\n");
}
PrintMaze(&m);
system("pause");
return 0;
}
测试结果:
三、树
树状图是一种数据结构,它是由n(n>=1)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
它具有以下的特点:
每个结点有零个或多个子结点;没有父结点的结点称为根结点;每一个非根结点有且只有一个父结点;除了根结点外,每个子结点可以分为多个不相交的子树;
3.1、二叉树的原理精讲
二叉树是一个每个结点最多只能有两个分支的树,左边的分支称之为左子树,右边的分支称之为右子树。
常见二叉树分类:
(1)完全二叉树 :若设二叉树的高度为h,除第h层外,其它各层(1~h-1)的结点数都达到最大个数,第h层有叶子节点,并且叶子结点都是从左到右依次排布,这就是完全二叉树(堆就是完全二叉树)。
(2)满二叉树 :除了叶结点外每一个结点都有左右子节点且叶子结点都处在最底层的二叉树。
(3)平衡二叉树:又被称为AVL树,它是一颗空树或左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
(4)二叉搜索树:又称二叉查找树、二叉排序树(BinarySortTree)。它是一颗空树或是满足下列性质的二叉树:
1)若左子树不空,则左子树上所有节点的值均小于或等于它的根节点的值;
2)若右子树不空,则右子树上所有节点的值均大于或等于它的根节点的值;
3)左、右子树也分别为二叉排序树。
(5)红黑树:是每个节点都带有颜色属性(颜色为红色或黑色)的自平衡二叉查找树,满足下列性质:
1)节点是红色或黑色;
2)根节点是黑色;
3)所有叶子节点都是黑色;
4)每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。
5)从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
3.2、二叉搜索树的算法实现
二叉树一般采用链式存储方式:每个结点包含两个指针域,指向两个孩子结点,还包含一个数据域,存储结点信息。
3.2.1、结构体定义
typedef int ElemType;
typedef struct _Bnode {
ElemType data; // 元素类型
struct _Bnode* lchild, * rchild; // 指向左右孩子
}Bnode, Btree;
3.2.2、插入节点
将要插入的结点e,与节点root节点进行比较,若小于则去到左子树进行比较,若大于则去到右子树进行比较,重复以上操作直到找到一个空位置用于放置该新节点。
// 插入节点
bool InsertBtree(Btree** root, Bnode* node) {
Bnode* tmp = NULL;
Bnode* parent = NULL;
if (!node) {
return false;
}
else {//清空新节点的左右子树
node->lchild = NULL;
node->rchild = NULL;
}
if (*root) {//存在根节点
tmp = *root;
}
else { //不存在根节点
*root = node;
return true;
}
while (tmp != NULL) {
parent = tmp;//保存父节点
//printf("父节点:%d\n",parent->data);
if (isLess(node->data, tmp->data)) {
tmp = tmp->lchild;
}
else {
tmp = tmp->rchild;
}
}
//若该树为空树,则直接将node放置在根节点上
if (isLess(node->data, parent->data)) {//找到空位置后,进行插入
parent->lchild = node;
}
else {
parent->rchild = node;
}
return true;
}
3.2.3、删除节点
将要删除的节点的值,与节点root节点进行比较,若小于则去到左子树进行比较,若大于则去到右子树进行比较,重复以上操作直到找到一个节点的值等于删除的值,则将此节点删除。删除时有4中情况须分别处理:
1. 删除节点不存在左右子节点,即为叶子节点,直接删除
2.删除节点存在左子节点,不存在右子节点,直接把左子节点替代删除节点:
3.删除节点存在右子节点,不存在左子节点,直接把右子节点替代删除节点
4.删除节点存在左右子节点,则取左子树最大节点或者右子树最小节点替换删除节点
/************************
*采用二叉搜索树上最大的结点
*************************/
int findMax(Btree* root)
{
if (root->rchild == NULL) {
return root->data;
}
return findMax(root->rchild);
}
/************************
*采用递归方式查找结点
*************************/
Btree* DeleteNode(Btree* root, int key, Bnode*& deletedNode) {
if (root == NULL)return NULL;
if (root->data > key)
{
root->lchild = DeleteNode(root->lchild, key, deletedNode);
return root;
}
if (root->data < key)
{
root->rchild = DeleteNode(root->rchild, key, deletedNode);
return root;
}
deletedNode = root;
//删除节点不存在左右子节点,即为叶子节点,直接删除
if (root->lchild == NULL && root->rchild == NULL)return NULL;
//删除节点存在右子节点,直接用右子节点取代删除节点
if (root->lchild == NULL && root->rchild != NULL)return root->rchild;
//删除节点存在左子节点,直接用左子节点取代删除节点
if (root->lchild != NULL && root->rchild == NULL)return root->lchild;
删除节点存在左右子节点,直接用左子节点最大值取代删除节点
int val = findMax(root->lchild);
root->data = val;
root->lchild = DeleteNode(root->lchild, val, deletedNode);
return root;
}
2.2.4、搜索节点
/************************
*采用递归方式查找结点
*************************/
Bnode* QueryByRec(Btree* root, ElemType e) {
if (isEqual(root->data, e) || NULL == root) {
return root;
}else if(isLess(e, root->data)) {
return QueryByRec(root->lchild, e);
}
else {
return QueryByRec(root->rchild, e);
}
}
/**
*使用非递归方式查找结点
*/
Bnode* QueryByLoop(Bnode* root, int e) {
while (NULL != root && !isEqual(root->data, e)) {
if (isLess(e, root->data)) {
root = root->lchild;
}
else {
root = root->rchild;
}
}
return root;
}
2.2.5、二叉树遍历
二叉树遍历有4种方式:
前序:根左右
中序:左根右
后序:左右根
层序遍历:
递归实现:
/************************
*采用递归方式实现前序遍历
*************************/
void PreOrderRec(Btree* root)
{
if (root == NULL)
{
return;
}
printf("-%d-", root->data);
PreOrderRec(root->lchild);
PreOrderRec(root->rchild);
}
也可以通过遍历,非递归方式
具体过程:
1.首先申请一个新的栈,记为stack;
2.将头结点head压入stack中;
3.每次从stack中弹出栈顶节点,记为cur,然后打印cur值,如果cur右孩子不为空,则将右孩子压入栈中;如果cur的左孩子不为空,将其压入stack中;
重复步骤3,直到stack为空.
/************************
*借助栈实现前序遍历
*************************/
void PreOrder(Btree* root)
{
Bnode cur;
if (root == NULL)
{
return;
}
SqStack stack;
InitStack(stack);
PushStack(stack, *root); //头节点先入栈
while (!(IsEmpty(stack))) //栈为空,所有节点均已处理
{
PopStack(stack, cur); //要遍历的节点
printf("-%d-", cur.data);
if (cur.rchild != NULL)
{
PushStack(stack, *(cur.rchild)); //右子节点先入栈,后处理
}
if (cur.lchild != NULL)
{
PushStack(stack, *(cur.lchild));//左子节点后入栈,接下来先处理
}
}
DestroyStack(stack);
}
2.2.6、完整代码:
// stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include"tree.h"
#define MaxSize 128 //预先分配空间,这个数值根据实际需要预估确定
typedef struct _SqStack{
Bnode* base; //栈底指针
Bnode* top; //栈顶指针
}SqStack;
bool InitStack(SqStack& S)//构造一个空栈S
{
S.base = new Bnode[MaxSize];//为顺序栈分配一个最大容量为Maxsize的空间
if (!S.base)//空间分配失败
return false;
S.top = S.base;//top初始为base,空栈
return true;
}
bool PushStack(SqStack& S, Bnode e)//插入元素e为新的栈顶元素
{
if (S.top - S.base == MaxSize)//栈满
return false;
*(S.top++) = e; //元素e压入栈顶,然后栈顶指针加1,等价于*S.top=e;
S.top++;
return true;
}
bool PopStack(SqStack& S, Bnode& e)//删除S的栈顶元素,暂存在变量e中
{
if (S.base == S.top) {//栈空
return false;
}
e = *(--S.top);//栈顶指针减1,将栈顶元素赋给e
return true;
}
Bnode* GetTop(SqStack& S)//返回S的栈顶元素,栈顶指针不变
{
if (S.top != S.base) {//栈非空
return S.top - 1;//返回栈顶元素的值,栈顶指针不变
}
else {
return NULL;
}
}
int GetSize(SqStack& S) {//返回栈中元素个数
return(S.top - S.base);
}
bool IsEmpty(SqStack& S) {//判断栈是否为空
if (S.top == S.base) {
return true;
}
else {
return false;
}
}
void DestroyStack(SqStack& S) {//销毁栈
if (S.base) {
free(S.base);
S.base = NULL;
S.top = NULL;
}
}
// tree.h
#ifndef __TREE_H__
#define __TREE_H__
#define MAX_NODE 1024
#define isLess(a, b)(a < b)
#define isEqual(a, b)(a == b)
typedef int ElemType;
typedef struct _Bnode {
ElemType data; //元素类型
struct _Bnode* lchild, * rchild;//指向左右孩子节点
}Bnode, Btree;
#endif
// tree.cpp
#include<stdio.h>
#include<stdlib.h>
#include"tree.h"
#include"stack.h"
// 插入节点
bool InsertBtree(Btree** root, Bnode* node) {
Bnode* tmp = NULL;
Bnode* parent = NULL;
if (!node) {
return false;
}
else {//清空新节点的左右子树
node->lchild = NULL;
node->rchild = NULL;
}
if (*root) {//存在根节点
tmp = *root;
}
else { //不存在根节点
*root = node;
return true;
}
while (tmp != NULL) {
parent = tmp;//保存父节点
//printf("父节点:%d\n",parent->data);
if (isLess(node->data, tmp->data)) {
tmp = tmp->lchild;
}
else {
tmp = tmp->rchild;
}
}
//若该树为空树,则直接将node放置在根节点上
if (isLess(node->data, parent->data)) {//找到空位置后,进行插入
parent->lchild = node;
}
else {
parent->rchild = node;
}
return true;
}
/************************
*采用二叉搜索树上最大的结点
*************************/
int findMax(Btree* root)
{
if (root->rchild == NULL) {
return root->data;
}
return findMax(root->rchild);
}
/************************
*采用递归方式查找结点
*************************/
Btree* DeleteNode(Btree* root, int key, Bnode*& deletedNode) {
if (root == NULL)return NULL;
if (root->data > key)
{
root->lchild = DeleteNode(root->lchild, key, deletedNode);
return root;
}
if (root->data < key)
{
root->rchild = DeleteNode(root->rchild, key, deletedNode);
return root;
}
deletedNode = root;
//删除节点不存在左右子节点,即为叶子节点,直接删除
if (root->lchild == NULL && root->rchild == NULL)return NULL;
//删除节点存在右子节点,直接用右子节点取代删除节点
if (root->lchild == NULL && root->rchild != NULL)return root->rchild;
//删除节点存在左子节点,直接用左子节点取代删除节点
if (root->lchild != NULL && root->rchild == NULL)return root->lchild;
删除节点存在左右子节点,直接用左子节点最大值取代删除节点
int val = findMax(root->lchild);
root->data = val;
root->lchild = DeleteNode(root->lchild, val, deletedNode);
return root;
}
/************************
*采用递归方式查找结点
*************************/
Bnode* QueryByRec(Btree* root, ElemType e) {
if (isEqual(root->data, e) || NULL == root) {
return root;
}else if(isLess(e, root->data)) {
return QueryByRec(root->lchild, e);
}
else {
return QueryByRec(root->rchild, e);
}
}
/**
*使用非递归方式查找结点
*/
Bnode* QueryByLoop(Bnode* root, int e) {
while (NULL != root && !isEqual(root->data, e)) {
if (isLess(e, root->data)) {
root = root->lchild;
}
else {
root = root->rchild;
}
}
return root;
}
/************************
*采用递归方式实现前序遍历
*************************/
void PreOrderRec(Btree* root)
{
if (root == NULL)
{
return;
}
printf("-%d-", root->data);
PreOrderRec(root->lchild);
PreOrderRec(root->rchild);
}
/************************
*借助栈实现前序遍历
*************************/
void PreOrder(Btree* root)
{
Bnode cur;
if (root == NULL)
{
return;
}
SqStack stack;
InitStack(stack);
PushStack(stack, *root); //头节点先入栈
while (!(IsEmpty(stack))) //栈为空,所有节点均已处理
{
PopStack(stack, cur); //要遍历的节点
printf("-%d-", cur.data);
if (cur.rchild != NULL)
{
PushStack(stack, *(cur.rchild)); //右子节点先入栈,后处理
}
if (cur.lchild != NULL)
{
PushStack(stack, *(cur.lchild));//左子节点后入栈,接下来先处理
}
}
DestroyStack(stack);
}
int main(void) {
int test[] = { 19,7,25,5,11,15,21,61 };
Bnode* root = NULL, * node = NULL;
node = new Bnode;
node->data = test[0];
InsertBtree(&root, node);//插入根节点
for (int i = 1; i < sizeof(test) / sizeof(test[0]); i++) {
node = new Bnode;
node->data = test[i];
if (InsertBtree(&root, node)) {
printf("节点%d插入成功\n", node->data);
}
else {
}
}
printf("前序遍历结果:\n");
PreOrderRec(root);
printf("\n");
system("pause");
//二叉搜索树删除
printf("删除节点15\n");
Bnode* deletedNode = NULL;
root = DeleteNode(root, 15, deletedNode);
printf("二叉搜索树删除节点15,%s\n", deletedNode ? "删除成功" : "删除不成功,节点不存在");
if (deletedNode) delete deletedNode;
printf("删除后,再次前序遍历结果:\n");
PreOrderRec(root);
printf("\n");
//二叉搜索树查找节点
Bnode* node1 = QueryByLoop(root, 20);
printf("搜索二叉搜索树,节点20%s\n", node1 ? "存在" : "不存在");
Bnode* node2 = QueryByLoop(root, 21);
printf("搜索二叉搜索树,节点21%s\n", node2 ? "存在" : "不存在");
system("pause");
return 0;
}
测试结果: