【数据结构】二叉树——链式结构的实现(代码演示)

news/2024/11/19 11:48:48/

目录

1 二叉树的链式结构

2  二叉树的创建

 3  二叉树的遍历

3.1 前序遍历

3.1.1运行结果:

3.1.2代码演示图:

3.1.3 演示分析:

3.2 中序遍历

 3.3 后序遍历

3.4 层序遍历

4  判断是否是完全二叉树

5  二叉树节点的个数

5.1 总个数

5.2 叶子节点个数

6  二叉树的深度

7  二叉树的销毁

7.1 二级指针销毁二叉树

7.2 单指针销毁二叉树

8  完整代码

8.1 test.c

8.2 Quene.c

8.3 Quene.h

8.4 运行结果截图


1 二叉树的链式结构

typedef int BTDataType;
typedef struct BinaryTreeNode
{BTDataType _data;struct BinaryTreeNode* _left;struct BinaryTreeNode* _right;
}BTNode;

该结构包含根节点、左子树(_left)和右子树(_right)以及它们所存储的数据(data)

二叉树是:

  1. 空树
  2. 非空:根节点,根节点的左子树、根节点的右子树组成的。

2  二叉树的创建

// 通过前序遍历的数组"123###45##6##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)  
{BTNode* cur = (BTNode*)malloc(sizeof(BTNode));if (cur == NULL){perror("malloc fail");exit(-1);}if (*pi >= n || a[*pi] == '#'){(*pi)++;return NULL;}cur->_data = a[*pi];(*pi)++;cur->_left = BinaryTreeCreate(a, n, pi);cur->_right = BinaryTreeCreate(a, n, pi);return cur;
}

代码表示用前序遍历创建二叉树,参数a表示传进来的数组首元素地址,参数n表示数组长度,参数pi表示下标的地址,这里用指针表示,因为如果不用指针,则递归的过程中重新定义的pi已经不是原来的pi,即找不到下标了。

第一个if语句表示创建失败后执行;第二个if语句表示如果下标pi大于数组长度n或者数组元素等于'#',下标的位置往后移动一个位置,返回值为NULL; cur->_data = a[*pi]表示将数组中下标为pi的元素赋给当前节点cur->_data;

这里创建了一个二叉树作为例子:

 3  二叉树的遍历

二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉 树中的节点进行相应的操作,并且每个节点只操作一次。

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

前序遍历:访问根结点的操作发生在遍历其左右子树之前。根、左子树、右子树

中序遍历:访问根结点的操作发生在遍历其左右子树之中(间)。左子树、根、右子树

后序遍历:—访问根结点的操作发生在遍历其左右子树之后。左子树、右子树、根

层序遍历先访问根节点,然后从左往右访问第二层,依次访问下面的节点

3.1 前序遍历


void BinaryTreePrevOrder(BTNode* root)
{if (root == NULL){printf("NULL");return ;}
//	printf("%c ", root->_data);printf("%d ", root->_data);BinaryTreePrevOrder(root->_left);BinaryTreePrevOrder(root->_right);
}

代码表示:如果节点为空,则返回空值;打印根节点的值,左子树递归,右子树递归。

这里给出运行前序遍历的完整代码:

#define  _CRT_SECURE_NO_WARNINGS
//#include"Heap.h"
#include<time.h>
#include"Quene.h"//typedef char BTDataType;
typedef int BTDataType;
typedef struct BinaryTreeNode
{BTDataType _data;struct BinaryTreeNode* _left;struct BinaryTreeNode* _right;
}BTNode;void BinaryTreePrevOrder(BTNode* root)
{if (root == NULL){printf("NULL ");return ;}
//	printf("%c ", root->_data);printf("%d ", root->_data);BinaryTreePrevOrder(root->_left);BinaryTreePrevOrder(root->_right);
}int main()
{//BTDataType a[] = { 'A','B','D','#','#','E','#','H','#','#','C','F','#','#','G','#','#' };BTDataType a[] = { 1,2,3,'#','#','#',4,5,'#','#',6,'#','#' };int n = sizeof(a) / sizeof(a[0]);int i = 0;BTNode* root = BinaryTreeCreate(a, n, &i);BinaryTreePrevOrder(root);                 //前序遍历:根,左子树,右子树printf("\n");
return 0;
}

3.1.1运行结果:

3.1.2代码演示图:

 

3.1.3 演示分析:

(跟着箭头看)首先从根节点“1”开始递归,printf("%d ",root->_data)打印根节点“1”的值;BinaryTreePrevOrder(root->_left)对左子树进行递归;打印左子树根节点 “2"的值;BinaryTreePrevOrder(root->_left)继续对左子树进行递归,打印左子树根节点“3”的值;

BinaryTreePreOrder(root->_left)继续对左子树进行递归,root==NULL,打印空值,返回空,此时函数回到调用它的地方,并销毁栈帧,即回到它的父节点"3"的BinaryTreePrevOrder(root->_left)语句哪里;接下来执行BinaryTreePreOrder(root->_right)语句,root==NULL,打印空值,返回空,此时函数回到调用它的地方,并销毁栈帧,即回到它的父节点"2"的BinaryTreePreOrder(root->_right)语句哪里;然后依此类推。

3.2 中序遍历

void BinaryTreeInOrder(BTNode* root)
{if (root == NULL){return ;}BinaryTreeInOrder(root->_left);
//	printf("%c ", root->_data);printf("%d ", root->_data);BinaryTreeInOrder(root->_right);
}

其实它的代码和前序遍历差不多,只是中序遍历要先走完左子树,才走根节点,最后右子树。

运行结果:

 3.3 后序遍历

void BinaryTreePostOrder(BTNode* root)
{if (root == NULL){return ;}BinaryTreePostOrder(root->_left);BinaryTreePostOrder(root->_right);//printf("%c ", root->_data);printf("%d ", root->_data);
}

 其实后续遍历的代码和前序遍历的代码差别不大,唯一差别就是,后序遍历是递归完左子树和右子树之后才打印根节点。

3.4 层序遍历

void BinaryTreeLevelOrder(BTNode* root)
{Quene qu;QueneInit(&qu);if(root)QuenePush(&qu, root);while (!QueneEmpty(&qu)){BTNode* cur = QueneFront(&qu);  
//		printf("%c ", cur->_data);printf("%d ", cur->_data);QuenePop(&qu);if (cur->_left){QuenePush(&qu, cur->_left);}if (cur->_right){QuenePush(&qu, cur->_right);}}printf("\n");QuenDestroy(&qu);
}

层序遍历,我采用队列的链式结构对二叉树的节点的值进行存储。

思路:出上一层,带下一层

代码分析:    按照思路,队头先出“1”,然后把“1”的左节点"2"和右节点"4"尾插进队列,然后出“2”,把“2”的左节点“3”尾插进队列,然后队头出“4”,把“4”的左节点“5”和右节点“6”尾插进队列。以此类推。QueneInit(&qu);表示初始化队列"qu";  QuenePush(&qu, root);表示将二叉树的值尾插到队列中;BTNode* cur = QueneFront(&qu);  表示创建一个二叉树结构体类型的节点cur来存放队列首元素的值;    QuenePop(&qu);表示出队列。

4  判断是否是完全二叉树

bool BinaryTreeComplete(BTNode* root)
{Quene qu;QueneInit(&qu);
if(root)QuenePush(&qu, root);while (!QueneEmpty(&qu)) {BTNode* front = QueneFront(&qu);QuenePop(&qu);if (front == NULL)               //如果当前节点不为空,则将它的左右节点放入队列{break;}else{QuenePush(&qu, front->_left);QuenePush(&qu, front->_right);}}//出到空以后,如果后面全是空,则是完全二叉树while (!QueneEmpty(&qu)){BTNode* front = QueneFront(&qu);QuenePop(&qu);if (front != NULL){QuenDestroy(&qu);return false;}}QuenDestroy(&qu);return true;}

思路:二叉树的节点出队列遇到空值以后,往后都是空值则是完全二叉树,否则不是。

代码分析:如果二叉树的当前节点不为空,入队列;如果队列不为空,先出队列,然后将它的左右节点尾插进队列;第二个while哪里,队列不为空,出队列,遇到空值后,往后都是空值,则为完全二叉树,否则不是完全二叉树。

5  二叉树节点的个数

5.1 总个数

int BinaryTreeSize(BTNode* root)
{//if (root == NULL)//{//	return 0;//}//int left = BinaryTreeSize(root->_left);//int right = BinaryTreeSize(root->_right);//return left + right +1;//返回总节点个数return root==NULL ? 0:BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1; 
}

思路:如果二叉树的节点为空,则返回0,否则计算出左边 + 右边 + 1。

5.2 叶子节点个数

int BinaryTreeLeafSize(BTNode* root)
{if (root == NULL){return 0;}if (root->_left == NULL && root->_right == NULL){return 1;}return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}

 思路:如果该节点为空,返回0;如果该节点的左右节点都为空,返回1;对左子树和右子树递归。

6  二叉树的深度

int TreeHeight(BTNode* root)
{if (root == NULL){return 0;}int leftHeight = TreeHeight(root->_left);int rightHeight = TreeHeight(root->_right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

这里需要注意的是要定义两个整形变量来存放左节点和右节点的个数,如果不定义直接

return TreeHeight(root->_left) >TreeHeight(root->_right) ?  TreeHeight(root->_left)+1: TreeHeight(root->_right)+1;性能会下降很多,因为在"?"前面的递归只是做了比较,并没有保存比较的值,使得出现很多重复的计算,会极大的浪费资源,上一层会连累下一层,每一层都要重复计算。

7  二叉树的销毁

7.1 二级指针销毁二叉树

void BinaryTreeDestory(BTNode** root) //参数root是一个指向指针的指针,因为需要修改每个节点的指针
{if (*root){BinaryTreeDestory(&(*root)->_left);BinaryTreeDestory(&(*root)->_right);free(*root);*root = NULL;      //防止内存泄露}}

说明:这里使用后续遍历对二叉树的节点进行置空。

BinaryTreeDestroy(&((*root)->left)) 中的 (*root)->left 是一个指向左子树根节点的指针,它的类型为 TreeNode*。因此,我们需要先用 * 运算符解引用 root 指向的地址,得到指向二叉树根节点的指针 *root,然后再使用箭头运算符 -> 访问其左子树,并取其地址作为参数传递给 BinaryTreeDestroy() 函数。

这样做是因为我们需要修改根节点的左子树指针,使其指向空,并释放原来左子树所占用的内存。如果直接传递 (*root)->left 作为参数,则只是将一个指向左子树根节点的临时指针传递给了函数,在函数内部无法修改原来的左子树指针。
 

在这个函数中,使用了一个BTNode**类型的指针作为参数。这个指针指向了二叉树根结点的地址。在函数内部,首先判断当前节点是否为空,如果为空则直接返回。然后递归地销毁当前节点的左子树和右子树,直到最底层的叶子节点。最后释放当前节点所占用的内存,并将其只想空指针以避免野指针。

7.2 单指针销毁二叉树

void TreeDestory(BTNode* root)
{if(root==NULL)return;TreeDestory(root->_left);TreeDestory(root->_right);free(root);
}

然后再主函数哪里调用TreeDestory()函数之后需要添加"root==NULL"

8  完整代码

8.1 test.c

#define  _CRT_SECURE_NO_WARNINGS
//#include"Heap.h"
#include<time.h>
#include"Quene.h"//typedef char BTDataType;
typedef int BTDataType;
typedef struct BinaryTreeNode
{BTDataType _data;struct BinaryTreeNode* _left;struct BinaryTreeNode* _right;
}BTNode;// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
//void BinaryTreeDestory(BTNode** root);
void BinaryTreeDestory(BTNode* root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
//二叉树的深度
int TreeHeight(BTNode* root);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);// 判断二叉树是否是完全二叉树
//int BinaryTreeComplete(BTNode* root);
bool BinaryTreeComplete(BTNode* root);// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)  
{BTNode* cur = (BTNode*)malloc(sizeof(BTNode));if (cur == NULL){perror("malloc fail");exit(-1);}if (*pi >= n || a[*pi] == '#'){(*pi)++;return NULL;}cur->_data = a[*pi];(*pi)++;cur->_left = BinaryTreeCreate(a, n, pi);cur->_right = BinaryTreeCreate(a, n, pi);return cur;
}//void BinaryTreeDestory(BTNode** root) //参数root是一个指向指针的指针,因为需要修改每个节点的指针
//{
//	if (*root)
//	{
//		BinaryTreeDestory(&(*root)->_left);
//		BinaryTreeDestory(&(*root)->_right);
//		free(*root);
//		*root = NULL;      //防止内存泄露
//	}
//
//}void BinaryTreeDestory(BTNode* root)
{if (root == NULL)return;BinaryTreeDestory(root->_left);BinaryTreeDestory(root->_right);free(root);
}int BinaryTreeSize(BTNode* root)
{//if (root == NULL)//{//	return 0;//}//int left = BinaryTreeSize(root->_left);//int right = BinaryTreeSize(root->_right);//return left + right +1;//返回总节点个数return root==NULL ? 0:BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1; 
}int BinaryTreeLeafSize(BTNode* root)
{if (root == NULL){return 0;}if (root->_left == NULL && root->_right == NULL){return 1;}return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}int BinaryTreeLevelKSize(BTNode* root, int k)
{if (root == NULL){return 0;}if (k == 1){return 1;}return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);
}BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{if (root == NULL){return NULL;}if (root->_data == x){return root;}BTNode* ret1 = BinaryTreeFind(root->_left, x);if (ret1)return ret1;BTNode* ret2 = BinaryTreeFind(root->_right, x);if (ret2)return ret2;return NULL;
}int TreeHeight(BTNode* root)
{if (root == NULL){return 0;}int leftHeight = TreeHeight(root->_left);int rightHeight = TreeHeight(root->_right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}void BinaryTreePrevOrder(BTNode* root)
{if (root == NULL){//	printf("NULL ");return ;}
//	printf("%c ", root->_data);printf("%d ", root->_data);BinaryTreePrevOrder(root->_left);BinaryTreePrevOrder(root->_right);
}void BinaryTreeInOrder(BTNode* root)
{if (root == NULL){
//		printf("NULL ");return ;}BinaryTreeInOrder(root->_left);
//	printf("%c ", root->_data);printf("%d ", root->_data);BinaryTreeInOrder(root->_right);
}void BinaryTreePostOrder(BTNode* root)
{if (root == NULL){return ;}BinaryTreePostOrder(root->_left);BinaryTreePostOrder(root->_right);//printf("%c ", root->_data);printf("%d ", root->_data);
}void BinaryTreeLevelOrder(BTNode* root)
{Quene qu;QueneInit(&qu);if(root)QuenePush(&qu, root);while (!QueneEmpty(&qu)){BTNode* cur = QueneFront(&qu);  
//		printf("%c ", cur->_data);printf("%d ", cur->_data);QuenePop(&qu);if (cur->_left){QuenePush(&qu, cur->_left);}if (cur->_right){QuenePush(&qu, cur->_right);}}printf("\n");QuenDestroy(&qu);
}//int BinaryTreeComplete(BTNode* root) 
//{
//	Quene qu;
//	BTNode* cur;
//	int tag = 0;
//	QueneInit(&qu);
//	QuenePush(&qu, root);
//
//	while (!QueneEmpty(&qu))
//	{
//		cur = QueneFront(&qu);printf("%c ", cur->_data);
//		printf("%d ", cur->_data);
//		if (cur->_right && !cur->_left)
//		{
//			return 0;
//		}
//		if (tag && (cur->_left || cur->_right))   //tag?
//		{
//			return 0;
//		}
//
//
//		if (cur->_left)
//		{
//			QuenePush(&qu, cur->_left);
//		}
//
//		if (cur->_right)
//		{
//			QuenePush(&qu, cur->_right);
//		}
//		else
//		{
//			tag = 1;
//		}
//		QuenePop(&qu);
//	}
//
//	QuenDestroy(&qu);
//	return 1;
//}bool BinaryTreeComplete(BTNode* root)
{Quene qu;QueneInit(&qu);QuenePush(&qu, root);while (!QueneEmpty(&qu)) {BTNode* front = QueneFront(&qu);QuenePop(&qu);if (front == NULL)               //如果当前节点不为空,则将它的左右节点放入队列{break;}else{QuenePush(&qu, front->_left);QuenePush(&qu, front->_right);}}//出到空以后,如果后面全是空,则是完全二叉树while (!QueneEmpty(&qu)){BTNode* front = QueneFront(&qu);QuenePop(&qu);if (front != NULL){QuenDestroy(&qu);return false;}}QuenDestroy(&qu);return true;}int main()
{//BTDataType a[] = { 'A','B','D','#','#','E','#','H','#','#','C','F','#','#','G','#','#' };BTDataType a[] = { 1,2,3,'#','#','#',4,5,'#','#',6,'#','#' };int n = sizeof(a) / sizeof(a[0]);int i = 0;BTNode* root = BinaryTreeCreate(a, n, &i);BinaryTreePrevOrder(root);                 //前序遍历:根,左子树,右子树printf("\n");BinaryTreeInOrder(root);				    //中序遍历:左子树,根,右子树printf("\n");BinaryTreePostOrder(root);                  //后序遍历:左子树,右子树,根printf("\n");BinaryTreeLevelOrder(root);                 //层序遍历printf("\n");printf("Size:%d\n", BinaryTreeSize(root));printf("leafsize:%d\n", BinaryTreeLeafSize(root));printf("TreeHeight:%d\n", TreeHeight(root));//int iscompleate= BinaryTreeComplete(root);//if (iscompleate == 1)//	printf("是完全二叉树\n");//else//	printf("不是完全二叉树\n");printf("TreeComplete:%d \n", BinaryTreeComplete(root));BinaryTreeDestory(root);root = NULL;return 0;}

8.2 Quene.c

#define  _CRT_SECURE_NO_WARNINGS
#include"Quene.h"void QueneInit(Quene* pq)              
{assert(pq);pq->head = pq->tail = NULL;pq->size = 0;
}void QuenDestroy(Quene* pq)
{assert(pq);QNode* cur = pq->head;while (cur){QNode* del = cur;cur = cur->next;free(del);}pq->head = pq->tail = NULL;pq->size = 0;
}void QuenePush(Quene* pq, QDataType x)               //进队列
{assert(pq);QNode* newnode = (QNode*)malloc(sizeof(QNode)); //插入值时开辟空间if (newnode == NULL){perror("malloc fail");exit(-1);}newnode->next = NULL; newnode->data = x;if (pq->tail == NULL){pq->head = pq->tail = newnode;}else{pq->tail->next = newnode;                        //尾插到pq->tail后面pq->tail = newnode;                              //重新调整pq->tail的位置}pq->size++;                                      //记录数据
}void QuenePop(Quene* pq)                             //出队列
{assert(pq);                                      //判断队列是否为空,假如传进来的是一个空队列则不能出队列assert(!QueneEmpty(pq));if (pq->head->next == NULL){free(pq->head);pq->head = pq->tail = NULL;}else{QNode* del = pq->head;pq->head = pq->head->next;free(del);}pq->size--;
}QDataType QueneFront(Quene* pq)
{assert(pq);assert(!QueneEmpty(pq));return pq->head->data;
}
QDataType QueneBack(Quene* pq)
{assert(pq);assert(!QueneEmpty(pq));return pq->tail->data;
}bool QueneEmpty(Quene* pq)
{assert(pq);return pq->head == NULL && pq->tail == NULL;
}
int QueneSize(Quene* pq)
{assert(pq);return pq->size;
}

8.3 Quene.h

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>//前置声明
struct BinaryTreeNode;typedef struct BinaryTreeNode* QDataType;                   //定义队列数据类型
typedef struct QueneNode
{QDataType data;                      //队列中的数据struct QueneNode* next;              //队列的下一个节点
}QNode;typedef struct Queue
{QNode* head;                   //队头QNode* tail;                   //队尾int size;                     //队列的长度}Quene;void QueneInit(Quene* pq);        //队列初始化
void QuenDestroy(Quene* pq);      //队列的销毁
void QuenePush(Quene* pq, QDataType x);   //数据进队列
void QuenePop(Quene* pq);                 //数据出队列
QDataType QueneFront(Quene* pq);          //队头的值
QDataType QueneBack(Quene* pq);           //队尾的值bool QueneEmpty(Quene* ps);               //队列判空
int QueneSize(Quene* pq);                 //队列大小

8.4 运行结果截图


http://www.ppmy.cn/news/103614.html

相关文章

​​​​Linux Shell 实现一键部署Oracle21 rpm包方式

oracle前言 Oracle开发的关系数据库产品因性能卓越而闻名&#xff0c;Oracle数据库产品为财富排行榜上的前1000家公司所采用&#xff0c;许多大型网站也选用了Oracle系统&#xff0c;是世界最好的数据库产品。此外&#xff0c;Oracle公司还开发其他应用程序和软件。同时&#…

深度学习笔记之循环神经网络(八)LSTM的轻量级变体——门控循环单元(GRU)

深度学习笔记之LSTM轻量级变体——门控循环单元[GRU] 引言回顾&#xff1a; LSTM \text{LSTM} LSTM的前馈计算过程 LSTM \text{LSTM} LSTM的问题 GRU \text{GRU} GRU的前馈计算过程 GRU \text{GRU} GRU的优势 引言 上一节介绍了从反向传播过程的角度认识 LSTM \text{LSTM} LST…

将文件base64解码后输出

Map taxPdf alService.getFile();//得到文件 String filestr String.valueOf(taxPdf.get("data")); if (taxPdf ! null) {BASE64Decoder decoder new BASE64Decoder();byte[] fileBytes decoder.decodeBuffer(filestr);HttpServletResponse response getRespons…

30分钟开发微信小程序并部署

30分钟开发微信小程序并部署 首先&#xff0c;今天我们会使用到腾讯为我们提供的云平台来协助我们小程序的开发和部署。 环境准备&#xff1a; 微信开发者账号&#xff08;自己在微信公众号平台注册一个账号&#xff09;&#xff1a;https://mp.weixin.qq.com/&#xff0c;同时…

[GUET-CTF2019]number_game[数独]

目录 题目 学到的知识点&#xff1a; 题目 在buu上看到了一道数独题&#xff0c;没见过&#xff0c;记录一下 下载附件&#xff0c;查壳&#xff0c;无壳&#xff0c;在IDA中打开&#xff0c;直接找到主函数 unsigned __int64 __fastcall main(int a1, char **a2, char **a3…

CH341的SPI接口编程说明

CH341的SPI接口特性&#xff1a; 1、提供3个片选信号&#xff0c;SCS0~SCS2&#xff1b; 2、SPI 默认工作模式0&#xff0c;不支持修改&#xff1b; 3、SPI Clock速率固定&#xff0c;小于2MHz&#xff1b; 4、支持MSB/LSB传输&#xff1b; 引脚序号功能说明15~17SCS0~SCS…

在网络安全领域,这些都是比较牛的中国黑客!

1994年前后&#xff0c;国内出现最早一批黑客&#xff0c;其中以龚蔚、天山等顶级黑客为代表&#xff1b;2000年左右&#xff0c;第二代黑客出现&#xff0c;他们的技术特点与前辈相仿&#xff0c;深入研究网络安全技术&#xff0c;有自己的理论和产品&#xff1b;而后第三代黑…

记录两个Windows和Mac上部署阿里Canal无法启动的神坑

目录 一、问题列表 二、解决方案 三、参考资料 四、配置详解 五、数据库相关操作 一、问题列表 1、问题一&#xff1a;点击 startup.bat 窗口出现后立马闪退的问题。 2、问题二&#xff1a;启动后日志文件报错&#xff1a; ERROR com.alibaba.otter.canal.deployer.Cana…