数据结构-顺序表及链表结构分析

news/2025/1/17 20:34:17/

文章目录

  • 一、顺序表的插入&删除
    • Ⅰ.顺序表的插入(增)
      • 1.1 顺序表的尾插
      • 1.2 顺序表的头插
      • 1.3 顺序表的指定位置插入
    • Ⅱ.顺序表的移除元素(删)
      • 2.1 顺序表的尾删
      • 2.2 顺序表的头删
      • 2.3 顺序表的指定位置删除
    • Ⅲ.顺序表数据的查找
    • Ⅳ.顺序表的销毁
  • 二、链表的插入(增)
    • 1.单链表的尾插
    • 2.单链表的头插
    • 3.单链表的指定位置插入
      • 3.1 在指定位置之前插入
      • 3.2 在指定位置之后插入
    • 4.双向链表的尾插
    • 5.双向链表的头插
    • 6.双向链表的指定位置插入
      • 6.1 在指定位置之前插入
  • 三、链表的移除节点(删)
    • 1.单链表的尾删
    • 2.单链表的头删
    • 3.单链表的指定位置删除
      • 3.1 删除指定位置的节点
      • 3.2 删除指定位置之前的节点
    • 4.双向链表的尾删
    • 5.双向链表的头删
    • 6.双向链表的指定位置删除
      • 6.1 删除指定位置的节点
      • 6.2 删除指定位置之后的节点
    • 7.双向链表的判空
  • 四、链表的销毁

一、顺序表的插入&删除

Ⅰ.顺序表的插入(增)

1.1 顺序表的尾插

(1) 顺序表的结构如下:

typedef int SLDataType;
typedef struct SeqList
{SLDataType* arr; int size; //有效数据个数int capacity; //空间容量
}SL;

假设现定义了一个结构体变量:

SL sl;

(1) 顺序表一开始需要初始化。注意:size是指向数组最后一个元素的下一个位置。

void SLInit(SL* ps)
{assert(ps != NULL);ps->arr = NULL;ps->size = ps->capacity = 0;
}

(2) 顺序表的尾插:
在插入数据之前我们都要检查空间是否满了:

void SLCheckCapacity(SL* ps)
{assert(ps != NULL);if (ps->size == ps->capacity){int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity*sizeof(SLDataType));if (tmp == NULL){perror("realloc fail!");return;}ps->arr = tmp;ps->capacity = newCapacity;}
}

如果size==capacity说明有两种情况:一种是数组还没有开辟,capacity为0;另一种就是数组满了size才会等于capacity。realloc能实现空间的动态增容。

现在开始尾插:

void SLPushBack(SL* ps,SLDataType x)
{assert(ps != NULL);SLCheckCapacity(ps);ps->arr[ps->size++] = x;
}

在这里插入图片描述
由于size位置正好是尾插的位置,所以在检查空间是否满了以后,直接在size位置插入数据即可。由于增加了一个数据,所以在插入完毕后记得要将size+1。由于要改变的是结构体变量sl,所以要传结构体的指针,则要对ps进行断言不能为空。

1.2 顺序表的头插

void SLPushFront(SL* ps,SLDataType x)
{assert(ps != NULL);SLCheckCapacity(ps);for (int i = ps->size-1; i >= 0; i--){ps->arr[i + 1] = ps->arr[i];}ps->arr[0] = x;ps->size++;
}

在这里插入图片描述先检查空间是否已满,然后再进行头插。头插数据就要涉及到数据的挪动,头插即在数组下标为0的位置插入一个数据,则数组中的元素要整体往后挪动一位。插入数据后,记得要让size+1。

1.3 顺序表的指定位置插入

void SLInsert(SL* ps,int pos,SLDataType x)
{assert(ps != NULL);assert(pos >= 0 && pos < ps->size);SLCheckCapacity(ps);for (int i = ps->size - 1; i >= pos; i--){ps->arr[i + 1] = ps->arr[i];}ps->arr[pos] = x;ps->size++;
}

在这里插入图片描述在指定位置插入数据,即在指定数据的前面插入数据,也要涉及挪动数据,比如上面图:在下标为2的位置插入一个数据,就要把pos位置及以后的数据整体往后挪一位,需要注意循环的结束条件。当然指定的位置也可能在头,也可能在尾;所以需要考虑周全。

Ⅱ.顺序表的移除元素(删)

2.1 顺序表的尾删

void SLPopBack(SL* ps)
{assert(ps != NULL);assert(ps->size > 0);ps->size--;
}

在这里插入图片描述顺序表的尾删直接让size-1即可,不需要对数组中的数据做任何改动。但是需要注意如果顺序表为空,就不能进行删除。所以要断言size>0才行。

2.2 顺序表的头删

void SLPopFront(SL* ps)
{assert(ps != NULL);assert(ps->size > 0);for (int i = 0; i < ps->size - 1; i++){ps->arr[i] = ps->arr[i + 1];}ps->size--;
}

在这里插入图片描述头删需要把后面的元素往前挪动一位,但需要注意挪动会覆盖数据,所以要考虑数据到底是从前往后挪,还是从后往前挪。当然删除就需要判断数组是否为空?为空就不能再进行删除操作。所以size要断言大于0,删除数据后记得要让size-1。

2.3 顺序表的指定位置删除

void SLErase(SL* ps,int pos)
{assert(ps != NULL);assert(pos >= 0 && pos < ps->size);for (int i = pos; i < ps->size - 1; i++){ps->arr[i] = ps->arr[i + 1];}ps->size--;
}

在这里插入图片描述删除指定位置的元素,pos需要在数组的有效数据的下标范围以内。这里本应该判断数组是否为空,但断言pos>=0&&pos<size其实就已经判断了数组是否为空。如果数组为空,则size==0,那pos取数组的任意下标即使是0,pos<size就都会为假,直接就判空了。当删除了指定位置的数据后,记得要让size-1。

Ⅲ.顺序表数据的查找

int SLFind(SL* ps, SLDataType x)
{assert(ps != NULL);for (int i = 0; i < ps->size; i++){if (ps->arr[i] == x){return i;}}return -1; //返回-1说明没找到
}

查找指定数据的位置,直接遍历链表即可,切记数组中不要出现相同的元素,不然不知道是要查找那一个。注意:这里是返回查找到的数据的下标,若找不到数据就返回-1。SLDataType是int类型的重命名,自己要根据自己所定义的类型写查找函数。

Ⅳ.顺序表的销毁

void SLDestroy(SL* ps)
{assert(ps != NULL);if (ps->arr != NULL){free(ps->arr);ps->arr = NULL;}ps->size = ps->capacity = 0;
}

由于这里是动态开辟的一块连续空间,所以直接释放这块空间即可。如果arr本就是空指针,就不需要再free。size和capacity赋值为0。记住:只要是动态开辟的空间,到后面不使用了就需要释放,把空间还给操作系统,不然可能会造成内存泄漏。

二、链表的插入(增)

1.单链表的尾插

链表的结构如下:

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

假设一开始我们就创建了一个节点指针plist,并赋值为空(NULL):

SLTNode* plist = NULL;

对于单链表的尾插,我们需要考虑链为空的情况:
(1) 如果链表为空,对于尾插函数的参数我们就需要注意一下,到底是传一级指针呢?还是传二级指针?首先要明确一点,我们创建的是一个节点指针plist,一开始还没有指向任何有效节点。如果尾插函数传的是一级指针,那就是把plist的值拷贝给了phead,而plist实参和phead形参是两个不同的指针变量,修改形参phead是不会影响实参plist的。这里就需要理解一下。我们可以看一下plist和phead这两个指针变量的地址:

//plist传给phead是值传递,不能改变plist
void SLTPushBack(SLTNode* phead,SLTDataType x)
{if (phead == NULL){phead = SLTBuyNode(x);}else{SLTNode* tail = phead;while (tail->next){tail = tail->next;}tail->next = SLTBuyNode(x);}
}

在这里插入图片描述可以看到plist和phead两个指针变量的地址是不同的,所以修改phead是不会影响plist的。由于phead是一个局部变量,出函数就会销毁;而且在函数里动态申请空间的地址随着phead的销毁也就找不到了,这样就可能造成内存泄漏。所以想修改实参plist,就要通过plist指针的地址才能实现:

void SLTPushBack(SLTNode** pphead,SLTDataType x)
{if (*pphead == NULL){*pphead = SLTBuyNode(x);}else{SLTNode* tail = *pphead;while (tail->next){tail = tail->next;}tail->next = SLTBuyNode(x);}
}

在这里插入图片描述可以看到通过传plist指针的地址,就可以实现修改指针plist的效果。如果是链表不为空的情况下,通过修改尾节点的next指向新节点即可,这时通过节点的指针就可以修改节点,所以上面的代码通过tail就可以修改节点。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述

在这里插入图片描述上面对pphead进行断言,是确保pphead这个指针不能为空。要记住传过来的一级指针的地址是一定不能为空的。(若pphead为空,就会出现对空指针进行解引用)

2.单链表的头插

(1) 单链表的头插相对于尾插较为简单,因为不用找尾节点,直接在第一节点的前面插入节点即可:

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead != NULL);SLTNode* newnode = SLTBuyNode(x);newnode->next = *pphead;*pphead = newnode;
}

在这里插入图片描述

这里依然是要传二级指针,因为要改变plist的指向,所以还是要传plist的地址;pphead依然要断言不能为空。但头插不需要判断plist是否为空,链表为空我们也能进行头插:(记住不能断言*pphead!=NULL,因为即使是空链表也要插入数据,否则这句断言直接就会让程序终止)
在这里插入图片描述

🍑🍑🍑(2) 记住:要想修改一个指针,那就要通过这个指针的地址才能修改。所以想要修改一个节点,那就通过这个节点的地址来进行修改即可。要想修改形参可以影响到实参的话,就必须传实参的地址。

(3) 其实单链表可以实现传一级指针给形参,只是因为需要兼顾链表为空的情况,我们需要改变plist指向我们申请的节点,所以要传二级指针。当链表不为空了,那通过plist就可以修改节点,不管是尾插头插,通过节点的指针就可以访问节点。

要想改变plist的指向,让函数返回malloc申请的空间地址,用plist接收即可:(以头插为例)

SLTNode* SLTPushFront(SLTNode* phead, SLTDataType x)
{SLTNode* newnode = SLTBuyNode(x);newnode->next = phead;phead = newnode;return phead;
}
plist = SLTPushFront(plist, 5);//让plist接收头插函数的返回值,即让plist指向头插的新节点

在这里插入图片描述

3.单链表的指定位置插入

对于单链表在指定位置插入数据分为在指定位置之前和之后插入。

3.1 在指定位置之前插入

(1) 若是在链表的指定位置之前插入,就需要先找到这个位置的节点。在此我们先封装一个查找节点的函数:

SLTNode* SLTFind(SLTNode* phead,SLTDataType x)
{SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}

这个函数只是要查找存储指定值的节点,找到了就返回存储该值节点的地址,所以不用传二级指针给函数,而且phead可以为空,为空就是找不到数据嘛!直接返回空即可。

这个函数还有一个功能就是修改查找节点的数据:

SLTNode* pos = SLTFind(plist, 6);//一个函数两个功能
if (pos != NULL)
{pos->data = 10;
}
else
{printf("数据不存在!\n");
}

有了SLTFind函数,我们就可以查找指定值x所在的节点位置,然后在该位置的前面插入数据:

void SLTInsert(SLTNode** pphead,SLTNode* pos, SLTDataType x)
{assert(pphead != NULL);assert(pos != NULL);if (*pphead == pos){SLTPushFront(pphead, x);}else{SLTNode* newnode = SLTBuyNode(x);SLTNode* posPrev = *pphead;while (posPrev->next != pos){posPrev = posPrev->next;}posPrev->next = newnode;newnode->next = pos;}
}

要在指定节点的前面插入节点,首先要找到pos位置的前一个节点,才能进行插入。所以这是单链表的缺点,我们要通过头节点找pos的前一个节点。其次我们查找的x值要在链表里面,否则SLTFind就找不到,返回值就为空,所以要断言pos不能为空。其次如果查找的pos是在第一个节点处,即头节点处,我们就不能找头节点的前一个节点,这时就相当于头插:
在这里插入图片描述

3.2 在指定位置之后插入

(1) 在指定位置之后插入节点,就简单很多了,因为对于单链表来说,pos位置的下一个节点是找得到的,通过pos的next就可以找到:

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos != NULL);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}

在这里插入图片描述如果链表中只有一个节点或是在尾结点后面插入也没问题:
在这里插入图片描述
注意:在指定位置之后插入是有先后顺序的,是先让newnode的next指向pos的next,再让pos的next指向newnode。如果顺序反过来会导致pos的next先指向newnode,而原来链表pos的下一个节点就找不到了。在指定位置的前后插入数据配合SLTFind函数使用最佳,而且对pos != NULL也断言了,说明pos是有效的节点。

4.双向链表的尾插

(1) 双向链表的全称是双向带头循环链表,这里的带头意思就是这个链表一开始就有一个头节点,这个节点不存储任何有效数据,称这个节点为哨兵位。当我们向链表中插入节点时,就是在这个哨兵位的前后插入节点。有了这个哨兵位其实就方便我们进行头插尾插,不用考虑链表是否为空。

哨兵位节点也是需要动态开辟的,所以初始化链表,即在链表中为哨兵位这个节点会动态开辟空间,可能会对函数传二级指针,也可以将函数的返回值给给phead。双向链表的节点结构如下:

typedef int LTDataType;
typedef struct ListNode
{LTDataType data;struct ListNode* next;//后驱指针struct ListNode* prev;//前驱指针
}LTNode;

现在为链表创建一个头节点指针:

LTNode* phead = NULL;

初始化链表:(两种方式都可以)

//1.传二级指针,通过*pphead直接就可以改变实参phead
void LTInit(LTNode** pphead)
{assert(pphead != NULL);*pphead = LTBuyNode(-1);(*pphead)->next = *pphead;(*pphead)->prev = *pphead;
}
//2.也可以让phead接收函数的返回值
LTNode* LTInit()
{LTNode* guard = LTBuyNode(-1);guard->next = guard;guard->prev = guard;return guard;
}
LTNode* phead = LTInit();//接收LTInit函数的返回值

初始化就是让链表中有一个哨兵位,且这个节点的next和prev指针都是指向自己的:
在这里插入图片描述(2)如果现在链表中只有哨兵位节点,我们要尾插一个节点:

void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead != NULL);LTNode* newnode = LTBuyNode(x);LTNode* tail = phead->prev;newnode->prev = tail;newnode->next = phead;tail->next = newnode;phead->prev = newnode;
}

双向链表尾节点就是头结点的前一个节点phead->prev,我们将其存储在tail指针中,这样tail指针就指向了链表的尾结点。
在这里插入图片描述LTPushBack函数的参数phead依然要断言不能为空,因为我们说过双向链表里面至少要有一个节点(哨兵位)。
对于链表为空的情况下(有哨兵位)的头插即是尾插,不再详述。

5.双向链表的头插

(2) 头插节点是要在phead的后面插入,也就是在phead指向的下一个节点的前面插入。若链表中已有节点(除哨兵位外),我们现进行头插:(注意:phead还是要断言不能为空,因为链表中至少要有哨兵位节点才能进行头插)

void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead != NULL);LTNode* newnode = LTBuyNode(x);LTNode* First = phead->next;newnode->prev = phead;newnode->next = First;phead->next = newnode;First->prev = newnode;
}

在这里插入图片描述头插我们依然定义了一个指针First用来指向phead的下一个节点,这样做的好处就是不会因为修改了phead的next而找不到原来链表的节点。如果不使用指针来存储phead的next,那在修改节点的prev和next时就要有先后顺序:

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

注意:上面的第三四行的相对顺序是不能交换的,如果先让phead->next指向newnode,再让phead->next->prev = newnode就会让newnode的prev指向自己。

6.双向链表的指定位置插入

6.1 在指定位置之前插入

(1) 由于双向链表是有前驱和后驱指针的,这对于我们访问链表的节点非常方便,所以就只讲解在指定位置之前插入节点。同样的,我们先封装一个用来查找数据位置的函数:

LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}

通过这个函数就可以查找链表的数据是否存在,而且通过这个函数还可以修改节点中的数据:

LTNode* pos = LTFind(phead, 4);
if (pos != NULL)
{pos->data = 100;
}
else
{printf("数据不存在!\n");
}

那通过pos的prev就可以找到pos的前一个节点,所以对于在特定位置前后插入数据,只需要知道pos节点即可,不需要知道头结点(哨兵位)。现在pos之前插入节点:

void LTInsert(LTNode* pos,LTDataType x)
{assert(pos != NULL);LTNode* newnode = LTBuyNode(x);newnode->prev = pos->prev;newnode->next = pos;pos->prev->next = newnode;pos->prev = newnode;
}

在这里插入图片描述pos是通过LTFind函数返回的,如果pos为空说明查找的节点不存在,那就不能进行插入操作,所以要对pos进行断言不能为空。另外上面八九行代码的顺序是不能改变的。依然是相同的问题,如果先让pos的prev指向newnode节点,就会导致原来pos的prev节点找不到。或者可以事先定义一个节点指针posPrev指向pos的prev节点,上面的问题就解决了。

三、链表的移除节点(删)

1.单链表的尾删

(1) 单链表的尾删要先遍历链表找尾结点,然后再释放尾结点:

void SLTPopBack(SLTNode** pphead)
{assert(pphead != NULL);assert(*pphead != NULL);if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* tail = *pphead;SLTNode* tailPrev = *pphead;while (tail->next != NULL){tailPrev = tail;tail = tail->next;}free(tail);tailPrev->next = NULL;}
}

在这里插入图片描述

在找尾结点的时候,还要用一个指针记录下尾结点tail的前一个节点tailPrev。因为当我们释放掉尾结点后,原来尾结点的前一个节点的next还指向着尾结点,这就是一个野指针,所以要把tailPrev的next置空才行。当然也可以不定义tailPrev指针,还有一种方法,当链表中至少有两个节点时:

SLTNode* tail = *pphead;
while (tail->next->next != NULL)
{tail = tail->next;
}
free(tail->next);
tail->next = NULL;

在这里插入图片描述注意:要对*pphead断言不能为空,如果链表为空,就不能再进行尾删了。

2.单链表的头删

(1) 单链表的头删,即释放头结点,让原来头结点的下一个节点成为链表新的头:

void SLTPopFront(SLTNode** pphead)
{assert(pphead != NULL);assert(*pphead != NULL);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}

在这里插入图片描述头删依然要断言*pphead不能为空,因为空链表本就不需要进行删除。当链表中只有一个头结点时上面的代码依然可以跑过。

3.单链表的指定位置删除

3.1 删除指定位置的节点

(1) 删除指定位置的节点,分两种情况:pos不是头结点:

void SLTErase(SLTNode** pphead,SLTNode* pos)
{assert(pphead != NULL);assert(pos != NULL);if (pos == *pphead){SLTPopFront(pphead);}else{SLTNode* posPrev = *pphead;while (posPrev->next != pos){posPrev = posPrev->next;}posPrev->next = pos->next;free(pos);}
}

在这里插入图片描述(2) 另一种就是pos就在头结点位置:
在这里插入图片描述这里本应该断言*pphead不为空,链表为空就不用删除。但是因为断言pos不为空其实就已经断言了链表不为空,所以断言*pphead不为空可有可无。

3.2 删除指定位置之前的节点

(1) 删除指定位置之前的节点,分三种情况的:

void SLTErasePrev(SLTNode** pphead,SLTNode* pos)
{assert(pphead != NULL);assert(pos != NULL);assert(pos != *pphead);if ((*pphead)->next == pos){SLTPopFront(pphead);}else{SLTNode* pprev = *pphead;while (pprev->next->next != pos){pprev = pprev->next;}free(pprev->next);pprev->next = pos;}
}

在这里插入图片描述
注意:若指定位置pos为头结点,则不能进行删除,因为头节点前面没有节点。所以要断言pos!=plist。对于在指定位置之后删除节点较于简单,不再叙述。但是需注意:如果pos是在尾结点处,则不能删除pos位置的下一个节点,因为尾结点后面没有节点。

4.双向链表的尾删

(1) 尾删指的是删除哨兵位节点的前一个节点(phead->prev):

void LTPopBack(LTNode* phead)
{assert(phead != NULL);assert(phead->prev != phead); //等价于assert(!LTEmpty(phead))LTNode* tail = phead->prev;LTNode* tailPrev = tail->prev;tailPrev->next = phead;phead->prev = tailPrev;free(tail);
}

在这里插入图片描述注意:如果链表中只有哨兵位节点时,不能进行删除操作。
所以断言phead->prev!=phead。

5.双向链表的头删

(1) 头删指的删除头结点(哨兵位)的后一个节点(phead->next):

void LTPopFront(LTNode* phead)
{assert(phead != NULL);assert(phead->next != phead);//等价于assert(!LTEmpty(phead))LTNode* First = phead->next;LTNode* Second = phead->next->next;Second->prev = phead;phead->next = Second;free(First);
}

在这里插入图片描述头删依然不能删除哨兵位节点,所以断言phead->next!=phead。

6.双向链表的指定位置删除

6.1 删除指定位置的节点

(1) 双向链表删除指定位置的节点比较简单,由于每个节点是有前驱和后驱指针的,所以通过pos就可以找到pos的前后节点。但是需要注意:由于链表中至少是有哨兵位节点的,所以pos位置理论上是不会找到头结点的,因为LTFind函数是自己封装的,自己能清楚地知道能不能找到某个节点位置,且不会找到哨兵位节点,因为在LTFind函数里断言过不能找哨兵位这个节点。但是在涉及头删尾删或者指定位置删除时,删到最后是不能把哨兵位也给删除掉的,至少在还没有使用完链表之前不能删,因为后面可能还要继续插入数据。而且到后面链表不使用了,所有节点是由LTDestroy函数来专门释放,包括哨兵位。

void LTErase(LTNode* phead,LTNode* pos)
{assert(!LTEmpty(phead));assert(pos != NULL);assert(pos != phead);LTNode* posPrev = pos->prev;LTNode* posNext = pos->next;posPrev->next = posNext;posNext->prev = posPrev;free(pos);
}

在这里插入图片描述通过定义临时指针来存储pos的前后节点很直观的就能理解。但也可以像上图一样不定义临时指针变量,但需要注意①②的顺序要在③的前面,①②的顺序可以交换。不能提前释放pos节点,这样会导致找不到pos节点的前后节点,除非像上面的代码中一样提前定义临时指针存储pos的前后节点地址,这样问题就解决了。

6.2 删除指定位置之后的节点

(1) 删除指定位置前后的节点都比较简单,只讲删除指定位置之后的节点:

void LTEraseAfter(LTNode* phead, LTNode* pos)
{assert(!LTEmpty(phead));assert(pos != NULL);assert(pos != phead);assert(pos->next != phead);LTNode* Next = pos->next;LTNode* NNext = pos->next->next;NNext->prev = pos;pos->next = NNext;free(Next);
}

在这里插入图片描述注意:在删除pos节点后面的节点时,pos节点的下一个节点不能是哨兵位,不能删除链表中的哨兵位节点。理论上pos不会是哨兵位节点,因为查找函数LTFind是跳过了哨兵位节点进行查找的。对于删除指定位置之前的节点也很类似,但是依然要注意:删除pos节点的前一个节点,也要保证不能删除哨兵位节点。

7.双向链表的判空

对于双向链表的删除,需要注意的是链表中只有哨兵位节点时,不能再进行删除,链表中必须要保留哨兵位节点,因为后面可能还要继续插入数据。当然若链表中不止有哨兵位节点时,删到最后也不能把哨兵位节点给删掉。只有在完全不使用双向链表了,由LTDestroy函数来释放链表中的节点,包括哨兵位。所以在进行删除之前,要先判断双向链表是否为空?链表中只有哨兵位节点时就是为空:

bool LTEmpty(LTNode* phead)
{assert(phead != NULL);return phead->next == phead;
}

四、链表的销毁

1.单链表的销毁

(1)单链表销毁就是将链表中的所有节点依次释放掉,即把链表删为空:

void SLTDestroy(SLTNode** pphead)
{assert(pphead != NULL);SLTNode* cur = *pphead;while (cur){SLTNode* next = cur->next;free(cur);cur = next;}*pphead = NULL;
}

pphead是要断言不为空的,但*pphead不用断言,因为即使是空链表上面的代码也能跑过。由于是单链表,所以从头往后释放节点需要先记录下释放节点的后一个节点才行,不然就会导致后面的节点找不到。单链表销毁后,通过SLTPrint函数可以把NULL打印出来。

2.双向链表的销毁

(1) 双向链表的销毁也简单,以传二级指针的方式销毁:

void LTDestroy(LTNode** pphead)
{assert(*pphead != NULL);LTNode* cur = (*pphead)->next;while (cur != *pphead){LTNode* next = cur->next;free(cur);cur = next;}free(*pphead);//释放哨兵位*pphead = NULL;
}

或者传一级指针也可以,只不过函数外面的phead还要手动置空,因为形参phead是实参phead的值拷贝:

void LTDestroy(LTNode* phead)
{assert(phead != NULL);LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);//释放哨兵位
}
LTDestroy(phead);
phead = NULL;

注意双向链表销毁后就不能再通过LTPrint函数打印了,否则会造成空指针的解引用。

🏈🏈对于链表的查和改操作,其实在上面的插入删除中已经讲到了,通过SLTFind(单链表)、LTFind(双向链表)可以去查找链表中是否有存储x值的节点,而且找到存储x值的节点pos以后,还可以通过pos指针修改该节点的data值。

🥐链表需要理解其逻辑结构和物理结构,对于链表节点的创建更是要理解其是在堆上申请的,每个节点尤其只能有一个地址,但是存储这些节点地址的指针变量却能有多个,这就是需要理解的地方。还有就是函数到底是传一级指针还是传二级指针要视情况而定。如果是想改变节点,那就传节点的指针(地址)即可;要是想改变指针,那就要传指针的地址才行。🥑


http://www.ppmy.cn/news/1563967.html

相关文章

HTTP:Nagle算法与TCP_NODELAY

背景&#xff1a;TCP 有一个数据流接口&#xff0c;应用程序可以通过它将任意尺寸的数据放入 TCP 栈中—— 即使一次只放一个字节也可以!但是&#xff0c;每个 TCP 段中都至少装载了 40 个字节的标记和首部&#xff0c;所以如果 TCP 发送了大量包含少量数据的分组&#xff0c;网…

【 MySQL学习1】简介

文章目录 一、一些概念1.1 什么是数据库1.2 数据库管理系统1.3 SQL1.4 三者之间的关系 二 安装MySQL数据库三、命令启动3.1 停止服务3.2 启动服务 一、一些概念 1.1 什么是数据库 英文单词DataBase&#xff0c; 简称DB。 按照一定格式存储数据的一些文件的组合。 存储数据的仓…

微信小程序:实现首页权限菜单效果

一、效果 二、数据库表 1、菜单表 包含权限名称,模块名称,图标名称,页面跳转的方法,菜单是否显示栏位 2、角色对应权限表 包含角色id和权限id,这个表将角色和菜单角色连接,给角色赋予权限功能 3、 账户表 用于绑定账号隶属于什么角色,绑定的角色表 4、角色表 菜单的…

配置AOSP下载环境

1#curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo 2#repo init -u https://android.googlesource.com/platform/manifest 3#清华镜像站帮助页 https://mirrors.tuna.tsinghua.edu.cn/help/AOSP/ 4#同步安卓AOSP 这里是安卓13 repo init -u htt…

深入解析:Python机器学习在气象领域中的应用实例——GFS数值模式的风速预报订正。数据处理、科学计算、数学建模、数据挖掘和数据可视化

在气象领域&#xff0c;全球预报系统&#xff08;GFS&#xff09;数值模式的风速预报订正是一个关键的应用场景&#xff0c;其中机器学习技术发挥了重要作用。本文将详细解析这一应用实例&#xff0c;包括所用到的公式和代码&#xff0c;帮助读者更好地理解如何利用Python和机器…

AI Agent:软件测试自动化的新纪元

在信息技术日新月异的今天&#xff0c;人工智能&#xff08;AI&#xff09;技术的蓬勃发展正引领着各个行业的深刻变革&#xff0c;软件测试领域同样迎来了前所未有的机遇与挑战。AI Agent&#xff0c;这一融合了先进机器学习与自然语言处理技术的智能实体&#xff0c;正悄然成…

提高互联网Web安全性:避免越权漏洞的技术方案

目录 一、越权漏洞概述 二、常见的越权漏洞类型 三、越权漏洞的影响 四、越权漏洞的技术解决方案 一、越权漏洞概述 越权&#xff08;Authorization Bypass&#xff09;类漏洞是指在系统中&#xff0c;攻击者通过绕过身份验证或访问控制&#xff0c;获取本不应访问的资源或…

记录一次RPC服务有损上线的分析过程

作者&#xff1a;京东零售 郭宏宇 1. 问题背景 某应用在启动完提供JSF服务后&#xff0c;短时间内出现了大量的空指针异常。 分析日志&#xff0c;发现是服务依赖的藏经阁配置数据未加载完成导致。即所谓的有损上线或者是直接发布&#xff0c;*当\\\*\应用启动时&#xff0c;se…