经典七大比较排序算法 ·上

经典七大比较排序算法 ·上

  • 1 选择排序
    • 1.1 算法思想
    • 1.2 代码实现
    • 1.3 选择排序特性
  • 2 冒泡排序
    • 2.1 算法思想
    • 2.2 代码实现
    • 2.3 冒泡排序特性
  • 3 堆排序
    • 3.1 堆排序特性:
  • 4 快速排序
    • 4.1 算法思想
    • 4.2 代码实现
    • 4.3 快速排序特性
  • 5 归并排序
    • 5.1 算法思想
    • 5.2 代码实现
    • 5.3 归并排序特性

1 选择排序

1.1 算法思想

选择排序,每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
大致操作步骤如下:

  • 第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置;
  • 然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾;
  • 重复第二步操作,直到全部待排序的数据元素的个数为零。

选择排序视频演示

1.2 代码实现

选择排序还是很好理解的。但这里的实现会对直接选择排序做一些小的改进。
既然直接选择排序遍历一遍可以找出最小的数据,那遍历一遍同样可以找出最大的元素。这样每遍历一次,待排序数据元素的起始位置就变成最小的了,结束位置就变成最大的了。
参考代码如下:

//n - 数据个数
void SelectSort(int* a, int n)
{
	int begin = 0; //待排序数据起始位置下标
	int end = n - 1;//待排序数据结束位置下标
	int minIndex = 0;//记录最小数据的下标
	int maxIndex = 0;//记录最大数据的下标
	
	//待排序数据大于1时进入循环
	while (begin < end)
	{
		//假设一开始最小元素和最大元素下标都是第一个待排序数据下标
		minIndex = begin;
		maxIndex = begin;
		
		//遍历一遍
		for (int i = begin + 1; i <= end; ++i)
		{
			//选出最小元素下标
			if (a[i] < a[minIndex])
			{
				minIndex = i;
			}
			
			//选出最大元素下标
			if (a[i] > a[maxIndex])
			{
				maxIndex = i;
			}
		}
		
		//将最小元素和待排序序列的起始位置交换
		Swap(&a[begin], &a[minIndex]);
		
		//将最大元素和待排序序列的结束位置交换
		Swap(&a[end], &a[maxIndex]);
		
		//缩小待排序区间,重复上述过程
		++begin;
		--end;
	}
}

很多老铁在敲完上面代码之后,觉得已经没有问题了。
在这里插入图片描述
但实际上,像遇到上面情况。如果[maxIndex]指向的是[begin]中存放的内容,[minIndex]的内容先和[begin]交换之后,[maxIndex]指向的就不在是待排序序列中的最大元素了。需要做相应的修改。

		Swap(&a[begin], &a[minIndex]);
		
		//如果begin和maxIndex重叠,那么修正maxInedx
		if (begin == maxIndex)
		{
			maxIndex = minIndex;
		}
		Swap(&a[end], &a[maxIndex]);

1.3 选择排序特性

选择排序作为一种简单直观的排序算法,虽然好理解,但效率并不高。
无论是在面对什么样的排序序列,时间复杂度都是稳定的 O ( n 2 ) O(n^2) O(n2)量级,实际中很少使用。

2 冒泡排序

2.1 算法思想

冒泡排序作为一种交换排序,它会重复地走访要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到不再需要交换,也就是说该数列已经排序完成。
大致操作步骤如下:

  • 比较相邻的两个元素。如果第一个和第二个大元素的顺序不符合要求,就交换他们两个。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

冒泡排序视频演示

2.2 代码实现

//n - 数据个数
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; ++i)
	{
		for (int j = 0; j < n - 1 - i; ++j)
		{
			//排升序
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
			}
		}
	}
}

冒泡排序还有一种优化的方法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序,不需要再继续进行冒泡排序了。

void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; ++i)
	{
		int flag = 1;
		for (int j = 0; j < n - 1 - i; ++j)
		{
			if (a[j] > a[j + 1])
			{
				flag = 0;
				Swap(&a[j], &a[j + 1]);
			}
		}
		if (1 == flag)
		{
			break;
		}
	}
}

但这种改进优化在实际上对于提升性能来说并没有什么作用并不是很大。

2.3 冒泡排序特性

冒泡排序也是一种非常简单直观的排序。
在最好的情况下(序列接近有序),冒泡排序的时间复杂度是 O ( n ) O(n) O(n)量级的;
在最坏的情况下(序列逆序),冒泡排序的时间复杂度是 O ( n 2 ) O(n^2) O(n2)量级的。

3 堆排序

关于堆排序的算法思想代码实现相关内容,可以参考阿顺的堆结构的两个应用这篇博客,里面有比较详细的介绍。这里就不在赘述了。
请添加图片描述

3.1 堆排序特性:

堆排序使用堆的结构来选数,效率就高了很多。
堆排序的平均时间复杂度是 O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n)量级的。

4 快速排序

4.1 算法思想

快速排序是Hoare提出的一种二叉树结构的交换排序方法。
在这里插入图片描述
其基本思想为:
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两个子序列,其中左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后对左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
算法步骤:

  1. 从序列中挑出一个元素作为基准
  2. 排序数列,将所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆放在基准后面(相同的数可以在任一边)。在这个分区排完之后,该基准就处于数列的中间位置
  3. 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序

快速排序视频演示

4.2 代码实现

快速排序在发展的过程中涌现出了很多种不同的版本,但核心思想是不会变的。这里会介绍3种常见版本,以供大家参考。

//a - 待排序序列
//begin - 待排序序列起始位置
//end - 待排序序列结束位置
void QuickSort(int* a, int begin, int end)
{
	//序列只有一个值或没有值就不再排序
	if (begin >= end)
	{
		return;
	}
	
	//对区间进行快速排序,并返回排序后的基准值位置
	int keyIndex = QuickPartSort(a, begin, end);
	
	//[begin, keyIndex - 1] keyIndex [keyIndex + 1, end]
	//左区间排序
	QuickSort(a, begin, keyIndex - 1);
	//右区间排序
	QuickSort(a, keyIndex + 1, end);
	
}

上面代码就是快速排序实现的主框架,QuickPartSort函数有不同的实现方式。
其实从主框架上可以看出快速排序的递归实现和二叉树的前序遍历还是很像的。

  1. hoare版本
//以排升序为例
int QuickPartSort1(int* a, int begin, int end)
{
	int left = begin;
	int right = end;
	
	//取序列第1个数据做基准值
	int keyIndex = left;
	while (left < right)
	{
		//右边先走,找小
		while (left < right && a[right] >= a[keyIndex])
		{
			--right;
		}

		//左边再走,找大
		while (left < right && a[left] <= a[keyIndex])
		{
			++left;
		}
		
		//左大右小-交换
		Swap(&a[left], &a[right]);
	}
	
	//left和right相遇,此时将基准值和相遇值交换
	Swap(&a[keyIndex], &a[left]);
	//基准值下标跟着改变
	keyIndex = left;

	return keyIndex;
}

观察hoare版本的代码发现,选择的是最左边的值做基准值。此时,却是让右边的索引先走,那能不能让左边的值先走呢?又为什么选择右边的值先走呢?
答案是左边的值先走也可以,但是处理起来细节上更复杂一些。
选择右边先走的原因是为了保证相遇位置的值,比基准值小(或者就是基准位置的值)
右边先走的情况无非就是:

  • right先走,right遇到比基准值小的,right停下来,让left走,left去遇到了right
    这种情况相遇位置就是right停下来的位置,right停的位置也就是比基准值要小的位置。
  • right先走,right没有遇到比基准值要小的值,right去遇到了left
    这种情况下,相遇位置是left上一轮停下来的位置,该位置要么是基准值的位置,要么就是比基准值小的位置。
    这时,反过来想一下,如果选最左边为基准值,left先走的话,相遇位置的值就是大于基准值的,这样相遇位置的值就不能和基准值交换,这里需要做一些处理,来让基准值交换到合适的位置去,最终徒劳增加了麻烦。
    所以,一般建议是:
    左边做基准值,让右边先走;
    右边做基准值,让左边先走。
  1. 挖坑版本
    在面对上面hoare版本比较不好理解的左边做基准值,右边先走的问题时,挖坑法的版本能让快速排序更好理解一点点。
int QuickPartSort2(int* a, int begin, int end)
{
	//最左边做基准值
	int key = a[begin];
	//坑位给到基准值位置
	int pitIndex = begin;

	int left = begin;
	int right = end;
	
	while (left < right)
	{
		//右边找小,填到左边的坑里面去。这个位置形成新的坑。
		while (left < right && a[right] >= key)
		{
			--right;
		}
		a[pitIndex] = a[right];
		pitIndex = right;

		//左边找大,填到右边的坑里面去。这个位置形成新的坑。
		while (left < right && a[right] <= key)
		{
			++left;
		}
		a[pitIndex] = a[left];
		pitIndex = left;
	}
	//坑位再填上基准值
	a[pitIndex] = key;

	return pitIndex;
}

因为坑位的存在,左边有坑,右边就走,然后填坑;右边有坑,左边就走,然后填坑。这个过程就更好理解了。但是挖坑法在算法性能上相比hoare版本并没有什么大的改进。
3. 前后指针版本
前后指针的使用和前两个版本的排序方式就有较大的区别了。

int QuickPartSort3(int* a, int begin, int end)
{
	//基准值选择
	int keyIndex = begin;
	int prev = begin;//前指针
	int cur = begin + 1;//后指针

	while (cur <= end)
	{
		//cur位置的值小于keyIndex位置的值
		if (a[cur] < a[keyIndex])
		{
			++prev;
			Swap(&a[prev], &a[cur]);
		}

		++cur;
	}
	
	Swap(&a[prev], &a[keyIndex]);
	keyIndex = prev;
	return keyIndex;
}

前后指针方法中两个指针都是从左向右的方向在走,直到cur走完整个序列。
但是在走的过程中,prev的位置+1后,如果和cur位置相等,因为指向同一个位置,就不需要交换了,代码进一步优化如下:

if (a[cur] < a[keyIndex] && ++prev != cur)
{
	Swap(&a[prev], &a[cur]);
}

前后指针版本不仅好理解,而且代码也很简洁,是几个版本中最推荐的了。
但是,以上方法选基准值还是存在一定问题的。
鉴于以上几个版本都选择的是最左边作为基准值,如果是排已经有序的序列的话,如下图所示:
在这里插入图片描述
序列有多少的数据就递归多少次,很容易栈溢出。
所以,为了让每次选择基准值避免选择到序列中的最值,有的地方给出用随机数的方法选择基准值,但这里主要介绍三数取中选择基准值的方法。
所谓三数取中,不过是在序列的起止位置,中间位置,末位位置选择这三个数出来,通过比较,找到不是最大也不是最小的一个数,让这个数做基准值。

int GetMidIndex(int* a, int begin, int end)
{
	int mid = begin + (end - begin) / 2;

	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[begin] < a[end])
		{
			return end;
		}
		else
		{
			return begin;
		}
	}
	else//begin>=mid
	{
		if (a[mid] > a[end])
		{
			return mid;
		}
		else if (a[begin] < a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}

找到基准值之后,可以将基准值和序列的起始位置进行一个交换,这样就和之前排序的代码一样仍是选择序列的起始位置做基准值了。代码也不需要更多的修改了。

//三数取中优化
int mid = GetMidIndex(a, begin, end);
Swap(&a[keyIndex], &a[mid]);

因为三数取中的优化,使的每次递归左右子序列更加的二分,代码效率自然就得到了提高。
当然,我们知道递归需要是创建销毁和销毁栈帧的(具体参考阿顺的这篇博文你也能看懂的的函数栈帧创建与销毁)
因为快速排序的递归和二叉树前序递归很像,如果快速递归的划分接近二分的话,就会像满二叉树的遍历一样,最后一层的数量( 2 h − 1 2^{h-1} 2h1)会是整棵树数量( 2 h − 1 2^h-1 2h1)的一半,也就是快速排序最后一层的递归次数是整个过程递归次数的一半。在面对小数据量时,还要递归这么多次,进行栈帧的开销,确实会影响到程序的效率。
所以,可以对小区间进行优化:当递归划分小区间,区间比较小的时候,就不再递归划分去排序这个小区间。而是可以考虑直接用其它排序对小区间进行处理。
这里就给出对小区间元素小于10时的插入排序处理。

if (end - begin > 10)
{
	//int keyIndex = PartSort1(a, begin, end);
	//int keyIndex = PartSort2(a, begin, end);
	int keyIndex = QuickPartSort3(a, begin, end);

	QuickSort(a, begin, keyIndex - 1);
	QuickSort(a, keyIndex + 1, end);
}
else
{
	InsertSort(a + begin, end - begin + 1);
}

在这里插入图片描述
通过计算可以得出,以上代码在采用小区间优化后,函数调用次数可以减少 80 % 80\% 80%左右,这种提升还是很明显的。
以上都是对快速排序的递归版本的介绍。但是递归有一个死穴就是递归太深,栈溢出,程序会崩溃。所以,也要能够对快速排序进行非递归的实现。
可以借助栈数据结构或者队列数据结构进行非递归实现。这里就选择栈数据结构来进行实现。借助栈来实现的运行逻辑是可以和递归实现的递归逻辑达成一致的。
因为是C语言实现,所以对于栈数据结构的需要可以参考阿顺的这篇博文顺序表实现栈(C语言)

void QuickSortNonR(int* a, int begin, int end)
{
	Sk stack;
	StackInit(&stack);
	
	StackPush(&stack, end);
	StackPush(&stack, begin);

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

		int keyIndex = QuickPartSort3(a, left, right);
		// [left, keyIndex-1] keyIndex [keyIndex+1, right]

		if (keyIndex + 1 < right)
		{
			StackPush(&stack, right);
			StackPush(&stack, keyIndex + 1);
		}

		if (left < keyIndex - 1)
		{
			StackPush(&stack, keyIndex - 1);
			StackPush(&stack, left);
		}
	}

	StackDestroy(&stack);
}

在这里插入图片描述
代码结合画图就很容易理解快速排序的非递归了。

4.3 快速排序特性

快速排序,一听到这个名字你就知道它存在的意义l。而且快速排序整体的综合性能和使用场景都是比较好的,所以也才敢叫快速排序。
在这里插入图片描述
快速排序的一次划分算法从两头交替搜索,直到left和right重合,因此其时间复杂度是 O ( n ) O(n) O(n);而整个快速排序算法的时间复杂度与划分的趟数有关。
理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过 l o g 2 n log_2n log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为 O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n)
最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为 O ( n 2 ) O(n2) O(n2)
从空间性能上看,尽管快速排序只需要一个元素的辅助空间,但快速排序需要一个栈空间来实现递归。最好的情况下,即快速排序的每一趟排序都将元素序列均匀地分割成长度相近的两个子表,所需栈的最大深度为 l o g 2 ( n + 1 ) log_2{(n+1)} log2(n+1);但最坏的情况下,栈的最大深度为n。

5 归并排序

5.1 算法思想

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序。若将两个有序表合并成一个有序表,称为二路归并。
算法步骤:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别指向两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针到达序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。
    在这里插入图片描述

归并排序视频演示

5.2 代码实现

作为一种典型的分而治之思想的算法应用,归并排序的实现有两种方法:

  • 自上而下的递归;
//n - 数据个数
void MergeSort(int* a, int n)
{
	//开辟归并空间
	int* tmp = (int*)malloc(n * sizeof(int));
	assert(tmp != NULL);
	
	//归并排序
	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
}
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	//递归区间不足1个数据就不再递归
	//因为1个数据即有序
	if (begin >= end)
	{
		return;
	}
	
	int mid = begin + (end - begin) / 2;
	// [begin, mid][mid+1, end] 分治递归,让子区间有序

	//左区间递归
	_MergeSort(a, begin, mid, tmp);
	//又去见递归
	_MergeSort(a, mid+1, end, tmp);

	//归并过程
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int i = begin1;
	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++];
	}

	//把归并数据拷贝回原数组
	memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
  • 自下而上的迭代。
    在这里插入图片描述
    所谓自下而上的迭代,不过是最开始从1个和1个开始归并(因为1个数据即有序)。
    之后每次归并数据都扩2倍,直到最后一组的数据已经大于等于整个序列的数据为止就完成归并了。可以用循环来解决。
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(n * sizeof(int));
	assert(tmp != NULL);
	
	//最开始,一组只有一个数据
	int gap = 1;
	//一组的数据要小于序列长度
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			//[i,i+gap-1]和[i+gap, i+2*gap-1]区间的数据进行归并
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;

			int j = begin1;
			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++];
			}
		}
		//一趟归并结束进行拷贝
		memcpy(a, tmp, n * sizeof(int));
		
		//一组数据扩2倍
		gap *= 2;
	}
	
	free(tmp);
}

但是上面代码有一个致命的bug就是,只能适用于 2 n 2^n 2n个数据的归并。否则就存在越界的错误。
所以需要对归并区间进行界线的判断和修正。
因为begin1=i不会越界,而end1 begin2 end2都可能越界。
所以存在三种情况下的修正:

//修正边界
if (end1 >= n)
{
	end1 = n - 1;
	begin2 = n;
	end2 = n - 1;
}
else if (begin2 >= n)
{
	begin2 = n;
	end2 = n - 1;
}
else if (end2 >= n)
{
	end2 = n - 1;
}

上述是修正的一种方法。还可以采用更省事的策略:在遇到越界的区间时,就不再归并了。

if (end1 >= n || begin1 >= n)
{
	break;
}
else if (end2 >= n)
{
	end2 = n - 1;
}

但这种方法,需要每归并一次,就拷贝一次,而不是等到一趟归并完在拷贝。memcpy函数应该放在for循环之内。

5.3 归并排序特性

归并排序的时间复杂度是稳定的 O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n),而因为要开辟额外的归并数组,所以空间复杂度是 O ( n ) O(n) O(n)
归并排序速度仅次于快速排序,一般用于对总体无序,但是各子项相对有序的数列排序。
归并排序的比较次数小于快速排序的比较次数,移动次数一般多于快速排序的移动次数。
虽然归并排序比较占用内存,但却是一种效率高且稳定的算法。

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

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

相关文章

QT的使用3:鼠标事件

鼠标事件0 事件1 需求2 查看控件的事件处理函数3 UI设计4 新建一个类&#xff0c;继承QLabel5 对已有对象进行类型提升6 重写事件处理函数7 项目进一步拓展&#xff08;1&#xff09;获取鼠标按键&#xff08;2&#xff09;鼠标移动&#xff08;3&#xff09;显示多个按键&…

【数据结构】Java实现栈

目录 1. 概念 2. 栈的使用 3. 自己动手实现栈&#xff08;使用动态数组实现栈&#xff09; 1. 创建一个MyStack类 2. push入栈 3. pop出栈 4. 查看栈顶元素 5. 判断栈是否为空与获取栈长 6. toString方法 4. 整体实现 4.1 MyStack类 4.2 Test类 4.3 测试结果 1.…

计算机网络笔记——物理层

计算机网络笔记——物理层2. 物理层2.1 通信基础2.1.1 信号2.1.2 信源、信道及信宿2.1.3 速率、波特及码元2.1.4 带宽2.1.5 奈奎斯特定理采样定理奈奎斯特定理2.1.6 香农定理2.1.7 编码与调制调制数字信号调制为模拟信号模拟数据调制为模拟信号编码数字数据编码为数字信号模拟数…

C#中WPF实现依赖注入和MVVM,以及服务定位ServiceLocator

最近在想重写架构于是就研究了一套WPF的相关内容&#xff0c;WPF不像MAUI内置了容器&#xff0c;需要我们自己手动添加&#xff0c;于是就有了今天的内容。 首先&#xff0c;我们新建一个.net6.0的WPF项目 由于WPF没有内置容器,我们先安装一下依赖注入的nuget包 Microsoft.Ex…

网络技术与应用概论(上)——“计算机网络”

各位CSDN的uu们你们好呀&#xff0c;今天&#xff0c;小雅兰的内容依旧是计算机网络的一些知识点噢&#xff0c;下面&#xff0c;让我们进入计算机网络的世界吧 网络内涵 网络特征 网络定义 互联网发展过程 从ARPA网络到Internet 从低速互联网到高速互联网 从数据结构到统一网…

【C语言】通讯录的实现(静态版)

【C语言】通讯录的实现(静态版一.前言1.前期准备a.菜单实现b.联系人结构体的构建c.菜单选项的功能d.#define 的定义2.功能的实现a.初始化通讯录b.增加联系人c.显示通讯录d.查找联系人e.修改联系人d.删除联系人3. 总代码test.ccontact.ccontact.h一.前言 本文将会用c语言实现一…

Golang每日一练(leetDay0013)

目录 37. 解数独 Sudoku Solver &#x1f31f;&#x1f31f;&#x1f31f; 38. 外观数列 Count and Say &#x1f31f;&#x1f31f; 39. 组合总和 Combination Sum &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Py…

大数据技术之Hive

第1章Hive基本概念1.1 Hive1.1.1 Hive的产生背景在那一年的大数据开源社区&#xff0c;我们有了HDFS来存储海量数据、MapReduce来对海量数据进行分布式并行计算、Yarn来实现资源管理和作业调度。但是面对海量数据和负责的业务逻辑&#xff0c;开发人员要编写MR来对数据进行统计…

【Go】K8s 管理系统项目[Jenkins Pipeline K8s环境–应用部署]

K8s 管理系统项目[Jenkins Pipeline K8s环境–应用部署] 1. k8s-plantform-api-Pipeline 考虑到实际工作中前后端可能是不同的同学完成,一般Api部分完成后改动会比较小,web部分改动会比较频繁.于是将api和web分了2个pipeline实现 1.1 GIt仓库 docker目录存放镜像构建相关文件…

简介虚拟地址空间:保障进程间独立性的机制

我们知道&#xff0c;进程之间是相互独立的&#xff0c;在操作系统级别中&#xff0c;一个进程所执行的程序无法直接访问另一个进程所执行的内存区域&#xff08;即实现进程间通信比较困难&#xff09;&#xff1b;一个进程运行的失败也不会影响其它进程的运行。这使我们的操作…

vue编程方法

1&#xff0c;app.vue 其中的moundted只是被执行一次。 系统中所有的组件都放到app。vue文件中。放到根组件中的只是被执行一次的代码可以放到main.js中码&#xff1f; 不可以&#xff0c;因为main文件只是一个js文件不是一个组件。组件中的一些属性不能被使用。比如&#xff…

VS Code上搭建Vue开发环境超详细教程

这篇关于在Visual Studio Code上搭建vue开发环境的超详细教程手把手教会你! 首先在Visual Studio Code上搭建vue开发环境有几个步骤&#xff1a; 1、下载安装node.js 2、安装npm 3、安装cnpm 4、安装vue/cli脚手架 5、创建vue项目 6、运行vue项目 1.下载安装node.js 地址&…

鸟哥的Linux私房菜 正则表示法与文件格式化处理

第十一章、正则表示法与文件格式化处理 https://linux.vbird.org/linux_basic/centos7/0330regularex.php 简体版 http://cn.linux.vbird.org/linux_basic/0330regularex.php 11.2.2 grep的一些高级选项 例题一、搜索特定字符串 例题二、利用中括号 [] 来搜寻集合字符 例题四…

8个python自动化脚本提高打工人幸福感~比心~

人生苦短&#xff0c;我用Python 最近有许多打工人都找我说打工好难 每天都是执行许多重复的任务&#xff0c; 例如阅读新闻、发邮件、查看天气、打开书签、清理文件夹等等&#xff0c; 使用自动化脚本&#xff0c;就无需手动一次又一次地完成这些任务&#xff0c; 非常方便…

蓝桥杯嵌入式RTC实时时钟

文章目录 前言一、RTC是什么二、cubemx的配置三、函数的使用总结前言 本篇文章将给大家介绍RTC实时时钟。 一、RTC是什么 STM32的实时时钟RTC是一个独立的定时器,RTC时钟内部依靠BCD码计数。RTC实时时钟提高时钟、闹钟、日历功能。RTC功耗较低,可以使用在低功耗设备上。 …

Redis为什么选择单线程?Redis为什么这么快?

目录专栏导读一、Redis版本迭代二、Redis4.0之前为什么一直采用单线程&#xff1f;三、Redis6.0引入多线程四、Redis主线程和IO线程是如何完成请求的&#xff1f;1、服务端和客户端建立socket连接2、IO线程读取并解析请求3、主线程执行请求命令4、IO线程会写回socket和主线程清…

DM8:LINUX环境安装DM8数据库安装条件--GLIBC版本要求

DM8&#xff1a;LINUX环境安装DM8数据库安装条件--GLIBC版本要求环境介绍1 检查 GLIBC 版本号2 /tmp 临时目录空间要等于或大于2GB3 报错截图3.1 导入授权报错3.2 设置时区报错3.3 DmAPService启动失败3.4 初始化实例报错4 更多达梦数据库使用经验环境介绍 在LINUX环境安装达梦…

一线大厂软件测试常见面试题1500问,背完直接拿捏面试官,

三、测试理论 3.1 你们原来项目的测试流程是怎么样的? 我们的测试流程主要有三个阶段&#xff1a;需求了解分析、测试准备、测试执行。 1、需求了解分析阶段 我们的SE会把需求文档给我们自己先去了解一到两天这样&#xff0c;之后我们会有一个需求澄清会议&#xff0c; 我…

基于Springboot实现口腔牙诊所网站平台【源码+论文】

基于Springboot实现口腔牙诊所网站平台【源码论文】开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea M…

整合SpringCache

整合SpringCache 1、引入依赖cache还有redis <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId> </dependency>2、写配置 spring:cache:type: redis3、测试使用缓存 Cache…