排序算法:堆排序

news2024/11/25 10:41:48

朋友们、伙计们,我们又见面了,本期来给大家解读一下栈和队列方面的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

数据结构与算法专栏:数据结构与算法

个  人  主  页 :stackY、

C 语 言 专 栏:C语言:从入门到精通

 

目录

 

前言:

1.堆的应用:堆排序

1.1直接使用堆进行插入

1.2向上调整算法建堆

1.3向下调整算法建堆

2.算法时间复杂度

2.1向下调整建堆算法时间复杂度

2.2向上调整建堆的时间复杂度 

3.堆排序的时间复杂度 

4.完整堆排序代码


前言:

通过前面的学习和了解我们了解到了二叉树使用顺序结构实现的方式为堆,并且对堆的实现都做了详细的介绍,那么本期我们来看看堆到底都有哪些应用场景呢?

堆这个数据结构我们经常使用它来进行堆排序和解决Top-K的问题,本期我们先来重点的了解堆是怎么进行排序的呢?

 1.堆的应用:堆排序

前面我们讲到过冒泡排序和qsort排序,那么本期再来一种新的排序算法,它的效率也是蛮高的。它所要用到的相关知识点就是数据结构:堆

实现堆排序这里有三种思路:

1. 直接将数组插入到堆中,然后再将堆中的数据插入到数组中。

2. 向上调整算法建堆,然后使用堆删除的思想来排序。

3. 向下调整算法建堆,然后使用堆删除的思想来排序。 

1.1直接使用堆进行插入

先创建一个堆,然后将数组中的元素依次插入到堆中,如果是大堆在插入完之后就排序成了降序,然后将堆中的数据依次拷贝到数组中,这样子就完成了排序。

代码演示:

//    堆

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

//创建堆
//顺序表
typedef int HPDataType;

typedef struct Heap
{
	HPDataType* a;
	int size;  //有效元素个数
	int capacity;  //容量
}Heap;

//初始化
void HeapInit(Heap* php);

//销毁
void HeapDestroy(Heap* php);

//插入
void HeapPush(Heap* php, HPDataType x);

//删除
void HeapPop(Heap* php);

//判空
bool HeapEmpty(Heap* php);

//获取堆顶元素
HPDataType HeapTop(Heap* php);

//初始化
void HeapInit(Heap* php)
{
	php->a = NULL;
	php->size = 0;
	php->capacity = 0;
}

//销毁
void HeapDestroy(Heap* php)
{
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

//交换函数
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 (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)
	{
		int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		//为堆开辟空间
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->capacity = newcapacity;
		php->a = tmp;
	}
	//插入数据
	php->a[php->size] = x;
	php->size++;
	//向上调整
	AdjustUp(php->a, php->size - 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[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			//更新父亲和孩子节点
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//删除
void HeapPop(Heap* php)
{
	assert(php);
	//判断堆是否为空
	assert(!HeapEmpty(php));
	//交换堆顶的数据和最后的数据
	Swap(&php->a[0], &php->a[php->size - 1]);
	//删除最后的数据
	php->size--;
	//向下调整
	AdjustDown(php->a, php->size, 0);
}

//判空
bool HeapEmpty(Heap* php)
{
	assert(php);
	return php->size == 0;
}

//获取堆顶元素
HPDataType HeapTop(Heap* php)
{
	assert(php);
	//判断堆是否为空
	assert(!HeapEmpty(php));
	return php->a[0];
}

//堆排序
void HeapSort(int* a, int n)
{
	//建堆
	Heap hp;
	HeapInit(&hp);
	//将数组先插入到堆中实现堆中的大小堆排序
	for (int i = 0; i < n; i++)
	{
		HeapPush(&hp, a[i]);
	}
	//再将堆中的数据拷贝到数组中
	int i = 0;
	while (!HeapEmpty(&hp))
	{
		//保存堆顶元素
		int top = HeapTop(&hp);
		//拷贝
		a[i++] = top;
		HeapPop(&hp);
	}
	HeapDestroy(&hp);
}

int main()
{
	int a[] = { 5,6,8,9,7,4,5,2,3,6 };
	int sz = sizeof(a) / sizeof(int);
	HeapSort(a, sz);
	return 0;
}

我们在这里建的是小堆,所以是排成了升序的状态,建大堆只要将向上调整和向下调整的小于号换成大于号即可。

这种算法有点弊端:

1. 必须先要有一个堆,太麻烦。

2. 将数组的数据拷贝至堆,又将堆的数据拷贝至数组,空间复杂度上面代价有点大。

但是这种算法的时间复杂度也是不错的,在前面我们讲堆的时候提到过向上调整算法和向下调整算法,这两种算法的时间复杂度都是O(logN),那么在这个堆排序的算法中的时间复杂度加上遍历数组的时间复杂度合起来为O(N*logN)

1.2向上调整算法建堆

在使用向上调整算法和向下调整算法的时候如果要达到降序的目的应该建大堆还是小堆?如果要达到升序的目的应该建大堆还是小堆?

我们在这里来思考一下:

按照以往的思路:要排成降序需要建大堆,这样就保证了最大的一个在堆顶,然后将除了堆顶剩下的数据看作一个堆,再找到次大的,这时就面临两个问题:

1. 将剩下的数据看作一个堆时又需要重新建堆,代价有点大。

2. 如果将剩下的数据重新建堆,那么它们原来的父子关系就乱套了,从之前的兄弟变成父子或许有点扯淡。

那么我们就来换一种思路,这种思路和删除数据时的思路一样,我们要排成降序我们就建小堆,那么这时堆顶的数据就是最小的,然后将堆顶的数据和最后一个数据交换,这时堆的最后一个数据就是最小的,此时,就不需要将最后一个数据看作堆中的数据,然后再对剩余数据进行向下调整再找出次小的,再交换至倒数第二个位置......这样子依次类推,直到全部找出来。同样的,要排成升序就建大堆,找出最大的交换,再找次大的,再交换......依次类推同样也可以完成升序。

这里还有需要注意的一个点,我们要从哪里开始进行向上调整建堆?之前说过向上调整算法调整的节点的上面必须是一个堆,因此我们需要从第一个结点开始进行向上调整建堆,因为第一个结点就是一个堆,所以我们将第一个结点看作堆,然后从第二个位置开始插入,前面的结点是堆,符合。

代码演示:

//交换函数
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向上调整算法
void AdjustUp(int* a, 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 AdjustDown(int* 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[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			//更新父亲和孩子节点
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//建堆--向上调整算法建堆
	//模拟的是一个插入堆的过程
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}

	int end = n - 1;
	while (end > 0)
	{
		//交换堆顶和最后一个数据的位置
		Swap(&a[0], &a[end]);
		//向下调整,找次小的
		AdjustDown(a, end, 0);
		end--;
	}
}

int main()
{
	int a[] = { 5,6,8,9,7,4,5,2,3,6 };
	int sz = sizeof(a) / sizeof(int);
	HeapSort(a, sz);
	return 0;
}

我们在这里建的是小堆,所以是排成了降序的状态,建大堆只要将向上调整和向下调整的小于号换成大于号即可。

1.3向下调整算法建堆

排降序建小堆,排升序建大堆,当我们使用向上调整算法来建堆实现堆排序时,需要用到向上调整算法和向下调整算法,这个有点太麻烦,那既然在交换数据之后要进行向下调整,那不妨直接使用向下调整算法来直接建堆。

向下调整算法使用的前提是向下调整的结点的下面必须是堆,我们不妨画图来观察一下:

我们知道一个叶子节点本身就是大堆或者小堆,因此我们可以在叶子节点上面下下功夫。

向下调整算法的下面必须是堆:
而叶子节点恰好又是堆,那么叶子节点就可以不用处理,那么我们就可以倒着来进行建堆,从最后一个非叶子节点开始调整倒数第一个叶子节点的父亲开始调整)。根据孩子与父亲的关系找到父亲然后调整建堆,由于叶子节点本身就是堆,从它的父亲开始调整也符合向下调整算法的逻辑。

 

代码演示:

//交换函数
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向下调整
void AdjustDown(int* 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[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			//更新父亲和孩子节点
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//建堆--向下调整算法建堆

	for (int i = ((n - 1) - 1) / 2; i >= 0; --i)
	{//       这里的n-1表示最后一个叶子节点
		//       最后一个叶子节点的父亲就是:
		//           (n-1)-1 /2;
		AdjustDown(a, n, i);
	}

	int end = n - 1;
	while (end > 0)
	{
		//交换堆顶和最后一个数据的位置
		Swap(&a[0], &a[end]);
		//向下调整,找次小的
		AdjustDown(a, end, 0);
		end--;
	}
}

int main()
{
	int a[] = { 5,6,8,9,7,4,5,2,3,6 };
	int sz = sizeof(a) / sizeof(int);
	HeapSort(a, sz);
	return 0;
}

2.算法时间复杂度

一个算法的好与坏取决与它的时间和空间,那么对于向上调整建堆和向下调整建堆哪种方法的时间复杂度更优呢?我们可以一一来计算一下。

 2.1向下调整建堆算法时间复杂度

 

假设二叉树的高度为h,总结点个数为N

第1层: 有2^0个节点,需要向下挪动h-1次
第2层: 有2^1个节点,需要向下挪动h-2次
第3层: 有2^2个节点,需要向下挪动h-3次
......
第h-1层: 有2^(h-2)个节点,需要向下挪动1次
第h层: 有2^(h-1)个节点,需要向下挪动0次

总共合计需要挪动:

F(h) = 2^(h-2) * 1 + 2^(h-3) * 2 + ...... + 2^1 * (h-2) + 2^0 * (h-1)

使用错位相减法:

2F(h) = 2^(h-1) * 1 + 2^(h-2) * 2 + 2^(h-3) * 2 + ...... + 2^2 * (h-2) + 2^1 * (h-1)

 -

  F(h) =                      2^(h-2) * 1 + 2^(h-3) * 2 + ...... + 2^1 * (h-2) + 2^0 * (h-1)

       

F(h)  = 2^h - 1 - h

这棵树的中的节点的个数为N = 2^h-1 ==>  h = log(N+1)

F(N) = N - log(N+1)

因此向下调整算法的时间复杂度为O(N)

2.2向上调整建堆的时间复杂度 

 向上调整很明显就是一个多乘多的问题,单单只看最后一层,节点越多,调整的次数越多,那这就完蛋了,向下调整算法最后一层的节点时不需要调整的,那这就很明显了,向下调整算法明显优势于向上调整算法,那么到底能快多少呢?我们一起来计算一下:

假设二叉树的高度为h,总结点个数为N

第1层: 有2^0个节点,需要向下挪动0次
第2层: 有2^1个节点,需要向下挪动1次
第3层: 有2^2个节点,需要向下挪动2次
......
第h-1层: 有2^(h-2)个节点,需要向下挪动h-2次
第h层: 有2^(h-1)个节点,需要向下挪动h-1次

总共合计需要挪动:

F(h) = 2^1 * 1 + 2^2 * 2 + ...... + 2^(h-2) * (h-2) + 2^(h-1) * (h-1)

使用错位相减法:

2F(h) =                  2^2 * 1 + 2^3 * 2 + ...... + 2^(h-1) * (h-2) + 2^h * (h-1)
 - 
 F(h) =   2^1 * 1 + 2^2 * 2 + ...... + 2^(h-2) * (h-2) + 2^(h-1) * (h-1)

F(h) = 2^1 * 1 + 2^2 * 2 + ...... + 2^(h-2) * (h-2) + 2^(h-1) * (h-1)
       = - 2^1 - 2^2 - 2^3 - ...... - 2^(h-2) - 2^(h-1) + 2^h * (h-1)
       = - 2^h + 2 - 2^h + 2^h * h
       = 2^h * (h-2) + 2

 

这棵树的中的节点的个数为N = 2^h-1 ==>  h = log(N+1)

F(N) = (N + 1) * (log(N + 1) - 2) + 2

因此向上调整算法的时间复杂度为O(N * logN)

3.堆排序的时间复杂度 

计算出了向下调整建堆的时间复杂度,我们再来看看堆排序算法的整体的时间复杂度。

这时我们就要计算筛选数据操作时的交换数据和向下调整的时间复杂度:

这里为什么最上面的反而不用向下调整那么多次了,是因为在调整上面的数据时,下面的数据已经是一个堆了,所以筛选数据的过程就和向上调整建堆的时间复杂度一样。

时间复杂度是O(N * logN )

那么再加上向下调整建堆的时间复杂度就是O(N) + O(N * logN)

所以整个堆排序算法的时间复杂度为O(N * logN)

4.完整堆排序代码

//交换函数
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//向下调整
void AdjustDown(int* 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[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			//更新父亲和孩子节点
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//O(N * logN)
void HeapSort(int* a, int n)
{
	//建堆--向下调整算法建堆
	//时间复杂度为O(N)
	for (int i = ((n - 1) - 1) / 2; i >= 0; --i)
	{//       这里的n-1表示最后一个叶子节点
		//       最后一个叶子节点的父亲就是:
		//           (n-1)-1/2;
		AdjustDown(a, n, i);
	}

	//O(N * logN)
	int end = n - 1;
	while (end > 0)
	{
		//交换堆顶和最后一个数据的位置
		Swap(&a[0], &a[end]);
		//向下调整,找次小的
		AdjustDown(a, end, 0);
		end--;
	}
}

int main()
{
	int a[] = { 5,6,8,9,7,4,5,2,3,6 };
	int sz = sizeof(a) / sizeof(int);
	HeapSort(a, sz);
	return 0;
}

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/578879.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法|9.从暴力递归到动态规划2

9.算法|从暴力递归到动态规划2 1.数字字符串转英文字符串 题意&#xff1a;规定1和A对应、2和B对应、3和C对应…26和Z对应&#xff0c;那么一个数字字符串比如"111”就可以转化为:“AAA”、“KA"和"AK” 给定一个只有数字字符组成的字符串str&#xff0c;返回…

windows安装python开发环境

最近因工作需要&#xff0c;要学习一下python&#xff0c;所以先安装一下python的开发环境&#xff0c;比较简单 下载和安装Python 首先&#xff0c;在浏览器中打开Python的官方网站&#xff08;https://www.python.org/downloads/) 然后&#xff0c;从该网站下载与你的操…

NCI架构-1

1、NFCC和DH通过物理连线相连&#xff0c;物理连线对应为Transport Layer&#xff08;传输层&#xff09;&#xff0c;支持SPI、I2C、UART、USB等&#xff1b; 2、DH中所有和NFC相关的应用程序都可视为DH-NFCEE(EE:Execution Enviroment)&#xff0c;图左的NFCEE模块可运行一些…

Linux系统中源码安装1.8.x版本Arduino IDE

本文内容参考&#xff1a; Ubuntu22.04安装Arduino IDE及Arduino UNO&#xff08;使用CH341驱动&#xff09;调试方法__KILLMILEDC_的博客-CSDN博客 在Linux上下载arduino_不说话的白帽子的博客-CSDN博客 https://guoqing.blog.csdn.net/article/details/88913063?spm1001.…

【JVM】8. 对象实例化及直接内存

文章目录 8.1. 对象实例化8.1.1. 创建对象的方式8.1.2. 创建对象的步骤1. 判断对象对应的类是否加载、链接、初始化2. 为对象分配内存3. 处理并发问题4. 初始化分配到的内存5. 设置对象的对象头6. 执行init方法进行初始化 8.2. 对象内存布局8.2.1. 对象头&#xff08;Header&am…

python+vue新能源汽车在线租赁管理系统pycharm项目

开发语言&#xff1a;Python 框架&#xff1a;django/flask Python版本&#xff1a;python3.7.7 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat 开发软件&#xff1a;PyCharm 在当今高度发达的信息中&#xff0c;信息管理改革已成为一种更加广泛和全面的趋势。 “新…

SpringBoot——原理(自动配置+原理分析@Conditional)

在上一篇有说到&#xff0c;进行源码跟踪时可以看见一个以Conditional开头的注解&#xff0c;这些都是条件装配的注解。 加在方法上时只对该方法生效&#xff0c;加在类上时是对整个配置类都有效。 这里只说三个常用的Conditional的子注解 案例演示 在启动类上加上一个Enabl…

第二章:ShardingSphere简介

什么是ShardingSphere 何为ShardingSphere呢?其实我们总结如下三点就能很好的理解: 1、一整套开源的分布式数据库中间件解决方案 2、有三个产品组成:Sharding-JDBC、Sharding-Proxy、Sharding-Sidecar(规划中) 3、他的定位是关系型数据库的中间件,在分布式环境下合理的…

【20】SCI易中期刊推荐——计算机信息系统工程电子与电气(中科院3区)

💖💖>>>加勒比海带,QQ2479200884<<<💖💖 🍀🍀>>>【YOLO魔法搭配&论文投稿咨询】<<<🍀🍀 ✨✨>>>学习交流 | 温澜潮生 | 合作共赢 | 共同进步<<<✨✨ 📚📚>>>人工智能 | 计算机视觉…

(转载)matlab遗传算法工具箱

以下内容大部分来源于《MATLAB智能算法30个案例分析》&#xff0c;仅为学习交流所用。 1理论基础 1.1遗传算法概述 遗传算法(genetic algorithm,GA)是一种进化算法,其基本原理是仿效生物界中的“物竞天择、适者生存”的演化法则。遗传算法是把问题参数编码为染色体,再利用迭代…

Qiskit系列(1)---Qiskit安装

1.qiskit与anaconda简介 Qiskit并不是一门独立的语言&#xff0c;它是基于Python的一个框架&#xff0c;就好比Pytorch, Tensorflow。而Qiskit这个框架需要配套一些其他的package&#xff08;各种大小DLC&#xff09;一起运行&#xff0c;这些运行Qiskit所必须的package就构成了…

vue前端分页功能怎么实现

Vue前端分页功能可以通过以下几个步骤实现&#xff1a; 1. 安装分页组件库&#xff08;如vue-pagination-2&#xff09;&#xff1a; bash npm install vue-pagination-2 2. 在Vue项目中引入并注册分页组件&#xff1a; javascript import Vue from vue; import Pagination fr…

【商品详情 +关键词搜索】API 接口系列

首先&#xff0c;大家要到官方主页去申请一个 appkey&#xff0c;这个是做什么用的呢&#xff1f;App Key 是应用的唯一标识&#xff0c;TOP 通过 App Key 来鉴别应用的身份。AppSecret 是 TOP 给应用分配的密钥&#xff0c;开发者需要妥善保存这个密钥&#xff0c;这个密钥用来…

增强语言模型导读

以ChatGPT为主的大语言模型出现已有半年时间&#xff0c;研究逐渐从针对模型本身的进化和功能&#xff0c;延展到如何更为有效地利用大模型&#xff0c;将它与其它工具结合&#xff0c;落地&#xff0c;以解决实际领域中的问题。 这里的增强主要指让大语言模型&#xff08;LM&…

【21】SCI易中期刊推荐——计算机科学人工智能领域(中科院4区)

💖💖>>>加勒比海带,QQ2479200884<<<💖💖 🍀🍀>>>【YOLO魔法搭配&论文投稿咨询】<<<🍀🍀 ✨✨>>>学习交流 | 温澜潮生 | 合作共赢 | 共同进步<<<✨✨ 📚📚>>>人工智能 | 计算机视觉…

Unity 动画系统基本概念

一、动画的基本概念 1、帧 在古代&#xff0c;一幅字画叫一帧&#xff0c;而在计算机中&#xff0c;每次渲染完毕一幅画面并显示出来&#xff0c;这一幅画就是一帧。 连续切换的帧就形成了动态的画面。每秒刷新帧的次数称为频率&#xff0c;单位是FPS&#xff08;Frames Per…

JavaEE Tomcat Servelet第一个helloworld程序

Tomcat & Servelet第一个程序helloworld&#xff01; 文章目录 JavaEE & Tomcat & 第一个Servelet程序1. HTTP服务器 - Tomcat1.1 Tomcat的目录结构&#xff1a;1.2 启动Tomcat1.3 Tomcat的优点 2. Servelet框架2.1 创建Maven项目2.2 引入依赖2.3 创建目录2.4 写代…

Mac电脑读写移动硬盘软件Tuxera NTFS2023中文版

日常工作中&#xff0c;我们经常会使用移动硬盘拷贝文件&#xff0c;因为移动硬盘传输文件方便、传输速度快。但我们在mac电脑上使用移动硬盘却发现硬盘无法正常读写。本文向大家介绍mac能读写的移动硬盘有哪些以及移动硬盘怎么在mac上读写。 一、Mac能读写的移动硬盘有哪些 移…

数据挖掘(5.1)--贝叶斯分类

目录 前言 正文 1.主观概率 2.贝叶斯定理 1.基础知识 2.贝叶斯决策准则 3.极大后验假设 4.例题 2.朴素贝叶斯分类模型 朴素贝叶斯分类器的算法描述&#xff1a; 朴素贝叶斯算法特点 3.贝叶斯信念网 贝叶斯网络的建模包括两个步骤 贝叶斯信念网特点 开往夏天的列…

C++第七章:类

类 一、定义抽象数据类型1.1 定义抽象数据类型类的用户 1.2 定义一个书籍类引入this引入const成员函数类作用域和成员函数在类的外部定义成员函数定义一个返回this对象的函数 1.3 定义类相关的非成员函数定义read和print函数最终代码 1.4 构造函数合成的默认构造函数某些类不能…