数据结构初阶

一.线性表

1. 顺序表

(1)顺序表特点

d986ad87199744919bf290c55690188a.png  

473df9827a1b4a1786423fe14c399d33.png

(2)顺序表创建

#define maxsize 100

typedef int ElemType;  //定义基本的数据类型 

 
typedef struct   //定义线性表的物理储存方式
{
	ElemType data[maxsize];
	int length;
}SqList;


//由数组元素a[0...n-1]创建顺序表L

void CreatList(SqList* L, ElemType a[], int n)
{
	int i = 0, k = 0;
	L = (SqList*)malloc(sizeof(SqList));
	while (i < n)
	{
		L->data[k] = a[i];     //将元素a[i]存到L中去
		k++;
		i++;
	}
	L->length = k;    //设置表L的长度为k
}

(3)顺序表基本操作

//初始化线性表
void InitList(SqList* L)
{
	L = (SqList*)malloc(sizeof(SqList));
	L->length = 0;
}


//销毁线性表
void DestroyList(SqList* L)
{
	free(L);
	L->length = 0;
}


//判断是否为空表
bool ListEmpty(SqList* L)
{
	return (L->length == 0);
}


//求线性表的长度
int ListLength(SqList* L)
{
	return (L->length);
}


//输出线性表
void DisList(SqList* L)
{
	for (int i = 0; i < L->length; i++)
	{
		printf("%d ", L->data[i]);
	}
	printf("\n");
}


//取出线性表中的元素
bool GetElem(SqList* L, int i, ElemType e)
{
	if (i<1 || i>L->length)
		return false;
	else
	{
		e = L->data[i - 1];    //第i-1的单元存储着第i个数据
	}
	return true;
}


//按(元素)值查找
int LocateElem(SqList* L, ElemType e)
{
	int i = 0;
	for (i = 0; i < L->length; i++)
	{
		if (e == L->data[i])
			return i + 1;    //返回逻辑序号
	}
	return 0;   //查找失败
}


//插入数据元素
bool ListInsert(SqList* L, int i, ElemType e)
{
	if (i<1 || i>L->length || (L->length == maxsize))
	{
		return false;
	}
	i = i - 1;         //先将逻辑符号转成数组下标(物理符号)
	for (int j = L->length; j < i; j--)   //L->length记录的数组下标
	{
		L->data[j] = L->data[j - 1];     //都往后移动一位
	}
	L->data[i] = e;            //放入新数据
	L->length++;              //长度加一
	return true;
}


//删除数据元素
bool ListDelete(SqList* L, int i)
{
	if (i<1 || i>L->length)
		return false;
	i = i - 1;        //将逻辑符号转成数组下标(物理符号)
	for (int j = i; j < L->length; j++)
	{
		L->data[j] = L->data[j + 1];
	}
	L->length--;
	return true;
}

2.链式存储结构——链表

(1)基础知识

d155fe36141145c28a4d949c795e7267.png

(2)单链表

(传值和传址)

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode *next;
}SLTNode;

//打印链表
void SListPrint(SLTNode* phead)
{
	SLTNode* p = phead;
	while(p != NULL)
	{
		printf("%d ", p->data);
		p = p->next;
	}
	prinft("NULL\n");
}

//创建结点
SLTNode* Creatnode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);       //完全结束程序
	}
	newnode->data = x;
	newnode->next = NULL; 
	return newnode;
}


//头插
void SList_HeadInsert(SLTNode** phead, SLTDataType x)
{
	SLTNode* newnode = Creatnode(x);
	newnode->next = *phead;  
	*phead = newnode;  //使用二级指针,就可以改变phead了
}

//尾插
void SList_EndInsert(SLTNode** phead,SLTDataType x)  
//要改变链表phead需要用到二级指针
//传指针,然后用指针接受,当然是传值调用
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); 
	//因为内存上不相邻,所以先开辟和后开辟内存都是一样的
	newnode->data = x;
	newnode->next = NULL;

	if (*phead == NULL)  //对二级指针解引用
	{
		*phead = newnode;   //把一级指针改成newnode,而不是二级指针
	}
	else
	{
		SLTNode *p = *phead;  //要存一级指针(*phead),phead是个二级指针,需要先解引用
		while (p->next != NULL)
		{
			p = p->next;
		}
		p->next = newnode;
	}
}


//头删
void SList_HeadDelete(SLTNode** phead)
{
	if (*phead == NULL)
	{
		return;
	}
	else
	{
		SLTNode* q = (*phead)->next;
		free(*phead);
		*phead = q;
	}
}


//尾删
void SList_EndDelete(SLTNode** phead) 
//因为删到最后一个的时候要改变原指针为空,所以还是传二级
{
	if (*phead == NULL)
	{
		return;
	}

	if ((*phead)->next == NULL)
	{
		free(*phead);
		*phead = NULL;
	}
	else
	{
		SLTNode* p = *phead;
		while (p->next->next != NULL)
		{
			p = p->next;
		}
		free(p->next);
		p->next = NULL;
	}
}



//头插
void SList_HeadInsert(SLTNode** phead, SLTDataType x)
{
	SLTNode* newnode = Creatnode(x);
	newnode->next = *phead;
	*phead = newnode;  //使用二级指针,就可以改变phead了
}

//查找
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* p = phead;
	while (p)
	{
		if (p->data == x)
		{
			return p;
		}
		else
		{
			p = p->next;
		}
	}
	return NULL;
}

//在pos之前一个位置插入
void SListInsert_1(SLTNode** phead, SLTNode* pos, SLTDataType x)
{
	if (pos==*phead)
	{
		SList_HeadInsert(phead, x);  //当在最前面插入时,为头插
	}
	else
	{
		SLTNode* newnode = Creatnode(x);
		SLTNode* p = *phead;
		while (p->next != pos)  //找到pos前一个结点
		{
			p = p->next;
		}
		p->next = newnode;
		newnode->next = pos;
	}
}

//在pos之后一个位置插入(更适合单链表)
void SListInsert(SLTNode* pos, SLTDataType x)
{
	SLTNode* newnode = Creatnode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}



//删除pos位置的结点
void SListDelete(SLTNode** phead,SLTNode* pos)
{
	if (pos==*phead) //用头删
	{
		SList_HeadDelete(phead);
	}
	else
	{
		SLTNode* p = *phead;
		while (p->next != pos)
		{
			p = p->next;
		}
		p->next = pos->next;
		free(pos);
	}
}

//销毁整个链表
void DistoryList(SLTNode** phead)
{
	assert(phead);   //assert是当括号内为假(0)的时候报错
	SLTNode* p = *phead;
	while (p)
	{
		SLTNode* q = p->next;
		free(p);
		p = q;
	}
	*phead = NULL; //销毁链表后,phead也用不到了,所以置空
}
//手动构建链表,方便调试oj代码

int main()
{
	struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
	n1->val = 7;
	n2->val = 7;
	n3->val = 7;
	n4->val = 7;
	n1->next = n2;
	n2->next = n3;
	n3->next = n4;
	n4->next = NULL;
	return 0;
}

(3)单循环链表

循环链表大多用尾指针,因为更方便,时间复杂度更低

//带尾指针循环链表的合并
LinkNode* Connect(LinkNode* Ta, LinkNode* Tb)
{
    LinkNode* p = Ta->next;  //因为之后Ta->next 要被改变,所以先保存起来
    Ta->next = Tb->next->next;  //因为头结点合并时不保留,所以是Tb->next->next
    free(Tb->next); //释放Tb的表头(头结点)
    Tb->next = p;  //Tb->next指向p,把Tb表尾指向Ta表头
    return Tb;
}

(4)带头双向循环链表

typedef int LTdataType;

typedef struct ListNode
{
	LTdataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;


//双链表初始化
LTNode* ListInit()
{
	LTNode* phead =(LTNode*)malloc(sizeof(LTNode));
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

//创造新结点
LTNode* Creatnode(LTdataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}


//打印链表
void ListPrint(LTNode* phead)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		prinft("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

//尾插
void List_EndInsert(LTNode* phead, LTdataType x)
{
	LTNode* tail = phead->prev;
	//先找尾
	LTNode* newnode = Creatnode(x);
	//链接
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

//尾删
void List_EndDelete(LTNode* phead)
{
	if ((phead->next == phead) || phead == NULL) //头结点不能删
	{
		printf("删空了!\n");
		return;
	}
	LTNode* tail = phead->prev;
	LTNode* newtail = phead->prev;
	free(tail);

	newtail->next = phead;
	phead->prev = newtail;
}


//头插
void List_HeadInsert(LTNode* phead, LTdataType x)
{
	if (!phead)
	{
		printf("phead为空指针!\n");
		return; 
	}
	LTNode* newnode = Creatnode(x);
	LTNode* cur = phead->next;

	newnode->next = cur;
	cur->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;
}

//头删
void List_HeadDelete(LTNode* phead) //因为有头结点,所以不会改变phead,所以一级指针就够了
{
	if ((phead->next == phead) || phead == NULL) //头结点不能删
	{
		printf("删空了!\n");
		return;
	}
	LTNode* target = phead->next; //尽量多定义变量
	LTNode* next = target->next;

	phead->next = next;
	next->prev = phead;
	free(target);
}


//查找
LTNode* FindNode(LTNode* phead, LTdataType x)
{
	if (!phead)
	{
		printf("phead为空指针!\n");
		return;
	}
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}


//在pos之前插入
void PosInsert(LTNode* pos, LTdataType x)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* newnode = Creatnode(x);

	posPrev->next = newnode;
	newnode->prev = posPrev;
	newnode->next = pos;
	pos->prev = newnode;

}
//对应的头插
void pos_head(LTNode* phead, LTdataType x)
{
	assert(phead);
	PosInsert(phead->next, x);
}
//对应的尾插
void pos_End(LTNode* phead, LTdataType x)
{
	assert(phead);
	PosInsert(phead, x);
}


//删除pos位置
void pos_Erase(LTNode* pos)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;

	posPrev->next = posNext;
	posNext->prev = posPrev;
	free(pos);
}
//对应的头删
void pos_delete(LTNode* phead, LTdataType x)
{
	assert(phead);
	pos_Erase(phead->next);
}
//对应的尾删
void pos_delete(LTNode* phead, LTdataType x)
{
	assert(phead);
	PosInsert(phead->prev);
}

//销毁链表
void ListDestroy(LTNode* phead)
//因为是一级指针,所以最后需要在函数引用完之后把phead置空
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

(6)链式存储结构优缺点

762562332e7f4c0e8f678e028ed62200.png

三.栈和队列

1.栈

typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}ST;

void StackInit(ST* st)
{
	assert(st);
	st->a = NULL;
	st->top = 0;
	st->capacity = 0;
}

bool StackEmpty(ST* st)
{
	assert(st);
	return st->top == 0;
}

void StackDestroy(ST* st)
{
	assert(st);
	free(st->a);
	st->a = NULL;
	st->top = st->capacity = 0;
}

//压入栈中
void StackPush(ST* st,STDataType x)
{
	assert(st);
	if (st->top == st->capacity) //扩容
	{
		int newCapacity = (st->capacity == 0 ? 4 : st->capacity * 2);
		STDataType* tmp = (STDataType*)realloc(st->a, sizeof(STDataType) * newCapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		st->a = tmp;
		st->capacity = newCapacity;
	}
	st->a[st->top] = x;
	st->top++;
}

//从栈中拿出
void StackPop(ST* st)
{
	assert(st);
	if (StackEmpty(st))
	{
		printf("栈是空的\n");
		return ;
	}
	st->top--;  //之后的空间还有可能用到,当真正用不到的时候再用StackDestroy来释放
}

STDataType StackTop(ST* st)
{
	assert(st);
	if (StackEmpty(st))
	{
		printf("栈是空的\n");
		return -1;
	}
	return st->a[st->top - 1];  //栈顶与数组实际上还差一
}

STDataType StackSize(ST* st)
{
	assert(st);
	return st->top;
}

2.队列 

typedef int QDataType;
typedef struct QueueNode  //队列的结点(像链表)
{
	struct QueueNode* next;
	QDataType data;
}QueueNode;

typedef struct Queue  //队列只需要控制两个出入口就行了
					  //如果不用结构体就要把两个指针拆开写,而且还要用二级指针
{
	QueueNode* head;
	QueueNode* tail;
	int size;
}Queue;


void QueueInit(Queue* pq)
{
	assert(pq);
	pq->head = NULL;
	pq->tail = NULL;
	pq->size = 0;
}

void QueueDestroy(Queue* pq)
{
	assert(pq);
	QueueNode* cur = pq->head;
	while (cur != NULL)
	{
		QueueNode* next = cur->next;
		free(cur);
		cur = next;
	}
	pq->head = pq->tail = NULL;
	pq->size = 0;
}

//判断队列空不空
bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return (pq->head == NULL && pq->tail == NULL);
}

void QueuePrint(Queue* pq)
{
	while (!QueueEmpty(pq))
	{
		QDataType front = QueueFront(pq);
		printf("%d ", front);
		QueuePop(pq);
	}
	printf("\n");
}

//进数据 (因为队列进入的方向被固定了,只有一个方向进)
void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);
	QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
	newnode->data = x;
	newnode->next = NULL;

	if (pq->head == NULL)
	{
		pq->head = pq->tail = newnode;
	}
	else  //链接到队尾
	{
		pq->tail->next = newnode;
		pq->tail = newnode;
	}
	pq->size++;
}

//出数据(一个一个出)
void QueuePop(Queue* pq)
{
	assert(pq);
	if (QueueEmpty(pq))
	{
		printf("队列的头指针为空\n");
		return;
	}

	//一个结点
	if (pq->head->next == NULL)
	{
		free(pq->head);
		pq->head = pq->tail = NULL;  //当head指向最后的空指针时,队列已经被删完了,tail要置空,不然tail野指针
	}
	else //头删
	{
		QueueNode* next = pq->head->next;
		free(pq->head);
		pq->head = next;
	}
	pq->size--;
}

//取队头的数据
QDataType QueueFront(Queue* pq)
{
	assert(pq);
	if (QueueEmpty(pq))
	{
		printf("队列的头指针为空\n");
		return -1;
	}
	return pq->head->data;
}

//取队尾的数据
QDataType QueueBack(Queue* pq)
{
	assert(pq);
	if (QueueEmpty(pq))
	{
		printf("队列的头指针为空\n");
		return -1;
	}
	return pq->tail->data;
}

//队列个数
int QueueSize(Queue* pq) 
{
	assert(pq);
	return pq->size;
}

四.树 

1.基本概念

左孩子右兄弟表示法:

2. 基础二叉树

(1)概念

 

 (图示位置就是从左到右不连续的)

(2)数组存储 

(3)堆

(4)堆的实现(以小堆为例)

 改成大堆需要修改Adjustup,AdjustDown,HeapPush等的大小于号。

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

void HeapInit(HP* php);
void Swap(HPDataType* p1, HPDataType* p2);
void HeapDestroy(HP* php);
void HeapPush(HP* php, HPDataType x);
void AdjustUp(HPDataType *a, int child);  //从孩子的位置开始向上调
void AdjustDown(int* a, int n, int parent);  //从父亲的位置开始向下调
//这两个函数的前提都是左子树和右子树是大堆/小堆
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
bool HeapEmpty(HP* php);
int HeapSize(HP* php);
void PrintHeap(HP* php);

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void HeapInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = 0;
	php->size = 0;
}

void HeapDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;
}

void AdjustUp(HPDataType* a, int child) //调试: php->a,6 看6个
{
	int parent = (child - 1) / 2;
	while (child>0) //到根的时候child等于0了
	{
		if (a[child] < a[parent]) //这是小堆排列,大堆相反 a[child] > a[parent]
		{
			Swap(&a[child], &a[parent]);   //别忘了取地址

			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity) //扩容
	{
		int newcapacity = (php->capacity == 0 ? 4 : php->capacity * 2);
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			return;
		}
		php->capacity=newcapacity;
		php->a = tmp;
	}
//1.插入
	php->a[php->size] = x;
	php->size++;
//2.向上调整
	AdjustUp(php->a, php->size-1);  //传过去的是孩子的位置,而孩子的位置(size)刚++了,所以减掉
}

void AdjustDown(int *a,int size,int parent)
{
	int child = parent * 2 + 1;  //假设左孩子是小的
	while (child < size) //孩子出界了结束
	{
		//选出左右孩子中小/大的那一个,child+1<size 保证了只有左孩子没有右孩子的情况,就是说不用与右孩子作比较了,最小的就是左孩子!
		if (child+1<size && a[child + 1] < a[child]) //要是右孩子比左孩子小,那么孩子就是右孩子(= parent * 2 + 2) 大堆:a[child + 1] > a[child]
		{
			child++;
		}

		if (a[child] < a[parent]) //大堆: a[child] > a[parent]
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;  //继续假设左孩子是小的
		}
		else
		{
			break;
		}
	}
}

//删除堆顶数据
void HeapPop(HP* php)
{
	assert(php);
	if (HeapEmpty(php))
	{
		printf("堆是空的\n");
		return;
	}
//1.先首尾交换,再删除,不然直接删除会破坏堆的结构
	Swap(&php->a[0], &php->a[php->size - 1]);   //别忘了取地址,取地址
	php->size--;  //删除操作

//2.删除之后就不是堆了,向下调整为堆
	AdjustDown(php->a, php->size, 0);
}


HPDataType HeapTop(HP* php)
{
	assert(php);
	if (HeapEmpty(php))
	{
		printf("堆是空的\n");
		return;
	}
	return php->a[0];  //注意取堆顶元素是a[0],不是a[php->size - 1]
}

bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}


void PrintHeap(HP* php)
{
	assert(php);
	while (!HeapEmpty(php))
	{
		int top = HeapTop(php);
		printf("%d ", top);
		HeapPop(php);
	}
}

(5)堆的应用

1.  TopK问题

 

第K小就用大根堆,第K大就用小根堆 

void CreateNData() //造数据
{
	int n = 10000;
	srand(time(0));
	const char* file = "data.txt"; //创建文件
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}
	for (size_t i = 0; i < n; i++)
	{
		int x = rand() % 10000;
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}

void printTopk(int k)
{
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}

	int* kmin_heap = (int*)malloc(sizeof(int) * k);
	if (kmin_heap == NULL)
	{
		perror("malloc fail");
		return;
	}


	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &kmin_heap[i]); //从文件中读取到堆里
	}

	//建小堆
	for (int parent = (k - 1 - 1) / 2; parent >= 0; parent--) //parent就是parent的下标
	{
		AdjustDown(kmin_heap,k,parent);
	}

	int val = 0;
	while (!feof(fout))
	{
		fscanf(fout, "%d", &val);
		if (val > kmin_heap[0]) //一直与堆顶比
		{
			kmin_heap[0] = val;
			AdjustDown(kmin_heap, k, 0);
		}
	}

	for (int i = 0; i < k; i++)
	{
		printf("%d\n", kmin_heap[i]);
	}
}

int main()
{
	CreateNData();
	printTopk(10);
	return 0;
}

2.堆排序

TopK只是输出了把数据按顺序输出,并没有改变数据内在的顺序。

void AdjustDown(int *a,int size,int parent)
{
	int child = parent * 2 + 1;  //假设左孩子是小的
	while (child < size) //孩子出界了结束
	{
		//选出左右孩子中小/大的那一个,child+1<size 保证了只有左孩子没有右孩子的情况(防止越界),就是说不用与右孩子作比较了,最小的就是左孩子!
		if (child+1<size && a[child + 1] < a[child]) 
//要是右孩子比左孩子小,那么孩子就是右孩子(= parent * 2 + 2) 大堆:a[child + 1] > a[child]
		{
			child++;
		}

		if (a[child] < a[parent]) //大堆: a[child] > a[parent]
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;  //继续假设左孩子是小的
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int *a,int n)
{
    
    /*建堆——向上调整建堆
    for (int i = 1; i < n; i++) //第0个可以看做堆,所以从1开始
    {
	    AdjustUp(a, i);
    }
    */

	//建堆——向下调整建堆
	//倒着调整,叶子节点不调整,从倒数第一个非叶子结点(最后一个结点的父亲)开始调整
	for (int i = (n-1-1)/2 ; i>=0; i--) //最后一个结点下标是n-1,算他的父亲还要再减一除2, i就是所求的父亲结点的下标
	{
		AdjustDown(a, n, i);
	}

	int end = n - 1; //最后一个位置的下标
	while (end>0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0); //end同时还是前面的数据个数,因为不把最后一个看到数组内
		end--;
	}	
}


1. 升序——建大堆;降序——建小堆
2. 小堆,选出最小的,首尾交换,最小的放到最后,再把最后一个数据,不看做堆里的元素,向下依次调整选出次小的
3. 向下调整比向上调整更好 ,空间复杂度更低,向上调整的最后一层的复杂度占整个的一半

    为什么升序建大堆:如果建小堆的话,每次都要重头开始向下调整建堆,比如你来一个比较小的数,
就每次都要把堆顶替换掉,就会很麻烦,建大堆只需要每次把最大的(堆顶)拿到最后,与新来的交换,
把新来的调整到合适的位置,而不会随便改变原来堆顶所在位置。
    到最后每个大的都被拿到后面去了,出来的自然是升序。

3.二叉树

 (1)读取顺序以及实现

1.前序
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	printf("%d ", root->data);  //打印根(先访问根)
	PreOrder(root->left);   //再访问左子树
	PreOrder(root->right);  //再访问右子树
}

2.中序
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	InOrder(root->left);   //先访问左子树
	printf("%d ", root->data);  //再访问根
	InOrder(root->right);  //再访问右子树
}
3.后序
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	PostOrder(root->left);   //先访问左子树
	PostOrder(root->right);  //再访问右子树
	printf("%d ", root->data);  //再访问根
}

(2)递归计算结点个数

int BTreeSize(BTNode* root)
{
	return NULL ? 0 : (BTreeSize(root->left) + BTreeSize(root->right) + 1);
}

  扩展: 递归计算叶子结点个数

//递归计算叶子结点个数
int BTreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}

	return BTreeLeafSize(root->left) + BTreeLeafSize(root->right);
}

(3) 递归计算二叉树高度

子问题:左子树和右子树高度高的那个加一

//求二叉树高度(用后序,先计算左右,在计算根)
int BTreeHeight(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	int leftHeight = BTreeHeight(root->left);
	int rightHeight = BTreeHeight(root->right);

	return (leftHeight > rightHeight) ? leftHeight+1 : rightHeight+1;
}

(4)第k层结点个数

子问题:转换成统计左子树的第K-1层和右子树的第K-1层的结点个数

结束条件:K==1且结点不为空 或  结点为空

int BTreeLevelKsize(BTNode* root, int k)
{
	assert(k > 0);
	if (root == NULL)
	{
		return 0;
	}

	if (k == 1) //root已经不为空了
	{
		return 1;
	}
	return BTreeLevelKsize(root->left, k - 1) + BTreeLevelKsize(root->right, k - 1);
}

(5)查找值为X的结点,返回地址

BTNode* BTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}

	if (root->data == x)
	{
		return root;
	}

	BTNode* ret1 = BTreeFind(root->left, x);
	if (ret1)
	{
		return ret1;
	}

	BTNode* ret2 = BTreeFind(root->right, x);
	if (ret2)
	{
		return ret2;
	}
	return NULL;  //到最后也没找到,返回空
}

用前序

(6)层序遍历

//层序遍历
void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		printf("%d ", front->data);

		if (front->left)
			QueuePush(&q, front->left);

		if (front->right)
			QueuePush(&q, front->right);
	}
	printf("\n");

	QueueDestroy(&q);
}

 (7)二叉数销毁(用后序)

void BTreeDestory(BTNode* root) 
{
	if (root == NULL)
		return;

	BTreeDestory(root->left);
	BTreeDestory(root->right);
	free(root);
}
//因为销毁根之前要保存左右子树,所以最好用后序

(8)判断是否为完全二叉树(用队列)

 当出队列的时候让其左右子树进队列。

bool BTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);

	if (root)
		QueuePush(&q, root);

	while (!QueueEmpty(&q)) //进数据
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		
		//遇到空就跳出
		if (front == NULL)
		{
			break;
		}

		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
	}

	//检查后面的结点有没有非空,完全二叉树空之后不会再有非空节点
	while (!QueueEmpty(&q)) //出数据
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		if (front != NULL)
		{
			QueueDestroy(&q);
			return false;
		}
	}

	QueueDestroy(&q);
	return true;  //走到这里说明是完全二叉树了
}

完全二叉树应该是NULL之后全是NULL了;

 (9)性质

五.排序 

1.插入排序 

(1)直接插入排序

void InsertSort(int* a,int n)
{
	for (int i = 1; i < n; i++)
	{
		// [0,end] 有序,插入tmp之后依然有序
		int end = i - 1; //从后往前比更好
		int tmp = a[i]; //tmp是end的后面一个数

		while (end >= 0) //找到空
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}

		a[end + 1] = tmp;
	}
}

 插入排序优于冒泡排序

 (2)希尔排序

void ShellSort(int* a, int n)
{
	int gap = n;

	//1.当gap > 1时,预排序
	//2.当gap = 1时,直接插入排序

	while (gap > 1) //不能大于等于,不然最后一直死循环
	{
		gap = gap / 3 + 1; //+1可以保证最后一次循环的时候gap=1
		//gap = gap / 2   也可以

		//多组并排,效率跟下面的二层循环是一样的
		for (int i = 0; i < n - gap; i++)  //每一(gap)组,i < n - gap是为了防止越界
		{
			int end = i;
			int tmp = a[end + gap]; //要新插入的(要比较的)是end后面的第gap个

			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end = end - gap;
					//end被减是为了要跳出循环,要不然之后直接写 a[end] = tmp 了,也不需要a[end + gap] = tmp 了
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}

}

gap越大,跳的越快,大的数可以更快的到后面,小的数可以更快的到前面,
但是gap越大预排序越不接近有序;




void ShellSort(int* a, int n)
{
	int gap = n;

	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int j = 0; j < gap; j++) //分gap组
		{
			for (int i = j; i < n - gap; i = i + gap) //每一(gap)组,i < n - gap是为了防止越界
			{
				int end = i;
				int tmp = a[end + gap]; //要新插入的(要比较的)是end后面的第gap个

				while (end >= 0)
				{
					if (tmp < a[end])
					{
						a[end + gap] = a[end];
						end = end - gap;
						//end被减是为了要跳出循环,要不然之后直接写 a[end] = tmp 了,也不需要a[end + gap] = tmp 了
					}
					else
					{
						break;
					}
				}
				a[end + gap] = tmp;
			}
		}
	}

}

 希尔更优。显示的时运行时间(ms)

*希尔排序的时间复杂度:O(N^1.3)

2.选择排序 

(1)直接选择排序

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 max_i = begin, min_i = begin;
		for (int i = begin; i <= end; i++) //找出较大的和较小的
		{
			if (a[i] > a[max_i])
			{
				max_i = i;
			}
			if (a[i] < a[min_i])
			{
				min_i = i;
			}
		}

		Swap(&a[begin], &a[min_i]);
		//如果max_i与begin重叠,修正一下
		if (begin == max_i)//现在已经交换完了,大的数来到了min_i的位置
		{
			max_i = min_i; //那么就让max_i修正到min_i的位置
		}
		Swap(&a[end], &a[max_i]);

		begin++;
		end--;
	}
}

每遍历一趟,选出最小的数和最大的数,与最左边,最右边交换;

(2)堆排序

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

void AdjustDonw(int* a, int n,int parent)
{
	int child = parent * 2 + 1; //假设左孩子是小的
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] < a[child]) //左孩子加一就是右孩子
		{
			child = child + 1;
		}

		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else //不用调了
		{
			break;
		}
	}
}


void HeapSort(int* a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDonw(a, n, i); //从结点i这个位置开始向下调整
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDonw(a, end, 0);
		end--;
	}
}

升序建大堆,降序建小堆

3.交换排序

(1)冒泡排序

一般,不讲了。

(2)快速排序

1.介绍

2.key的位置选取 
 

3.递归过程

4. 代码 
1. hoare版本

int PartSort(int* a,int left,int right) //单趟排序
{
	int key = left;
	while (left < right)
	{
		//右边找到小与key的
		while (left<right && a[right] >= a[key]) //要先检查有没有越界
		{
			right--;
		}
		//左边找到大于key的
		while (left < right && a[left] <= a[key])
		{
			left++;
		}
		//交换left与right,让大于key的去右边,小于key的去左边
		Swap(&a[left], &a[right]);
	}
	//当相遇时,交换相遇点和key
	Swap(&a[left], &a[key]);

	return left; //left与key交换了位置,别返回key了
	//因为之后递归要划分区间,所以返回的表示下标的那个
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end) //结束条件
	{
		return;
	}
	int key = PartSort(a, begin, end);
	//之后被分成了三个区间:[begin,key-1], key, [key+1,end]
	QuickSort(a, begin, key - 1); //对左区间递归   //不要把quicksort与partsrot的递归弄混了
	QuickSort(a, key + 1, end);  //对右区间递归
}
2. 挖坑法

int PartSort2(int* a, int left, int right)
{
	int key = a[left]; 
	int hole = left; //key记录值,坑记录下标
	while (left < right)
	{
		//右边找到小与key的
		while (left < right && a[right] >= key) //要先检查有没有越界
		{
			right--;
		}

		a[hole] = a[right];  //找到小的之后,把小的填到坑里
		hole = right;   //再把那个新位置标记为坑

		//左边找到大于key的
		while (left < right && a[left] <= key)
		{
			left++;
		}

		a[hole] = a[left];  //找到大的之后,把大的填到坑里
		hole = left;   //再把那个新位置标记为坑

	}

	a[hole] = key;  //最后一个坑就是key的位置
	return hole;  //返回的是hole,不是key
	//因为之后递归要划分区间,所以返回的表示下标的那个
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end) //结束条件
	{
		return;
	}
	int key = PartSort2(a, begin, end);
	//之后被分成了三个区间:[begin,key-1], key, [key+1,end]
	QuickSort(a, begin, key - 1); //对左区间递归   //不要把quicksort与partsrot的递归弄混了
	QuickSort(a, key + 1, end);  //对右区间递归
}
3.前后指针法

int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = left + 1;
	int key = left;  //key在这里是下标

	while (cur <= right)
	{
		if (a[cur] < a[key] )
		{
			prev++;
			Swap(&a[prev], &a[cur]);
		}

		cur++;
	}
	Swap(&a[prev], &a[key]);
	key = prev;
	return key; //也可以直接return prve,(区间中间的下标)
}

1.最开始prev和cur是相邻的
2.当cur遇到比key大的值以后,此时prev与cur之间的值都是比key大的
3.cur找小,找到小的以后,跟++prev位置的值交换,相当于把大的丢后面,小的放前面了

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end) //结束条件
	{
		return;
	}
	int key = PartSort3(a, begin, end);
	//之后被分成了三个区间:[begin,key-1], key, [key+1,end]
	QuickSort(a, begin, key - 1); //对左区间递归   //不要把quicksort与partsrot的递归弄混了
	QuickSort(a, key + 1, end);  //对右区间递归
}
5.效率分析

当每次key都取中位数的时候效率最高 ,为N*logN;

当数组接近有序的时候效率最低,为N^2; (因为每次key都只能往后取一到两个)

解决方案:1.随机数选key

                  2. 三个数中取不大不小的那个作为key

//例子:前后指针法的改进

//三数取中——取的是下标
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;

	if (a[left] < a[mid])
	{
		if (a[mid] <= a[right]) //1 2 3
		{
			return mid;  //left mid right ,mid是中位数
		}
		else if (a[left] < a[right]) //1 3 2
		{
			return right;
		}
		else  //2 3 1 
		{
			return left;
		}
	}
	else // a[left] > a[mid]
	{
		if (a[mid] > a[right]) //3 2 1
		{
			return mid;
		}
		else if (a[right] > a[left]) //2 1 3
		{
			return left;
		}
		else  //3 1 2
		{
			return right;
		}
	}
}


int PartSort3(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);  //三数取中
	Swap(&a[left], &a[mid]);

	int prev = left;
	int cur = left + 1;
	int key = left;  //key在这里是下标

	while (cur <= right)
	{
		if (a[cur] < a[key] )
		{
			prev++;
			Swap(&a[prev], &a[cur]);
		}

		cur++;
	}
	Swap(&a[prev], &a[key]);
	key = prev;
	return key; //也可以直接return prve,(区间中间的下标)
}



void QuickSort(int* a, int begin, int end)
{
	if (begin >= end) //结束条件
	{
		return;
	}
	int key = PartSort3(a, begin, end);
	//之后被分成了三个区间:[begin,key-1], key, [key+1,end]
	QuickSort(a, begin, key - 1); //对左区间递归   //不要把quicksort与partsrot的递归弄混了
	QuickSort(a, key + 1, end);  //对右区间递归
}

6.用栈模拟递归:也是N*logN

void QuickSortNonR(int* a, int begin,int end) //非递归快排
{
	ST st;
	StackInit(&st);

	StackPush(&st, end);
	StackPush(&st, begin); //等会先出begin
	while (!StackEmpty(&st))
	{
		int left = StackTop(&st); //先出begin
		StackPop(&st);
		
		int right = StackTop(&st);
		StackPop(&st);

		int key = PartSort3(a, left, right);
		//分割出了三段区间 [left,key-1] key [key+1,right]

		//入栈先入后区间,这样前区间后进先出
		if (key + 1 < right) //判断区间合法
		{
			StackPush(&st, right); //跟上面一样,先压入区间的end
			StackPush(&st, key+1);
		}
		if (left < key - 1)
		{
			StackPush(&st, key - 1);
			StackPush(&st, left);
		}
	}
	StackDestroy(&st);
}

用队列模拟:

7.三路划分的优化

防止的问题:

思路:

//快排的三路划分
void QuickSort(int* a, int begin, int end)
{
	if (begin == end)
	{
		return;
	}
	
	int left = begin;
	int right = end;
	int cur = left + 1;

	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int key = a[left];  //让key尽量取中间值

	while (cur <= right)
	{
		if (a[cur] < key)
		{
			Swap(&a[left], &a[cur]);
			left++;
			cur++;
		}
		else if (a[cur] > key)
		{
			Swap(&a[right], &a[cur]);
			right--; //换过来的不知道大于还是小于key,所以不用cur++
		}
		else  //cur == key
		{
			cur++;
		}
	}
	//排完之后的结果:[begin,left-1],[left,right],[right+1,end]

	QuickSort(a, begin, left - 1); //递归左边和右边
	QuickSort(a, right + 1, end);
}

4.归并排序

(1)介绍

 (2)代码


void _MergeSort(int* a, int begin,int end,int* tmp)
{
	//递归结束条件
	if (begin == end)
	{
		return;
	}

	int mid = (begin + end) / 2;
	//分成了两个区间: [begin,mid] [mid+1,end]

	_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 //a[begin1] >= a[begin2]
		{
			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)); //再给他拷贝回去
	//之所以 a+begin, tmp+begin 是因为a到tmp应该是一个区间,而不加上begin就不是一个区间了

}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	_MergeSort(a, 0, n - 1, tmp); //还是用区间
	free(tmp);
}

时间复杂度:N*logN  高度是logN,每一次归并O(N),所以是N*logN

(3)非循环:用gap的二倍依次乘(2个,4个....16个)

gap导致的数组越界: 

 

 解决:

法一:归并一组,拷贝一组

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;  //gap为每组的数据个数

	while (gap < n)
	{
		int j = 0;
		for (int i = 0; i < n; i = i + 2 * gap) //i每加两组gap,跳到下两组,这整个for循环是一个gap为 的归并排序,拷贝操作应该在外面
		{
			int begin1 = i, end1 = i + gap - 1; //从i开始,到i+gap结束,这个结束的下标是i+gap-1
			int begin2 = i + gap, end2 = i + gap * 2 - 1;
			
			//第一种,第二种情况
			if (end1 >= n || begin2 >= n)
			{
				break;
			}

			//第三种情况,修正的思路
			if (end2 >= n)
			{
				end2 = n - 1; 
				//end2越界,那么end2之后的都没用了,直接让end2等于n-1(最后一个的下标)
			}

			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else //a[begin1] >= a[begin2]
				{
					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));
			//i是起始位置,end2是结束位置,end2-i就是差值,再加一就是个数
		}
		gap = gap * 2;
	}
	free(tmp);
}

法二:先修正,再整体拷贝

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;  //gap为每组的数据个数

	while (gap < n)
	{
		int j = 0;
		for (int i = 0; i < n; i = i + 2 * gap) //i每加两组gap,跳到下两组,这整个for循环是一个gap为 的归并排序,拷贝操作应该在外面
		{
			int begin1 = i, end1 = i + gap - 1; //从i开始,到i+gap结束,这个结束的下标是i+gap-1
			int begin2 = i + gap, end2 = i + gap * 2 - 1;
			
			//修正:
			if (end1 >= n)
			{
				end1 = n - 1;
				//end1越界,那么end1之后的越界了,直接让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 (end2 >= n)
			{
				end2 = n - 1; 
				//end2越界,那么end2之后的都没用了,直接让end2等于n-1(最后一个的下标)
			}

			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else //a[begin1] >= a[begin2]
				{
					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 = gap * 2;
	}
	free(tmp);
}

(4)归并的小区间优化(针对递归) 

void _MergeSort(int* a, int begin,int end,int* tmp)
{
	//递归结束条件
	if (begin == end)
	{
		return;
	}

	//判断小区间:小区间用递归太浪费,用个小排序就行
	if (end - begin + 1 < 10)
	{
		InsertSort(a+begin, end - begin + 1); 
		return;
	}

	int mid = (begin + end) / 2;
	//分成了两个区间: [begin,mid] [mid+1,end]

	_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 //a[begin1] >= a[begin2]
		{
			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)); //再给他拷贝回去
	//之所以 a+begin, tmp+begin 是因为a到tmp应该是一个区间,而不加上begin就不是一个区间了

}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	_MergeSort(a, 0, n - 1, tmp); //还是用区间
	free(tmp);
}

5.计数排序

(1)

(2)

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

	int range = max - min + 1; //相对映射
	int* countA = (int*)malloc(sizeof(int) * range);
	memset(countA, 0, sizeof(int) * range);

	//统计次数
	for (int i = 0; i < n; i++)
	{
		countA[a[i] - min]++;
	}

	//排序——遍历计数数组,不是原数组
	for (int j = 0; j < range; j++)
	{
		while (countA[j]--)
		{
			a[k++] = min + j;
		}
	}
}

6.稳定性

(1)介绍

 (2)例子

(3) 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

对玛导至昏

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

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

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

打赏作者

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

抵扣说明:

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

余额充值