【数据结构与算法篇】八种排序 (C++实现)

news2024/11/27 12:33:05

多种排序算法的Cpp实现

    • 一. 排序的概念及其运用
      • 排序的概念
    • 二. 一图速览常见排序
    • 三. 排序的C++实现
      • 1> 直接插入排序
      • 2> 希尔排序
        • 希尔排序代码实现(希尔所实现)
        • 希尔排序代码实现(优化版)
      • 3> 选择排序
        • 选择排序的代码实现(同时选出最大和最小的元素)
      • 4> 堆排序
        • 堆排序的代码实现
      • 5> 冒泡排序
      • 6> 快速排序
        • 快速排序(递归版)
        • 快速排序(迭代版)
      • 7> 归并排序
        • 归并排序(递归版)
        • 归并排序(迭代版)
      • 8> 基数排序

一. 排序的概念及其运用

排序的概念

  • 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,将其递增或递减的排列起来的操作。
  • 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变的程度被称为排序的稳定性。
    • 比如,在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
  • 内部排序:数据元素全部放在内存中的排序。(内存中读取数据, 排序的结果依旧可以写入到内存中)
  • 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。(内存中读取数据, 排序的结果最终写入到磁盘中)

二. 一图速览常见排序

在这里插入图片描述

三. 排序的C++实现

1> 直接插入排序

插入排序, 又叫直接插入排序, 简称插排。
基本思想 :

  • 一组待排序的元素, 我们要使其变得有序,例如升序。 此时我们认为前 i 个元素是有序的 (i从1开始), 要将第i+1个元素插入到这前i个元素中。
  • 具体的插入过程是, 将第 i+1个元素与前面的元素依次进行比较(从第i个元素开始), 直至找到某个元素小于 第i+1个元素(或者前i个元素均大于第i+1个元素),
  • 此时 , 我们找到该元素小于第i+1个元素, 那么我们将第i+1个元素插入到该元素的后面, 最终使得这共i+1个元素变得有序
  • 重复上述过程对数组中的所有元素依次进行排序, 最终使得该数组中的元素全部变得有序
// 直接插入排序 (升序排列)
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)          // n-1 防止 temp = a[end+1] 越界 , 也可以在数组中只有一个元素或是空数组时不进入循环
	{
		int end = i;
		int temp = a[end + 1];
		while (end >= 0)
		{
			if (temp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = temp;  
		// 代码执行到该位置有两种情况, (1) 前i个元素均 > 待排序元素, 此时待排序元素插入到数组首元素位置, 其他元素后移
		//                          (2) 待排序元素在前i个元素中找到了插入位置, 此位置不为 数组中首元素位置。 原位置的元素及其之后元素后移
	}   
}
时间复杂度: O(N^2)    // 逆序下最坏为0(N^2) , 有序时为O(N)
空间复杂度: O(1)      // 原数组中进行排序

2> 希尔排序

希尔排序是按其设计者希尔的名字命名的, 希尔排序又称为 缩小增量排序。
基本思想:

  • 对待排序序列进行多次预排序, 通过这多次预排序最终使得待排序序列极为接近有序, 从而降低排序算法的时间消耗(也就是降低了其时间复杂度)
    1. 设置一个int类型的变量, 假设该变量名为 grep (一般grep=n/2)。
    1. 将待排序系列中间隔为 grep的元素划分为同组元素
    1. 如此一来, 待排序元素也就被分为了grep组, 对于这grep组中的元素, 在同组元素之间进行直接插入排序, 使得其在同组中变得有序
    1. 不断缩小grep的值, 循环往复进行上述过程(一般 grep = grep/2)
    1. grep不断缩小的过程被称为缩小增量
    1. 最终grep = 1时, 待排序序列中的元素变得接近有序, 此时在对其进行直接插入排序(相当于对新序列进行直接插入排序), 最终使得整个序列变得有序

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的
    希尔排序的时间复杂度都不固定
希尔排序代码实现(希尔所实现)
void ShellSort(int* a, int n)
{
	int gap = n;      
	// 设置变量 gap 
	while(gap>1)
	{
		gap = gap / 2;    
		// 每次循环缩小增量
		for (int j = 0; j < gap; j++)   
		// 同组元素依次进行插排
		{
			for (int i = j; i < n - gap; i += gap)  
			// 某个元素进行一次完整的插入排序
			{
				int end = i;
				int temp = a[end + gap];
				while (end >= 0)     
				// while循环遍历 前 i个元素, 直至某个元素 小于 temp
				{
					if (temp < a[end])
					{
						a[end + gap] = a[end];  
						// 比 待排序元素大的元素向后移动gap位
						end -= grep;
					}
					else
					{
						break;
					}
				}
				a[end + gap] = temp;
				// 待排序元素插入到正确的位置
			}
		}
	}
}

时间复杂度: O(N^1.3)   //(这是平均时间复杂度)
空间复杂度: O(1)
希尔排序代码实现(优化版)
// 希尔排序代码优化
void ShellSort(int* a, int n)
{
	int gap = n;                               
	 // 是同组元素与元素之间的间隔
	while (gap > 1)  
	 //  这个while循环是控制预排序的次数 
	 // 如果while循环可以运行, 那么当grep = 1时, 一定会运行一次插入排序。 因为是先更新grep的值, 在进行希尔排序。                               
	{
		gap = gap / 2;                         
		// 将n个元素分成了grep组, 分组依据是所有间隔为 grep 的元素位于同一组
		//gap = gap / 3 + 1;
		//  也可以改变gap每次缩小的规模,从而减少排序次数, +1是为了最终使得gap =1 ; 因为 
		for (int i = 0; i < n - gap; i++) 
		// 这个for循环是控制同组中的元素依次进行插入排序, 不过是多组并排     
		// 采取多组并排的方式进行排序, 一共grep个组, 每次将靠前组中的一个元素排完后, 去排后面的组; 后面的组都排完后, 再次循环排
		// 前一个组中的下一个元素(按顺序排列)  (按组序, 按组中元素序列)
		{                                       
			int end = i;
			int temp = a[end + gap];
			while (end >= 0)
			{
				if (temp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = temp;
		}
	}
}

时间复杂度: O(N^1.3) //(这是平均时间复杂度)
空间复杂度: O(1)

3> 选择排序

选择排序顾名思义, 从待排序序列中选出指定元素, 最后将其放到指定位置
基本思路:

  • 遍历待排序序列, 从其中选出最大(或最小)的元素, 将其放置在数组中的最前面(降序)或者最后面(升序),
  • 第二次遍历待排序序列, 从其中选出次大(或次小)的元素, 将其放置在数组中的最前面 – 降序(升序)或者最后面 – 升序(降序),
  • 如此循环往复, 直至数组中的元素全部有序。
  • 也可以同时从数组中选出最大和最小的元素, 放在数组的两侧。
选择排序的代码实现(同时选出最大和最小的元素)
// 选择排序
void SelectSort(int* a, int n)
{ 
	int begin = 0, end = n - 1;
	while (begin <= end)
	{
		int min = begin, max = end;          
		// a[min]默认为数组中第一个元素, a[max]默认为数组中最后一个元素
		for (int i = begin; i <= end; i++)   
		// 因此 a[min] 需要和 [begin+1, end] 中的元素比较
		{                                    
		//      a[max]   需要和 [begin, end-1] 中的元素比较
			if (a[i] < a[min])               
			// 取两者并集, 因此 区间为[begin, end]
			{
				min = i;
			}
			if (a[i] > a[max])
			{
				max = i;
			}
		}
		Swap(&a[begin], &a[min]);
		if (max == begin)              
		// 如果 最大值a[max]恰好在索引为begin的位置,那么会将最大值交换至 索引为min的位置
		{
			max = min;
		}
		Swap(&a[end], &a[max]);
		++begin;
		--end;
	}
}

时间复杂度: O(N^2)    // 最坏和最好下均为O(N^2)
空间复杂度: O(1)      // 原数组中进行排序

4> 堆排序

数据结构 - 堆, 通过堆进行的排序被称为堆排序
基本思路:

  • 将待排序序列通过多次向下调整, 最终建立一个堆的结构
  • 按升序排序 :
    • 建大堆,
    • 堆中的父节点 >= 子节点
    • 每次排序, 将堆顶的元素(最大的元素)与堆末尾的元素交换位置,
    • (末尾所得末尾元素不计入)然后向下调整, 得到新堆
    • 如此循环往复最终使得待排序序列变为升序序列
  • 按降序排列:
    • 建小堆
    • 堆中的父节点 <= 子节点
    • 每次排序, 将堆顶的元素(最小的元素)与堆末尾的元素交换位置,
    • (末尾所得末尾元素不计入)然后向下调整, 得到新堆
    • 如此循环往复最终使得待排序序列变为降序序列
堆排序的代码实现
// 堆的向下调整
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])
		{
			++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) / 2; i >= 0; i--) 
	//  for循环建堆 
	//  建堆时间复杂度为 O(N)        
	{
		AdjustDown(a, n, i); 
		// 向下调整时间复杂度为O(logN)
	}

	int end = n - 1;
	while (end > 0)
	// while循环进行堆排序, 时间复杂度为 O(NlogN)
	{  // O(logN)
		Swap(&a[0], &a[end]);                        
		// 将堆顶元素换至堆末尾  , 在这里end代表堆中最后一个元素的索引
		--end;
		// 每次交换完, 进行--end, 原堆中最后一个元素不计入新堆
		AdjustDown(a, end + 1, 0);                      
		 // 向下调整排序 , 在这里end+1代表新堆中元素的个数
		 // 向下调整 时间复杂度为O(logN)
	}
}

时间复杂度: O(NlogN)     
// 使用该种方法 从最后一个父节点开始向下调整建堆的时间复杂度为O(3n) ,也就是O(N)
// 从根节点开始 向下调整建堆的时间复杂度为 O(NlogN)
空间复杂度: O(1)

5> 冒泡排序

冒泡排序的命名十分形象, 该种排序方式就像是 将待排序序列中的元素挑选出来, 使其像一个气泡从水中冒出水面一样冒到数组的最后面
基本思路:

  • 定义两个指针, 一前一后,使其指向相邻元素。 从数组首元素开始,对相邻元素进行比较
  • 每次比较依据的原则是 使得后指针始终指向较大的那个元素 – 升序(或较小)且不改变两个指针的相对位置
  • 做法便是, 对两个指针指向的元素进行比较, 如若前指针指向的元素小于后指针指向的元素, 那么同时使两个指针移动一个位置, 然后再次进行比较
  • 如果前指针指向的元素大于后指针指向的元素, 那么交换两个元素的位置, 使得后指针指向的位置存储的是较大的那个元素。
  • 当后指针指向空时循环结束,此时就将最大的那个元素冒到了数组的最后面
  • 如此循环往复, 最终使得待排序序列变为升序
// 冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		int count = 0;
		for (int j = 1; j < n - i; j++)
		{
			if (a[j - 1] > a[j])
			{
				Swap(&a[j - 1], &a[j]);
				count = 1;
			}
		}
		if (count == 0)       
		// 对于本就按照升序排列的序列, 冒泡排序外循环运行一次, 没有发生交换, 那么代表该序列有序, 直接结束冒泡排序 此时时间复杂度为最好: O(N)
			break;
	}
}

时间复杂度: O(N^2)   // 最坏: O(N^2) 数组有序时可为O(N)
空间复杂度: O(1)

6> 快速排序

快速排序在大多数情况下,其排序速度远快于其他排序算法, 因此叫做快速排序
基本思路:

  • 在待排序序列中, 选定一个元素作为 key值, (一般key值为数组中第一个元素)
  • 然后定义两个变量begin , end 分别指向数组的开头和结尾
  • 然后通过whille循环遍历整个数组, begin指针在指向小于等于key值的元素时就向后移动, end指针在指向大于等于key值的元素时就向前移动
  • 当begin指向的元素大于key值时, begin停止移动; 当end指向的元素小于key值时, end也停止移动, 当两个指针均停止移动时, 我们交换这两个元素的位置
  • 当 begin与end指针指向同一位置时, 循环结束
  • 当循环结束时, 我们将开始选定作为key值的那个元素与 begin指向的元素进行交换位置
  • 上述步骤的思想是 : 使得所有小于key值的元素都在 key值之前, 所有大于key值的元素都在key值之后 (其他等于key值的元素不改变其位置)
  • 经过上述步骤, 最终使得被选定为key值的这个元素放置在了未来有序序列的正确位置
  • 然后, 我们将这个待排序序列分为 排在key值前面的和排在key值后面的这两组
  • 我们在对这两组元素分别进行上述步骤
  • 最终待排序序列变为升序序列
快速排序(递归版)
// 快速排序的第一步优化 (三数取中)
int GetMidi(int* a, int left, int right)
{
	int midi = (left + right) / 2;
	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
			return midi;
		else if (a[left] < a[right])
			return right;
		else
			return left;
	}
	else
	{
		if (a[midi] > a[right])
			return midi;
		else if (a[left] > a[right])
			return right;
		else
			return left;
	}
}


// 快速排序之单趟排序的实现(Hoare版 -- 霍尔所编写)
int PartSort1(int* a, int left, int right)
{
	int midi = GetMidi(a, left, right); 
	                   
	 // 调用函数 GetMidi(), 返回三个索引所指向的元素中位于中间的那个元素的索引
	Swap(&a[midi], &a[left]);                            
	 // 将这个元素交换至数组首位置。 

	int keyi = left;                                      
	// keyi = left ,记录Left的初值,用于一趟排序完成后, 将数组首元素交换至正确位置
	while (left < right)
	{
		while (a[right] >= a[keyi] && left<right)        
		 // "<=" 避免 a[left] = a[right] = a[key] 带来的死循环
		 // && left < right, 避免极端情况, left(或right)不动, right(或left)动而出现的数组越界情况
		{
			--right;                                      
			
		}
		while (a[left] <= a[keyi] && left < right)        
		// 先 ++right, 最终保证 a[left] < a[keyi]  
		{
			++left;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);
	return left;
}



// 快速排序之单趟排序的实现(挖坑法)
int PartSort2(int* a, int left, int right)
{
	int midi = GetMidi(a, left, right);
	Swap(&a[midi], &a[left]);

	int key = a[left];

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

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


//  快速排序之单趟排序的实现(前后指针法)
int PartSort3(int* a, int left, int right)
{
	int midi = GetMidi(a, left, right);
	Swap(&a[midi], &a[left]);

	int keyi = left;

	int prev = left, cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != right)  
		 // 在这里不会出现死循环, 因此不需要等号
		 // 前置 ++ , 先++ 再使用prev的新值
		{                        
		
			Swap(&a[prev], &a[cur]);   
		}
		++cur;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}


// 完整的快速排序(升序 递归版)
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;

	if(end-begin+1 > 10)
	{
		//int keyi = PartSort1(a, begin, end);      // 快排 最初霍尔所写 
		
		//int keyi = PartSort2(a, begin, end);      // 快排 挖坑法 
		
		int keyi = PartSort3(a, begin, end);        // 前后指针法
		
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
	else      
	// 快速排序第二步优化 , 小区间元素进行直接采取插入排序
	{
		InsertSort(a + begin, end - begin + 1);
	}
}

时间复杂度: O(NlogN)     // 极端情况下, 快排为线性划分区间时为 时间复杂度为O(N^2)
空间复杂度: O(logN)      // 极端情况下, 快排为线性划分区间时为 空间复杂度为O(N)
快速排序(迭代版)

通过利用数据结构 - 栈, 控制快速排序的区间
迭代版待优化 :

  • 三数取中
  • 小区间直接插排的优化
void QuickSortNonR(int* a, int begin, int end)
//  一般begin = 0, end = n-1 , 这是为了使得begin, end分别指向数组的首尾元素 
{
	std::stack<int> stk;   
	//  STL的stack容器, 声明一个stack对象stk 
	stk.push(end);
	stk.push(begin);
	// 将用于控制快排第一趟的首元素位置和尾元素位置存入栈中
	while (!stk.empty())
	{
		int left = stk.top();
		stk.pop();

		int right = stk.top();
		stk.pop();

		int keyi = PartSort3(a, left, right);
		// 调用快排单趟排序的实现, 对区间[left, right]中的元素进行排序
		if (keyi + 1 < right)
		{
			stk.push(right);
			stk.push(keyi + 1);
			// 将右区间元素的首尾存入栈中
		}
		if (left < keyi - 1)
		{
			stk.push(keyi - 1);
			stk.push(begin);
			// 将左区间元素的首尾存入栈中
		}
	}
}

7> 归并排序

归并排序是对分治思想的一个典型运用 。 分治法 – 分而治之
基本思路:

  • 将待排序序列不断拆一分为二 (理想上是均等拆分)
  • 通过多次递归拆分, 最后所得序列中为 只含有一个元素的序列, 此时该序列一定为有序序列 (也就是 N 个有序序列),
  • 然后我们对已有序的序列进行两两归并(每相邻两个序列进行归并),
  • 归并之后得到新的有序序列(也就是 n/2个有序序列, 每个序列中含有两个元素)
  • 最后我们多次重复上述步骤,
  • 最后一次归并使得两个最大的有序子序列归并为完整有序序列
  • 此时 待排序序列变为有序
归并排序(递归版)
// 归并排序 -- 内层实现
void _MergeSort(int* a, int* temp, int begin, int end)
{
	if (begin >= end)                     
	 // 递归至 空数组或是数组中只存在一个元素
	{
		return;                           
		 // 函数返回
	}
	int midi = (begin + end) / 2;        
	  // 将数组分隔
	_MergeSort(a, temp, begin, midi);      
	// 递归调用函数_MergeSort() , 传入左区间的序列
	_MergeSort(a, temp, midi + 1, end);    
	// 递归调用函数_MergeSort() , 传入右区间的序列

	int begin1 = begin, end1 = midi;      
	 // 当递归函数开始返回时, 此时左右区间均为有序序列, 进行归并 (第一次归并是将 单个元素数组与单个元素数组(或空数组)进行归并)
	int begin2 = midi + 1, end2 = end;    
	int index = begin;                                 
	// 定义变量 分别确定左右区间 begin1, end1 确定左区间首尾元素的索引 变量index用于将元素归并插入动态数组 temp 时,可以有序的连续插入
	
	while (begin1 <= end1 && begin2 <= end2 )
	{    // 循环结束的条件是 其中一个数组遍历完, 也就是将其中一个区间数组中的元素完全插入temp指向的动态数组中
		if (a[begin1] < a[begin2])                       
		// 判断左右区间数组中首元素那个较小, 将较小的元素尾插入temp数组
		{
			temp[index++] = a[begin1++];
		}
		else
		{
			temp[index++] = a[begin2++];
		}
	}

	while (begin1 <= end1)                         
	// 判断两个区间数组中, 谁未被遍历完, 然后继续遍历元素并插入temp指向的数组中    
	{
		temp[index++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		temp[index++] = a[begin2++];
	}
	for (int i = begin; i <= end; i++)               
	// 将temp数组中存储的元素拷贝至a数组中指定位置中。
	// 将变量 index 定义的值为 begin时, 在最后一步的拷贝时, 可以起到巧妙的作用(无需定义多个变量)
	{
		a[i] = temp[i];
	}
}


// 完整的归并排序(递归版)
void MergeSort(int* a, int n)
{
	int* temp = new int[n];       
	// 在堆区new一个int类型的数组, 大小为 4n个字节, 可存储n个元素, 用于后续元素归并
	if (!temp)
	{
		perror("new is failed");
		exit(-1);
	}
	_MergeSort(a, temp, 0, n - 1);    
	// 调用实际归并排序函数
}

时间复杂度: O(NlogN)
空间复杂度: O(N)
归并排序(迭代版)
// 归并排序非递归
void MergeSortNonR(int* a, int n)
{
	int grep = 1;
	int* temp = new int[n];
	while (grep < n)
	{
		for (int i = 0; i < n; i += 2 * grep)
		{
			int begin1 = i, end1 = i + grep - 1;
			int begin2 = i + grep, end2 = i + 2 * grep - 1;

			int index = i;

			if (begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			while (begin1 <= end1 && begin2 <= end2)
			// 循环结束的条件是 其中一个数组遍历完, 也就是将其中一个区间数组中的元素完全插入temp指向的动态数组中
			{    
				if (a[begin1] <= a[begin2])     
				// 判断左右区间数组中首元素那个较小, 将较小的元素尾插入temp数组                                 
				{
					temp[index++] = a[begin1++];
				}
				else
				{
					temp[index++] = a[begin2++];
				}
			}

			while (begin1 <= end1)                         
			// 判断两个区间数组中, 谁未被遍历完, 然后继续遍历元素并插入temp指向的数组中    
			{
				temp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				temp[index++] = a[begin2++];
			}
			for (int j = i; j <= end2; j++)              
			// 将temp数组中存储的元素拷贝至a数组中指定位置中。
			{
				a[j] = temp[j];
			}
		}
		grep*=2;
	}
	delete[]temp;
}

时间复杂度: O(NlogN)
空间复杂度: O(N)

8> 基数排序

基数排序,又叫非比较排序。 是通过不同的关键字对待排序序列中的元素进行计数并排序
基本思路:

  • 通过哈希映射的方式将待排序序列的各个元素映射到数组的下标中
  • 然后计算每个元素出现的次数, 出现一次就对其所映射的指定位置进行+1
  • 不断地遍历整个数组, 最终映射所得数组记录了待排序序列中的不同元素的位置与相同元素的个数
  • 最后遍历映射数组, 在通过映射的方式将映射数组中的记录写回到原数组中
  • 最终, 待排序序列变为有序
// 计数排序
void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
			min = a[i];
		if (a[i] > max)
			max = a[i];
	}

	int range = max - min + 1;
	int* temp = new int[range] {0};

	for (int i = 0; i < n; i++)
	{
		int num = a[i] - min;
		temp[num]++;
	}

	int index = 0;

	for (int i = 0; i < range; i++)
	{
		while (temp[i])
		{
			a[index++] = i + min;
			temp[i]--;
		}
	}
	delete[] temp;
}

时间复杂度: O(N+range)
空间复杂度: O(range) 

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

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

相关文章

CSS面经(未完待续)

1. CSS选择器及其优先级 !important > 行内样式 > id选择器 > 类/伪类/属性选择器 > 标签/伪元素选择器 > 子/后台选择器 > *通配符 2. 重排和重绘是什么&#xff1f;浏览器的渲染机制是什么&#xff1f; 重排(回流)&#xff1a;当增加或删除dom节点&…

面试就是这么简单,offer拿到手软(三)—— 常见中间件框架面试题,es,redis,dubbo,zookeeper kafka 等

面试就是这么简单&#xff0c;offer拿到手软&#xff08;一&#xff09;—— 常见非技术问题回答思路 面试就是这么简单&#xff0c;offer拿到手软&#xff08;二&#xff09;—— 常见65道非技术面试问题 面试就是这么简单&#xff0c;offer拿到手软&#xff08;三&#xff…

DAPP开发【09】NFT交易市场开发(hardhat测试)

测试文件下新建market.js文件 扁平化&#xff0c;将所有依赖放在tmp.sol&#xff0c;可以去给他人使用 npx hardhat flatten > tmp.sol 测试文件 const {expect} require(chai); const {ethers} require(hardhat);describe(Market,async function(){//定义三个合约&a…

long转int类型转换问题

在业务代码中排序时需要根据日期排序&#xff0c;写了如下代码 sorted((o1, o2) -> {String str1 null;String str2 null;try {Field field getField(fieldMap, configBO.getCodeName());str1 String.valueOf(field.get(o1));str2 String.valueOf(field.get(o2));} ca…

kubectl获取命名空间下所有configmap集合的方法

前言&#xff1a; 获取单个configmap并忽略特定字段的操作可参照&#xff1a;kubectl获取ConfigMap导出YAML时如何忽略某些字段。 要获取命名空间下所有ConfigMap并忽略特定字段&#xff0c;你可以使用kubectl命令与例如yq这样的工具结合使用来忽略或删除不需要的字段。以下是…

分布式ID服务实践

背景 分布式场景下需要一个全局 ID 来标识唯一性&#xff0c;比如在单数据库时通过表唯一主键即可实现唯一 ID&#xff0c;分库分表时就需要全局唯一 ID。 业务对唯一 ID 的要求如下&#xff1a; 全局唯一性 不能出现重复的 ID 号&#xff0c;既然是唯一标识&#xff0c;这…

CGAL的周期三角剖分(相关信息较少)

CGAL的周期二维三角剖分类旨在表示二维平面上的一组点的三角剖分。该三角剖分形成其计算空间的分区。它是一个单纯复体&#xff0c;即它包含任何k-单纯形的所有关联j-单纯形&#xff08;j<k&#xff09;&#xff0c;并且两个k-单纯形要么不重叠&#xff0c;要么共享一个公共…

数据结构与算法-D2D3线性表之顺序表

线性表&#xff1a;包含若干数据元素的一个线性序列&#xff0c;特征如下&#xff1a; 1&#xff09;对非空表&#xff0c;a0是表头&#xff0c;无前驱&#xff1b; 2&#xff09;an-1是表尾&#xff0c;无后继&#xff1b; 3&#xff09;其他元素仅且仅有一个前驱&#xff0c;…

Day02 Liunx高级程序设计2-文件IO

系统调用 概念 是操作系统提供给用户使其可以操作内核提供服务的一组函数接口 用户态和内核态 其中 ring 0 权限最高&#xff0c;可以使用所有 CPU 指令&#xff0c; ring 3 权限最低&#xff0c;仅能使用 常规 CPU 指令&#xff0c;这个级别的权限不能使用访问硬件资…

解决 vite 中 import.meta.globEager is not function 的问题

本人正在重构两年前搭建到一半的博客网站&#xff0c;相关依赖都很陈旧&#xff0c;用到了 npm-check-updates 检测项目可升级依赖&#xff1a; 升级完成后解决完依赖发现控制台报错 import.meta.globEager is not function解决方案&#xff1a; vite版本降至4.3.0 亲测有效&…

Shell数组函数:数组(二)

关联数组 注意&#xff1a;先声明关联数组 一、定义关联数组 方法一 #一次赋一值 #数组名[索引]变量值 [rootlocalhost ~]# declare -A ass_array1 [rootlocalhost ~]# ass_array1[index1]pear [rootlocalhost ~]# ass_array1[index2]apple [rootlocalhost ~]# ass_array1[ind…

gpt阅读论文利器

1. txyz.ai 读论文 严伯钧 3. consensus 两亿科学论文的资源库. 用英文. 中国经济发展, 美国加州没有,减肥没有. 2. chrome插件 gpt sidebar 3. gpt academic 论文润色和学术翻译 ,一键输出公式. 英语口语8000句. 托福备考计划表. 百词斩托福. 薄荷外刊. 分区笔记精读法.…

Container容器技术简介

本文介绍了容器技术出现背景&#xff0c;docker技术与容器编排技术的简单说明 背景 在传统项目的生产环境中&#xff0c;迁移一个用户态进程往往非常麻烦&#xff0c;因为一个用户态进程背后会附带这非常多例如函数库、中间件等的依赖项&#xff0c;但又没有像apt和yum一样的…

广州华锐互动:钢铁厂烫伤事故3D模拟急救演练提高企业应对突发事故能力

随着工业化进程的加快&#xff0c;钢铁行业作为国家经济的重要支柱产业&#xff0c;其安全生产问题日益受到社会各界的关注。近年来&#xff0c;我国钢铁企业不断加大安全生产投入&#xff0c;但事故频发的现象仍然时有发生。为了提高钢铁企业应对突发事故的能力&#xff0c;确…

如何使用llm 制作多模态

首先将任何非字符的序列信息使用特殊n个token 编码。 具体编码方法以图像为例子说明&#xff1a; 将固定尺寸图像如256256 的图像分割为1616 的子图像块。 将已知的所有图像数据都分割后进行str将其看做是一个长的字符&#xff0c;而后去重后方式一个词表。 使用特殊1024 个tok…

推荐6款本周 火火火火 的开源项目

本周 GitHub项目圈选 节选自微博、知乎、掘金等社区。 &#x1f525;&#x1f525;&#x1f525;本周推荐的开源项目是&#xff1a; kopia 日常备份工具 screenshot-to-code 截屏生成代码 MiniSearch 全文搜索 clone-voice 声音克隆 NvChad 高颜值终端 DB-GPT-Hub 文本到…

关于 Kubernetes中Admission Controllers(准入控制器) 认知的一些笔记

写在前面 工作中遇到&#xff0c;简单整理记忆博文为官方文档整理涉及内置准入控制的分类理解理解不足小伙伴帮忙指正 人活着就是为了忍受摧残&#xff0c;一直到死&#xff0c;想明了这一点&#xff0c;一切事情都能泰然处之 —— 王小波《黄金时代》 为什么需要准入控制器 准…

怎样的软件测试工程师才算“大神“?“我“的测试之路...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 个优秀的软件测试…

6.Eclipse里下载Subclipse插件

方法一&#xff1a;从Eclipse Marketplace里面下载 具体操作&#xff1a;打开Eclipse --> Help --> Eclipse Marketplace --> 在Find中输入subclipse搜索 --> 找到subclipse点击install 方法二&#xff1a;从Install New Software里下载 具体操作&#xff1a;打开…

RPC基础

RPC基础知识 RPC 是什么? RPC&#xff08;Remote Procedure Call&#xff09; 即远程过程调用&#xff0c;通过名字我们就能看出 RPC 关注的是远程调用而非本地调用。 为什么要 RPC &#xff1f; 因为&#xff0c;两个不同的服务器上的服务提供的方法不在一个内存空间&…