【数据结构与算法】(13):交换排序之冒泡排序和快速排序

🤡博客主页:Code_文晓

🥰本文专栏:数据结构与算法

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多数据结构与算法点击专栏链接查看💛💜✨✨    


        前面我们学习了插入类排序中的直接插入排序和希尔排序,这次,我们学习另一类排序—— 交换排序。 

        所谓交换类排序,就是根据序列中两个关键字的比较结果,来决定是否要交换这两个关键字对应的记录在序列中的位置。交换类排序主要包括冒泡排序快速排序。接下来我们先讲冒泡排序再讲快速排序。

1. 冒泡排序 

        冒泡排序的英文名称是Bubble Sort,也叫起泡排序。

        按照从小到大排序来说,它的基本思想是:在有n个元素的序列中,先将第一个记录的关键字和第二个记录的关键字进行比较,也就是两两 比较相邻 记录关键字,如果第一个记录的关键字大于第二个记录的关键字,则交换这两个记录。接着,比较第二个记录的关键字和第三个记录的关键字,如果第二个记录的关键字大于第三个记录的关键字,则交换这两个记录。依次类推,直到第n-1个记录和第n个记录比较完为止。

        上述这些过程叫 第一趟 冒泡排序。这样做的结果就是将关键字最大的记录放到了最后一个记录位置上。  下面来一个动态效果图,让我们看的更直观一些:

        显然,对于数组{16,1,45,23,99,2,18,67,42,10},第一趟冒泡排序后,结果为 {1,16,23,45,2,18,67,42,10,99}。如图 1 所示: 

        之所以称为冒泡排序,是因为大的数据往最下面(后面)沉,小的数据自然就会向上冒,这个过程就好像气泡在水中上浮的过程,所以叫做冒泡排序。

        接着要开始第二趟冒泡排序了。但因为最后一个记录已经是最大值,因此第二趟冒泡排序只需要两两比较前n-1个元素,这样就会把第二大的记录放到倒数第二个记录位置上。显然,接着第一趟冒泡排序的结果,第二趟冒泡排序的结果为{1,16,23,2,18,45,42,10,67,99}。如图2所示:

接着开始第三趟、第四趟……冒泡排序,当 在一趟排序中没有进行过记录交换的操作 时,就可以认为冒泡排序结束了。具体实现我会在下面的冒泡排序算法的改进代码中进行展示。  

        冒泡排序的代码编写有很多种方法,比如有的方法会从序列中的最后两条记录开始比较,把关键字最小的记录不断向最前面移动。这里我选择的编码方式还是按照本节最开始描述的冒泡排序基本思想来实现,下面是具体代码。  

实现代码: 

//冒泡排序(从小到大)
template<typename T>
void BubbleSort(T myarray[], int length)
{
	if (length <= 1) //不超过1个元素的数组,没必要排序
		return;

	//外层循环只控制排序的趟数
	for (int i = 0; i < length - 1; ++i)
	{
		//内层循环控制元素的大小比较和交换位置
		for (int j = 0; j < length - i - 1; ++j) //每趟比较的次数都会减少
		{
			if (myarray[j] > myarray[j + 1])  //前面的数据如果比后面的数据大
			{
				//交换元素位置
				T temp = myarray[j + 1];
				myarray[j + 1] = myarray[j];
				myarray[j] = temp;
			}
		} //end for j

		//每走一趟显示一下结果
		cout <<"第"<< i+1 <<"趟冒泡排序结果为: ";
		for (int i = 0; i < length; ++i) cout << myarray[i] <<"";
		cout << endl;
	} //end for i
	return;
}

在main主函数中,加入下面的测试代码。  

int arr[] = {16,1,45,23,99,2,18,67,42,10};
int length = sizeof(arr) / sizeof(arr[0]);   //数组中元素个数
BubbleSort(arr, length);//对数组元素进行冒泡排序

cout <<"冒泡排序结果为:";
for (int i = 0; i < length; ++i)
{
	cout << arr[i] <<" ";
}
cout << endl; //换行

 执行结果如下:

        从结果可以看到,第7、8、9趟冒泡排序的结果相同,这意味着第8和第9趟排序是没有必要的,也就是这个算法可以提前结束。换句话说。 冒泡排序的结束条件应该是“在一趟排序中没有进行过记录交换的操作”

        所以,可以对上述冒泡排序算法BubbleSort进行改进,下面是改进后的代码。

//冒泡排序(从小到大)
template<typename T>
void BubbleSort(T myarray[], int length)
{
	if (length <= 1) //不超过1个元素的数组,没必要排序
		return;

	//外层循环只控制排序的趟数
	for (int i = 0; i < length - 1; ++i)
	{
		bool cgflag=false;//表本趟冒泡排序是否发生过记录交换,false:无;true:有
		//内层循环控制元素的大小比较和交换位置
		for (int j = 0; j < length - i - 1; ++j) //每趟比较的次数都会减少
		{
			if (myarray[j] > myarray[j + 1])  //前面的数据如果比后面的数据大
			{
				//交换元素位置
				T temp = myarray[j + 1];
				myarray[j + 1] = myarray[j];
				myarray[j] = temp;

				cgflag = true; //标记本趟冒泡排序发生过记录交换(可能1次或者多次)
			}
		} //end for j
		if (cgflag == false) //本趟冒泡排序没有发生过记录交换,表示整个冒泡排序结束
			break;

		//每走一趟显示一下结果
		cout <<"第"<< i+1 <<"趟冒泡排序结果为: ";
		for (int i = 0; i < length; ++i) cout << myarray[i] <<"";
		cout << endl;
	} //end for i
	return;
}

main主函数中代码不变,执行结果如下:

        分析代码和结果可以看到,当进行第8趟冒泡排序后,因为没有发生记录交换,所以直接跳出外循环从而结束整个冒泡排序的过程。

        从代码中可以看到,冒泡排序实现代码比较简单。空间复杂度为O(1)。

        在时间复杂度方面,对于具有n个元素的数组,在最好的情况下,即数组中元素已经是排好序的情况下,则只需要一趟排序并且这趟排序只需要进行n-1次比较次数且不需要做任何数据交换,所以最好情况时间复杂度为O(n)。

        在最坏情况下,即数组中元素正好是逆序排列的情况下,此时需要进行n-1趟排序,比较次数和记录交换次数都是1+2+3+……+(n-1)= \frac{n(n-1)}{2}次,即最坏情况时间复杂度为O(n^{2})。

        平均情况时间复杂度的分析要结合一些概率论知识,这里就不详细说明,结论也是O(n^{2})。此外,从实现代码中不难看到,即使遇到了关键字相同的两条记录,这两条记录的相对顺序也不会发生改变,所以此排序算法是 稳定 的。

总结:冒泡排序会进行多趟排序,每趟排序都会把当前参与排序的数字中的最大数字下沉到最后,当然,不能影响已经在最后面的排好序的数字。  


2. 快速排序 

        前面我们一起学习了交换类排序中的冒泡排序,这次我们继续学习交换类排序中的快速排序。这两种排序算法的主要区别在于排序的效率和实现代码。如果说冒泡排序是通过相邻元素的比较和交换达成排序,那么快速排序就是一种分而治之的思想,是对冒泡排序的改进。

        快速排序的英文名称是Quick Sort,他通过分而治之的思想,把待排序的表分隔成若干个子表,每个子表都以一个称为枢轴的元素为基准进行排序。

        一般来说,在元素数量一定的内部排序算法中,快速排序算法平均性能是最优秀的,因此,C++标准库中也提供了qsort函数来实现快速排序功能(其实qsort的实现版本中,还可能会用到其他排序)。 

        快速排序是Hoare于1962年提出的一种交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值(这个元素通常是首元素),按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

        通过 一趟排序 将所有关键字小于枢轴的元素都放置在枢轴前面,大于枢轴的元素都放置在枢轴后面。这样,这趟排序就将待排元素 分割 成了两个独立的部分。而且这个时候,枢轴元素所在的位置其实也就是该元素 最终 应该在的位置了。

// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int myarray[], int left, int right)
{
   if(right - left <= 1)
       return;

   // 按照基准值对array数组的 [left, right)区间中的元素进行划分
   int div = partion(myarray, left, right);

   // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
   // 递归排[left, div)
   QuickSort(myarray, left, div);

   // 递归排[div+1, right)
   QuickSort(myarray, div+1, right);
}

        上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。

这里为了下面的代码,先说一些结论: 

快速排序最好的情况下,时间复杂度为O(nlog^{n}_{2})。这是因为快速排序采用分治的思想,将数组分成较小和较大的两部分,并对两部分分别进行排序,最终得到有序的数组。

平均情况时间复杂度为 O(nlog^{n}_{2} )

快速排序最坏情况下,时间复杂度为O(n^{2})。这种情况发生在待排序的数组已经是有序或基本有序的情况下。在这种情况下,快速排序的划分过程可能会将数组分成极度不平衡的两部分,导致递归深度增加,从而导致性能下降。

快速排序的效果受到输入数据的分布情况的影响。当输入数据近似随机分布时,快速排序的效果通常最好。而当输入数据具有一定的有序性或逆序性时,快速排序的效果可能最差。

结论中可以知道,快速排序中基准元素的选取非常重要,影响着快速排序的效率,所以需要对其进行优化! 

QuickSort的优化(如何解决上述存在的问题?)

        为了尽可能提高快速排序的效果,可以采用以下几种方法选择基准元素:

  1. 随机选择:随机选择数组中的一个元素作为划分元素。这种方法可以减少最坏情况发生的概率,并且在平均情况下能够获得较好的效果。
  2. 中位数选择:选择待排序数组的中位数作为划分元素。这种方法可以尽可能地将数组均分为两部分,避免极度不平衡的划分。
  3. 三数取中:选择待排序数组的头部、中间和尾部的三个元素,取它们的中位数作为划分元素。这种方法是一种2折中的选择,既考虑了性能,又避免了极端情况的发生。

我们选择三数取中这种方法:选取a[left],a[mid],a[right]中的中位数。这个方案能够在很大程度上改善快速排序算法在最坏情况下的性能。

注意!mid只是索引的中位数,不代表a[mid]三个数中的中位数,所以需要判断大小,下面是选取中位数的实现代码:

int GetMidIndex(int* myarray, int left, int right)
{
	int mid = left + (right - left) / 2;

	// 数组的头部是中位数
	if ((myarray[mid] < myarray[left] && myarray[left] < myarray[right]) || (myarray[right] < myarray[left] && myarray[left] < myarray[mid]))
	{
		return left;
	}
	// 数组的中部是中位数
	else if ((myarray[left] < myarray[mid] && myarray[mid] < myarray[right]) || (myarray[right] < myarray[mid] && myarray[mid] < myarray[left]))
	{
		return mid;
	}
	// 数组的尾部是中位数
	else
	{
		return right;
	}
}

将区间按照基准值划分为左右两半部分进行分区的常见方式有: 

  • hoare法分区

选择一个基准元素(通常是数组的第一个元素),将其索引记为keyi。

使用两个指针left和right分别指向数组的左右边界。

从右边开始,找到第一个小于基准元素的值,将右指针right向左移动。

从左边开始,找到第一个大于基准元素的值,将左指针left向右移动。

如果左指针left小于右指针right,交换左右指针指向的元素。

继续执行上述步骤,直到左指针left大于等于右指针right。

最后将基准元素a[keyi]与左指针left指向的元素交换位置。

返回左指针left作为分区的分界点。

代码: 

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

// [left, right]
int Partition1(int* myarray, int left, int right)
{
	int mididx = GetMidIndex(myarray, left, right);
	Swap(&myarray[left], &myarray[mididx]);

	int keyi = left;
	while (left < right)
	{
		// 右指针从右往左找比key小的值
		while (left < right && myarray[right] >= myarray[keyi])
		{
			--right;
		}
		// 左指针从左往右找比key大的值
		while (left < right && myarray[left] <= myarray[keyi])
		{
			++left;
		}

		Swap(&myarray[left], &myarray[right]); 
	}

	Swap(&myarray[keyi], &myarray[left]);

	return left;
}
  • 挖坑法分区

选择一个基准元素(通常是数组的第一个元素),将其值保存在key中。

使用两个指针left和right分别指向数组的左右边界。

从右边开始,找到第一个小于基准元素的值,将右指针right向左移动。

将找到的小于基准元素的值赋给基准元素所在的空位(hole),并将hole更新为右指针right。

从左边开始,找到第一个大于基准元素的值,将左指针left向右移动。

将找到的大于基准元素的值赋给基准元素所在的空位(hole),并将hole更新为左指针left。

继续执行上述步骤,直到左指针left大于等于右指针right。

最后将基准元素的值放入最后一个空位hole中。

返回hole作为分区的分界点。

代码:  

int Partition2(int* myarray, int left, int right)
{
	int mididx = GetMidIndex(myarray, left, right);
	Swap(&myarray[left], &myarray[mididx]);

	int key = myarray[left];
	int hole = left;

	while (left < right)
	{
		while (left < right && myarray[right] >= key)
		{
			--right;
		}

		myarray[hole] = myarray[right];
		hole = right;

		while (left < right && myarray[left] <= key)
		{
			++left;
		}

		myarray[hole] = myarray[left];
		hole = left;
	}

	myarray[hole] = key;
	
	return hole;
}
  • 前后指针法分区

选择一个基准元素(通常是数组的第一个元素),将其索引记为keyi。

使用两个指针prev和cur分别指向数组的左边界和左边界的下一个位置。

从左到右遍历数组,如果当前指针cur指向的元素小于基准元素a[keyi],则将prev指针向后移动一位,并交换prev和cur指向的元素。

最后将基准元素a[keyi]与最后一个小于基准元素的元素a[prev]交换位置。

返回keyi作为分区的分界点。

这三个方法都是通过不同的指针移动方式和元素交换操作来实现数组的分区,从而将数组分成左右两个部分,

左边部分的元素都小于等于基准元素,右边部分的元素都大于等于基准元素。

这样,在快速排序算法中,可以通过递归调用这些分区方法来实现对整个数组的快速排序。

//1.最开始prev与cur相邻
//2.当cur遇到比key大的值后,它们之间的值都是比key大的值
//3.cur来找比key小的值,找到小的以后,prev+1,跟加1后的prev位置的值交换。(如果此时prev位置跟cur位置一样,可以交换也可以不交换)
//相当于把prev与cur之间比key大的值翻滚式的往右边推,同时把小的换到左边
int Partition3(int* myarray, int left, int right)
{
	int mididx = GetMidIndex(myarray, left, right);
	Swap(&myarray[left], &myarray[mididx]);

	int prev = left;
	int cur = left + 1;
	int keyi = left;

	while (cur <= right)
	{
		if (myarray[cur] < myarray[keyi] && ++prev != cur)
		{
			Swap(&myarray[prev], &myarray[cur]);
		}
		
		++cur;
	}

	Swap(&myarray[prev], &myarray[keyi]);
	keyi = prev;
	return keyi;
}

上面分区函数写完之后,就可以使用递归来排序了,三种方法使用哪一个都可以,下面代码使用的是第一种:

void QuickSort(int* myarray, int begin, int end)
{
	// 在快速排序算法中,当需要排序的子数组的长度为0或1时,已经是有序的,不需要再进行排序。
	// 因此,当begin大于等于end时,即子数组的长度为0或1,就可以终止递归,直接返回。
	if (begin >= end)
		return;
	int keyi = Partition1(myarray, begin, end);
	// [begin, keyi-1] keyi [keyi+1, end]

	QuickSort(myarray, begin, keyi - 1);
	QuickSort(myarray, keyi + 1, end);
}

快速排序算法效率分析

从代码和显示的结果可以看到,Partition是核心的分割函数, 这里以数组{16,1,45,23,99,2,18,67,42,10}为例进行排序,来看看是怎么样的排序过程呢?

用hoare法分区,以每一部分的首元素作为基准元素来演示:

  • 第一次调用Partition分割,元素16将整个数组分割成了两块,第一块包括元素10、1、2,第二块包括元素99、23、18、67、42、45。

  • 第二次调用Partition分割(第几次调用该函数如图中圆形编号所示),元素10将数组(这里的数组当然是上面已经分割开的子数组)分割出了第三块,第三块包含元素2、1。

  • 第三次调用Partition分割,元素2将数组分割出了第四块,第四块包含元素1。

  • 第四次调用Partition分割,元素99将数组分割出了第五块,第五块包含元素45、23、18、67、42。

  • 第五次调用Partition分割,元素45将数组分割出了第六块和第七块,第六块包含元素42、23、18,第七块包含元素67。

  • 第六次调动Partition分割,元素42将数组分割出了第八块,第八块包含元素18、23。

  • 第七次调用Partition分割,元素18将数组分割出了第九块,第九块包含元素23。

  • 至此,调用了七次Partition进行分割后,整个快速排序执行完毕。

第一次Partition调用会将数组中的n个元素,也就是从low到high之间的数据全部扫描一次,时间复杂度为O(n)。第二次、第三次……,调用Partition函数所需要扫描的数据会越来越少,都会<n,所以每次调用Partition的时间复杂度都不会超过O(n)。  

其实,通过统计Partition被调用的次数来求解快速排序算法的时间复杂度,与通过统计QuickSort递归函数的调用深度来求解快速排序算法的时间复杂度是一回事。  

所以,整个快速排序算法的时间复杂度为O(n*递归调用深度),这意味着快速排序算法的时间复杂度可以看成是和递归层数紧密相关。当然,快速排序算法的空间复杂度也和递归层数相关,因为每次递归都会用到栈空间来暂存很多信息。所以,快速排序算法的空间复杂度为O(递归调用深度)。  

因为每次递归调用QuickSort都会把当前需要处理的区间再次划分成左右两个子区间。所以图2换一种绘制方式其实可以变成一棵二叉树,如图3所示:  

        图3中,快速排序把数组中的n个元素组织成了一棵二叉树,二叉树的层数也就代表着递归调用的深度。所以 快速排序算法的递归调用深度问题就可以转换为对二叉树高度范围的判断。

(下面快速排序的效率分析是优化前,默认让每一个区域首元素当作基准元素的情况) 

        本专栏二叉树文章中曾经讲过,对于有n个节点的二叉树,它的最小高度是⌊log^{n}_{2}⌋ +1,最大高度是n(斜树)。所以,对于快速排序算法,最少的递归深度(递归层数)应该是⌊log^{n}_{2}⌋ +1,而最大的递归深度应该是n,才可以完成整个排序过程。

        所以,根据前面所说——整个快速排序算法的时间复杂度为O(n*递归调用深度)。不难看到,快速排序算法最好情况时间复杂度为O(nlog^{n}_{2}),最坏情况时间复杂度为O(n^{2}),平均情况时间复杂度为O(nlog^{n}_{2})。而因为快速排序算法的空间复杂度为O(递归调用深度),所以快速排序算法最好情况空间复杂度为O(log^{n}_{2}),最坏情况空间复杂度为O(n),平均情况空间复杂度为O(log^{n}_{2})。

        设想一下,如果每一趟快速排序选中的枢轴都能够将数组元素均匀的划分为两个部分,那么调用QuickSort递归的深度就会最小,算法效率就会达到最高。 但如果数组元素原本就是有序(顺序逆序都可以)的,比如数组元素是int arr[] = { 1,2,3,4,5,6,7,8,9,10 };,那么此时枢轴就完全无法将数组元素做均匀划分,此时递归调用的深度将达到9层,此时的算法效率会达到最低。

        换句话说,如果给定的数组原本就是有序的(顺序或者逆序),此时快速排序算法的性能最差。这也称为快速排序算法的退化,退化成了冒泡排序算法。


快速排序非递归法 (需要用到栈)

这里可以参考本专栏的有关栈实现的文章《深入理解栈》 ,当然下面这个代码是一种方法,高级语言C++,Java等都有官方库已经实现的栈,直接用即可。

// 快速排序的非递归方式

void QuickSortNonR(int* myarray, int begin, int end)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, end);
	StackPush(&st, begin);

	while (!StackEmpty(&st))
	{
		int left = StackTop(&st);
		StackPop(&st);

		int right = StackTop(&st);
		StackPop(&st);
		
		int keyi = Partition1(myarray, left, right);

		if (keyi + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st, keyi + 1);
		}

		if (left < keyi - 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, left);
		}
	}
	StackDestroy(&st);

}

快速排序方法之三路归并

// 快速排序方法之三路归并
void QuickSort3Ways(int* myarray, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	int left = begin;
	int right = end;
	int cur = left + 1;

	int mididx = GetMidIndex(myarray, left, right);
	Swap(&myarray[left], &myarray[mididx]);
	int key = myarray[left];

	while (cur <= right)
	{
		if (myarray[cur] < key)
		{
			Swap(&myarray[left], &myarray[cur]);
			++left;
			++cur;
		}
		else if (myarray[cur] > key)
		{
			Swap(&myarray[right], &myarray[cur]);
			--right;
		}
		else
		{
			++cur;
		}
	}

	// 小 l  r  大
	//[begin, left-1] [left, right][right+1, end]
	QuickSort3Ways(myarray, begin, left - 1);
	QuickSort3Ways(myarray, right+1, end);
}

myarray[c] < key :交换c和l位置的值,++l,++c

myarray[c] > key :交换c和r位置的值,--r

myarray[c] == key :++c

三路划分的本质:

1.小的甩到左边,大的甩到右边

2.跟key相等的值推到中间

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

生成器模式(软考uml C++版)

按照软考中级软件设计师中指定的生成器模式uml图&#xff0c;可编写对应的C&#xff0b;&#xff0b;代码&#xff1a; #include<iostream> #include<vector> #include<string> using namespace std;/*创建者模式&#xff0c;又名生成器模式意图&#xff1a…

每日五道java面试题之springMVC篇(四)

目录&#xff1a; 第一题. Spring MVC怎么样设定重定向和转发的&#xff1f;第二题.Spring MVC怎么和AJAX相互调用的&#xff1f;第三题. 如何解决POST请求中文乱码问题&#xff0c;GET的又如何处理呢&#xff1f;第四题. Spring MVC的异常处理&#xff1f;第五题. 如果在拦截请…

【JWT】入门 *JWT*,并封装一个实用的 *JWT* 工具类

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 【JWT】入门 *JWT*&#xff0c;并封装一个实用…

SQLiteC/C++接口详细介绍之sqlite3类(八)

返回目录&#xff1a;SQLite—免费开源数据库系列文章目录 上一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;七&#xff09; 下一篇&#xff1a; SQLiteC/C接口详细介绍之sqlite3类&#xff08;八&#xff09;&#xff08;暂未发表&#xff09; 24.sqlite3_cr…

网络安全msf学习1

工具&#xff1a;netcat 用途 &#xff1a;端口连接、数据提交 工具nmap 用途&#xff1a;端口扫描、服务识别、操作系统指纹识别 工具 httprint 用途&#xff1a;通过远程http指纹判断http服务类型 工具&#xff1a; tamper ie 用途&#xff1a; http数据包修改、转发工…

SpringMVC基础之工作流程

文章目录 SpringMVC 的工作流程1. 总图2. DispatcherServlet3. 必需的配置4. 加载配置文件的两个时机5. 定义控制器6. 创建 JSP 视图 SpringMVC 的工作流程 1. 总图 如上图&#xff0c;Spring MVC 程序的完整执行流程如下&#xff1a; 用户通过浏览器发送请求&#xff0c;请求…

怎样提升小程序日活?签到抽奖可行吗?

一、 日活运营策略 小程序应该是即用即走的&#xff0c;每个小程序都在用户中有自己的独特定位&#xff0c;可能是生活日常必备&#xff08;美食、团购、商城&#xff09;&#xff0c;也可能是工作办公必备&#xff08;文档、打卡、工具&#xff09;。 如果你想要让自己的小程…

sqllab第十九关通关笔记

知识点&#xff1a; 错误注入 最大长度为32位&#xff1b;如果目标长度>32时&#xff0c;需要利用截取函数进行分段读取referer注入 insert语句update语句 通过admin admin进行登录发现页面打印除了referer字段的信息 这应该是一个referer注入 首先进行测试一下 构造payl…

实现elasticsearch和数据库的数据同步

1. 数据同步 elasticsearch中的酒店数据来自于mysql数据库&#xff0c;因此mysql数据发生改变时&#xff0c;elasticsearch也必须跟着改变&#xff0c;这个就是elasticsearch与mysql之间的数据同步。 1.1. 思路分析 常见的数据同步方案有三种&#xff1a; 同步调用 异步通知…

在macOS上安装Homebrew教程

1.打开终端&#xff1a; 打开Finder&#xff0c;转到应用程序 > 实用工具文件夹&#xff0c;然后双击终端.app。 或者&#xff0c;使用Spotlight搜索&#xff08;按下 Command(⌘) Spacebar&#xff09;并输入“终端”&#xff0c;然后回车以打开。 也可以像我一样把终端…

【SQL Server】实验五 视图

1 实验目的 掌握SQL视图语句的基本使用方法&#xff0c;如CREATE VIEW、DROP VIEW。掌握视图更新、WITH CHECK OPTION等高级功能的使用。 2 实验内容 2.1 掌握SQL视图语句的基本使用方法 创建视图&#xff08;省略视图列名&#xff09;。创建视图&#xff08;不能省略列名的…

(三)丶RabbitMQ的四种类型交换机

前言&#xff1a;四大交换机工作原理及实战应用 1.交换机的概念 交换机可以理解成具有路由表的路由程序&#xff0c;仅此而已。每个消息都有一个称为路由键&#xff08;routing key&#xff09;的属性&#xff0c;就是一个简单的字符串。最新版本的RabbitMQ有四种交换机类型&a…

专业无网设备如何远程运维?向日葵远程控制能源场景案例解析

清洁能源领域&#xff0c;拥有庞大的上下游产业链&#xff0c;涉及的相关工业设备门类多、技术覆盖全、行业应用广。在这一领域内&#xff0c;相关专业设备的供应商的核心竞争力除了本身产品的技术能力之外&#xff0c;服务也是重要的一环。 某企业作为致力于节能环保方向的气…

XML语言的学习记录1

学习笔记&#xff1a; xml&#xff08;可扩展标记语言&#xff09;语言没有预定义的标签&#xff0c;都是使用者自定义&#xff1b;xml是纯文本&#xff0c;是不作为的&#xff1b;语法 每个标签必须有关闭标签&#xff1b;对大小写敏感&#xff1b;最外层必须有根元素&#x…

使用FFmpeg源码配置程序configure查看所有支持的编码器/解码器/封装/解封装及网络协议

查看支持编码器: configure --list-encoders 查看支持编码器: configure --list-decoders 查看所有支持的封装: configure --list-muxers 查看所有支持的解封装: configure --list-demuxers 查看所有支持的网络通信协议: configure --list-protocols

微服务学习day02 -- nacos配置管理 -- Feign远程调用 -- Gateway服务网关

0.学习目标 1.Nacos配置管理 Nacos除了可以做注册中心&#xff0c;同样可以做配置管理来使用。 1.1.统一配置管理 当微服务部署的实例越来越多&#xff0c;达到数十、数百时&#xff0c;逐个修改微服务配置就会让人抓狂&#xff0c;而且很容易出错。我们需要一种统一配置管理…

K8S CNI

OCI概念 OCI&#xff0c;Open Container Initiative&#xff0c;开放容器标准&#xff0c;是一个轻量级&#xff0c;开放的治理结构&#xff08;项目&#xff09;&#xff0c;在 Linux 基金会的支持下成立&#xff0c;致力于围绕容器格式和运行时创建开放的行业标准。 OCI 项目…

Unity中的网格创建和曲线变形

Unity中的网格创建和曲线变形 3D贝塞尔曲线变形贝塞尔曲线基础线性公式二次方公式三次方公式 Unity 实现3D贝塞尔曲线变形准备工作脚本概述变量定义 变量解析函数解析 获取所有子节点GetAllChildren 获取所有子节点UpdateBezierBend 控制点更新CalculateBezier Bezier 曲线公式…

JumpServer部署使用

1. 简介 JumpServer 是全球首款开源的堡垒机&#xff0c;使用 GNU GPL v3.0 开源协议&#xff0c;是符合 4A 规范的运维安全审计系统&#xff0c;使用 Python 开发&#xff0c;遵循 Web 2.0 规范&#xff0c;配备了业界领先的 Web Terminal 方案&#xff0c;交互界面美观、用户…

常见的实时操作系统(RTOS)(嵌入式和物联网操作系统)介绍

在嵌入式系统和物联网&#xff08;IoT&#xff09;设备中&#xff0c;实时操作系统&#xff08;RTOS&#xff09;是至关重要的&#xff0c;因为它们负责管理有限的硬件资源&#xff0c;并提供确保任务在特定时间内完成的机制。开源实时操作系统&#xff08;RTOS&#xff09;允许…