【数据结构】—— 内部排序算法详解

news2024/11/14 21:55:30

    • 1、前言
    • 2、常见排序算法
    • 3、排序算法实现
      • 3.1 直接插入排序
      • 3.2 希尔排序
      • 3.3 选择排序
      • 3.4 堆排序
      • 3.5 冒泡排序
      • 3.6 快速排序
        • 3.6.1 单趟排序
          • hoare法
          • 挖坑法
          • 双指针法
        • 3.6.2 非递归实现
        • 3.6.3 常见问题
          • 基准值的选取
          • 小区间优化
      • 3.7 归并排序
        • 3.7.1 递归实现
        • 3.7.2 非递归实现
      • 3.8 计数排序
    • 4、算法稳定性及复杂度分析

1、前言

使一串数据按照特定规则重新排列的过程,以便于后续的搜索、插入、删除等操作更高效地进行。
稳定性

稳定:当未排序时a在b前面且a=b,排序后a仍然在b前面
不稳定:当未排序时a在b前面且a=b,排序后a可能会出现在b后面

排序分为 内部排序外部排序

内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序

2、常见排序算法

在这里插入图片描述

3、排序算法实现

3.1 直接插入排序

遍历数组,每一位和比当前下标小的所有数比较大小,找到第一位比自己小(或者相等)的数后直接插入该数的后面,比自己大的数进行移位操作。(假设要求升序排序)
算法实现:简单的遍历加移位,具体看代码

//时间复杂度O(N^2)
//最坏情况:逆序
//最好情况:顺序或接近有序 O(N)
void InsertSort(int* a, int n)
{
	int length = n;
	for (int i = 0; i < length; i++)
	{
		int end = i - 1;//从i-1开始遍历(0~i-1已经有序)
		int tmp = a[end + 1];//保存插入的数
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];//移位
				end--;//下标后移
			}
			else break;
		}
		a[end + 1] = tmp;//插入第一个比自己小的数后面
	}
}

直接插入排序特性总结

  • 元素集合越接近有序,直接插入排序算法的时间效率越高
  • 时间复杂度;O(N^2)
  • 空间复杂度:O(1),它使一种稳定的排序算法
  • 稳定性:稳定

3.2 希尔排序

  • 希尔排序法(Shell Sort)又称缩小增量法。它将待排序的数组分割成若干个较小的子序列,对子序列分别进行插入排序,最终合并这些有序的子序列完成排序过程。
  • 如何分,分成多少组(如何选择合适的步长序列),分割间距对性能有什么影响?

选择合适的步长序列对希尔排序的性能有着显著的影响。这是因为步长序列决定了希尔排序在不同阶段的跳跃间隔,直接影响到排序过程中元素之间的比较和交换次数,从而影响整体的时间复杂度和排序效率。

常见的步长序列选择方法
1)希尔建议的步长序列

原始的希尔排序建议步长序列为 n/2, n/4, …, 1。这种序列保证了每次迭代的步长逐渐减半,直到最后一次步长为1。这种方法简单直接,通常能够提供不错的性能。

2)Hibbard步长序列
更好的增量序列选择是增量序列中的任何2个元素都是互素的

  • Hibbard序列的步长为 2k -1,其中k为正整数。这种步长序列的选择更加复杂,步长的增长速度比较快,能够快速减少序列的逆序对数目,从而提高排序效率。但这种序列可能会导致较多的移动和比较操作。
  • 使用 Hibbard 增量的希尔排序,其最坏情形的运行时间为 Θ(n3/2);
  • 其平均情形的运行时间被认为是 O(n5/4)(基于模拟结果)。

3)Sedgewick步长序列
Sedgewick序列是经验性的步长序列,通过一系列的增量(比如 1, 5, 19, 41, 109, …)来选择步长。这种序列在实际应用中表现良好,能够有效地减少排序时间。

实现思路

1,5,19,41,109,… 这个序列中的项交替地取自以下2个序列
1,19,109,505,2161,…,9∗(4k−2k)+1 (k=0,1,2,3,…)
5,41,209,929,3905,…,2k+2(2k+2−3)+1 (k=0,1,2,3,…)
使用 Hibbard 增量的希尔排序平均运行时间猜测为O(n7/6),最坏情形为O(n4/3)。

希尔排序的时间复杂度和增量h的选取(gap)有关。
在这里插入图片描述

算法实现:带步长序列的插入排序,不断插排直到gap=1


void ShellSort(int* a, int n)
{
	assert(a);
	int gap = n;
	while (gap > 1)
	{
		//gap /= 2;
		gap = gap / 3 + 1;
		//相比于简单地每次除以2,gap = gap / 3 + 1 的选择可以更快地将数组中的大元素向前移动,减少逆序对的数量,从而加快排序的速度。
		for(int i=0;i<n-gap;i++)
		{
			int end=i;
			int x=a[end+gap];
			//移位得到正确插入位置end+gap
			while(end>=0)
			{
				if(a[end]>a[end+gap])
				{
					a[end+gap]=a[end];//相当于移位操作
					end-=gap;
				}
				else break;//0~end已经排序好,直接break即可
			}
			//插入
			a[end+gap]=x;
	}
}

希尔排序特性总结

  • 希尔排序是对直接插入排序的优化。
  • 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
  • 希尔排序的时间复杂度由gap决定,实际应用中可以通过测试得出较为合适的步长序列。
  • 时间复杂度O(N1.25)~ O(1.6*N1.25)
  • 空间复杂度O(1)
  • 稳定性:不稳定。

3.3 选择排序

每次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后选出次小(或次大)的一个元素,存放在最大(最小)元素的下一个位置,重复这样的步骤直到全部待排序数据元素排完。

算法实现

void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;//和快排很类似哈
	while (begin < end)
	{
		int mini = begin, maxi = begin;//找最小值和最大值的下标
		for (size_t i = begin; i <= end; i++)
		{
			if (a[i] < a[mini]) mini = i;
			if (a[i] > a[maxi]) maxi = i;
		}
		Swap(&a[mini], &a[begin]);
		Swap(&a[maxi], &a[end]);
		if(maxi==begin) Swap(&a[maxi], &a[end]);//重叠问题,再交换一次就行
		++begin;
		--end;
	}
}

直接选择排序的特性总结

  • 直接选择排序思考非常好理解,但是效率不是很好(不论数组是否有序都会执行原步骤)。实际中很少使用
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

3.4 堆排序

利用堆积树(堆)这种数据结构所设计的一种排序算法,是选择排序的一种。通过堆来进行选择数据,需要注意的是排升序需建大堆,排降序建小堆

算法实现:建堆+向下调整

void AdjustDown(int* a, int size, int parent)
{
	int child = parent * 2 + 1;//左右孩子和parent比较大小决定是否down操作
	//第一种方式:递归
	if (a[child + 1] < a[child]&&child+1<size) child++;
	if (a[child] < a[parent]&&child<size)
	{
		Swap(&a[parent], &a[child]);
		AdjustDown(a,size, child);
	}
	//第二种方式:循环
	/*while (child < size)
	{
		if (child + 1 < size && 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--) AdjustDown(a, n, i);
	//堆排序(向下调整->O(N*logN)) (向上调整->O(N))
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

堆排序的特性总结

  • 堆排序使用堆来选数,效率就高了很多。
  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

3.5 冒泡排序

冒泡排序(Bubble Sort)是一种简单直观的排序算法,它重复地走访要排序的元素列,依次比较相邻的两个元素,如果顺序不对就交换它们。这个过程一直持续到没有再需要交换,也就是说该数组已经排序完成。
算法实现

//时间复杂度O(N^2)
//空间复杂度:O(1)
void BubbleSort(int* a, int n)
{
	bool f = false;
	for (int i = 0; i < n - 1; i++)
	{
		f = false;
		for (int j = 0; j < n - i - 1; j++)
			if (a[j + 1] < a[j])
			{
				Swap(&a[j + 1], &a[j]);
				f = true;
			}
		if (!f) break;//如果已经有序则无需继续遍历
	}
}

冒泡排序的特性总结

  • 冒泡排序是一种非常容易理解的排序
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定

3.6 快速排序

快速排序(Quick Sort)是一种经典的分治(Divide and Conquer)排序算法,其核心思想是通过将原始数组分割成较小的子数组来解决问题,然后递归地排序这些子数组。

  • 快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

实现步骤

1)选择基准值:从数组中选择一个基准值。通常情况下选择第一个元素、最后一个元素或者数组中间的元素作为基准值。

2)分割数组:重新排序数组,所有比基准值小的元素摆放在基准值前面,所有比基准值大的元素摆放在基准值后面。在这个分割结束之后,该基准值就处于数组的中间位置。这个称为分割(partition)操作。

3)递归排序子数组:递归地调用上述分割操作,对左右两个子数组分别进行排序。

4)合并结果:将左子数组、基准值和右子数组合并成最终的排序数组。

3.6.1 单趟排序
hoare法

取第一个元素作为基准(key)

int PartSort(int* a, int left, int right)
{
	int keyi = left;
	//找出最适合key插入的位置
	while (left < right)
	{
		//right先走,找小
		while (a[right] >=a[keyi]&&left<right) right--;

		//left再走,找大
		while (a[left] <= a[keyi]&&left<right) left++;

		Swap(&a[left], &a[right]);//交换,使得所有比a[keyi]小的元素在左区间,比a[keyi]大的元素在右区间
	}
	Swap(&a[keyi], &a[left]);//将序列头部作为基准的数换到最合适的位置

	return left;//返回left位置,方便获取a[left]
}

得到目标下标后可以进行分治操作

void QuickSort(int* a, int left, int right)
{
	if (left >= right)//递归结束条件
	{
		return;
	}
	int keyi = PartSort1(a, left, right);//找出key的下标,然后进行左右分组递归
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

以首元素为基准的hoare法的快速排序的时间复杂度是:O(N*log(N)),但是当面对有序数组的时候,快排的时间复杂度就会变成O(N^2)。

挖坑法

是hoare法的变形.
基本思路
(1)将待排序序列头部的元素存放到一个临时变量中作为key值,将key值原先的位置作为坑位

(2)从右往左寻找比key小的值,找到后将其填入坑中,并且其原来的位置成为新的坑

(3)从左往右寻找比key大的值,找到后将其填入坑中,并且其原来的位置成为新的坑

(4)重复步骤2~3直到待排序序列扫描完毕

(5)最后将key值填入坑中,返回key值下标

int PartSort(int* a, int left, int right) //单趟排序挖坑法
{
	int tmp = a[left];
	int hole = left;
	while (left < right)
	{
		while (left < right && a[right] >= tmp)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;
		while (left < right && a[left] <= tmp)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = tmp;
	return left;
}
双指针法

1)prev指针指向待排序序列开头,cur指针指向prev的下一个位置,将序列头部元素作为key值

2)cur向后走寻找比key小的值,如果找到则prev++,并交换元素

3)重复步骤2直到cur走到结尾,将key值与prev位置的值交换并返回key值此时的下标

int PartSort3(int* a, int left, int right)
{
	if(left>=right) return;
	int mid = GetmidIndex(a, left, right);
	swap(&a[mid], &a[left]);
	int cur = left + 1;
	int prev = left;
	int key = a[left];
	while (cur <= right)
	{
		if (a[cur] < key && ++prev != cur)//++prev != cur是为了防止prev与cur相等的时候还进行互换操作
			swap(&a[prev], &a[cur]);
			cur++;
	}
	swap(&a[prev], &a[left]);
	return prev;//此时在prev左边都是比a[prev]要小的数,右边都是比a[prev]要大的数
}

3.6.2 非递归实现

上述方法采用递归实现(深度太深容易栈溢出),这里提供一种用栈通过对待排序区间下标的压栈和出栈来实现快排的方法。

void QuickSortNonR(int* a, int left, int right) //非递归实现快排
{
	ST st;
	STInit(&st);
	STPush(&st, right);
	STPush(&st, left);

	while (!STEmpty(&st))
	{
		int begin = STTop(&st);
		STPop(&st);
		int end = STTop(&st);
		STPop(&st);

		//单趟排序,得出两个区间
		int cur = begin + 1;
		int prev = begin;
		int key = a[begin];
		int keyi = begin;
		while (cur <= end)
		{
			if (a[cur] < key && ++prev != cur)//++prev != cur是为了防止prev与cur相等的时候还进行互换操作
				Swap(&a[prev], &a[cur]);
			cur++;
		}
		Swap(&a[prev], &a[begin]);
		keyi = prev;
		//先进右后进左
		//QuickSort(a, prev + 1, end); 递归版本
		if (keyi + 1 < end)//非递归
		{
			STPush(&st, end);
			STPush(&st, prev + 1);
		}
		//QuickSort(a, begin, prev - 1); 递归版本
		if (keyi - 1 > begin)//非递归
		{
			STPush(&st, prev-1);
			STPush(&st, begin);
		}
	}
}

用栈实现快排的非递归。

3.6.3 常见问题
基准值的选取

虽然无论基准值选择数组当中的哪个元素都能完成排序工作,但是有些选择显然更优。下面会给出几类选法的分析。
1)不好的选取作法
通常的,这类选法是指将数组中第一个元素作为基准。如果输入是随机的,那么这是可以接受,并不会影响算法的性能。但是如果输入数据是预排序(升序)或是反序的,就会产生一个劣质的分割。

以升序为例

  • 如果数组已经是升序排列(预排序),而选择第一个元素作为基准值,快速排序的分割过程将导致基准值的左右两侧子数组极度不平衡。即基准值的右侧子数组为空,左侧子数组包含除基准值外的所有元素。这种极端情况下,快速排序的时间复杂度可能达到O(n^2),因为它需要进行大量的比较和交换操作
  • 影响:导致快速排序退化为最坏情况,性能下降到与插入排序相近的时间复杂度。

以降序为例

  • 类似于预排序情况,选择第一个元素作为基准值会导致快速排序的不平衡分割。基准值右侧的子数组会变得非常大,而左侧的子数组会很小。
  • 影响:同样可能导致时间复杂度达到O(n^2),性能严重下降。

为了避免快速排序在预排序或反序情况下的性能问题,下面给出一些较优的选取方法

2)随机选择基准值

优点:每次在数组中随机选择一个元素作为基准值。这种方法可以有效地减少出现最坏情况的概率,因为随机选择可以平均分配可能的数据分布情况,降低快速排序的平均时间复杂度。
缺点:与固定选择基准值的方法相比,随机选择基准值需要额外的步骤来生成随机数并选取相应的元素作为基准值。这在一定程度上增加了算法的实现复杂度和运行时间。虽然在大多数情况,这种增加的开销可以接受。

3)三数中值分割法

在选择基准值时,可以考虑数组的第一个元素、中间元素(N/2)和最后一个元素,并选择它们的中位数作为基准值。这种方法在一定程度上减少了最坏情况发生的概率(预排序和反序的坏情况),但需要更多的比较操作。(用大白话来说:取三个数的中位数的下标,交换数组首元素(或尾元素)和该中位数

使用这种方法可以减少快速排序约5%的运行时间

int GetMidIndex(int* a, int left, int right)//三数取中
{
	//第一种写法(个人认为不好写,容易出错)
/*
	int mid = (left + right) / 2;
	if (a[left] > a[mid])
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] > a[right])
			return right;
		else
			return left;
	}
	else
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left] < a[right])
			return right;
		else
			return left;
	}
	*/
	//第二种写法:直接列出三种情况即可(比较容易理解且好看)
	int mid = left + right >> 1;//取中间的数
	//比较得出中位数
	if (a[mid] <= a[left] && a[mid] >= a[right] || a[mid] >= a[left] && a[mid] <= a[right]) return mid;
	else if (a[left] <= a[mid] && a[left] >= a[right] || a[left] >= a[mid] && a[left] <= a[right]) return left;
	else return right;
}

具体的使用,我们只需要在单趟排序代码的开头加上这么一段就行:

int keyi = GetMidi(a,left,right);
Swap(&a[keyi], &a[left]);//交换left和mid的值,避免出现最小或最大(防止出现有序),避免了不必要的最坏情况

通过三数取中法,我们确保选择了一个较为中间的值作为基准,而将其置于序列的开头有助于后续的分割操作。

小区间优化

对于很小的数组(N<=20),快速排序的性能还不如插入排序好。

快速排序是一个递归算法,它每次递归调用都会消耗一定的栈空间(尽管现代编译器会优化尾递归)。对于非常小的数组,递归调用的开销可能会占据排序总时间的一个显著比例。(使用插入排序可以减少90%做优的递归)

void QuickSort(int* a, int left, int right) //小区间优化版快排:直接插入替代
{
	if (left >= right)
		return;
	if (right - left + 1 < 20)
	{
		InsertSort(a + left, right - left + 1);
		return;
	}
	int key = PartSort2(a, left, right);
	QuickSort(a, left, key - 1);
	QuickSort(a, key + 1, right);
}

3.7 归并排序

核心思想分治

  • 将原始的数组分解成较小的数组,直到每个小数组只有一个元素
  • 通过逐步合并这些小数组,最终得到一个有序的数组。
3.7.1 递归实现

算法实现

  • 分解过程
    归并排序使用递归来实现分解过程。假设我们有一个数组 a,要对其进行归并排序,过程如下:找到数组的中间点,将数组分成两半。 对左半部分和右半部分分别进行归并排序(递归调用)。 当每个子数组只包含一个元素时,递归停止。
  • 合并过程
    合并过程是归并排序的关键步骤,它将两个有序的子数组合并成一个更大的有序数组:创建一个临时数组 tmp,用来存放合并后的结果。 使用两个指针(或索引),分别指向要合并的两个子数组的开头。比较两个指针指向的元素,将较小(或较大)的元素放入 tmp 数组中。 移动指针,直到其中一个子数组的所有元素都被放入 tmp 中。将剩余的子数组的所有元素依次复制到 temp 中。
//子函数
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	//只有一个数返回
	if (begin == end) return;

	int mid = (begin + end) / 2;
	//分区间———— [begin,mid-1]  [mid,end] 
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid+1, end, tmp);

	//到这里分区间操作全部完成
	//从底部一步步归并,直到完成所有数据的排序
	//归并过程
	int i = begin;
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	//双指针算法描述
	//从两个区间的begin开始,两个指针不断比较,把小的数据插入到tmp,完成排序、
	//注意此时两个区间都有序
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2]) //加上=可以使归并排序稳定
			tmp[i++] = a[begin1++];
		else
			tmp[i++] = a[begin2++];
	}
	//没走完把剩下的走完
	while (begin1 <= end1) tmp[i++] = a[begin1++];
	while (begin2 <= end2) tmp[i++] = a[begin2++];

	//把tmp中排序好的区间传入a中
	memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

void MergeSort(int* a, int n)
{
	//我们不能每次递归都开辟一次空间,所以需要将主要的代码都放在子函数中,对子函数进行递归
	//开辟空间
	int* tmp = (int*)malloc(sizeof(int) *(n));

	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	_MergeSort(a, 0, n - 1, tmp);
	//销毁空间
	free(tmp);
	tmp = NULL;
}
3.7.2 非递归实现

和递归实现的区别是:要处理边界问题
基本思路

1)设定一个初始值为1的gap
2)通过gap来分割子序列。每次分割出两个相邻的子序列进行归并,归并好一组就将其覆盖到原序列中
3)重复步骤2直到第一轮归并结束
4)gap乘2,重复步骤2~3,直到gap超过原序列的大小
边界问题请见代码注释

//非递归实现归并排序
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * (n));

	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int gap = 1;

	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = begin1 + gap - 1;
			int begin2 = begin1 + gap, end2 = begin2 + gap - 1;


			//非递归容易越界(左区间或右区间超过n)
			//因此需要分类讨论
			//在所有越界情况中:只有end2越界才需要归并,其余情况的越界无需归并(已经排序好)
			if (end1 >= n || begin2 >= n) break;
			//如果end2越界,修改end2至数组边界n-1
			if (end2 >= n) end2 = n - 1;
			
			int j = i;
			//归并
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
					tmp[j++] = a[begin1++];
				else
					tmp[j++] = a[begin2++];
			}
			//没走完把剩下的走完
			while (begin1 <= end1) tmp[j++] = a[begin1++];
			while (begin2 <= end2) tmp[j++] = a[begin2++];

			//把tmp中排序好的区间传入a中
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
	}
}

上述的排序均为比较排序,下面给出几种非比较排序

3.8 计数排序

  • 计数排序是一个不基于比较的排序算法,又被称为鸽巢原理,是对哈希直接定址法的变形应用。

  • 其优势在于对一定范围内较集中的数据排序时,其时间复杂度低于任何一个基于比较的算法。但是空间需求较大。(空间换时间

//计数排序(适合小范围)
//时间复杂度:O(N+Range)
//空间复杂度:O(Range)
void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];
	for (int i = 1; i < n; i++)
	{
		if (a[i] > max) max = a[i];
		if (a[i] < min) min = a[i];
	}

	int range = max - min + 1;
	//申请空间
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc fail");
		return;
	}
	//初始化为0,用count来记某个数出现的次数
	//Q:为什么不能直接sizeof count?
	//count 是一个指针:count 是一个 int* 类型的指针,
	// sizeof(count) 返回的是指针的大小,通常是 4 或 8 字节(取决于平台和编译器)。
	memset(count, 0, sizeof(int)*range);

	//统计次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;//映射对应下标位置
	}

	//排序
	int j = 0;
	//0~range遍历,自动排序好了,只需要存入a数组即可
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)//出现相同的,依次放入数组
		{
			a[j++] = i + min;//i+min为原来数的大小
		}
	}
}

局限性

  • 范围限制:计数排序依赖于数据的范围。如果数据范围很大,则需要大量的内存来存储计数数组,导致空间浪费
  • 只适合整形:字符串(转化为整形可以排),浮点,结构体(实际应用多)都很少采用,因此实际应用较少。

4、算法稳定性及复杂度分析

在这里插入图片描述
在这里插入图片描述

稳定性:相同的值相对顺序是否改变。

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

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

相关文章

SAM2分割模型微调指南

SAM2&#xff08;Segment Anything 2&#xff09;是 Meta 推出的一款新模型&#xff0c;旨在对图像中的任何内容进行分割&#xff0c;而不局限于特定的类别或领域。该模型的独特之处在于其训练数据规模&#xff1a;1100 万张图像和 110 亿个掩码。这种广泛的训练使 SAM2 成为训…

AI大语言模型对消防工程知多少?

在过去2年的时间里&#xff0c;大语言模型受到前所未有的关注。ChatGPT的出现更是让人工智能对话风靡一时。我们不再把搜索引擎当作求解问题的唯一途径&#xff0c;AI聊天成为了当前最受欢迎的问题求助工具。 让ChatGPT用通俗的语言解释什么是ChatGPT 什么是大语言模型&#x…

SD微调 dreambooth Lora controlNet 持续更新中

微调&#xff1a;步骤 1 选择预训练模型 如ResNet VGG 2 准备新的数据集 3 构建模型 4 冻结部分模型 5 定义损失函数和优化器 6 微调模型 7 评估模型 8 微调的策略 https://www.zhangzhenhu.com/aigc/dreamBooth.html dreambooth (fix the object, then generate the same obje…

Android Media Framework(十五)ACodec - Ⅲ

这一篇我们一起来了解ACodec的Buffer分配流程。 1、initiateStart 首先对上一篇内容做一点补充&#xff0c;configureCodec执行完成后组件的状态没有变化&#xff0c;仍处在OMX_StateLoaded。因此&#xff0c;当我们调用initiateStart时&#xff0c;发出的消息将由ACodec::Loa…

SVN使用教程 - 快速上手

参考视频&#xff1a; SVN使用教程 - 快速上手 一、SVN简介 1、SVN的功能 &#xff08;1&#xff09;SVN是一种代码版本管理工具&#xff0c;它能记住程序员每次修改的内容&#xff0c;可以查看所有的历史修改记录&#xff0c;可以将代码恢复到任何历史版本&#xff0c;可以恢…

【Mysql】第十一章 事务-重点(原子性+持久性+隔离性+一致性)

文章目录 一、概念1.查看事务支持版本-show engines2.事务提交方式-show variables like autocommit3.事务常见操作方式1.将mysql的默认隔离级别设置成读未提交&#xff0c;方便看到实验现象2.需要重启终端&#xff0c;进行查看隔离级别3.创建一个银行用户表4.演示 - 证明事务的…

【Transformer】关于RNN以及transformer的相关介绍

文章目录 RNNTransformer是干什么的&#xff1f;什么是 Word Embedding &#xff1f;什么是 Word2vec &#xff1f;CBOW(Continuous Bag-of-Words Model)Skip-gram(Continuous Skip-gram Model)Word2vec 的优缺点 Transformer整体架构注意力机制self-attention&#xff08;自注…

生成式人工智能助力6G核心技术

崔曙光 加拿大皇家科学院 加拿大工程院双院院士 主要工作&#xff1a;适配改造人工智能算法&#xff0c;来满足通信网络性能 从基础LLM到专用LLM&#xff1a;四个必须面对的问题 如何选择合适的基础LLM规模如何让基础LLM读懂专用领域信息如何避免基础LLM的幻觉现象&#xf…

第9天 xxl-job

使用xxl-job需要建表 引入依赖 添加配置 Bean public XxlJobSpringExecutor xxlJobExecutor() {logger.info(">>>>>>>>>>> xxl-job config init.");XxlJobSpringExecutor xxlJobSpringExecutor new XxlJobSpringExecutor();xxlJo…

sql注入——sqlilabs16-26

文章目录 less-163.注入 less-172.数据库名2.1 floor报错注入数据库名 3.查到数据表3.1floor 报错注入数据表 4.查取列名4.1 floor报错注入 列名 5.查取内容 less-181.添加X-Forwarded-For测试2修改User-Agent测试3.查数据表名4.查数据列5.查取数据 less-192.查数据库3.查数据表…

医疗大健康解决方案HIS方案

本篇接上篇文章医疗大健康解决方案HIS方案-CSDN博客&#xff0c;介绍第二部分区域医疗解决方案。 依托腾讯云优势&#xff0c;联合合作伙伴&#xff0c;连接政府、医疗服务机构、医药研发与流通、康养等&#xff0c;构建医疗大健康产业云生态&#xff0c;助力数字化升级。 方…

小怡分享之数据结构基础知识准备

前言&#xff1a; &#x1f308;✨之前小怡给大家分享了JavaSE的知识&#xff0c;今天小怡要给大家分享一下数据结构基础知识。 一、初识集合框架 1.什么是集合框架 Java集合框架Java Collection Framework&#xff0c; 又称为容器container&#xff0c;是定义在Java.util 包…

Linux服务器基于NFS实现共享目录

NFS简介&#xff1a;NFS&#xff08;Network File System&#xff09;是一种分布式文件系统协议&#xff0c;允许用户通过网络访问远程计算机上的文件和目录&#xff0c;就像访问本地文件一样。NFS 最初由 Sun Microsystems 在 1984 年开发&#xff0c;现在已经成为类 Unix 系统…

SpringBoot企业人事管理系统-附源码与配套论文

1.1引言 随着计算机技术的飞速发展&#xff0c;计算机在各种单位机构管理中应用的普及﹐管理信息系统的开发在强调管理、强调信息的现代社会中也显得越来越重要。因此,利用计算机高效率地完成人事管理的日常事务&#xff0c;是适应现代各种单位机构制度要求、推动各种单位机构…

【项目】火灾烟雾检测管理系统。PyQT5+QT Designe+YOLOv8_ssod半监督算法+OpenCV

【项目】火灾烟雾检测管理系统。PyQT5QT DesigneYOLOv8_ssod半监督算法OpenCV 0.摘要1.引言2.烟雾检测算法2.0图像标注2.1 YOLOv8全监督算法结构2.2 Efficient-Teacher半监督算法结构 3.性能对比图4.源码、论文获取 0.摘要 火灾是常见而危险的自然灾害&#xff0c;不仅对人类生…

数值分析【3】

目录 第四章 插值 边角料&#xff1a; 分段二次插值——三个一插​编辑 三次样条插值 小结&#xff1a;等距看差分​编辑 第五章 最小二乘 第六章 数值积分 代数精度​编辑 第四章 插值 边角料&#xff1a; 分段二次插值——三个一插 三次样条插值 三次阳台函数是光滑…

Oracle一对多(一主多备)的DG环境如何进行switchover切换?

本文主要分享Oracle一对多(一主多备)的DG环境的switchover切换&#xff0c;如何进行主从切换&#xff0c;切换后怎么恢复正常同步&#xff1f; 1、环境说明 本文的环境为一主两备&#xff0c;数据库版本为11.2.0.4&#xff0c;主要信息如下&#xff1a; 数据库IPdb_unique_n…

落子“用户Happy”,vivo的“做活”与“长气”之道

有人说&#xff0c;中国手机行业&#xff0c;是名副其实的“Hard”模式。竞争焦灼&#xff0c;内卷不止。然而&#xff0c;这种主观的判断&#xff0c;也许从侧面反映出另一个客观事实&#xff1a;中国手机市场&#xff0c;凭借巨大的用户规模、多元化的消费倾向、自由展开的科…

从微软蓝屏事件聊到数据库系统中的纸牌屋

2024 年 7 月 19 日&#xff0c;全球约有 850 万台 Windows 电脑崩溃&#xff0c;无法重启&#xff0c;陷入蓝屏死机状态。这次故障影响了全球各地的企业和政府&#xff0c;波及运输、金融服务、医疗保健等绝大多数行业。 故障发生几小时后&#xff0c;蓝屏原因找到&#xff0…

Python 数组计算逻辑

a{1,2,3} b{2,3,4} 与 & 交集(取中) a&b{2, 3} 或 | 并集 (左中右) a&b{1,2,3,4} 差集 ^ 取左右 a^b {1,4} 减 - 取左 a - b {1} a-b {1}