【初阶数据结构】星河中的光影 “排” 象:排序(下)

gpt-image 2动漫海报提示词 手绘 2D 动画风格,超精细背景绘制,富有表现力的线条,浓郁的赛璐璐着色。底部是巨大优雅的中文书法标题,下方是简洁现代的英文副标题,标题上方是小字斜体标语,左下角标注出品方信息,右下角是上映日期。恋与深空风格,极其美丽的年轻女子,【黑色头发】,【紫罗兰色眼眸】,【传统汉服配飘逸袖子】,【忧郁的眼神与脆弱的表情】,【星系与星尘】,直视观众,风格化写实,电影级3D渲染,柔和电影光效,戏剧性轮廓光,浪漫氛围,电影海报质感,虚幻引擎5,光线追踪,Octane渲染器,8K杰作。請生成一張【古風】主題的人物畫面。 阅读详情

接上一篇,解决掉剩余的排序方法,本篇有些许难度,建议巩固好上一篇再来进行本篇的学习

传送门:【初阶数据结构】星河中的光影 “排” 象:排序(上)

4.交换排序

4.1 冒泡排序(BubbleSort)

基本思想: 所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动

冒泡排序在C语言部分进行过详细的解析,这里就不过多赘述

传送门:关于我、重生到500年前凭借C语言改变世界科技vlog.14——常见C语言算法

4.2 快速排序(QuickSort)

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

4.2.1 hoare版本

动图理解:

请添加图片描述

💻排序实现:

void QuickSort1(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left, end = right;

	//随机选key
	int randi = left + (rand() % (right - left + 1));
	Swap(&a[left], &a[randi]);

	//三数取中
	//int midi = GetMidNumi(a, left, right);
	//Swap(&a[midi], &a[left]);

	int keyi = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= a[keyi])
			--right;

		//左边找大
		while (left < right && a[left] <= a[keyi])
			++left;

		Swap(&a[left], &a[right]);
	}
		Swap(&a[keyi], &a[left]);
		keyi = left;

		QuickSort1(a, begin, keyi - 1);
		QuickSort1(a, keyi + 1, end);
}

⌨️代码解读:

  1. if (left >= right), 如果左边界大于等于右边界,说明子数组已经有序或为空,直接返回
  2. 随机选 key
  3. 当左边界小于右边界时,继续分区操作。右边找小,从右向左找到第一个小于基准元素的元素;左边找大,从左向右找到第一个大于基准元素的元素。交换左右找到的元素
  4. 最后将基准元素放到正确的位置,更新基准元素的索引
  5. 此时基准元素所放置的位置就是正确的排序位置,基准元素左右两边任然无序,所以对左右两边进行循环操作,每循环一次就确定一个数的位置

🔥值得注意的是:
选取 key 的方式有三种

  1. 选第一个数为 key
  2. 三数取中值
int GetMidNumi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[left] > a[right])
			return left;
		else if (a[mid] < a[right])
			return mid;
		else
			return right;
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[right] > a[left])
			return left;
		else
			return right;
	}
}
  1. 随机选 key,经过算法研究发现该选 key 方式是最有效的

4.2.2 挖坑法

动图理解:

请添加图片描述

💻排序实现:

void QuickSort2(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left, end = right;

	//随机选key
	int randi = left + (rand() % (right - left + 1));
	Swap(&a[left], &a[randi]);

	//三数取中
	//int midi = GetMidNumi(a, left, right);
	//Swap(&a[midi], &a[left]);

	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] >= key)
			--right;

		a[hole] = a[right];
		hole = right;

		//左边找大
		while (left < right && a[left] <= key)
			++left;

		a[hole] = a[left];
		hole = left;
	}

	a[hole] = key;

	QuickSort2(a, begin, hole - 1);
	QuickSort2(a, hole + 1, end);
}

⌨️代码解读:

  1. 选择左边界元素作为基准元素 key,并将其位置标记为 hole(坑)
  2. 从右向左扫描,找到第一个小于 key 的元素,将其填入 hole 位置,并更新 hole 为该元素的位置。
  3. 从左向右扫描,找到第一个大于 key 的元素,将其填入 hole 位置,并更新 hole 为该元素的位置。
  4. 不断重复上述两个步骤,直到 left 和 right 相遇。
  5. 最后将基准元素 key 填入最终的 hole 位置

🔥值得注意的是: 在从右向左查找小于 key 的元素时,right 指针会不断向左移动。如果不添加 left < right 这个条件,right 指针可能会一直向左移动,越过 left 指针,导致访问到数组范围之外的元素,从而引发越界错误。例如,当数组中的元素都大于等于 key 时,如果没有 left < right 的限制,right 指针会一直向左移动,最终可能访问到数组的负索引位置,这是不合法的。从左向右查找大于 key 的元素也是同理

4.2.3 前后指针法

动图理解:

请添加图片描述

💻排序实现:

void QuickSort3(int* a, int left, int right)
{
	if (left >= right)
		return;

	int randi = left + (rand() % (right - left + 1));
	Swap(&a[left], &a[randi]);

	//定义基准值以及cur和dest
	int keyi = left;
	int dest = left, cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++dest != cur)
		{
			Swap(&a[cur], &a[dest]);
		}
		//无论cur位置的值是否小于基准值,cur都要++,所以留到这里做调整
		cur++;
	}
	Swap(&a[keyi], &a[dest]);
	QuickSort3(a, left, dest - 1);
	QuickSort3(a, dest + 1, right);
}

⌨️代码解读:

  1. keyi 是基准元素的索引,初始化为左边界 left,dest 指针用于标记小于基准元素的元素应该放置的位置,初始化为左边界 left,cur 指针用于遍历数组,初始化为 left + 1
  2. 如果 cur 位置的值小于基准值,要和 dest 后面的元素进行交换,但是有可能 dest 后面就是 cur,所以我们可以让 dest 先 ++ ,再和 cur 比较,如果 ++dest == cur,说明它们暂时指向同一个元素,无需交换;如果 ++dest != cur,说明它们不指向同一个元素,直接交换

🖱️复杂度分析:

• 时间复杂度:

○ 最好情况: 当每次选择的基准元素都能将数组均匀地分成两部分时,递归树的深度为 log n ,每层需要处理的元素总数为 n,因此总的时间复杂度为 O(n * log n)

○ 最坏情况: 最坏情况下,快速排序的时间复杂度会退化为 O(n²)

○ 平均情况: 时间复杂度为 O(n * log n)

• 空间复杂度: 时间复杂度为 O(log n)

总结: 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

4.2.4 非递归实现

因为上面的三种方法均涉及递归,考虑到递归太多数据会导致栈溢出的风险,所以非递归实现快排也很重要

那么根据我们学过的栈发现其原理:先进后出,比较符合我们排序的效果

这里我们引入栈的头文件和源文件

传送门:【初阶数据结构】先来后到的秩序:栈和队列

💻排序实现:

int PartSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int randi = left + (rand() % (right - left + 1));
	Swap(&a[left], &a[randi]);

	int keyi = left;
	int dest = left, cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++dest != cur)
		{
			Swap(&a[cur], &a[dest]);
		}
		cur++;
	}
	Swap(&a[keyi], &a[dest]);
	return dest;
}

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 keyi = PartSort(a, begin, end);

		if (keyi + 1 < end)
		{
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}

	STDestroy(&st);
}

⌨️代码解读:

在这里插入图片描述

  1. 将初始的排序区间 [left, right] 压入栈中。注意,先压入右边界 right,再压入左边界 left,后续出栈时会先得到左边界,符合后续处理逻辑
  2. 从栈中弹出一个区间 [begin, end],先弹出的是左边界 begin,再弹出右边界 end
  3. 调用 PartSort 函数对当前区间 [begin, end] 进行分区操作,得到基准元素的最终位置 keyi
  4. 根据基准元素的位置 keyi,将左右子区间压入栈中。如果基准元素右边还有元素(keyi + 1 < end),则将右子区间 [keyi + 1, end] 压入栈;如果基准元素左边还有元素(begin < keyi - 1),则将左子区间 [begin, keyi - 1] 压入栈。这样后续会继续对这些子区间进行排序

🔥值得注意的是: 非递归的方式本质上就是借助栈来存区间,然后 PartSort(a, begin, end) 会对数进行交换排序,进行实质的交换

5.归并排序(MergeSort)

5.1 递归实现

动图理解:

请添加图片描述

在这里插入图片描述
假设有这么个区间,两个有序区间,就是不断对两个移动指针比较,把较小的数插入到新空间,形成新的有序序列,然后再赋值回原来的空间

💻排序实现:

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;

	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;

	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, 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);
}

⌨️代码解读:

  1. 计算中间索引 mid,将当前数组分成两个子数组,分别递归调用 _MergeSort 函数对左右子数组进行排序,从最下面的子数组依次往上归并
  2. 初始化两个指针 begin1 和 begin2 分别指向左右子数组的起始位置,end1 和 end2 指向左右子数组的结束位置
  3. 比较 a[begin1] 和 a[begin2] 的大小,将较小的元素放入临时数组 tmp 中,并将相应的指针后移
  4. 当其中一个子数组遍历完后,将另一个子数组中剩余的元素依次放入临时数组 tmp 中
  5. 使用 memcpy 函数将临时数组 tmp 中排好序的元素复制回原数组 a 中

🔥值得注意的是: memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1)),而不是 memcpy(a, tmp, sizeof(int) * (end - begin + 1)),为了在临时数组中准确找到当前子数组合并结果的起始位置,以便将排好序的数据正确地复制回原数组

5.2 非递归实现

同理,为了避免栈溢出,归并排序也有非递归实现

为什么不用栈处理?

栈的非递归实现其实是类似于前序遍历的,而归并的非递归实现类似于后序遍历

💻排序实现:

5.2.1 一次性全部拷贝回去:memcpy(a, tmp, sizeof(int) * n)

void MergeSortNonR1(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 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			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;
			}

			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++];
			}

			memcpy(a, tmp, sizeof(int) * n);
		}
		gap *= 2;
	}
	free(tmp);
}

⌨️代码解读:
在这里插入图片描述
gap 表示一组有多少个数据,gap = 1 时一个一个归并,gap = 2 时两个两个归并,依次向后推,不断扩大 gap 实现有序

根据规律写出 int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;

但是我们发现这套规律只适用于偶数个数的归并,当为奇数时会出现`多出来一个数无法进行归并的情况,所以我们要针对这种情况进行修正

  1. end1 越界示例
    假设我们有一个长度 n = 5 的数组 arr = [1, 2, 3, 4, 5],当 gap = 4 时,开始进行合并操作

在这里插入图片描述

计算过程:

当 i = 0 时:begin1 = i = 0,end1 = i + gap - 1 = 0 + 4 - 1 = 3,这是正常的,因为数组索引 3 在有效范围内(数组索引范围是 0 到 4)

当 i = 4 时:begin1 = i = 4,end1 = i + gap - 1 = 4 + 4 - 1 = 7,而数组的最大有效索引是 4,此时 end1 超出了数组的边界,出现越界情况

修正方法: end1 = n - 1,begin2 = n,end2 = n - 1,因为是整体赋值,让有效的数据按原路返回,后面的修正为一个不存在的区间,也就不会归并了,那么就有个问题,后面的数不就无序了吗?其实前面的排序保证了其有序性,在 gap 进一步扩大的过程中会将其逐步变为有序

  1. begin2 越界示例
    同样使用长度 n = 5 的数组 arr = [1, 2, 3, 4, 5],当 gap = 3 时进行分析

在这里插入图片描述

计算过程:

当 i = 2 时:begin1 = i = 2,end1 = i + gap - 1 = 2 + 3 - 1 = 4,这是正常的,begin2 = i + gap = 2 + 3 = 5,数组的最大有效索引是 4,所以 begin2 超出了数组的边界,出现越界情况

修正方法: 同 end1 越界处理情况相同

  1. end2 越界示例

还是以长度 n = 5 的数组 arr = [1, 2, 3, 4, 5] 为例,当 gap = 3 时进行分析

在这里插入图片描述

计算过程:

当 i = 0 时:begin1 = i = 0,end1 = i + gap - 1 = 0 + 3 - 1 = 2,begin2 = i + gap = 0 + 3 = 3,end2 = i + 2 * gap - 1 = 0 + 2 * 3 - 1 = 5,数组的最大有效索引是 4,所以 end2 超出了数组的边界,出现越界情况

修正方法: end2 = n - 1,将有效数据纳入进行排序,剩余的数在 gap 扩大时会得到处理

5.2.2 分批次拷贝回去:memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1))

void MergeSortNonR2(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 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			else 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++];
			}

			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
	}
	free(tmp);
}

⌨️代码解读:

一次性拷贝回去需要调整 end1、begin1 等边界:

一次性拷贝回去意味着我们要把合并好的结果一次性从临时数组 tmp 复制回原数组 a。在这个过程中,如果出现越界情况,比如 end1 或 end2 超出了数组长度,我们需要调整这些边界,以确保只处理有效的数组元素,避免访问非法内存

分批次拷贝回去直接 break:

分批次拷贝回去通常是指在发现越界情况时,由于后续没有有效的子数组可供合并,直接终止当前的合并操作,等待下一轮更大的 gap 再处理剩余元素。这种方式简化了越界处理逻辑,因为不需要对越界的边界进行复杂的调整

end1、begin2 越界: break,等待 gap 增大时处理未处理的数据

end2 越界: 和一次性拷贝的处理相同

🔥值得注意的是: memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1)) 不能写成 memcpy(a + begin1, tmp + begin1, sizeof(int) * (end2 - begin1 + 1)),因为 begin1 被移动了,i 才是原来的初始位置

6.排序复杂度及效率对比

请添加图片描述

💻测试代码:

void TestOP()
{
	srand(time(NULL));
	const int N = 100000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);
	int* a7 = (int*)malloc(sizeof(int) * N);
	int* a8 = (int*)malloc(sizeof(int) * N);
	int* a9 = (int*)malloc(sizeof(int) * N);
	int* a10 = (int*)malloc(sizeof(int) * N);
	int* a11 = (int*)malloc(sizeof(int) * N);
	int* a12 = (int*)malloc(sizeof(int) * N);

	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		a5[i] = a1[i];
		a6[i] = a1[i];
		a7[i] = a1[i];
		a8[i] = a1[i];
		a9[i] = a1[i];
		a10[i] = a1[i];
		a11[i] = a1[i];
		a12[i] = a1[i];
	}

	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();

	int begin3 = clock();
	SelectSort(a3, N);
	int end3 = clock();

	int begin4 = clock();
	HeapSort(a4, N);
	int end4 = clock();

	int begin5 = clock();
	QuickSort1(a5, 0, N - 1);
	int end5 = clock();

	int begin8 = clock();
	QuickSort2(a8, 0, N - 1);
	int end8 = clock();

	int begin9 = clock();
	QuickSort3(a9, 0, N - 1);
	int end9 = clock();

	int begin10 = clock();
	QuickSortNonR(a9, 0, N - 1);
	int end10 = clock();

	int begin6 = clock();
	MergeSort(a6, N);
	int end6 = clock();

	int begin11 = clock();
	MergeSortNonR1(a11, N);
	int end11 = clock();

	int begin12 = clock();
	MergeSortNonR1(a12, N);
	int end12 = clock();

	int begin7 = clock();
	BubbleSort(a7, N);
	int end7 = clock();

	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("HeapSort:%d\n", end4 - begin4);
	printf("BubbleSort:%d\n", end7 - begin7);
	printf("QuickSort1:%d\n", end5 - begin5);
	printf("QuickSort2:%d\n", end8 - begin8);
	printf("QuickSort3:%d\n", end9 - begin9);
	printf("QuickSortNonR:%d\n", end10 - begin10);
	printf("MergeSort:%d\n", end6 - begin6);	
	printf("MergeSortNonR1:%d\n", end11 - begin11);
	printf("MergeSortNonR2:%d\n", end12 - begin12);


	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
	free(a8);
	free(a9);
	free(a10);
	free(a11);
	free(a12);
}

⌨️测试结果:

在这里插入图片描述


希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

请添加图片描述

健康保险费用预测:可解释机器学习在精算建模中的实战落地 健康保险费用预测是医疗AI与保险科技交叉领域的核心任务,本质是基于临床变量与社会因素对个体医疗支出进行风险量化。其技术原理需兼顾统计建模能力与监管可解释性,XGBoost+SHAP组合因支持特征归因、边际效应分析和临床可干预推演,成为保险精算场景的主流选择。该技术显著提升保费定价精度、降低偿付风险,并支撑DRG费用预警、团险核保辅助及再保险尾部风险评估等关键应用。本文聚焦真实产线中的时间序列验证、临床分层特征工程与监管红线指标(如Bias@90th、Calibration Slope),深入解析如何让模型不 阅读详情

相关推荐

TCP报文 Flood攻击原理与防御方式

TCP交互过程中包含SYN、SYN-ACK、ACK、FIN和RST报文,这几类报文也可能会被攻击者利用,海量的攻击报文会导致被攻击目标系统资源耗尽、网络拥塞,无法正常提供服务。接下来我们介绍几种常见的Flood攻击的原理和防御方式。

qq_32044265的博客 4219

【初阶数据结构】星河中的光影 “排” 象:排序(上)

本章节是初阶数据结构最后一个部分,排序在数据中的应用是特备常见的,一般在企业中的排序数据量成万上亿,所以选取一种合适且效率高的排序尤为重要

Zero_VPN的博客 2586

山东大学机器学习(实验一内容)——线性回归

1. 描述 第一个练习将为你提供线性回归练习。这些练习已经用Matlab进行了广泛的测试,但它们也应该在Octave中工作,它被称为“免费版的Matlab”。如果你使用的是Octave,那就是 一定要安装Image包(可在Windows中作为选项使用)安装程序,可从Octave-Forge获得Linux。 2.线性回归 回想一下,线性回归模型是 hθ(x)=θTx=∑j=0nθjxj h_{\th...

HachiLin的博客 4108

Lingyuxiu MXJ LoRA实战落地:为非遗传承人生成传统服饰数字化人像档案

本文介绍了如何在星图GPU平台上自动化部署🎨 Lingyuxiu MXJ LoRA创作引擎镜像,高效生成非遗传承人的传统服饰数字化人像档案。该镜像专为真实手艺人定制,支持基于单张参考照批量产出多场景、高保真、可验证的4K人像,广泛应用于非遗数字存档与文化保护。

weixin_42361478的博客 307

ROS入门(一)——ROS安装(vmware16+ubuntu20.04+ROS-Noetic)

嵌套:ROS可以在Ubuntu上运行,Ubuntu在虚拟机Vmware中安装。 从 虚拟机vmware安装→ubuntu安装→ros安装,逐步介绍安装ROS环境的过程。整个过程是我一步步跟着各种博客安装实现的,亲测可行。 版本: Vmware :Vmware Workstation16 Ubuntu :20.04 ROS :Noetic 其中vmware直接载当前最新版本就好,而ubuntu的版本要注意一下。 在ROS官网(cn - ROS Wiki),我们进入到ROS安装的步骤界面,可以看..

Netceor的博客 9740

《测控仪器设计课后习题答案-浦昭邦-王宝光

《测控仪器设计课后习题答案-浦昭邦-王宝光 1.1. 测控仪器的概念是什么? 测控仪器则是利用测量和控制的理论,采用机、电、光各种计量测试原理及控制系统与计算机相结合的一种范围广泛的测量仪器。 1.2. 为什么说测控仪器的发展与科学技术发展密切相关? 仪器仪表的用途和重要性— 遍及国民经济各个部门,深入到人民生活的各个角落,仪器仪表中的计量测试仪器与控制仪器统称为测控仪器,可以说测控仪器的水平是科学技术现代化的重要标志。 仪器仪表的用途: 在机械制造业中:对产品的静态与动态性能测试;加工过程的控制与监测;设

无名何许人的博客 2976

从组件巧匠到智能画布:玩转DevUI与MateChat的奇幻漂流

想象一下,你为公司的业务流打造了一个独一无二的“步骤导航器”,它将复杂的流程可视化,极大地提升了用户体验。一个真实的复盘案例:某团队使用DevUI的导航、数据和反馈组件,在短短两个月内,就重构了一个老旧的内部运维系统,新系统不仅界面现代,开发效率更是提升了40%以上。所以,亲爱的开发者、产品设计师和梦想家们,拿起DevUI这把“组件巧匠”的刻刀,蘸上MateChat这抹“智能画布”的颜料,去创造吧!:不仅仅是颜色的反转,更是对对比度、光影的精细打磨,让你的应用在深夜也能提供舒适的阅读体验。

weixin_42300449的博客 636

SAP 权限配置(复合角色)

复合角色创建步骤:业务场景:通常单一角色权限颗粒度划分比较细,当需要分配单一角色权限A、B(多个单一角色)给某账号用户Y时,可以先建立一个复合角色C,将A、B绑定到复合角色C中,再将角色C分配给账号Y即完成权限配置。 业务模拟: 1、PFCG创建复合角色: 2、在角色页面添加子角色 在角色分页内添加子角色,点击“菜单”分页中的“读菜单”即可导入子角色中的菜单: 3、将用户分配给...

willieyuan的博客 7087

沙画流沙工坊引入基于HarmonyOS ArkTS API 24类型定义,如ResourceStr、ResourceColor 等资源类型,以及 Length、FontWeight 等 UI 相关类型

在移动互联网与智能穿戴设备高度普及的今天,"习惯养成"已经从一个模糊的自我管理理念,演变为一个可量化、可追踪、可视化的数字化产品赛道。越来越多的人希望借助手中的智能终端,把"早起、冥想、阅读、运动"这些抽象的自律行为,转化为一条条清晰的数据曲线和一张张可触摸的打卡卡片。而鸿蒙生态(HarmonyOS)凭借其跨设备协同、分布式软总线以及声明式 UI 开发框架 ArkUI,为这类注重"轻量化交互 + 即时反馈"的生活方式类应用提供了极为理想的生长土壤。本应用正是一款面向"晨间场景"深度打磨的习惯追踪工具。它聚焦

jin421352的博客 243

iStoreOS等软路由插件卸载的残留?分享一款软路由高级卸载工具,自定义卸载,轻松解决卸载残留问题

iStoreOS用户看过来!👀 还在被插件包残留困扰吗?本文分享一款“高级卸载”的插件,助你深度清理iStoreOS系统,彻底解决软件卸载不净的痛点,告别频繁恢复出厂设置的烦恼!🚀

XiaoqiangClub的博客 1541

AI:270-基于ASFF改进YOLOv8检测头的多尺度特征融合方法详解与实战

ASFF是一种基于自适应特征融合的策略,能够动态调整不同尺度特征的融合权重,适应场景中不同大小的目标。传统的YOLOv8检测头使用固定的特征融合策略,而ASFF则通过引入学习参数,使得网络能够根据输入图像的特征自适应地选择不同尺度特征的重要性。这一策略对于检测小目标或尺度变化大的目标具有显著优势。本文提出了一种改进YOLOv8检测头的新方法,通过引入自适应空间特征融合模块(ASFF),有效提升了多尺度目标检测的性能,特别是在小目标检测任务中的表现。

一键难忘的博客 3913

【数据结构-5】单链表的实现

单链表,具体来说是单向的、不带头的、不循环的链表。链表由一个一个节点组成,每个节点分为数据域和指针域,数据域存放数据,指针域存放下一节点的地址。这样,就可以根据指针域中的地址找到下一个节点,节点就串起来了构成了单链表。

weixin_44218929的博客 187

大数据领域数据产品的交通运输行业应用

您是否经历过早高峰被堵在桥上,看着导航显示“前方5公里缓行”却无能为力?是否见过货车司机绕远路导致成本飙升?这些“交通痛点”的背后,是传统交通管理依赖经验决策的局限。本文将聚焦“大数据数据产品”这一核心工具,从技术原理到实际应用,全面解析它如何为交通运输行业“开天眼”,解决从城市拥堵到物流效率的一系列难题。本文将按照“问题引入→概念拆解→技术原理→实战案例→应用场景→未来趋势”的逻辑展开,用生活化比喻降低理解门槛,用代码和数学模型提升专业性,最终帮您建立“大数据+交通”的完整认知框架。交通数据。

操作系统内核探秘的博客 237

pandas 深度入门:从数据结构到一份真实销售报表

本文系统讲解Pandas核心应用,从三分钟安装到完整月度报表生成,涵盖数据读取、清洗、筛选、分组聚合、表连接、透视表与时间序列等13个关键环节。强调“先看数据再动手”,掌握Series/DataFrame/Index结构本质,熟练运用loc/iloc、merge校验、agg/transform区别,通过真实案例实现高效报表自动化,告别手动Excel操作。最后提供性能优化与高频报错解决方案,助力读者快速上手,真正实现“写二十行代码,跑三秒出结果”。

信息安全领域优质创作者|操作系统领域专家 278

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

数据结构的基本概念和算法的初步认识

本文系统阐述数据结构与算法的基础概念,涵盖数据、数据元素、数据对象的层次关系,数据结构的三要素(逻辑结构、存储结构、数据运算),以及抽象数据类型。重点解析算法的特性与设计要求,并深入讲解时间复杂度分析的核心——大O表示法,通过“只保留最高阶项、忽略常数系数、取最坏情况”三大原则,结合实例剖析O(1)、O(n)等常见时间复杂度,揭示算法效率的本质评估方法。

ACH_YYX的博客 182

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

数据结构:9.Map和Set

有的讲解会说这里key是标识作用,这种说法其实没有太大意义,就算知道了他是标识value的作用又如何,在实际的使用过程中,我们只会根据具体的效果选择使用的方法等,不会执着于“标识”这种说法。2.由于哈希函数的设计原则太过理论,它仅仅是在认为:这些通过关键码算出来的index不同 的情况下设计的,然而这种理论化的设计原则在遇到冲突时,恰恰就出现了问题,而我们能做的,就是降低冲突率。虽然哈希表的使用过程中存在冲突,但是在实际的使用过程中,哈希表的冲突率是不高的,冲突的个数是可控的。

2602_95646088的博客 199

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

Vue3 低代码移动表单设计器实战(二):缩放画布下的拖拽系统与树状级联数据结构

本文深入剖析Vue3低代码表单设计器在缩放画布下的拖拽失效问题,揭示SortableJS在CSS Transform下坐标崩溃的数学根源。通过自研独立视觉克隆壳层与逆向几何补偿算法,实现精准拖拽;采用扁平数据存储+运行时树状计算结构,解决级联依赖与深克隆难题,确保逻辑一致性与性能优化,为复杂低代码系统提供可复用的工程范式。

NanChen的博客 342

排序算法深度笔记(上):比较类排序的思想演进与代码实现

与。本文聚焦前者 —— 也就是通过元素间两两比较来确定相对顺序的排序算法,从最朴素的 O (n²) 基础排序,到工业级主流的 O (nlogn) 进阶排序,逐层拆解设计思想、带注释的代码实现与边界细节。当我们理解了比较排序的性能边界,也就自然引出了非比较排序的存在意义。所有代码均遵循统一的实现规范:入口函数前置空数组与单元素边界校验,基于数组原地修改,主函数附带可直接运行的测试用例,与练习代码风格完全对齐。

2401_87069538的博客 610

CF2283F[数据结构]

首先考虑一种暴力解法,当我们建好i之前的图后,当我们添加i的时候,区间[li,ri]中的点都会向i建边,但是这些点中,如果有某两个顶点l,r,满足l<r,那么l就可以通过r间接到达i,因此可以删除l到i的边;初始时我们令所有点放入set中,然后对于每一个i的处理,我们将集合中仍在[l,r]的范围的点弹出,并且将f设置为i,这样我们就得到了所有点的最小跳板;对于每个区间[l,r] 我们要计算这下下标中的f(v)中大于r的数目有多少个;我们令f(v)表示顶点v可到达的最小的顶点;

蒟蒻 128

笔记-论数据结构2

typedef 数据类型 别名return 0;typedef也经常用在结构体上:每次声明结构体变量都要写struct关键字,很麻烦,而且逻辑上也很难受,typedef可以解决这个问题。int x;int y;p.x = 5;p.y = 10;return 0;而利用typedef解决有两种解法:typedef struct 结构体名数据类型 变量名;数据类型 变量名;……}别名;数据类型 变量名;数据类型 变量名;……}别名;实际的运用int x;

fish_zzzz的博客 110

【数据结构-3】算法美学之降复杂度

在while循环的循环体内,首先用临时变量保存最后一个位置的数据,再通过for循环把所有元素往后移动一位,最后把tmp中保存的值放到下标为0的位置上去。方法二的思路是:创建新数组,让原数组中后k个数据移动到新数组中前面的位置,让原数组中前numsSize-k个数据移动到新数组中后面的位置,这。这段代码的思路是:三次逆置,第一次逆置前numsSize-k个数据,第二次逆置后k个数据,最后整体逆置。这篇博文分享一道解法不一的算法题,求解过程的目的在于降低算法的时间复杂度,题源力扣。,属于空间换时间的操作。

weixin_44218929的博客 83

【数据结构-2】顺序表的实现

线性表是一种数据结构,它分为逻辑结构和物理结构,在逻辑结构上线性表是呈线性的,数据是是连续存储的;在物理结构(物理空间)上,线性表不一定是连续存储的。线性表的实现,主要分为数组和链式结构。顺序表是线性表的一种实现方式,顺序表在逻辑结构和物理结构上都是连续的。顺序表的底层实现是数组。

weixin_44218929的博客 208

数据结构:排序

将一组无序的记录(数据元素),按照某个关键字(key)递增或递减的顺序重新排列。

2502_92937908的博客 275

Python 开发的实践:从数据结构选型到框架决策

规模多大?决定用list还是set,Pandas 还是 Polars。瓶颈在哪?I/O 密集用异步,CPU 密集用进程/自由线程,内存受限用列式与惰性执行。代价是什么?消除手动维护窗口的出错面,TaskGroup消除失败任务不被取消的泄漏,消除 N+1,Protocol消除为类型约束而引入的继承层级——每个选择都在消除某类代价,同时也引入新的约束。能回答这三个问题,比记住更多 API 更有用。愿你我都能在各自的领域里不断成长,勇敢追求梦想,同时也保持对世界的好奇与善意!

weixin_45809780的博客 375

头歌 数据结构——C&C++线性表实训

本系列任务围绕线性表的链式实现,依次完成顺序构建、逆序构建、排序插入、查找、删除、求长度及栈、队列、集合等应用。通过动态内存分配与指针操作,掌握单链表的基本操作与实际应用,涵盖插入、删除、遍历、搜索等核心功能,最终实现数据结构在典型场景中的灵活运用。

一名敲代码的苦13程序员 238

CDGA DMBOK 第5章——数据建模和设计:如何将业务需求转化为规范的数据结构

本章系统阐述数据建模与设计的核心方法,强调将业务需求转化为规范数据结构的全过程。重点涵盖数据模型三层次(概念、逻辑、物理)、实体关系模型(ER)及主键、外键等核心要素,深入解析第一至第三范式,阐明规范化在消除冗余、保障数据一致性的关键作用,并介绍反规范化在性能优化中的合理应用。通过“概念看业务、逻辑看结构、物理看实现”的逻辑框架,实现从需求到数据库落地的完整闭环。

积跬步,慕至千里的博客 306
上一篇: 【初阶数据结构】星河中的光影 “排” 象:排序(上)
下一篇: 如何高效利用DeepSeek:深入探索AI搜索引擎的潜力
澪贰
澪贰 优质创作者: C/C++技术领域 优质创作者: C/C++技术领域
博客等级 码龄2年 1万+粉丝 222原创
评论 51
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值