【数据结构(C语言)】排序详解

目录

文章目录

前言

一、排序的概念

1.1  排序的概念

1.2 常见的排序算法

二、插入排序

2.1 直接插入排序

2.1.1 基本思想 

2.1.2 特性总结

2.1.3 代码实现

2.2 希尔排序

2.2.1 基本思想 

2.2.2 特性总结

2.2.3 代码实现

三、选择排序

3.1 直接选择排序

3.1.1 基本思想

3.1.2 特性总结

3.1.3 代码实现

3.2 堆排序

3.2.1 基本思想

3.2.2 特性总结

3.2.3 代码实现

四、交换排序

4.1 冒泡排序

4.1.1 基本思想

4.1.2 特性总结

4.1.3 代码实现

4.2 快速排序

4.2.1 基本思想

4.2.2 特性总结

4.2.3 代码实现

五、归并排序

5.1 归并排序

5.1.1 基本思想

5.1.2 特征总结

5.1.3 代码实现

六、计数排序

6.1 计数排序

6.1.1  基本思想

6.1.2 特征总结

6.1.3 代码实现

七、排序比较

总结


前言

在当今信息爆炸的时代,数据量不断攀升,排序算法作为处理数据的核心技术,其在计算机科学、大数据、人工智能等领域的重要性不言而喻。本文旨在介绍各种排序算法,帮助读者掌握其原理、特点及适用场景,从而提高数据处理效率,提升系统性能。

本文将详细介绍常见的排序算法,包括冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序等,并分析各种排序算法的时间复杂度、空间复杂度和稳定性,让你在实际应用中能够灵活选择合适的排序方法。


一、排序的概念

1.1  排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

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

1.2 常见的排序算法

二、插入排序

2.1 直接插入排序

2.1.1 基本思想 

直接插入排序(Insertion Sort)是一种简单直观的排序算法。它的基本思想是将待排序的序列分为已排序和未排序两部分,在每一趟排序中,从未排序的部分中选择一个元素,然后将其插入到已排序的部分中的正确位置,直到所有元素都被插入到已排序的部分中。

具体的步骤如下:

  1. 将序列的第一个元素看作已排序的部分,剩余部分看作未排序的部分。
  2. 从未排序的部分中选取一个元素,依次与已排序的部分中的元素进行比较,找到合适的位置插入。
  3. 插入元素时,需要将已排序的部分中比插入元素大的元素依次向后移动一个位置,给插入元素腾出位置。
  4. 重复步骤2和步骤3,直到所有元素都插入到已排序的部分中。

直接插入排序的时间复杂度为O(n^2),其中n为待排序序列的长度。它是稳定的排序算法,适用于小规模的数据或基本有序的数据。在实际应用中,当待排序序列规模较小或已经基本有序时,直接插入排序是一个简单有效的选择。

2.1.2 特性总结

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

2.1.3 代码实现

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}

		}
		a[end + 1] = tmp;
	}
}

2.2 希尔排序

2.2.1 基本思想 

希尔排序是一种排序算法,通过将待排序的数组分割成若干个子序列进行插入排序,从而达到整体有序的目的。它是插入排序的改进版,也被称为缩小增量排序。

希尔排序的基本思想是:对数组进行分组,按一定的间隔(增量)将元素分为若干个子序列,对每个子序列进行插入排序,然后减小增量再次分组并排序,直到增量为1时,进行最后的插入排序。

具体的步骤如下:

  1. 选择一个增量序列,通常初始增量为数组长度的一半,之后依次减半,直到增量为1。
  2. 对于每个增量,将数组分成若干个长度为增量的子序列。
  3. 对每个子序列进行插入排序。
  4. 缩小增量,重复步骤2和3,直到增量为1。
  5. 最后进行一次插入排序,完成排序。

希尔排序的时间复杂度和增量序列的选择有关,但最坏时间复杂度为O(n^2),平均时间复杂度为O(n^1.3)。相比于插入排序的O(n^2)时间复杂度,希尔排序有较好的性能。

希尔排序是一种原地排序算法,不需要额外的空间,因此它的空间复杂度为O(1)。但它不稳定,相同元素可能会被交换位置,因此对于有重复元素的数组,可能需要考虑其他的排序算法。

2.2.2 特性总结

1. 希尔排序是对直接插入排序的优化。

2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。

3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定:

因为咋们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:O(n^{1.25})O(1.6*n^{1.25})来算。

4. 稳定性:不稳定

2.2.3 代码实现

//平均O(n^1.3)
void ShellSort(int* a, int n)
{
	int gap = n;

	// gap > 1时是预排序,目的让他接近有序
	// gap == 1是直接插入排序,目的是让他有序
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}

	//优化之前
	/*while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int j = 0; j < gap; ++j)
		{
			for (int i = j; i < n - gap; i += gap)
			{
				int end = i;
				int tmp = a[end + gap];
				while (end >= 0)
				{
					if (tmp < a[end])
					{
						a[end + gap] = a[end];
						end -= gap;
					}
					else
					{
						break;
					}
				}
				a[end + gap] = tmp;
			}
		}
	}*/
}

三、选择排序

3.1 直接选择排序

3.1.1 基本思想

选择排序是一种简单直观的排序算法,它的基本思想是每次从待排序的数组中选择最小(或最大)的元素,放到已排序部分的末尾,直到整个数组有序为止。

具体的步骤如下:

  1. 遍历数组,从第一个元素开始,将其标记为最小元素。
  2. 在剩下的待排序部分中,找到最小的元素,并将其与待排序部分的第一个元素交换位置。
  3. 指针向后移动一位,进入下一轮遍历,重复步骤2,直到遍历完整个数组。

选择排序的时间复杂度为O(n^2),其中n为数组的长度。无论原始数组是否有序,每次都需要遍历剩余的待排序部分来找到最小元素,因此时间复杂度较高。

选择排序是一种原地排序算法,它只需要常数级的额外空间来存储交换时的临时变量,因此空间复杂度为O(1)。

选择排序是一种不稳定的排序算法,因为交换元素的位置可能会改变相同元素的相对顺序。例如,对于序列[5, 5, 2],第一轮选择排序后,第一个5会与2交换位置,导致相同元素的顺序改变。

尽管选择排序的性能不如快速排序、归并排序等高级排序算法,但它简单易懂,实现起来较为简单,适用于小规模的数据排序或作为其他高级排序算法的辅助排序步骤。

3.1.2 特性总结

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

3.1.3 代码实现

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

//优化了一步,一次选两个,一个最大,一个最小
void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;
	while(begin < end)
	{
		int mini = begin, maxi = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		swap(&a[begin], &a[mini]);
		if (maxi == begin)
		{
			maxi = mini;
		}
		swap(&a[end], &a[maxi]);

		begin++;
		end--;
	}
}

3.2 堆排序

3.2.1 基本思想

堆排序是一种基于完全二叉堆的排序算法。它的基本思想是通过将待排序的数组看作是一个完全二叉树,并将其转化为一个最大堆或最小堆。然后,每次将堆顶元素与堆的最后一个元素交换位置,然后调整堆,使得剩余元素满足堆的性质。重复这个过程,直到整个数组有序为止。

具体的步骤如下:

  1. 将待排序的数组构建成一个最大堆或最小堆。
  2. 交换堆顶元素与堆的最后一个元素,然后将剩余元素调整为一个新的堆。
  3. 重复步骤2,直到堆的大小为1,则整个数组有序。

堆排序的时间复杂度为O(nlogn),其中n为数组的长度。构建堆的过程需要O(n)的时间复杂度,而每次调整堆的时间复杂度为O(logn),总共需要进行n-1次交换和调整操作。

堆排序是一种原地排序算法,它只需要常数级别的额外空间来存储交换时的临时变量,因此空间复杂度为O(1)。

堆排序是一种不稳定的排序算法,因为交换堆顶元素与最后一个元素可能会改变相同元素的相对顺序。

虽然堆排序的时间复杂度较低,但它的常数项较大,而且在实际应用中不如快速排序或归并排序常用,一般用于特定场景或作为其他排序算法的辅助排序步骤。

3.2.2 特性总结

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

3.2.3 代码实现

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

void AdjustDown(int* a, int size, int parent) //这里的size表示最后要建成的最终形态的堆的大小
{
	int child = parent * 2 + 1;

	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 = child * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	for (int i = n - 1; i > 0; i--)
	{
		Swap(&a[0], &a[i]);
		AdjustDown(a, i, 0);
	}
}

 

四、交换排序

4.1 冒泡排序

4.1.1 基本思想

冒泡排序是一种简单的排序算法,它重复地遍历待排序数组,每次比较相邻的两个元素,并将较大(或较小)的元素交换到右侧。通过多次遍历,最大(或最小)的元素会逐渐移动到右侧,实现排序的目的。

具体的步骤如下:

  1. 从数组的第一个元素开始,依次比较相邻的两个元素,如果前一个元素大于(或小于)后一个元素,则交换它们的位置。
  2. 继续遍历剩下的元素,进行相同的比较和交换操作,直到遍历完整个数组。
  3. 重复上述步骤,每次遍历都会将当前最大(或最小)的元素移动到右侧。
  4. 重复执行n-1次遍历,直到所有元素都有序排列。

冒泡排序的时间复杂度为O(n^2),其中n为数组的长度。每次遍历需要比较n-1次,并且最多需要执行n-1次遍历。

冒泡排序是一种原地排序算法,它只需要常数级别的额外空间来存储交换时的临时变量,因此空间复杂度为O(1)。

冒泡排序是一种稳定的排序算法,因为在相邻元素比较时,只有前一个元素大于(或小于)后一个元素时才会交换它们的位置。

尽管冒泡排序的时间复杂度较高,但它的实现简单,适用于小规模的数据排序。在实际应用中,由于其性能较差,常常被其他高效的排序算法取代。


4.1.2 特性总结

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

4.1.3 代码实现

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

void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n - 1; j++)
	{
		bool exchange = false;
		for (int i = 0; i < n - 1 - j; i++)
		{
			if (a[i] > a[i + 1])
			{
				swap(&a[i], &a[i + 1]);
				exchange = true;
			}
		}

		if (exchange == false)
		{
			break;
		}
	}
}

4.2 快速排序

4.2.1 基本思想

快速排序是一种常用的排序算法,它采用分治的思想将数据分成较小和较大的两部分,然后递归地对两部分进行排序。

具体的步骤如下(hoare版本):

  1. 选择一个基准元素(pivot),通常选择数组的第一个或最后一个元素。
  2. 将数组分为两部分,使得左边部分的元素都小于等于基准元素,右边部分的元素都大于等于基准元素。可以使用两个指针分别从数组的左右两端开始遍历,当左指针指向的元素大于基准元素,右指针指向的元素小于基准元素时,交换它们的位置。
  3. 递归地对左右两部分进行排序,直到每个部分只有一个元素或为空。
  4. 合并排序后的左右部分,即可得到完整的有序数组。

快速排序的时间复杂度平均为O(nlogn),其中n为数组的长度。每次排序都会将数组分成大致等长的两部分,因此分治的次数为logn,而每次分治需要遍历n个元素,所以时间复杂度为nlogn。最坏情况下的时间复杂度为O(n^2),当数组已经有序或近乎有序时,每次分治只能得到一个元素,导致分治次数为n,时间复杂度变为n的平方。

快速排序是一种原地排序算法,它只需要常数级别的额外空间来存储分治时的临时变量,因此空间复杂度为O(1)。

快速排序是一种不稳定的排序算法,因为在交换元素时可能改变相同元素的相对顺序。

快速排序是一种高效的排序算法,常被用于大规模数据的排序。它的实现相对较复杂,但其时间复杂度的平均性能优势使其成为了主流的排序算法之一。

前后指针版本

4.2.2 特性总结

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

2. 时间复杂度:O(N*logN)

3. 空间复杂度:O(logN)

4. 稳定性:不稳定

4.2.3 代码实现

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

int GetMidi(int* a, int begin, int end)
{
	int midi = (begin + end) / 2;
	//begin midi end 三个数取中
	if (a[begin] < a[midi])
	{
		if (a[midi] < a[end])
			return midi;
		else if (a[begin] > a[end])
			return begin;
		else
			return end;
	}
	else // a[begin] > a[midi]
	{
		if (a[midi] > a[end])
			return midi;
		else if (a[begin] < a[end])
			return begin;
		else
			return end;
	}
}

//hoare版本
int PartSort1(int* a, int begin, int end)
{
	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);

	int left = begin, right = end;
	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]);

	return left;
}

//挖坑法版本
int PartSort2(int* a, int begin, int end)
{
	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);

	int key = a[begin];
	int hole = begin;
	while (begin < end)
	{
		// 右边找小,填到左边的坑
		while (begin < end && a[end] >= key)
		{
			--end;
		}

		a[hole] = a[end];
		hole = end;

		// 左边找大,填到右边的坑
		while (begin < end && a[begin] <= key)
		{
			++begin;
		}

		a[hole] = a[begin];
		hole = begin;
	}

	a[hole] = key;
	return hole;
}

//前后指针版本
int PartSort3(int* a, int begin, int end)
{
	int midi = GetMidi(a, begin, end);
	Swap(&a[midi], &a[begin]);
	int keyi = begin;

	int prev = begin;
	int cur = prev + 1;
	while (cur <= end)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

		++cur;
	}

	Swap(&a[prev], &a[keyi]);

	return prev;
}

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

	int keyi = PartSort3(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

 

五、归并排序

5.1 归并排序

5.1.1 基本思想

归并排序是一种分治算法,它将待排序数组分成两个部分,分别对每个部分进行排序,然后将两个排好序的部分合并成一个有序的数组。

具体的步骤如下:

  1. 将数组不断二分,直到每个部分只有一个元素或为空。
  2. 递归地对左右两部分进行排序,直到每个部分只有一个元素或为空。
  3. 合并排序后的左右两部分,即可得到完整的有序数组。合并时,从两个部分的头部开始比较元素大小,较小的元素先放入新数组,然后将指针向后移动,直到一个部分为空,再将另一个部分的剩余元素依次放入新数组。

归并排序的时间复杂度是O(nlogn),其中n为数组的长度。每次排序时,需要将数组分成大致等长的两部分,因此分治的次数为logn。每次分治中合并两个部分的时间复杂度为O(n),因为需要遍历n个元素。所以总的时间复杂度为nlogn。

归并排序是一种稳定的排序算法,因为在合并两部分时,如果遇到相同元素,会先将左半部分的元素放入新数组,保证了相同元素的相对顺序不会改变。

归并排序是一种稳定、可靠的排序算法,它的实现相对简单,但其时间复杂度的稳定性使其成为了主流的排序算法之一。它也适用于大规模数据的排序,特别适合用于链表等数据结构的排序。

5.1.2 特征总结

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定  

5.1.3 代码实现

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

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

 

六、计数排序

6.1 计数排序

6.1.1  基本思想

前面介绍的几种的排序都是比较排序算法,而计数排序是一种非比较排序算法,其基本思想是统计待排序数组中每个元素出现的次数,然后按照元素的大小顺序依次将元素放入新的数组中。

具体的步骤如下:

  1. 找出待排序数组中最大值max和最小值min。
  2. 创建一个大小为max-min+1的计数数组count,用于记录每个元素出现的次数。初始时,count数组的所有元素都为0。
  3. 遍历待排序数组,将每个元素的值作为count数组的索引,在对应的索引位置上将计数加1。
  4. 依次累加count数组中的元素,即将count中的元素与前一个元素相加,得到每个元素在排序后数组中的最后一个位置的索引。
  5. 创建一个大小与待排序数组相同的临时数组temp。
  6. 遍历待排序数组,根据元素的值在count数组中查找对应的位置,并将元素放入temp数组中。然后将count数组中对应位置的计数值减1。
  7. 将temp数组的元素复制回原始数组,即得到了排序后的数组。

计数排序的时间复杂度是O(n+k),其中n为数组的长度,k为待排序数组中最大值与最小值之差。由于需要遍历待排序数组两次,分别用于计数和复制元素,因此实际的时间复杂度可以近似为O(n)。

计数排序是一种稳定的排序算法,因为在将元素放入临时数组时,遇到相同值的元素会按照待排序数组中的顺序依次放入,保持了相同元素的相对顺序不变。计数排序适用于待排序数组中的元素范围相对较小的情况,对于较大范围的数据集合,计数排序的空间复杂度较高,不适合使用。

6.1.2 特征总结

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  2. 时间复杂度:O(MAX(N,范围))
  3. 空间复杂度:O(范围)
  4. 稳定性:稳定(可以做到稳定,但一般不谈稳定性)

6.1.3 代码实现

void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];
	for (int i = 1; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i] > max)
		{
			max = a[i];
		}
	}

	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("calloc fail\n");
		return;
	}

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

	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}

 

七、排序比较

 


总结

通过对各种排序算法的学习,希望读者能够了解排序算法的基本原理和特点,以便根据实际需求选择合适的算法。希望本文能为你在排序算法的学习和实践中提供有益的帮助,让你在未来的工作和学习中能够游刃有余地运用排序算法,提升数据处理效率,创造更多价值。

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

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

相关文章

【数据结构和算法初阶(C语言)】空间复杂度(例题剖析一起探究空间如何评价算法)

目录 1.衔接前言-时间复杂度的回顾 2.关于算法复杂度 3.本文主角-空间复杂度 3.1大O的渐进表示方法 4.空间复杂度例题----实际感受空间复杂度 4.1冒泡排序的空间复杂度 4.2计算递归函数的空间复杂度 4.3动态开辟内存版本求斐波那契数列的空间复杂度 4.4&#xff08;…

蓝桥杯_定时器的基本原理与应用

一 什么是定时器 定时器/计数器是一种能够对内部时钟信号或外部输入信号进行计数&#xff0c;当计数值达到设定要求时&#xff0c;向cpu提出中断处理请求&#xff0c;从而实现&#xff0c;定时或者计数功能的外设。 二 51单片机的定时/计数器 单片机外部晶振12MHZ&#xff0c;…

如何实现双向循环链表

博主主页&#xff1a;17_Kevin-CSDN博客 收录专栏&#xff1a;《数据结构》 引言 双向带头循环链表是一种常见的数据结构&#xff0c;它具有双向遍历的特性&#xff0c;并且在表头和表尾之间形成一个循环。本文将深入探讨双向带头循环链表的结构、操作和应用场景&#xff0c;帮…

el-table通过这样封装可以实现校验-表格校验的原理

我们一般在后台系统中&#xff0c;很常见的操作时表格里面嵌套表单&#xff0c;之前我的网上找到了一些封装的用法&#xff1a; <el-form :model"formData" :rules"ruleData" ref"formDom"><el-table :data"formData.tableData&q…

Vue.js入门指南:简介、环境配置与Yarn创建项目

一、Vue.js简介 Vue.js&#xff0c;一个流行的JavaScript框架&#xff0c;以其直观、灵活和高效的特点&#xff0c;在前端开发者中赢得了广泛的赞誉。Vue.js的核心库专注于视图层&#xff0c;使得开发者能够构建出响应式的数据绑定和组合的视图组件。Vue.js的目标是通过尽可能简…

CPU、GPU 混合推理,非常见大模型量化方案:“二三五六” 位量化,模型量化详细实现方案

CPU、GPU 混合推理&#xff0c;非常见大模型量化方案&#xff1a;“二三五六” 位量化&#xff0c;模型量化详细实现方案。 非常见整型位数的量化&#xff0c;来自让各种开源模型能够在 CPU 环境、CPU & GPU 环境混合推理的技术方案&#xff1a;llama.cpp 。为了能够在低配…

iOS群控软件功能分析与代码分享!

随着移动互联网的迅猛发展&#xff0c;iOS设备作为市场上一大主流平台&#xff0c;其应用开发和管理越来越受到开发者和企业的重视&#xff0c;iOS群控软件&#xff0c;作为一种能够批量控制、管理和监控iOS设备的工具&#xff0c;逐渐展现出其强大的实用价值。 本文将详细分析…

React回顾

一、基础 1、使用babel解析 2、不直接使用jsx&#xff0c;jsx写起来很繁琐 3、jsx语法规则 4、函数式组件的使用 5、函数式组件渲染 6、类组件渲染 7、类组件中事件调用this指向问题 8、类组件不能直接改变状态 9、props接收数据类型限制 类型限制放到类组件内部&#xff0c;用…

StarRocks实战——滴滴OLAP的技术实践与发展方向

原文大佬的这篇StarRocks实践文章整体写的很深入&#xff0c;介绍了StarRocks数仓架构设计、物化视图加速实时看板、全局字典精确去重等内容&#xff0c;这里直接摘抄下来用作学习和知识沉淀。 目录 一、背景介绍 1.1 滴滴OLAP的发展历程 1.2 OLAP引擎存在的痛点 1.2.1 运维…

IOC 和 AOP

IOC 所谓的IOC&#xff08;inversion of control&#xff09;&#xff0c;就是控制反转的意思。何为控制反转&#xff1f; 在传统的程序设计中&#xff0c;应用程序代码通常控制着对象的创建和管理。例如&#xff0c;一个对象需要依赖其他对象&#xff0c;那么它会直接new出来…

express+mysql+vue,从零搭建一个商城管理系统6--数据校验和登录

提示&#xff1a;学习express&#xff0c;搭建管理系统 文章目录 前言一、修改models/user.js二、修改routes下的user.js三、Api新建user/login接口四、删除数据库原有数据&#xff0c;添加新验证规则的用户四、用户登录总结 前言 需求&#xff1a;主要学习express&#xff0c;…

【Linux】基础篇-Linux四种环境搭建的方式(详细安装说明步骤,搭载下载安装地址)

目录 1. 使用虚拟机&#xff08;推荐VMware&#xff09;centos 7版本 1.1VMware虚拟机下载 1.2VMware 安装 1.3centos-7 清华大学镜像下载 1.4 centos-7 清华大学镜像导入虚拟机VMware 2.使用虚拟机ubuntu 20.04版本 2.1虚拟机下载同上 2.2虚拟机安装同上 2.3ubunt…

ROS-Ubuntu 版本相关

ROS-Ubuntu 版本相关&#xff1a;安装指引 年代ROS1版本Ubuntu 版本2014Indigo14.042016Kinetic16.042018Melodic18.042020Noetic20.04 & 22.04 ROS2兼顾了工业使用上的问题。 年代ROS2版本Ubuntu 版本2022Humble20.04 & 22.042023Iron16.04 相关参考&#xff1a; […

【Qt 学习之路】使用 cmake 在Windows上 编译 ZeroMQ

文章目录 1、概述2、编译2.1、用 Visual Studio 的解决方案方式2.1.1、找到 Builds 文件夹2.1.2、查看 deprecated-msvc 下的 libzmq.sln 文件2.1.3、使用 Visual Studio 打开 libzmq.sln 解决方案2.1.4、修改 libzmq.import.props 文件2.1.5、编译生成 2.2、用 C 的cmake方式2…

【前端入门】设计模式+单多页+React

设计模式是一种解决特定问题的经验总结&#xff0c;它提供了经过验证的解决方案&#xff0c;可以在软件开发过程中使用。设计模式可以帮助前端开发人员更有效地组织和管理代码&#xff0c;并提供一种共享的语言和框架&#xff0c;以便与其他开发人员进行交流。 以下是一些常见…

十二、Qt自定义Widget组件、静态库与动态库

一、自定义Widget组件 1、自定义Widget组件 使用步骤采用提升法&#xff08;promotion&#xff09;重新定义paintEvent事件 2、实现程序 &#xff08;1&#xff09;创建项目&#xff0c;基于QWidget &#xff08;2&#xff09;添加类&#xff0c;为Widget组件提升类 #inclu…

Delegate(P29 5.5delegate)

一、Delegate简介 每个代理都可以访问许多附加的属性&#xff0c;其中一些来自数据模型&#xff0c;另一些来自视图。 从模型中&#xff08;Model&#xff09;&#xff1a;属性将每个项目的数据传递给 delegate。 从视图中&#xff08;View&#xff09;&#xff1a;属性将状…

dcat admin 自定义页面

自定义用户详情页 整体分为两部分&#xff1a;用户信息、tab框 用户信息采用自定义页面加载&#xff0c;controller代码如下&#xff1a; protected function detail($id) {return Show::make($id, GameUser::with(finance), function (Show $show) {// 这段就是加载自定义页面…

pdf怎么合并在一起?

pdf怎么合并在一起&#xff1f;在日常工作和学习中&#xff0c;我们常常需要处理大量的PDF文件。有时候&#xff0c;我们可能希望将多个PDF文件合并成一个文件&#xff0c;以便于管理和分享。这时候&#xff0c;PDF文件合并工具就能派上用场了。PDF文件合并是一种将多个PDF文件…

MySQL 事务原理分析

事务 前提&#xff1a;有并发连接。定义&#xff1a;事务是用户定义的一系列操作&#xff0c;这些操作要么都做&#xff0c;要么都不做&#xff0c;是一个不可分割的单位。目的&#xff1a;事务将数据库从一种一致性状态转换为另一种一致性状态&#xff0c;保证系统始终处于一…