第八章--排序

排序算法性能比较

在这里插入图片描述

一些小结论

1)除上述之外,折半插入排序算法:

最坏时间复杂度为O(n²),平均时间复杂度O(n²),最好时间复杂度O(nlogn),空间O(1),也是一种稳定的排序算法

(2)可以发现有用到二叉树思想的算法(快、堆、归),其三种情况下的时间复杂度基本都在(除了快速排序在最坏情况下会退化到O(n²))

(3)具有稳定性的算法:合插基冒(在这家店喝茶几毛钱一直是稳定的)

(4)若n较小,可采用直接插入排序或简单选择排序。又由于直接插入排序所需的记录移动次数较简单选择排序的多,因此当记录本身信息量较大时,用简单选择排序较好。

(5)若文件的初始状态已按关键字基本有序,则选用直接插入排序或冒泡排序

(6)若n较大,则应采用时间复杂度为的排序方法:快速排序、堆排序或合并排序。

(7)当待排序的关键字随机分布时,快速排序的平均时间最短,平均性能最优;但当排序的关键字基本有序或基本逆序时,会得到最坏的时间复杂度和最坏的空间复杂度

(8)堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况

(9)若要求排序稳定,并且时间复杂度为,则可选用合并排序

(10)当数据量较大时,可以将合并排序和直接插入排序结合使用,先利用直接插入排序求得较长的有序子文件,然后再使用合并排序两两合并,由于两种方法都是稳定的,因此结合之后也是稳定的。

(11)当n很大,记录的关键字位数较少且可以分解时,采用基数排序较好

(12)当记录本身信息量较大时,为避免耗费大量时间移动记录,可以使用链表作为存储结构(当然,有的排序方法不适用于链表)

1、插入排序

<1>直接插入排序

思路:从第二个元素开始,从左向右依次处理每个元素,,每个元素不断向前寻找自己合适的位置,(以升序为例),和前一个元素比较,如果大于交换继续向前比较,反之停止。
在这里插入图片描述

//都是升序排列
#include <iostream>
int item[100000];
int size;

void directInsertSort(int n) {
	for (int i = 0; i < n; i++) {
		int tmp = item[i];//每次要处理的元素
		//开始向前寻找合适的位置

		int pos = i-1;//从目标钱一个位置开始

		for (; pos >= 0; pos--) {//开始向前找到这个目标书应该在的位置
			if (item[i] >= item[pos]) {//比自己大,就继续往前
				break;
			}
		}

		for (int j = i - 1; j > pos; j--) {//从i-1到pos+1的数都往后移动一位
			item[j + 1] = item[j];
		}

		item[pos + 1] = tmp;//最后放入目标数
	}
}
int main(){
	scanf("%d", &size);
	for (int i = 0; i < size; i++)
	{
		scanf("%d", &item[i]);
	}
	
	directInsertSort(size);
	
	for (int i = 0; i < size; i++) {
		printf("%d ", item[i]);
	}
	return 0;
}

效率分析:
在这里插入图片描述

<2>折半插入排序

真题中出现过,特此整理思路代码
在这里插入图片描述

思路:
1、把插入排序的,一项一项向前寻找合适位置的过程优化
2、因为处理第i处,那么从0到i-1之间一定已经满足递增了
3、所以把原本一项一项寻找的过程,改成了利用折半(二分)法,找目标位置
4、写二分法时候,如果在区间内找到了目标,说明这是重复元素,因为这个算法稳定,所以返回后面的一个位置
5、没重复元素,一定是查找失败,在left>right时候,因为right非法,所以目标位置就是right+1(自己举个例子就知道了)
6、因为二分法,是已经排好序得了,所以每次分割有目的性,直达目标,所以一旦停下来,说明已经找到合适位置了

#include <iostream>
#include <string>
#include <stack>
#include <queue>
#include <unordered_map>
#include <vector>
#include <algorithm>
using namespace std;
int findPos(int tar, int left, int right) {
	if (left > right) {//左大于右,非法情况,这个这么记忆,如果right==-1,则代表着插到 -1+1=0的位置处
		return right + 1;//right位置非法,所以返回right+1
	}
	int mid = (left + right) / 2;
	if (two[mid] == tar) {
		return mid + 1;//为了保持稳定性,所以返回当前位置+1
	}
	else if (two[mid] > tar) {
		return findPos(tar, left, mid - 1);//有目的性进行递归深入,只会到终点一次
	}
	else {
		return findPos(tar, mid + 1, right);
	}
}
void function_two() {
	cin >> n;
	for (int i = 0; i < n; i++) {
		cin >> two[i];
	}
	for (int i = 1; i < n; i++) {
		int tmp = two[i];
		int pos = findPos(tmp, 0, i - 1);//得到插入位置
		for (int j = i; j >= pos; j--) {//按照插入位置挪动
			two[j] = two[j - 1];
		}
		two[pos] = tmp;
	}
	for (int i = 0; i < n; i++) {
		cout << two[i] << ' ';
	}
	return;
}

<3>希尔排序

思路:为了解决直接插入排序,小元素太靠后,而造成的频繁移动问题,所以使用了希尔排序,所以,通过分组(如何分组,看的是增量K),先把小元素往左移,大元素右移,让最后的排序(就相当于对元素整理过后的直接插入)移动次数大幅减少。
在这里插入图片描述

#include <iostream>
int item[100000];
int size;

class shall_sort {
public:
	shall_sort() {
		this->n = size;
		for (int i = 0; i < n; i++) {
			arrays[i] = item[i];
		}
	}

	void shallSort() {
		int k = n / 2;//初始规模
		while (k >= 1) {//直到k=1作为最后一次移动,整体作为一组,此时小的元素整体在左面,大的在右面,在直接插入效率快很多,之前相当于预处理
			//k固定,分组确定了,所以把每个分组中的第二个节点以及往后的节点进行直接插入排序
			for (int i = k; i < n; i++) {//0->k-1为各个组的第一个节点,所以从k开始,即第一组第二节点开始,把后面的各个组的每个节点都直接插入到改组正确位置
				derectInsert(i, k);//针对每个点(start),在该分组中(pace决定)插入到合适位置
			}
			k /= 2;//缩小规模
		}
	}

	void Print() {
		for (int i = 0; i < n; i++) {
			printf("%d ", arrays[i]);
		}
	}

private:
	int arrays[100050];
	int n;
	//确定pace,一个组当中,从第二个节点开始,每个节点的向前移动寻找本组中合适位置的过程
	//和原本的直接插入不一样,每次处理的都是大数组中的一部分,所以需要处理位置,以及每个元素之间间隔(pace)
	void derectInsert(int start, int pace) {//参数:本次直接排序起点,以及每次向前的步伐(用于找到本组在他前面的节点)
		if (start >= n) {//边界判断
			return;
		}
		int pos = start - pace;//本组中离他最近的节点
		while (pos >= 0) {//防止越界
			if (arrays[pos] <= arrays[start]) {//因为升序排序,所以发现<=目标的停止
				break;
			}
			int tmp = arrays[start];//反之交换
			arrays[start] = arrays[pos];
			arrays[pos] = tmp;
			start = pos;//更新起点以及上一个元素位置,循环向前
			pos = start - pace;
		}
	}
};

int main(){
	scanf("%d", &size);
	for (int i = 0; i < size; i++)
	{
		scanf("%d", &item[i]);
	}

	shall_sort* ss = new shall_sort();
	ss->shallSort();
	ss->Print();
	
	return 0;
}

算法性能分析:
在这里插入图片描述

2、交换排序

<1>冒泡排序

思路:就是一趟一趟的把两两之间有顺序问题的两个元素进行交换,直到没问题为止
在这里插入图片描述

#include <iostream>
int item[100000];
int size;

void bubbleSort(int n) {
	//是否会产生交换标志
	bool flag = true;
	//开始不间断的交换,直到不再产生交换位置
	while (flag) {
		flag = false;//默认本轮不产生交换

		for (int i = 0; i < n - 1; i++) {//相邻的数据两两比较
			if (item[i] > item[i + 1]) {//发现可交换(因为是升序且稳定,所以条件是 前>后)
				//发生了交换,还得再来一轮
				flag = true;
				//交换
				int tmp = item[i + 1];
				item[i + 1] = item[i];
				item[i] = tmp;
			}
		}
	}
}
int main(){
	scanf("%d", &size);
	for (int i = 0; i < size; i++)
	{
		scanf("%d", &item[i]);
	}
	
	bubbleSort(size);

	for (int i = 0; i < size; i++) {
		printf("%d ", item[i]);
	}

	return 0;

}

算法性能分析
在这里插入图片描述

补充:二维数组的冒泡排序

1、不要被二维数组给骗了,因为其在电脑中的存储地址是连续的:
*(基地址+该元素前面的元素个数)
就可以访问任意一个二维数组的元素,所以就等效于把二维数组降为一维处理
2、共n^2个元素,最后一个元素表示为 *(基地址+n^2-1),因为要两两交换,所以枚举到 n^2-2

#include <iostream>
#include <string>
#include <stack>
#include <queue>
#include <unordered_map>
#include <vector>
#include <algorithm>
using namespace std;
//第四题
//如果排序之后满足行列都是递增,则必然满足题意,所以本题就是二维数组排序问题。
//一维数组,直接用下标寻址,二维数组用,(基地址+前方元素数)可以模拟出一维寻址效果,进而使用一维的冒泡排序
int four[5][5];
void bubbleSort(int n) {
	bool flag = true;
	int * base_address = &four[0][0];//基地址
	while (flag) {
		flag = false;
		for (int i = 0; i < n*n - 1; i++) {//枚举间隔数,比如a[0][1],前面有一个元素,间隔数为1,所以(基地址+1)就是它 
			if (*(base_address + i) > *(base_address + i + 1)) {
				flag = true;
				int tmp = *(base_address + i);
				*(base_address + i) = *(base_address + i + 1);
				*(base_address + i + 1) = tmp;
			}
		}
	}
}
int main()
{
	int four_n = 5;
	for (int i = 0; i < four_n; i++) {
		for (int j = 0; j < four_n; j++) {
			cin >> four[i][j];
		}
	}
	bubbleSort(four_n);

	cout << endl;
	for (int i = 0; i < four_n; i++) {
		for (int j = 0; j < four_n; j++) {
			cout << four[i][j] << ' ';
		}
		cout << endl;
	}
	return 0;
}

补充:链表的冒泡排序

在这里插入图片描述
像这种题,未规定方法,给链表排序,推荐直接使用冒泡,因为最简单,思路和数组排序最接近

思路:
1、也是用flag控制何时排序全部停止
2、因为涉及交换两个节点,共需要标记四个节点,但是由于但项链表属性,所以只需要标记前三个即可
3、内层循环的话,保证不越界就行,所以让third指针不是nullptr
4、余下和数组版本一致
5、默认有哨兵节点

#include <iostream>
#include <string>
#include <stack>
#include <queue>
#include <unordered_map>
#include <vector>
#include <algorithm>
using namespace std;
typedef struct node {
	int data;
	node* next;
};
node* function_seven(node* l) {//默认l有哨兵节点
	//预处理2000以内所有素数:欸氏筛法
	bool primeNum[2000];
	for (int i = 2; i < 2000; i++) {
		primeNum[i] = true;
	}
	for (int i = 2; i < 2000; i++) {
		if (primeNum[i]) {
			for (int j = 2 * i; j < 2000; j += i) {
				primeNum[i] = false;
			}
		}
	}
	//删除val为素数的节点
	node *cur = l->next, *pre = l;
	while (cur != nullptr) {
		if (primeNum[abs(cur->data)]) {//绝对值是素数就删除,并且删除之后,指针不用动,恰好符合下一次的判定
			node *tmp = cur;

			cur = cur->next;
			pre->next = cur;

			tmp->next = nullptr;//删除
			free(tmp);
		}
		else {
			pre = cur;
			cur = cur->next;
		}
	}

	//开始冒泡排序,按照递减的顺序,整体上和数组冒泡一个写法
	bool flag = true;
	while (flag) {
		flag = false;
		//first second third是用来辅助一次遍历过程中交换所用,所以定义在里面
		node *first = l, *second = l->next, *third = l->next->next;//交换两个节点,需要标记四个节点的位置,因为链表属性,只需要前三个就行
		while (third != nullptr) {
			if (second->data < third->data) {//递减排序
				flag = true;//标志产生交换

				first->next = third;//交换顺序绝对不能变
				second->next = third->next;
				third->next = second;

			}
			first = first->next;
			second = second->next;
			third = third->next;
		}
	}

	return l;
}

<2>快速排序

思路:分而治之,让每个元素左面全小于他,右面全大于他即可
(下图所示是快速排序一次的交换过程,此次交换完事之后,Pivot左面都小于他,右面都大于他,所以继续分化,直到每个元素都具备上述性质即可)
在这里插入图片描述

//都是升序排列
#include <iostream>
int item[100000];
int size;

class quick_sort {
public:
	quick_sort() {
		this->n = size;
		for (int i = 0; i < n; i++) {
			arrays[i] = item[i];
		}
	}
	//快速排序
	void quickSort(int left, int right) {
		if (left > right) {//递归退出条件
			return;
		}
		int mid = partition(left, right);//mid左面都小于他,右面都大于他
		quickSort(left, mid - 1);//同样的思路继续对左面分治
		quickSort(mid + 1, right);//同样的思路继续对右面分治
	}
	void Print() {
		for (int i = 0; i < n; i++){
			printf("%d ", arrays[i]);
		}
	}
private:
	int arrays[100050];
	int n;
	//每次快排内部的划分过程
	int partition(int low, int hight) {
		int base = arrays[low];//基点
		while (low < hight) {
			//注意:
			//1、基点选在左边界,那就从右面开始找,反之从左面开始找
			//2、时刻注意,low<hight,二者一旦相等,意味着这次划分就结束了
			while (low < hight&&arrays[hight] >= base) {//hight那边元素必须>=base
				hight--;
			}
			//发现 < base,交换到low那一侧
			arrays[low] = arrays[hight];
			while (low < hight&&arrays[low] <= base) {//low那边元素必须<=base
				low++;
			}
			//发现 > base, 交换到low那一侧
			arrays[hight] = arrays[low];
		}
		arrays[low] = base;//二者相遇,base左面都小于他,右面都大于他
		return low;
	}
};

int main(){
	scanf("%d", &size);
	for (int i = 0; i < size; i++)
	{
		scanf("%d", &item[i]);
	}
	
	quick_sort* qs = new quick_sort();
	qs->quickSort(0, size - 1);
	qs->Print();

	return 0;

}

算法性能分析:
在这里插入图片描述
在这里插入图片描述
(二图时空复杂度有地方反了,参考一图)

3、选择排序

<1> 堆排序

思路:利用大根堆性质,每次快速在堆顶获得最大值,和此规模下最后一个元素换位置,减小规模,再把堆顶元素下沉(重整堆,这也是删除堆中任何一个位置元素的思路)。
(下图中我们建堆的思路与之不同,我们先把数组输入进来,把每个位置的元素都下沉一遍来建堆)
在这里插入图片描述

//都是升序排列
#include <iostream>
int item[100000];
int size;

class heap_sort {//因为升序排列,所以使用大根堆
public:
	heap_sort() {
		this->n = size;//初始化规模
		for (int i = 1; i <= n; i++) {//初始化堆数组
			heaps[i] = item[i - 1];
		}
		//重整堆
		for (int i = n; i >= 1; i--) {
			Down(i, n);//把堆每个位置处的元素都下沉一次就行
		}
	}
	void heapSort() {
		for (int i = n; i >= 1; i--) {//i表示当前处理规模
			//每次把当前规模的:堆顶(最大)放到最后(因为是升序排列),进入结果集
			int tmp = heaps[1];
			heaps[1] = heaps[i];
			heaps[i] = tmp;
			Down(1, i - 1);//此时规模减一,相当于删除了堆顶,把堆顶元素1重新下沉重整堆即可。

		}
	}
	void print() {
		for (int i = 1; i <= n; i++) {
			printf("%d ", heaps[i]);
		}
	}
private:
	int heaps[100050];//完全二叉树用数组存储,儿子节点和父亲节点有固定的计算方式,从1开始
	int n;//总规模

	//封装堆操作--上浮
	void Up(int k, int size) {//第几个元素,此时规模是多大
		int fa = k >> 1;//父亲节点 / 2
		while (fa > 0) {//从零开始存储,边界就是0,不能超过边界
			if (heaps[fa] > heaps[k]) {//大根堆,父亲比自己大,停止
				break;
			}
			int tmp = heaps[fa];//交换
			heaps[fa] = heaps[k];
			heaps[k] = tmp;
			k = fa;//准备继续进行之前步骤
			fa >>= 1;
		}
	}
	//封装堆操作--下沉
	void Down(int k, int size) {
		int son = k << 1;//寻找儿子节点 *2
		while (son <= size) {//范围
			if (son + 1 <= size && heaps[son] < heaps[son + 1]) {//两个儿子,挑大的上来
				son++;
			}
			if (heaps[k] > heaps[son]) {//结束标志,儿子没有比他大的,因为大根堆
				break;
			}
			int tmp = heaps[son];//交换
			heaps[son] = heaps[k];
			heaps[k] = tmp;
			k = son;//准备下一次
			son <<= 1;
		}
	}
};

int main(){
	scanf("%d", &size);
	for (int i = 0; i < size; i++)
	{
		scanf("%d", &item[i]);
	}

	heap_sort* hs = new heap_sort();
	hs->heapSort();
	hs->print();

	return 0;

}

算法性能分析:
在这里插入图片描述

<2> 堆排序灵活应用

在这里插入图片描述
1、这种题就不要拘泥于传统的堆排序模型,那样的话复杂度会稳定在O(nlogn)在极大的数据量面前,复杂度还是太高
3、堆本质上还是一棵二叉树,所以我们不要让二叉树生长的太大,也是不好的,这样每次调整都会很慢很慢
2、这种题明确地指出了,我就要这100万人中的前二十,所以,我们只需要维护20个名额就行,这样,二叉树最多就5层,每次调整最多五次,常数级别的调整,大幅缩短了调整所耗费的时间。
4、综上所述,我们维护一个20个元素的 “ 小根堆 ”,每次新元素,如果比栈顶元素大,那么就完全可能是前二十中的一员,入堆,如果小于堆顶,就意味着,小于前二十中最小的元素(堆顶表示堆中最小的元素),一定不是答案
5、这样,时间复杂度就降低到了O(n)。

注:以上过程,可以更形象地理解为,我拿一个20个元素的过滤器,把所有元素扫一遍,把大的富集进来

4、合并排序

<1>二路归并排序

思路:逐层划分至只有一个元素,在逐层合并(所以后根遍历法)。因为是升序序列,所以每次从两个队头里面找最小
在这里插入图片描述
在这里插入图片描述

//都是升序排列
#include <iostream>
int item[100000];
int size;

class merge_sort {
public:
	merge_sort() {
		this->n = size;
		for (int i = 0; i < n; i++) {
			arrays[i] = item[i];
		}
	}
	//归并排序
	void mergeSort(int left, int right) {
		if (left >= right) {//退出条件
			return;
		}
		int mid = (left + right) / 2;
		//因为归并是要把元素先打散成逐个的,再逐一合并,所以后根遍历法
		mergeSort(left, mid);
		mergeSort(mid + 1, right);
		merge(left, mid, right);
	}
	void Print() {
		for (int i = 0; i < n; i++) {
			printf("%d ", arrays[i]);
		}
	}
private:
	int arrays[100050];
	int n;
	//合并过程
	void merge(int left, int mid, int right) {

		int point = left;//指向原数组指针,指示下一个元素插入位置
		int l = left;//左面子序列指针
		int r = mid + 1;//右面子序列指针
		int tmp[100050];//临时数组,用于记录当前子序列,帮助实现归并

		for (int i = left; i <= right; i++) {//初始化
			tmp[i] = arrays[i];
		}

		while (l <= mid && r <= right) {//两个小的子序列,每次比队头,因为是升序,所以谁小谁上
			if (tmp[l] <= tmp[r]) {
				arrays[point++] = tmp[l++];
			}
			else {
				arrays[point++] = tmp[r++];
			}
		}
		while (l <= mid) {//把剩下元素补齐
			arrays[point++] = tmp[l++];
		}
		while (r <= right) {//把剩下元素补齐
			arrays[point++] = tmp[r++];
		}
	}
};

int main(){
	scanf("%d", &size);
	for (int i = 0; i < size; i++)
	{
		scanf("%d", &item[i]);
	}

	merge_sort* ms = new merge_sort();
	ms->mergeSort(0, size - 1);
	ms->Print();

	return 0;

}

算法性能分析:
在这里插入图片描述

<2>链表法的归并排序

总体思想:链表和数组有本质不同,链表无法像数组一样随机存取,所以不能用其指针移动表示的方法,但是链表由于是链式结构,拆分很方便,所以使用归并排序最本质的思路,拆分法,不断拆分出子链(结尾用nullptr标识),最后合并也是拿两条子链不断合并成一个完整的,也并未多余开出空间(因为串糖葫芦法)

#include <iostream>
#include <string>
#include <stack>
#include <queue>
#include <unordered_map>
#include <vector>
#include <algorithm>
using namespace std;

typedef struct node {//按照教材来,这里使用typedef
	int val;
	node* next;
}node;

//以left和right开头的小链表,结尾是nullptr
node* merge(node* left, node* right) {//这个函数就是把这两个链表串糖葫芦穿起来

	//标识新的串的头部,哨兵节点,作为临时节点,最后去掉
	node* head = new node(); //串糖葫芦的头
	node* tail = head;//用于加入节点,串糖葫芦的线

	//left right作为标识节点不可动
	node *p1 = left, *p2 = right;//作用于其上的指示指针

	//合并的思想和数组一致,方法为串糖葫芦
	while (p1 != nullptr&&p2 != nullptr) {
		if (p1->val <= p2->val) {//p1为目标
			tail->next = p1;
			tail = p1;
			p1 = p1->next;
		}
		else {//p2为目标
			tail->next = p2;
			tail = p2;
			p2 = p2->next;
		}
	}

	while (p1 != nullptr) {
		tail->next = p1;
		tail = p1;
		p1 = p1->next;
	}
	while (p2 != nullptr) {
		tail->next = p2;
		tail = p2;
		p2 = p2->next;
	}

	tail->next = nullptr;//必须处理结尾,必须明确指向空
	return head->next;//去掉哨兵

}

//递归函数,思想和数组版本的一致
//出口判定,不要她是空,在之前就退出
//中点判定,快慢指针法
//分划思想,同数组,也是后根遍历
node* mergesort(node* cur) {
	//出口
	if (cur->next == nullptr) {//只有自己一个的时候出口
		return cur;
	}
	//准备快慢指针找中间节点
	node *slow = cur, *fast = cur;
	//用于标识中间节点位置,方便拆分字串(他作为左字串的末尾,fast作为右子串的末尾,cur作为左字串开头,slow作为右子串开头)
	node *lefttail = slow;
	while (fast != nullptr&&fast->next != nullptr) {
		lefttail = slow;
		slow = slow->next;
		fast = fast->next->next;
	}
	lefttail->next = nullptr;
	//后跟法
	node* left = mergesort(cur);
	node*right = mergesort(slow);
	return merge(left, right);//把串好的串返回去
}

int main()
{
	int a[7] = { 49, 38, 65, 97, 76, 13, 27 };
	int n = 7;
	node* head = (node*)malloc(sizeof(node));
	node* tail = head;
	for (int i = 0; i < n; i++)
	{
		node* tmp = (node*)malloc(sizeof(node));
		tmp->val = a[i];
		tail->next = tmp;
		tail = tmp;
	}
	tail->next = NULL;
	printf("排序前:\n");
	for (node* it = head->next; it != NULL; it = it->next)
	{
		printf("%d ", it->val);
	}printf("\n\n");

	head->next = mergesort(head->next);

	printf("排序后:\n");
	for (node* it = head->next; it != NULL; it = it->next)
	{
		printf("%d ", it->val);
	}
	return 0;
}

5、基数排序

算法流程(注:基数排序关键字比较次数为0)
在这里插入图片描述
基数排序的操作为:

在这里插入图片描述
效率分析(吉大教材上计算时间复杂度时不考虑基数r,只算O(nd)):
在这里插入图片描述

基数排序不只是用于排序若干个数字,其思想也可以用于其他信息的排序
在这里插入图片描述
基数排序的适用情况:
在这里插入图片描述
在这里插入图片描述

6、计数排序

多次在专硕真题之中,以程序填空的形式出现,特此记录
在这里插入图片描述
count数组记录前面有几个比它大的(从1到n)
步骤:
1、初始化,count数组全置为1
2、从n开始,枚举n-1次,到2
3、针对每个i,都要向前看,谁更大,谁的count数组+1
<1>Kj>Ki,马上给count[J]++
<2>反之 ,马上给count[I]++

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JLU_LYM

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值