数据结构之排序——排序知识小汇总,进来看看必有收获

    前言:这篇文章将带你学习以下排序


前提小知识:

排序中的稳定性是指,相同大小的两个值,排序之后它们的相对顺序不发生改变。

小总结:
排序方法平均情况最好情况最坏情况辅助空间稳定性
冒泡排序O(N²)O(N)O(N²)O(1)稳定
简单选择排序O(N²)O(N²)O(N²)O(1)不稳定
直接插入排序O(N²)O(N)O(N²)O(1)稳定
希尔排序O(N*logN)~O(N²)O(N^1.3)O(N²)O(1)不稳定
堆排序O(N*logN)O(N*logN)O(N*logN)O(1)不稳定
归并排序O(N*logN)O(N*logN)O(N*logN)O(N)稳定
快速排序O(N*logN)O(N*logN)O(N²)O(logN)~O(N)不稳定

一、直接插入排序

1.基本思想

当插入第i个对象时,前i-1个都已经排好序,第i个逐一往前比较,找到合适的位置进行插入,这个位置以及后面位置的对象向后顺移。

我们在玩扑克牌的时候,对手中的牌进行排序就是用到了插入排序的思想。

2.代码

void InsertSort(int* a, int n)n为数组个数
{
	int end,temp;
	
	for (int i = 0; i < n - 1; i++)
	{
		end = i;
		temp = a[end + 1];
		while (end >= 0 && temp < a[end])
		{
			a[end + 1] = a[end];
			end--;
		}
		a[end + 1] = temp;
	}
}

3.分析

在直接插入排序中,最坏的情况就是当前顺序和你要的顺序完全相反,例如你要将5,4,3,2,1排成升序1,2,3,4,5,此时时间复杂度为O(n²)。最好的情况是顺序相同有序,此时时间复杂度为O(n)。

在排序中,如果两个值相等不会变化位置,则该排序是稳定排序。

时间复杂度:O(n²)

空间复杂度:O(1)

稳定性:稳定


二、希尔排序

1.基本思想

选定一个整数,先将整个待排记录序列分割成为若干序列,对这些序列分别进行直接插入排序,等到整个序列“基本有序”时(意思是接近有序了),再对整体进行一次直接插入排序。

比如上面的动图中,排序前是7、5、1、4、2、3、0、6,排序后0,2,1,4,5,3,7,6,对比于排序前,排序后更加接近有序。

2.代码

void ShellSort(int* a, int n)n为数组个数
{
	
	//先进行预排序,让其接近有序
	//gap越大,大的越快来到后面,但越不接近有序
	//gap越小,大的越慢来到后面,但越接近有序
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;//最后gap一定会为1,此时就是插入排序
		for (int i = 0; i < n - gap; i++)//一组中一趟进行排序会和一组前进行比较
		{
			int end = i;
			int temp = a[end + gap];
			while (end >= 0 && temp < a[end])
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			a[end + gap] = temp;
		}
	}
	
}

当最后gap为1时,对比直接插入排序的代码,我们会发现循环里面的内容是相同的。

gap的值的选取通常是n/3+1

3.分析

希尔排序的时间复杂度大概是O(N^1.3)。

我们来对这里的时间复杂度进行一个大概的分析:

我们在计算中,为了方便计算,将gap=gap/3+1中的1忽略掉。

那么第一趟排序gap=n/3,在最坏情况下进行排序

我们将n分为gap组,那么每组就为3个,分别对每组进行直接插入排序,这里最坏情况是,每组都是逆序,则每组都要比较1+2次,而此处一共有gap组,总比较次数为3*gap=3*n/3=n

那么第二趟排序gap=gap/3=n/9,在最坏情况下排序:

我们将n分为gap组,那么每组就为9个,分别对每组进行直接插入排序,这里最坏情况是,每组都是逆序,则每组都要比较1+2+3+4+5+6+7+8=36次,而此处一共有gap组,总比较次数为36*gap=36*n/9=4n

依次计算.......

最后当gap=1,此时已经接近有序,就是对整体进行一次插入排序,按照接近有序的时间复杂度算这一趟时间复杂度应该为O(N)。

但是需要注意的是,在进行了一次排序后的排序,已经不能按最坏情况进行计算了,因为在进行了一次排序之后,此次排序已经不会是完全逆序了。

所以我们不能这样算出它的全部时间复杂度,但是我们可以画出一个大致的走向,我们大概可以了解刚开始的时候,时间复杂度在上升,但是它最终要来到最后一趟的O(N),所以上升到一定程度,它应该会下降:

至于该排序的稳定性,希尔排序是一个不稳定的排序,这里我举一个简单的例子帮助理解。

在这种情况下,第二个5会被换到第一个5的前面去。

时间复杂度:O(n^1.3)

空间复杂度:O(1)

稳定性:不稳定


三、选择排序

1.基本思想

将待排序的序列进行遍历选出极值,放到已排序的序列最后面。

在上面的动图中,我们遍历一遍选出最小值将其放到未排序列的第一个,未排序序列-1,再进行遍历。

2.代码

在这里我们对其进行一些改进,一次遍历选出最大值和最小值,让最小值和第一个交换,最大值与最后一个交换。

void SelectSort(int* a, int n)//n为数组个数
{
	int begin = 0, end = n-1;
	while (begin <end)
	{
		int maxi, mini;
		maxi = mini = begin;
		for (int i = begin+1; i <=end; i++)//后面的数依次与第一个进行比较
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
			if (a[i] < a[mini])
			{
				mini = i;
			}
		}
		if (maxi == begin)//这一步防止处于begin的max在第一次交换的时候被换走
			maxi = mini;
		//Swap交换两个数
		Swap(&a[mini], &a[begin]);
		Swap(&a[maxi], &a[end]);
		begin++;
		end--;
	}
}

3.分析

每一次选出极值,我们都需要对待排序列进行一次遍历,无论是一次选出一个极值还是一次选出两个极值,这两个的消耗都是N²。

值得注意的是,很多人会认为这是一个稳定的排序,实际上选择排序是一个不稳定的排序,我在这里举一个例子就能很清楚地说明:6、6、5、4、1。在排序后这两个6的相互顺序将会颠倒,因此,这是一个不稳定的排序。

时间复杂度:O(n²)

空间复杂度:O(1)

稳定性:不稳定


四、堆排序

1.基本思想

堆排序是选择排序的一种,它通过堆来选择数据。

我们先对数据进行建堆,再进行排序。在这里我们需要注意的是,升序建小堆,降序建大堆。

 在这里建堆有两种方式,向上调整建堆和向下调整建堆。

向上调整建堆在末尾插入数据,然后再向上进行调整

过程如下:

向下调整是最后一个树节点开始依次对树节点进行向下调整

过程如下:

2.代码

void AdjustUp(int* a, int child, int n)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[parent] < a[child])//建大堆
		{
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void AdjustDown(int* a, int parent,int n)
{
	int child = parent * 2 + 1;
	
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])//建大堆
		{
			child += 1;
		}
		if (a[parent] < a[child])//建大堆,如果是else就没有必要继续向下比较,因为向下调整的前提是左右都是堆
		{
			Swap(&a[parent], &a[child]);
			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, i, n);
	}

	//建堆
	/*for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i, n);
	}*/
	

	//排序
	int end = n - 1;
	while (end >0)
	{
		Swap(&a[0], &a[end]);
		end--;
		AdjustDown(a, 0, end + 1);
	}
}

3.分析

该排序不是稳定排序,它不能保证相同的数的相对位置不发生变化。我们照样举一个例子帮助理解,假如数据全为2、2、2、2、2、2、2,我们在排序的时候会发生最后一个数与第一个数进行交换,此时相对位置发生了变化。

我们再来对时间复杂度进行分析:

当二叉树的高度为h层时,我们对最多节点和最少节点进行讨论

当二叉树的高度为h层时,最多能有2^h-1个节点,最少能有2^(h-1)个节点。为了计算最坏情况的时间复杂度,这里我们使用最多节点进行计算。

首先我们来看看向下调整建堆的时间复杂度:

向下调整是从最后一个树节点依次往上进行向下调整

在向下调整中,节点个数多的层向下调整次数少,节点个数少的层向下调整个数多。

T(h)=2^0*(h-1)+2^1*(h-2)+2^2*(h-3)+...+2^(h-3)*2+2^(h-2)*1

用错位相减法进行计算,我们可以得到T(h)=2^h-1-h

根据N=2^h-1可以得到T(N)=N-\log _2 N+1  -- > O(N)

我们再来看看向上调整建堆的时间复杂度:

向上调整建堆是每一个节点(除了第一个)进行向上比较调整

在向上调整中,节点多的层调整次数多,节点少的层调整次数少。

T(h)=2^(h-1)*(h-1)+2^(h-2)*(h-2)+...+2^2*2+2^1*1

用错位相减法进行计算,可以得到T(h)=-(2^h-1)+2^h*(h-1)+1

根据N=2^h-1可以得到T(N)=-N+(N+1)(\log _2 N+1-1)+1 -- >O(N*logN)

可以看出,向上调整建堆比向下调整建堆消耗更大,所以我们通常使用向下调整建堆。

建完堆后我们再来看看排序的时间复杂度,排序这一过程与向上调整建堆非常相似,第h层的节点交换到第一层后需要向下调整h-1次,第h-1层的节点交换到第一层后需要向下调整h-2次,因此排序的时间复杂度与向上调整建堆相同,都是O(N*logN)

于是这两个过程总时间复杂度为O(N+N*logN),所以堆排序的时间复杂度为O(N*logN)。

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

空间复杂度:O(1)

稳定性:不稳定


五、冒泡排序

1.基本思想

冒泡排序是交换排序的一种,交换排序就是两个值根据比较的结果进行交换,特点是根据比较可以做到将较大值向序列后面移动,将较小值向序列前面移动。

2.代码

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

3.分析

它的一趟排序可以做到将最大值沉到序列后面,每一趟都会进行遍历两两比较,最坏情况下每一个数都要遍历一遍沉到后面,时间复杂度为O(N²)。最好情况是已经是排好序,只需要遍历一次,时间复杂度为O(N)。而时间复杂度是按照最坏情况,故该排序的时间复杂度为O(N²)。

我们可以看出,当两个数字相等时,不会进行交换,并且该排序是相邻的进行两两比较,所以两个相等的值的相对位置在排序过程中不会发生改变,故该排序为稳定排序。

时间复杂度:O(n²)

空间复杂度:O(1)

稳定性:稳定


六、快速排序

在快速排序中,我将介绍三种方法:

(1)hoare方法

(2)前后指针法

(3)挖坑法

1.基本思想

在快速排序中,我们任意选中序列中的一个值作为基准值,根据这个值将序列其它值分为比它大的和比它小的,比它大的都放在它的左边,比它小的都放在它的右边。每一趟都会将基准值排好序,再对基准值的左右序列进行相同的步骤去排序。

2.代码

在实现升序中,为了保证相遇位置比基准值小,我们通常选择左边作为基准值,右边先走。

这里相遇有两种情况:

1.右边先遇到左边,右边为了找较小值一直走,直到遇到了左边,而左边要么是基准值(左边还一次都没走过),要么左边这个位置的值是上一轮与右边交换得到的较小值,此时它们相遇的位置是较小值。

2.左边先遇到右边,此时右边找到了较小值停了下来,左边为了找较大值一直走直到遇到了右边,此时它们相遇位置的值是右边找到的较小值,故相遇位置还是较小值。

在递归版本中,如果排序的序列大小不大,此时递归也会消耗一定空间,所以这里引入了小区间优化,当排序序列较小时使用时间复杂度为O(N²)的排序。

再者,如果选中的最左边的基准值太小或者太大,会导致左右又一边的序列递归太深导致,所以这里又一次引入了三数取中,用简单的方式是基准值不至于太大或者太小。

递归版本

void Swap(int* x, int* y)
{
	int temp = *x;
	*x = *y;
	*y = temp;
}

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

int GetMidi(int* a, int left, int right)//三数取中
{
	int mid = left + (right - left) / 2;
	if (a[mid] > a[left])//mid left
	{
		if (a[right] > a[mid])//right mid left
		{
			return mid;
		}
		else if (a[right] > a[left])//mid right left
		{
			return right;
		}
		else //mid left right
		{
			return left;
		}
	}
	else//left mid
	{
		if (a[right] > a[left])//right left mid
		{
			return left;
		}
		else if (a[right] > a[mid])//left right mid
		{
			return right;
		}
		else//left mid right
		{
			return mid;
		}
	}
}

int QuickSort1(int* a, int left, int right)//hoare法  右找大,左找小
{
	int keyi = left;
	int begin = left, end = right;
	while (left < right)
	{
		while (left<right&&a[right] >= a[keyi])//加上=不会让keyi从一开始就被换走
		{
			right--;
		}
		while (left<right&&a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);
	keyi = left;
	return keyi;
}

int QuickSort2(int* a, int left, int right)//挖坑法
{
	int keyi = left;
	int begin = left, end = right;
	int temp = a[keyi];//begin作为坑位
	while (begin < end)
	{
		while (begin<end && a[end]>=temp)//左边作为keyi,让右边先走,保证相遇点比a[keyi]小
		{
			end--;
		}
		a[begin] = a[end];//将begin填好,end变成了坑位
		while (begin < end && a[begin] <= temp)
		{
			begin++;
		}
		a[end] = a[begin];//将end填好,begin变成了坑位
	}
	a[begin] = temp;
	keyi = begin;
	return keyi;
}

int QuickSort3(int* a, int left, int right)//前后指针法
{
	int keyi = left;
	int pre = left;
	int cur = pre + 1;
	while (cur <= right)
	{
		while (cur<=right&&a[cur++] < a[keyi])
		{
			Swap(&a[++pre], &a[cur-1]);
		}
	}
	Swap(&a[keyi], &a[pre]);
	keyi = pre;
	return keyi;
}

void QuickSort(int* a, int left,int right)
{
	if (left >= right)//区间不存在或者只有一个值
	{
		return;
	}
	//小区间优化
	if (right - left < 10)
	{
		BubbleSort(a, left, right);
        return;
	}

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

	//排序
	keyi = QuickSort1(a, left, right);//通过一趟排好一个并且进行分割
	//递归对左右区间进行排序
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

非递归版本

我们将序列的左右区间存入栈中,排序的时候再出栈取区间出来进行排序,一直到栈为空时,则整个序列都排好序了。

#include"Stack.h"
void QuickSortNonR(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int keyi = left;
	int end = right;

	S stack;
	SInit(&stack);

	//入最初的区间
	SPush(&stack, begin);
	SPush(&stack, end);
	while (!SEmpty(&stack))
	{
		//取区间
		end = SGetTop(&stack);
		SPop(&stack);
		begin = SGetTop(&stack);
		SPop(&stack);

		//三数取中
		keyi = GetMidi(a, begin, end);
		Swap(&a[begin], &a[keyi]);

		keyi = QuickSort1(a, begin, end);//随便选取一个方法
		if (begin < keyi - 1)//入左区间
		{
			SPush(&stack, begin);
			SPush(&stack, keyi-1);
		}
		if (keyi + 1 < end)//入右区间
		{
			SPush(&stack, keyi + 1);
			SPush(&stack, end);

		}
	}
	SDes(&stack);
}

3.分析

简要分析一下,在递归版本中,如果基准值选得比较好,每次都能将其二分时,它的递归跟二叉树差不多,递归层数大概时logN层。而每次排序的时候,左右遍历一次是N。即它的时间复杂度为O(N*logN)。在递归版本中,递归需要重复多次为函数开辟栈帧,一般情况的时间复杂度为O(logN),其最坏的空间复杂度为O(N)。而非递归也要用栈存储下标,空间复杂度为O(N),时间复杂度与递归相同都是O(N*logN)。

至于它的稳定性,我们还是举一个例子:5   3  5  7  8 22  2。在第一趟排序中,第一个5会排到第二个5的后面,它是一个不稳定的排序。

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

空间复杂度:O(logN)

稳定性:不稳定


七、归并排序

1.基本思想

将序列分为子序列,先使得子序列有序,再将子序列进行合并,将两个有序的子序列进行合并形成了有序的序列。

2.代码

(1)递归版本

将序列二分为两个子序列,一直分到子序列不存在或者只有一个值,对两个子序列进行合并,形成有序的序列。左右区间要选好,否则会陷入死循环。

在[left,mid-1],[mid,right]这种区间可能会造成死循环。

在0,1,2,3区间,当left=2,right=3时,mid=2+(3-2)/2=2。则mid和left会永远为2,无限递归导致栈溢出。

可以改成[left,mid],[mid+1,right]。

void _MergeSort(int* a, int left, int right, int* temp)
{
	if (left >= right)
	{
		return;
	}
	int mid = left + (right - left) / 2;//二分

	//对左右区间进行排序
	_MergeSort(a, left, mid,temp);
	_MergeSort(a, mid + 1, right,temp);

	//合并
	int i = 0;
	int begin1 = left;
	int begin2 = mid + 1;
	while (begin1 <= mid && begin2 <= right)//遍历左右两边有序序列,挑选小的放到temp
	{
		if (a[begin1] <= a[begin2])//  =保证稳定性
		{
			temp[i++] = a[begin1];
			begin1++;
		}
		else
		{
			temp[i++] = a[begin2];
			begin2++;
		}
	}
	//将未放完的序列中的数放到temp
	while (begin1 <= mid)
	{
		temp[i++] = a[begin1];
		begin1++;
	}
	while (begin2 <= right)
	{
		temp[i++] = a[begin2];
		begin2++;
	}
	//每合并一次都需要copy一次,因为下次合并需要用到上次合并产生的有序序列
	memcpy(a + left, temp, sizeof(int) * (right - left + 1));
}

void MergeSort(int* a, int left, int right)
{
	int n = right - left + 1;
	int* temp = (int*)malloc(sizeof(int) * n);
	if (temp == NULL)
	{
		perror("MergeSort::malloc fail");
		exit(1);
	}
	_MergeSort(a, left, right, temp);
	free(temp);
}

(2)非递归版本

显示两两合并,再是四四合并,再是八八合并......

 但由于序列不一定都是4的倍数,可能会产生下标越界,在这里面要做好相应的调整。

在这里我们也举一个例子帮助理解

在这一过程中,最后一次越界我们可以直接修改end2=right,而过程中的越界,我们可以不进行合并,留到最后一次合并时作总合并,所以在过程中检测begin2是否越界就行。

void MergeSortNonR(int* a, int left, int right)
{
	int* temp = (int*)malloc(sizeof(int) * (right - left + 1));
	if (temp == NULL)
	{
		perror("MergeSortNonR::malloc fail");
		exit(1);
	}
	int begin1, end1, begin2, end2;
	int gap = 1;
	while (gap < right-left+1)
	{
		int j = 0;
		for (int i = 0; i <= right; i += 2 * gap)
		{
			begin1 = i;//先进行两两合并,再是44合并,再是88合并
			end1 = i + gap - 1;
			begin2 = i + gap;
			end2 = i + 2 * gap - 1;
			if (begin2 > right)//序列中有超出范围,不用合并,留给最后一次汇总再进行合并,最后一次汇总begin2一定不会越界
			{
				break;
			}
			if (end2 > right)//最后一次合并,对其进行修正
			{
				end2 = right;
			}
			//合并
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])//  =保证稳定性
				{
					temp[j++] = a[begin1];
					begin1++;
				}
				else
				{
					temp[j++] = a[begin2];
					begin2++;
				}
			}
			while (begin1 <= end1)
			{
				temp[j++] = a[begin1];
				begin1++;
			}
			while (begin2 <= end2)
			{
				temp[j++] = a[begin2];
				begin2++;
			}
			memcpy(a+i, temp+i, sizeof(int) * (end2-i+1));//注意复制个数的表达
		}
		gap *= 2;
	}
	free(temp);
}

3.分析

在归并排序中,大概会递归logN层,而每层将会对接近N个数进行相互比较排序。所以时间复杂度为O(N*logN)。关于稳定性,归并排序是一个稳定的排序,当两边数值相同时,我们可以优先取前面的序列。由于在排序时,如果我们有n个数据,那我们就需要额外开辟n个int空间的数组,所以它的空间复杂度为O(N)。

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

空间复杂度:O(N)

稳定性:稳定


八、计数排序(小扩展)

1.基本思想

计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用

2.代码

(1)不稳定版本

我们在这里举一个例子对计数排序进行理解:

假设现在有10个数:11,9,9,4,6,2,3,8,2,2

void CountSort(int* a, int n)
{
	int maxi, mini;
	maxi = mini = 0;
	for (int i = 1; i < n; i++)
	{
		if (a[i] > a[maxi])
		{
			maxi = i;
		}
		if (a[i] < a[mini])
		{
			mini = i;
		}
	}
	int max = a[maxi];
	int min = a[mini];
	int range = a[maxi] - a[mini] + 1;
	int* temp = (int*)calloc(range, sizeof(int));
	if (temp == NULL)
	{
		perror("CountSort::calloc fail");
		exit(1);
	}
	for (int i = 0; i < n; i++)
	{
		temp[a[i] - min]++;
	}
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (temp[i]--)
		{
			a[j++] = i + min;
		}
	}
	free(temp);
	temp = NULL;
}

(2)稳定版本

void CountSort1(int* a, int n)
{
		int maxi, mini;
	maxi = mini = 0;
	for (int i = 1; i < n; i++)
	{
		if (a[i] > a[maxi])
		{
			maxi = i;
		}
		if (a[i] < a[mini])
		{
			mini = i;
		}
	}
	int max = a[maxi];
	int min = a[mini];
	int range = a[maxi] - a[mini] + 1;
	int* temp = (int*)calloc(range, sizeof(int));
	for (int i = 0; i < n; i++)
	{
		temp[a[i] - min]++;
	}
	for (int i = 1; i < range; i++)
	{
		temp[i] = temp[i] + temp[i - 1];
	}
	int* ans = (int*)calloc(n, sizeof(int));
	for (int i = n - 1; i >= 0; i--)
	{
		ans[temp[a[i] - min] - 1] = a[i];
		temp[a[i] - min]--;
	}
	for (int i = 0; i < n; i++)
	{
		a[i] = ans[i];
	}
	free(ans);
	ans = NULL;
	free(temp);
	temp = NULL;
}

3.分析

不难看出,在排序过程中,我们进行了两次遍历,第一次遍历的大小为N,第二次遍历的大小为数组中的最大值-最小值+1,取两次遍历中的最大值,即时间复杂度为O(MAX(N,range))。至于空间复杂度,在不稳定版本我们额外开了range个int空间,即O(range);在稳定版本我们开辟了n个int的空间和range个int的空间,即O(MAX(N,range))。

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值