数据结构之栈和队列

栈的理解

栈是⼀种特殊的线性表,它的逻辑结构肯定是线性的。
只允许在固定的⼀端进行插入和删除元素操作。在栈中,只允许从栈顶进行插入和删除数据。
栈顶中这一端进行数据插入和删除,另⼀端称为栈底
栈中的数据元素遵守 (也可以称为先进后出)后进先出LIFO(Last In First Out)的原则。
先进后出解释:最先进到栈中的数据,出栈时最后从栈里出去

进栈:栈的插⼊操作叫做进栈/压栈/入栈,入数据在栈顶
出栈:栈的删除操作叫做出栈。出数据也在栈顶

在这里插入图片描述
也可以理解为后来者居上!

那栈使用什么结构来实现呢?

如下:
在这里插入图片描述
这些结构都可以实现,我们要取最好的进行实现:

进行如下比较:
从时间复杂度角度分析,它们都为O(1),没法比较;
我们可以根据它们向内存中申请空间的大小进行比较:
双向链表有两个指针变量,向内存中申请的空间较大,所以先将双向链表排除了

数组增容所增的空间是一片连续的空间,而单链表的节点的申请则是不连续的,对于栈这样的频繁插入和删除数据的结构来说,用数组实现更好!

栈的实现

栈的定义:

思路:它的底层逻辑是数组,所以类比顺序表,定义如下:
核心代码:

//栈定义
typedef int STDatatype;
typedef struct stack
{
	STDatatype* arr;//
	int capacity;//栈的空间大小
	int top;//栈顶 -- 与顺序表的size中类似 它的既可以用来记录当前栈中的有效数据个数,
	//其次也可以作为在栈中插入删除数据的栈顶的位置
}ST;

栈的初始化

核心代码:

void StackInit(ST* ps)
{
	//先断言,再进行初始化操作
	assert(ps);

	ps->arr = NULL;
	ps->capacity = ps->top = 0;
}

入数据

思路:

00.断言
01.判断空间是否足够 – 这里可以不用单独将判断空间是否足够进行单独分装,因为这是栈,只有push,没有pushback frant等操作,再栈中只用一次
02.空间足够进行入数据

思路图:
在这里插入图片描述

void StackPush(ST* ps, STDatatype x)
{
	//00.断言
	assert(ps);
	//01.判断空间是否足够 -- 这里可以不用单独将判断空间是否足够进行单独分装,因为这是栈,只有push,没有pushback frant等操作,再栈中只用一次
	if (ps->capacity == ps->top)
	{
		int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		STDatatype* tmp =(STDatatype*) realloc(ps->arr, newCapacity * sizeof(STDatatype));
		//newCapacity的类型是int的,这里的ralloc申请的是字节数,所以这里是 多少个STDatatype类型的数据
		if (tmp == NULL)
		{
			perror("ralloc fail!");
			exit(1);
		}
		ps->arr = tmp;
		ps->capacity = newCapacity;

	}
	//02.空间足够进行入数据
	ps->arr[ps->top++] = x;
//在栈中,入数据时只能取栈顶的元素
}

判断栈是否为空
核心代码:

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

出数据

思路图:
在这里插入图片描述
核心代码:

void StackPop(ST* ps)
{
	//00.断言,除了传参不能为空,其栈也不能为空
	assert(ps);
	assert(!StackEmpty(ps));

	//01.栈不为空,直接将栈顶元素出了
	--ps->top;
}

取出栈顶元素

思路图
在这里插入图片描述

核心代码:

STDatatype StackTop(ST* ps)
{
	//00.断言
	assert(ps);
	assert(!StackEmpty(ps));
	//01.进行去栈顶元素
	return ps->arr[ps->top - 1];
	//top - 1 解释:top是指栈中有效数据的个数,这里它是作为下标,下标是从0开始的,所以他要 -1;
}

栈中的数据不能被遍历,也不能随机访问,只能将取出栈顶元素和出数据联合使用,达到打印的效果

以下是代码:

//这里的stackPop 可以和stacktop 联合使用以达到打印的效果
//循环出栈,直到栈为空
while (!StackEmpty(&st))
{
	//取出栈顶元素
	STDatatype ret = StackTop(&st);
	printf("%d ", ret);
	StackPop(&st);
}

记录栈中的元素个数

核心代码:

int STSize(ST* ps)
{
	assert(ps);
	return ps->top;//top记录的就是有效数据的个数
}

相关的算法题:

有效的括号

思路:

  1. 做这道题之前先将栈的基本功能实现一遍
  2. 创建栈并初始化, 创建指针遍历ps保存字符串s,开始循环比较:若为左括号,入栈;右括号,是否与栈顶元素相匹配:匹配,栈顶元素出栈,ps++往后进行遍历;不匹配,销毁并返回false
  3. 遍历完成后,跳出循环,进入只有在左括号的情况中进行判断

特殊情况:

只有左括号 :在最后判断栈是否为空,为空销毁并返回false;
只有右括号:在右括号的条件下,判断栈是否为空,为空,销毁并返回false
题目中的括号没有优先级,但需要正确的匹配
核心代码:

bool isValid(char* s) 
{
    //创建一个栈,并将其初始化
    ST st;
    StackInit(&st);
    char* ps = s;
    while(*ps != '\0')//这里应该是不等于
    {
    //左括号,入栈
        if (*ps == '(' || *ps == '[' || *ps == '{')
       {
        StackPush(&st, *ps);
       } 
       else//右括号进行匹配,若匹配,则让栈顶元素ch出栈,ps继续往后++;若不匹配,则销毁后返回false。
       {
           if (StackEmpty(&st))
           {
            StackDestroy(&st);
            return false;//如果一开始就是右括号的情况下,直接返回false
           }
        char ch = StackTop(&st);//取出栈顶元素进行比较
        
        if ((*ps == ')' && ch == '(') || (*ps == ']' && ch == '[') || (*ps == '}' && ch == '{'))
        {
           StackPop(&st);
        }
        else    
        {
            StackDestroy(&st);//必须做到先销毁后返回
            return false;
        }

       }
         ps++;//不能加*,这里是让他的地址往后进行循环的
    }
    bool ret = StackEmpty(&st) == true;//当只有一个左括号时,栈中不为空,返回false
    StackDestroy(&st);
    return ret;
}

队列

队列理解

指的是只在一端进行插入数据操作,在另外一端只进行删除数据操作的一种特殊的线性表。它具有先进行出的原则,也就是先进到队列的数据,从队列中出去时先出的原则。

入队列 :进行插入操作的一端叫队尾
出队列:进行删除操作的一端叫队头;
队列示意图

那它到底怎么实现呢?

数组? ---- 满足队列的基本规则后,我们将数组的起始下标作为队头,将另外一端作为队尾,后发现:在删除数据时,队头的上界(最坏的情况)的时间复杂度为O(N);插入数据时队尾的上界的时间复杂度为O(1);
单列表?---- 满足队列的基本规则后,单链表的两端,不管那一端作为队头和队尾,总会有一端的时间复杂度为O(N),另一端为O(1);

简单比较二者后,我们对单链表多定义一个尾结点ptail,让它作为队尾;头节点作为队头
两端的时间复杂度都为O(1)

下面是改进后单链表示意图:
在这里插入图片描述

那为什么不让phead作为队尾,让ptail作为队头?

让phead作为队尾插入数据可以,但如果让ptail作为队头删除数据,由于是单链表,删除节点后找不到该节点的前一个结点,所以不可以让他俩的位置进行互换
因此,我们将单链表作为队列的底层逻辑结构

队列的实现:

队列结构定义

由于队列的底层逻辑是单链表,而单链表又是由结点组成的,因此我们在定义队列结构时,需要定义两个结构体,一个是队列的,另外一个是队列结点的

核心代码:

//队列结点定义
typedef int QDataType;
typedef struct QueueNode
{
	QDataType x;
	struct QueueNode* next;
}Queue;
//队列定义
typedef struct Queue
{
	QueueNode* phead;
	QueueNode* ptail;
}Queue;

队列初始化

核心代码:

void QueueInit(Queue* pq)//函数记得带上返回值
{
	assert(pq);
	pq->size = 0;
	pq->phead = pq->ptail = NULL;
}

在队尾入队列

思路:先创建一个新的队列结点newnode,判断队列是否为空,若为空,将pq中的phead 和 ptail指向newnode;不为空,pq中的ptail的next 的指向为newnode,并让ptail的指向更改为newnode。
示意图:
在这里插入图片描述
核心代码:

void QueuePush(Queue* pq, QDataType x)
{
	//00.断言
	assert(pq);
	//01.创建新结点并将新结点初始化
	QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
	if (newnode == NULL)//只要是申请空间,就要进行判断是否申请失败
	{
		perror("malloc fail !");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	//02.从队尾中入新节点 需要分类讨论对列为空还是不为空
	//为空时
	if (pq->phead == NULL)
	{
		pq->phead = pq->ptail = newnode;
	}
	else//不为空
	{
		pq->ptail->next = newnode;
		pq->ptail = newnode;//等价于 pq->ptail = pq->ptail->next;
	}
}

判断队列是否为空

该函数必须要放到调用它的函数的上面,因为代码运行是从上而下运行的,没有找到定义的,自然就会报错。
核心代码:

bool QueueEmptory(Queue *pq)
{
	assert(pq);
	return pq->phead == NULL && pq->ptail == NULL;
	//二者写一个也可以
}

在队头出队列

示意图:
在这里插入图片描述
核心代码:

void QueuePop(Queue* pq)
{
	assert(pq);
	assert(!QueueEmptory(pq));

	//只有一个结点
	if (pq->phead == pq->ptail)
	{
		free(pq->phead);
		pq->phead == pq->ptail == NULL;
	}
	//有多个节点
	else
	{
		////方法1:
		//QueueNode* del = pq->phead;
		//pq->phead = del->next;
		//free(del);
		

		//方法2:
		QueueNode* Next = pq->phead->next;
		free(pq->phead);
		pq->phead = Next;
	}
}

取队尾的值

直接返回队尾的值即可
核心代码:

QDataType QueueFrant(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	return pq->phead->data;
}

取队头的值

直接返回队头的值即可

QDataType QueueBack(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	return pq->ptail->data;
}

记录队列有效数据个数

思路:在创建队列之初增加个变量size,入数据时pq->size++,出数据时pq->size–最后返回pq->size即可
核心代码:

int QueueSize(Queue* pq)
{
	assert(pq);
	
	//int size = 0;
	QueueNode* pcur = pq->phead;

	//while (pcur != NULL) 
	//{
	//	size++;
	//	pcur = pcur->next;
	//}
	////这种做法是不合理的,因为队列是一端进一端出的,队列不能够被遍历;
	// 这里的时间复杂度为O(N),若客户有大量的数据,当我们进行遍历求个数时会导致程序运行效率低下

	//优化方法:在创建之初就创建一个变量size,在初始化时个数为0,入数据时++;出数据时——
	//调用这个方法时,直接返回即可。s

	return pq->size;
}

队列的销毁

思路:遍历队列依次进行销毁,将队列中结构体的成员该置空的置空,该置零的置零
核心代码:

void QueueDestroy(Queue* pq)
{
	//断言
	assert(pq);
	assert(!QueueEmpty(pq));

	//遍历销毁
	QueueNode* pcur = pq->phead;

	while (pcur)
	{
		QueueNode* Next = pcur->next;
		free(pcur);
		pcur = Next;
	}
	//这里还需要手动释放phead 和ptail以及size
	pq->phead = pq->ptail = NULL;
	pq->size = 0;
}

相关算法题:

我们再做下面两道题时看它是需要实现谁,在具体实现对应功能时画图想想之前实现这个功能时的具体操作,然后用已有结构的功能实现另外一个结构功能的转化。不懂的可以画两个队列或者两个栈并找几个数据举例来说明

用队列实现栈

在做下面这些操作之前,需要手动完成队列功能的实现

我们在使用队列的大前提是必须保证有一个队列为空,才能实现栈的功能

栈的定义

思路:用队列实现栈,因此这里需要创建两个队列q1 和 q2

核心代码:

//创建两个队列
typedef struct {
 Queue q1;
 Queue q2;
} MyStack;

栈的初始化

思路:创建指向栈的指针变量,并向内存中申请MyStack个大小的空间,用已有队列初始化的功能对MyStack进行初始化,返回指针变量。

核心代码:

//stackInit()
MyStack* myStackCreate() {
  //现在还没有栈,需要我们创建栈并对其进行初始化
  MyStack* pst = (MyStack*)malloc(sizeof(MyStack));
  QueueInit(&pst->q1);
  QueueInit(&pst->q2);

  return pst;
}

入栈

思路:找不为空的队列,将数据通过队列入队的功能入到栈中
核心代码:

void myStackPush(MyStack* obj, int x) {
  if (!QueueEmpty(&obj->q1))
  {
    QueuePush(&obj->q1, x);
  }
  else
  {
    QueuePush(&obj->q2, x);
  }

}

出栈

思路: 找到不为空的队列,找到该队列,将其size-1数据导入到另外一个队列中去,最后剩下1个有效数据,直接出栈
核心代码:

int myStackPop(MyStack* obj) {
//01.创建两个指针变量指向两个队列,找不为空的队列,这里两个指针变量指向队列没有说必须是noneQ = &obj->q1, Queue* empQ = &obj->q2,也可以用noneQ = &obj->q2,Queue* empQ = &obj->q1只要找到不为空的就行;也算是另外一种方法
 Queue* noneQ = &obj->q1;//为什么这里和前面的不一样要带*号 -- 这里创建的是指针变量指向的是队列q1的地址
   Queue* empQ = &obj->q2;
   if (!QueueEmpty(&obj->q2))
   {
     noneQ = &obj->q2;
     empQ = &obj->q1; 
   }

    while(QueueSize(noneQ) > 1)
    {
        int frant = QueueFrant(noneQ);
        QueuePush(empQ, frant);
        QueuePop(noneQ);
    }

    int pop = QueueFrant(noneQ);//这里的类型为啥时int -- 因为取队头元素的返回值就是int
    QueuePop(noneQ);
    return pop;
}

取栈顶元素

思路:找不为空的队列,取队尾元素
下面注释的代码是错误的思路,下面的代码虽然在逻辑上是正确的,经过画图得知:这里在使用时,会导致两个队列都不为空,而我们在使用队列的大前提是必须保证有一个队列为空,才能实现栈的功能
核心代码:

int myStackTop(MyStack* obj) {
//    Queue* noneQ = &obj->q1;//为什么这里和前面的不一样要带*号
//    Queue* empQ = &obj->q2;
//    if (!QueueEmpty(&obj->q2))
//    {
//      noneQ = &obj->q2;
//      empQ = &obj->q1; 
//    }
//     while(QueueSize(noneQ) > 1)
//     {
//         int frant = QueueFrant(noneQ);
//         QueuePush(empQ, frant);
//         QueuePop(noneQ);
//     }
//      return QueueFrant(noneQ);
//这里的代码在逻辑上是正确的,经过画图得知:这里在使用时,会导致两个队列都不为空,而我们在使用队列的大前提是必须保证有一个队列为空,才能实现栈的功能
//优化后代码: --- 思路:找不为空的队列,取队尾元素
if (!QueueEmpty(&obj->q1))
{
    return QueueBack(&obj->q1);
}
else
{
    return QueueBack(&obj->q2);
}
}//记得带上大括号

栈的判空

思路:栈的判空就是队列的判空,直接调用即可,前提是两个队列必须同时满足不为空才可以
核心代码:

bool myStackEmpty(MyStack* obj) {
//栈的判空就是队列的判空,直接调用即可,它两必须同时满足才可以
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);

}

栈的销毁

思路:栈的销毁就是队列的销毁,这里需要注意的是我们要对初始化时创建的指针pst也需要进行置空
核心代码:

void myStackFree(MyStack* obj) {
//栈的销毁就是队列的销毁,在初始化时创建的指针pst也需要进行置空
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);

free(obj);
obj = NULL;
}

用实现队列

再实现这些之前,我们需自己将栈实现一遍

定义队列

思路:创建两个栈一个专门用来入栈的,另外一个专门用来出栈,从而实现队列
核心代码:

typedef struct {
    ST pushST;
    ST popST;
} MyQueue;

队列初始化

思路:创建指针变量向内存中申请MyQueue个大小的空间,用已有栈初始化的功能对MyQueue进行初始化,返回指针变量。
核心代码:

MyQueue* myQueueCreate() {
    MyQueue* pst = (MyQueue*)malloc(sizeof(MyQueue));
    StackInit(&pst->pushST);
    StackInit(&pst->popST); 
    return pst;
}

入队列

思路:往pushST中插入数据
核心代码:

void myQueuePush(MyQueue* obj, int x) {
    StackPush(&obj->pushST, x);
}

出队列

思路:判断popST是否为空,不为空,直接pop;为空,先将pushST中的数据导入到popST中再pop
若为空的具体操作:
使用入栈功能将pushST中的数据导入到popST中,然后将pushST中的数据出栈(也就是从栈中移除),依次循环进行,直到pushST为空
pop的具体操作:
取栈顶元素并保存,再将其出栈(也就是从栈中移除),最后将之前保存的栈顶元素进行返回
核心代码:

int myQueuePop(MyQueue* obj) {
    if (StackEmpty(&obj->popST))
    {
        //将数据从pushST导入到popST中
        while(!StackEmpty(&obj->pushST))
        {
            StackPush(&obj->popST, StackTop(&obj->pushST));//stackpop功能有返回值的,返回的是出栈后的值
            StackPop(&obj->pushST);
        }
    }
    //取栈顶,删除栈顶元素并返回栈顶数据
    int top = StackTop(&obj->popST);//stacktop 返回的是栈顶的值
    StackPop(&obj->popST);
    return top;
}

取队头数据

思路:和前面出队列一样,但是这里不需要移除栈顶元素(队列的先进先出原则,在栈pushST中插入数据1 2 3,此时栈顶元素为3,通过导数据导到栈popST中,此时该栈的栈顶元素为1,依次取栈顶元素,最后的取出的是1 2 3,满足队列的先进先出原则,因此该思路有效)
示意图:在这里插入图片描述
核心代码:

int myQueuePeek(MyQueue* obj) {
    if (StackEmpty(&obj->popST))
    {
        while(!StackEmpty(&obj->pushST))
        {
            StackPush(&obj->popST, StackTop(&obj->pushST));
            StackPop(&obj->pushST);
        }
    }
    return StackTop(&obj->popST);
}

队列判空

思路:我们是用两个栈实现的队列,所以要想队列为空,那就要做到两个栈同时为空
核心代码:

bool myQueueEmpty(MyQueue* obj) {
   
    return StackEmpty(&obj->pushST) &&  StackEmpty(&obj->popST);
   
}

队列的销毁

思路:我们是用两个栈实现的队列,所以要想销毁队列,那就要做到两个栈都要进行销毁,除此之外,我们在初始化时创建指向MyQueue的指针变量也要释放并置为空

void myQueueFree(MyQueue* obj) {
    StackDestroy(&obj->pushST);
    StackDestroy(&obj->popST);

    free(obj);
    obj = NULL;
}

设计循环队列

循环队列定义:循环队列⾸尾相连成环,环形队列可以使⽤数组实现,也可
以使⽤循环链表实现

循环队列的底层逻辑我们更推荐数组

原因:1.数组的空间固定且连续,若用链表实现,需要申请一个节点的空间,且这些节点的空间可能不连续,还需要使用指针的指向将它们连接起来,这势必会有空间的损耗;
2.在对数组的某个位置插入删除数据时,不需要考虑该位置是否为空,直接插入覆盖即可; 若使用链表,我们需要判断该节点是空的还是非空的,若为空才可将新数据插入到该节点中,很麻烦

循环队列的定义

//循环队列的定义
typedef struct {
    //底层结构是数组,需要创建数组
    int* arr;
    //一个指向头,一个指向尾
    int front;
    int rear;
    //防止后面判满时找不到k,这里才创建capacity用来记录空间大小
    int capacity;
} MyCircularQueue;

对循环队列进行插入数据之前,需要判断队列是否满了
判断方法如下:
在这里插入图片描述
核心代码:

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return (obj->rear + 1) % (obj->capacity + 1) == front;
}

往循环队列中入数据

思路:

  1. 插入数据之前判断队列是否满了,这里有判断队列为满的函数,直接调用即可,如果满了返回false,表示不能插入数据
  2. 从上面代码出来后,说明没满,可以插入数据:将要插入的数据插入到下标为rear的位置,插入后rear++,又因为这里是循环队列,如果继续让rear++,它会超过申请的空间造成越界,因此这里用 obj->rear %= obj->capacity + 1 来让rear回到起始的位置让它达到循环入数据的效果,最后插入成功返回true
//往循环队列插入数据
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    //01. 插入数据之前判断队列是否满了,这里有判断队列为满的函数,直接调用即可,如果满了返回false,表示不能插入数据
    if (myCircularQueueIsFull(obj))
    {
        return false;
    }
    //02.  从上面代码出来后,说明没满,可以插入数据:将要插入的数据插入到下标为rear的位置,插入后rear++,又因为这里是循环队列,不能越界,用 obj->rear %= obj->capacity + 1 来让rear回到起始的位置防止越界,最后插入成功返回true
    obj->arr[rear++] = value;
    obj->rear %= (obj->capacity + 1);//等价于 obj->rear = obj->rear % (obj->capacity + 1);

    return true;
}

在循环队列出数据时需要先判断队列是否为空
方法如下:
在这里插入图片描述
核心代码:

//判断队列是否为空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return obj->rear == obj->front;   
}

在循环队列中出数据

思路:
01.出数据时队列不能为空,这里需要判空
02.开始出数据,直接让front++,后面插入数据时直接覆盖即可
03.因为这里是循环队列,加到最后front会超出我们所申请的空间大小造成越界,这里用obj->front %= obj->capacity + 1 来让front来到起始位置让他继续出数据以达到循环的效果,最后出数据成功返回true

//在循环队列中出数据
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    //01.出数据时队列不能为空,这里需要判空
    if (myCircularQueueIsEmpty(obj))
    {
        return false;
    }
    //02.开始出数据,直接让front++,后面插入数据时直接覆盖即可
    obj->front++;
    //03.用obj->front %= obj->capacity + 1 来让front来到起始位置防止越界,最后出数据成功返回true
    obj->front %= obj->capacity + 1;//等价于obj->front = obj->front % (obj->capacity + 1);
   // capcacity + 1:为啥要 + 1,程序给的是k个空间,我们实际申请的是 k + 1 个 int的空间,要按照实际来,所以要 + 1;
    return true;
}

取队首元素

思路:先去判空,若为空返回-1,不为空直接返回队头元素

int myCircularQueueFront(MyCircularQueue* obj) {
   //01.判空
   if (myCircularQueueIsEmpty(obj))
   {
    return -1;
   }
   //02.若不为空,返回队头元素
    return obj->arr[obj->front];
}  

取队尾元素
在这里插入图片描述

思路:
01.取队尾元素时,队列不能为空,需要先去判空,若为空,返回-1
02.取队尾元素就是取下标为rear - 1;创建变量prev;
正常情况:让prev = rear - 1返回obj->arr[prev];
特殊情况:如果rear == 0,则让prev = capacity,返回obj->arr[prev]

int myCircularQueueRear(MyCircularQueue* obj) {
 	  //01.取队尾元素时,队列不能为空,需要先去判空,若为空,返回-1
    if (myCircularQueueIsEmpty(obj))
    {
        return -1;
    }
    //02.取队尾元素就是取下标为rear - 1;创建变量prev;
    //正常情况:让prev = rear - 1返回obj->arr[prev];
    //特殊情况:如果rear == 0,则让prev = capacity,返回obj->arr[prev]
    int prev = obj->rear - 1;
    if (obj->rear == 0)
    {
        prev = obj->capacity;
    }
    return obj->arr[prev];
}

循环队列的释放

思路: 释放掉创建指向数组的地址,和 指向循环队列的地址

//释放掉创建指向数组的地址,和指向循环队列的地址
void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->arr)
    free(obj);
    obj = NULL;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值