链表分为单链表、双链表、循环链表
先看最简单的单链表
单链表的实现
单链表:用链式存储实现了线性结构。一个结点存储一个数据元素,各结点间的前后关系用一个指针表示。
特点:
优点:不要求大片连续空间,改变容量方便。
缺点:不可随机存取,要耗费一定空间存放指针。
两种实现方式:
带头结点,写代码更方便。头结点不存储数据,头结点指向的下一个结点才存放实际数据。
不带头结点,麻烦。对第一个数据结点与后续数据结点的处理需要用不同的代码逻辑,对空表和非空表的处理需要用不同的代码逻辑。
定义单链表
typedef struct LNode { //定义单链表结点类型 ElemType data; //数据域 struct LNode *next;//指针域 }LNode, *LinkList;
注意,链表定义时是定义结点。然后需要链表时定义一个结点指针,表示一个链表的开始,也就可以用来表示链表了。
不带头结点初始化
typedef struct LNode{ ElemType data; struct LNode *next; }LNode, *LinkList; //初始化一个空的单链表 bool InitList(LinkList &L){ L = NULL; //空表,暂时还没有任何结点 return true; } void test(){ LinkList L; //声明一个指向单链表的头指针 //初始化一个空表 InitList(L); ... } //判断单链表是否为空 bool Empty(LinkList L){ return (L==NULL) }
带头结点初始化
typedef struct LNode { ElemType data; struct LNode *next; }LNode, *LinkList; //初始化一个单链表(带头结点) bool InitList(LinkList &L) { L = (LNode*) malloc(sizeof(LNode)); //头指针指向的结点——分配一个头结点(不存储数据) if (L == NULL) //内存不足,分配失败 return false; L -> next = NULL; //头结点之后暂时还没有结点 return true; } void test() { LinkList L; //声明一个指向单链表的指针: 头指针 //初始化一个空表 InitList(L); //... } //判断单链表是否为空(带头结点) bool Empty(LinkList L) { if (L->next == NULL) return true; else return false; }
头指针其实就是定义一个链表结点结构体的指针,用来表示一个链表,一般会指向头结点。
按位序插入(带头结点) Listlnsert(&Li,e): 插入操作。在表L中的第i个位置上插入指定元素e 找到第i-1个结点(前驱结点),将新结点插入其后;其中头结点可以看作第0个结点,故i=1时也适用。 平均时间复杂度:O(n)
typedef struct LNode { ElemType data; struct LNode *next; }LNode, *LinkList; //在第i个位置插入元素e(带头结点) bool ListInsert(LinkList &L, int i, ElemType e) { //判断i的合法性, i是位序号(从1开始) if(i<1) return False; LNode *p; //指针p指向当前扫描到的结点 int j=0; //当前p指向的是第几个结点 p = L; //L指向头结点,头结点是第0个结点(不存数据) //循环找到第i-1个结点 while(p!=NULL && j<i-1){ //如果i>lengh, p最后会等于NULL p = p->next; //p指向下一个结点 j++; } if (p==NULL) //如果p指针知道最后再往后就是NULL return false; //在第i-1个结点后插入新结点 LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点 s->data = e; s->next = p->next; p->next = s; //将结点s连到p后,后两步千万不能颠倒qwq return true; }
按位序插入(不带头结点) Listlnsert(&L,i,e): 插入操作。在表L中的第i个位置上插入指定元素e。将新结点插入其后; 因为不带头结点,所以不存在“第0个”结点,因此!i=1 时,需要特殊处理——插入(删除)第1个元素时,需要更改头指针L;
typedef struct LNode { ElemType data; struct LNode *next; }LNode, *LinkList; bool ListInsert(LinkList &L, int i, ElemType e) { if(i<1) return false; //插入到第1个位置时的操作有所不同! if(i==1){ LNode *s = (LNode *)malloc(size of(LNode)); s->data =e; s->next =L; L=s; //头指针指向新结点 return true; } //i>1的情况与带头结点一样!唯一区别是j的初始值为1 LNode *p; //指针p指向当前扫描到的结点 int j=1; //当前p指向的是第几个结点 p = L; //L指向头结点,头结点是第0个结点(不存数据) //循环找到第i-1个结点 while(p!=NULL && j<i-1){ //如果i>lengh, p最后会等于NULL p = p->next; //p指向下一个结点 j++; } if (p==NULL) //i值不合法 return false; //在第i-1个结点后插入新结点 LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点 s->data = e; s->next = p->next; p->next = s; return true; }
指定结点的后插操作 InsertNextNode(LNode *p, ElemType e); 给定一个结点p,在其之后插入元素e; 根据单链表的链接指针只能往后查找,故给定一个结点p,那么p之后的结点我们都可知,但是p结点之前的结点无法得知
typedef struct LNode { ElemType data; struct LNode *next; }LNode, *LinkList; bool InsertNextNode(LNode *p, ElemType e) { if(p==NULL){ return false; } LNode *s = (LNode *)malloc(sizeof(LNode)); //某些情况下分配失败,比如内存不足 if(s==NULL) return false; s->data = e; //用结点s保存数据元素e s->next = p->next; p->next = s; //将结点s连到p之后 return true; } //平均时间复杂度 = O(1) //有了后插操作,那么在第i个位置上插入指定元素e的代码可以改成: bool ListInsert(LinkList &L, int i, ElemType e) { if(i<1) return False; LNode *p; //指针p指向当前扫描到的结点 int j=0; //当前p指向的是第几个结点 p = L; //L指向头结点,头结点是第0个结点(不存数据) //循环找到第i-1个结点 while(p!=NULL && j<i-1){ //如果i>lengh, p最后4鸟会等于NULL p = p->next; //p指向下一个结点 j++; } return InsertNextNode(p, e) }
指定结点的前插操作 设待插入结点是s,将s插入到p的前面。我们仍然可以将s插入到*p的后面。然后将p->data与s->data交换,这样既能满足了逻辑关系,又能是的时间复杂度为O(1)
//前插操作:在p结点之前插入元素e bool InsertPriorNode(LNode *p, ElenType e){ if(p==NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); if(s==NULL) //内存分配失败 return false; //重点来了! s->next = p->next; p->next = s; //新结点s连到p之后 s->data = p->data; //将p中元素复制到s p->data = e; //p中元素覆盖为e return true; }
头插
LinkList List_HeadInsert(LinkList &L){ //逆向建立单链表 LNode *s; int x; L = (LinkList)malloc(sizeof(LNode)); //建立头结点 L->next = NULL; //初始为空链表,这步不能少! scanf("%d", &x); //输入要插入的结点的值 while(x!=9999){ //输入9999表结束 s = (LNode *)malloc(sizeof(LNode)); //创建新结点 s->data = x; s->next = L->next; L->next = s; //将新结点插入表中,L为头指针 scanf("%d", &x); } return L; }
尾插
// 使用尾插法建立单链表L LinkList List_TailInsert(LinkList &L){ int x; //设ElemType为整型int L = (LinkList)malloc(sizeof(LNode)); //建立头结点(初始化空表) LNode *s, *r = L; //r为表尾指针 scanf("%d", &x); //输入要插入的结点的值 while(x!=9999){ //输入9999表示结束 s = (LNode *)malloc(sizeof(LNode)); s->data = x; r->next = s; r = s; //r指针指向新的表尾结点 scanf("%d", &x); } r->next = NULL; //尾结点指针置空 return L; }
参考:
浅析线性表(链表)的头插法和尾插法的区别及优缺点 - swing·wang - 博客园 (cnblogs.com)
【数据结构】:单链表之头插法和尾插法(动图+图解)_头插法和尾插法图解-CSDN博客
按位序删除节点 ListDelete(&L, i, &e): 删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值;头结点视为“第0个”结点; 思路:找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点
typedef struct LNode{ ElemType data; struct LNode *next; }LNode, *LinkList; bool ListDelete(LinkList &L, int i, ElenType &e){ if(i<1) return false; LNode *p; //指针p指向当前扫描到的结点 int j=0; //当前p指向的是第几个结点 p = L; //L指向头结点,头结点是第0个结点(不存数据) //循环找到第i-1个结点 while(p!=NULL && j<i-1){ //如果i>lengh, p最后会等于NULL p = p->next; //p指向下一个结点 j++; } if(p==NULL) return false; if(p->next == NULL) //第i-1个结点之后已无其他结点 return false; LNode *q = p->next; //令q指向被删除的结点 e = q->data; //用e返回被删除元素的值 p->next = q->next; //将*q结点从链中“断开” free(q) //释放结点的存储空间 return true; }
指定结点的删除
bool DeleteNode(LNode *p){ if(p==NULL) return false; LNode *q = p->next; //令q指向*p的后继结点 p->data = p->next->data; //让p和后继结点交换数据域 p->next = q->next; //将*q结点从链中“断开” free(q); return true; } //时间复杂度 = O(1)
单链表的按位查找 GetElem(L, i): 按位查找操作,获取表L中第i个位置的元素的值; 平均时间复杂度O(n)
LNode * GetElem(LinkList L, int i){ if(i<0) return NULL; LNode *p; //指针p指向当前扫描到的结点 int j=0; //当前p指向的是第几个结点 p = L; //L指向头结点,头结点是第0个结点(不存数据) while(p!=NULL && j<i){ //循环找到第i个结点 p = p->next; j++; } return p; //返回p指针指向的值 }
单链表不具备随机访问的特性,查找时只能依次遍历。
单链表的按值查找 LocateElem(L, e):按值查找操作,在表L中查找具有给定关键字值的元素; 平均时间复杂度:O(n)
LNode * LocateElem(LinkList L, ElemType e){ LNode *P = L->next; //p指向第一个结点 //从第一个结点开始查找数据域为e的结点 while(p!=NULL && p->data != e){ p = p->next; } return p; //找到后返回该结点指针,否则返回NULL }
求单链表的长度 Length(LinkList L):计算单链表中数据结点(不含头结点)的个数,需要从第一个结点看是顺序依次访问表中的每个结点。 算法的时间复杂度为O(n)
int Length(LinkList L){ int len=0; //统计表长 LNode *p = L; while(p->next != NULL){ p = p->next; len++; } return len; }
更多待补充。
双链表
其实和单链表大同小异,只不过结点中既有后指针,又有前指针。
双链表中节点类型的描述
typedef struct DNode{ //定义双链表结点类型 ElemType data; //数据域 struct DNode *prior, *next; //前驱和后继指针 }DNode, *DLinklist;
双链表的初始化(带头结点)
typedef struct DNode{ //定义双链表结点类型 ElemType data; //数据域 struct DNode *prior, *next; //前驱和后继指针 }DNode, *DLinklist; //初始化双链表 bool InitDLinkList(Dlinklist &L){ L = (DNode *)malloc(sizeof(DNode)); //分配一个头结点 if(L==NULL) //内存不足,分配失败 return false; L->prior = NULL; //头结点的prior指针永远指向NULL L->next = NULL; //头结点之后暂时还没有结点 return true; } void testDLinkList(){ //初始化双链表 DLinklist L; // 定义指向头结点的指针L InitDLinkList(L); //申请一片空间用于存放头结点,指针L指向这个头结点 //... } //判断双链表是否为空 bool Empty(DLinklist L){ if(L->next == NULL) //判断头结点的next指针是否为空 return true; else return false; }
双链表的插入操作 后插操作 InsertNextDNode(p, s): 在p结点后插入s结点
bool InsertNextDNode(DNode *p, DNode *s){ //将结点 *s 插入到结点 *p之后 if(p==NULL || s==NULL) //非法参数 return false; s->next = p->next; if (p->next != NULL) //p不是最后一个结点=p有后继结点 p->next->prior = s; s->prior = p; p->next = s; return true; }
双链表的删除操作 删除p节点的后继节点
//删除p结点的后继结点 bool DeletNextDNode(DNode *p){ if(p==NULL) return false; DNode *q =p->next; //找到p的后继结点q if(q==NULL) return false; //p没有后继结点; p->next = q->next; if(q->next != NULL) //q结点不是最后一个结点 q->next->prior=p; free(q); return true; } //销毁一个双链表 bool DestoryList(DLinklist &L){ //循环释放各个数据结点 while(L->next != NULL){ DeletNextDNode(L); //删除头结点的后继结点 free(L); //释放头结点 L=NULL; //头指针指向NULL } }
双链表的遍历操作
前向遍历
while(p!=NULL){ //对结点p做相应处理,eg打印 p = p->prior; }
后向遍历
while(p!=NULL){ //对结点p做相应处理,eg打印 p = p->next; }
双链表不可随机存取,按位查找和按值查找操作都只能用遍历的方式实现,时间复杂度为O(n)
循环链表
循环单链表 结点定义方式和单链表一样,只不过最后一个结点的指针不是NULL,而是指向头结点。
循环双链表 结点定义方式和双链表一样,只不过表头结点的prior指向表尾结点,表尾结点的next指向头结点。
暂略。
链表补充
创建一个链表,最开始不是考虑头结点的问题,没有头结点也可以,而是先考虑头指针的问题,因为头指针用来标识一个链表。后续操作都是针对这个头指针来进行的,也就是针对该链表来进行的。
创建一个结点的功能,一般不会由外部直接调用,而主要是提供给插入结点函数使用的,剥离出来更灵活,因为插入不管是头插还是尾插,都需要先创建一个结点。
注意:堆上的数据得主动释放才会没了,不要以为在函数内创建的,离开函数就会被释放,别跟栈搞混了。
创建结点有什么需要注意的地方吗?
我们使用结构体指针去进行操作,因此创建节点后返回的是链表结点的指针;
传递进入的是创建的结点中要存储的数据;
新创建的结点中下一个结点指针应该先指向NULL,后面插入时如果是尾插,那就不用管,如果是头插,那就得把指针指向上一个首结点;
创建结点的函数可以限定为static,因为一般就是提供给插入函数使用的。
插入结点时应当注意什么?
浅析线性表(链表)的头插法和尾插法的区别及优缺点_链表头插法和尾插法哪个好-CSDN博客
尾插时:
不需要返回值;
传递进入链表,也就是一个结点指针,还有一个就是插入的结点里存放的数据,这个数据一会儿在创建结点时需要使用;
插入前先要创建一个结点;
头指针是用来存储第一个结点的指针的变量;
遍历链表
有了插入和遍历,就能初步验证链表了。
链表里的数据类型其实也是固定的。
以下提供不带头结点的链表示例:
#include "linkedlist.h" #include <stdlib.h> #include <stdio.h> //定义单链表结点里存放的数据类型 typedef int NODE_TYPE; //定义链表结点 typedef struct LinkedListNode { NODE_TYPE nodeData; struct LinkedListNode *nextNode; }ST_LLNODE; //创建结点 static ST_LLNODE *CreateOneNode(NODE_TYPE data) { ST_LLNODE *newNode = malloc(sizeof(ST_LLNODE));//结点创建必须malloc,在堆上创建,如果用局部变量,那么出函数就没了 if(newNode == NULL) { printf("warning: create newnode fail!"); return NULL; } newNode->nodeData = data; newNode->nextNode = NULL; return newNode; } //尾插 void InsertNodeAtTail(ST_LLNODE **linkedList, NODE_TYPE data) { if(linkedList == NULL) { return; } //先创建一个结点 ST_LLNODE *newNode = CreateOneNode(data); //判断链表是不是空链表 if(*linkedList == NULL) { *linkedList = newNode;//如果是空链表,就直接把第一个结点挂到链表下面 } else { ST_LLNODE *tail = *linkedList; while(tail->nextNode != NULL) { tail = tail->nextNode; }//循环结束后,就到了最后一个结点 tail->nextNode = newNode;//最后一个结点指向新结点 } } //遍历链表 void PrintLinkedList(ST_LLNODE *linkedList) { if(linkedList == NULL) { printf("warning: linkedlist is null!"); } else { ST_LLNODE *tail = linkedList; //直接判断每个结点是不是空,最后一个结点的下一个肯定是空,意味着遍历结束了 while(tail != NULL)//插入时遍历是为了找到最后一个结点,这里是为了定位到最后一个结点的下一个NULL { printf("data %d\r\n", tail->nodeData); tail = tail->nextNode; } printf("print is ok_____________\r\n"); } } //测试链表 void Test(void) { //创建一个链表 ST_LLNODE *myLLHead = NULL;//链表头指针,这个指针以后指向第一个结点 InsertNodeAtTail(&myLLHead, 0);//需要传入链表的指针,才能改变该链表,需要将该链表的NULL改成第一个结点的地址 InsertNodeAtTail(&myLLHead, 1);//后续尾差都不用动改地址,不过头插需要更改 InsertNodeAtTail(&myLLHead, 2); InsertNodeAtTail(&myLLHead, 3); InsertNodeAtTail(&myLLHead, 4); InsertNodeAtTail(&myLLHead, 5); InsertNodeAtTail(&myLLHead, 6); InsertNodeAtTail(&myLLHead, 7); InsertNodeAtTail(&myLLHead, 8); InsertNodeAtTail(&myLLHead, 9); InsertNodeAtTail(&myLLHead, 10); PrintLinkedList(myLLHead);//内部不需要修改指针本身,只是使用地址值,所以无需传递链表的指针 } //再次提醒,C语言的函数如果返回的是个局部变量的地址值,我们是无法通过该地址去访问局部变量的; //如果我们想要在一个函数内将一个局部变量传递给另一个函数,并且在另一个函数内部能影响到传递的数据,就必须传递地址值,否则传递的只是个副本(同理,返回值其实也是个副本); //如果这个要传递的局部变量本身就是个指针,那就需要传递二重指针。(这种情况特别容易搞混) //除非是个全局变量,此时,也不需要传参了,直接使用即可。 //头插 //删除结点 //修改结点
//再次提醒,C语言的函数如果返回的是个局部变量的地址值,我们是无法通过该地址去访问局部变量的;
//如果我们想要在一个函数内将一个局部变量传递给另一个函数,并且在另一个函数内部能影响到传递的数据,就必须传递地址值,否则传递的只是个副本(同理,返回值其实也是个副本);
//如果这个要传递的局部变量本身就是个指针,那就需要传递二重指针。(这种情况特别容易搞混)
//除非是个全局变量,此时,也不需要传参了,直接使用即可。
通常链表各结点都是在创建结点时malloc出来的,然后各结点的指针都保存在了上一个结点里面,所以,可以在删除结点时free释放结点内存。
链表插入时是插入结点元素,结点的指针是插入时通过malloc得到的。
网上关于链表的实现逻辑,有各种各样的, 还是先多看看总结吧。
头结点和头指针
链表中,头指针和头结点的理解,是一个难点。
看下链表的图示:
图中的phead指针中存放的是第一个结点的地址,那么根据指着地址我们就可以找到这个结构体,又因为这个结构体中存放了下一个结构体的地址,所以又可以找到第二个结构体,循环往复就可以找到所有的结点,直到存放空地址的结构体。
注意,每一个结点的地址都在上一个结点中,那么,问题就来了,第一个结点没有上一个结点,它的指针放在哪呢?找不到第一个结点,就表示这个链表丢失了。
我们实际中最常用还是这两种链表结构:
1. 不带头结点单向链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头结点双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
链表里说的带或者不带头结点,这个头结点应该指的是固定头结点,因为就算没有一个固定头结点,链表的第一个有效结点也可以称为头结点。所以,别理解错了。我们在链表中提到头结点,约定俗成都是指的固定头结点。固定头结点中不存储数据。
除了头结点,另外还有个头指针的概念,上面提到个问题,那就是第一个结点没有上一个结点,它的指针放在哪呢?为此,我们需要定义一个指针,让这个指针指向第一个结点,注意,头指针不是结点,只是一个指针变量,只不过这个指针变量指向的是一个结点类型的数据。
参考:数据结构:单链表——带头结点与不带头结点步骤详解_单链表带头结点和不带头结点各种操作-CSDN博客
头指针:通常使用“头指针”来标识一个链表,如单链表L,头指针为NULL的时表示一个空链表。链表非空时,头指针指向的是第一个结点的存储位置。
头结点:在单链表的第一个结点之前附加一个结点,称为头结点。头结点的Data域可以不设任何信息,也可以记录表长等相关信息。若链表是带有头结点的,则头指针指向头结点的存储位置。
无论是否有头结点,头指针始终指向链表的第一个结点。如果有头结点,头指针就指向头结点(只不过头结点的数据域为空而已)。
这里就有个问题:为什么链表要区分带头结点和不带头结点两种?
答案很简单,为了方便。
具体操作上方便在哪里,我们接下来看下。
如果带头结点,那么头插操作时,也相当于是在结点之间插入结点,和其他在任意结点之间插入结点的操作是一样的;如果不带头结点,那么头插操作时,第一个结点总是在变化的,涉及的是跟头指针之间的关联,跟其他在任意结点之间插入结点的操作是不一样的。
我们知道,头指针就是第一个结点的指针pHead。如果没有固定头结点,那么在头插时,操作是这样的。
pNode->next = pHead;//先把当前第一个结点的指针,也就是pHead,挂到待插入结点的后面; pHead = pNode;//然后重新给头指针赋值,也就是把待插入结点的指针赋值给头指针,结点指针通过malloc得到
不带头结点的链表,在表头以外的地方插入结点
pNode->next = pAimNode->next;//先把待插入位置的下一个结点指针挂到待插入结点里 pAimNode->next = pNode;//再把待插入结点挂到前一个结点后面
可见,头插和非头插时的操作是不一样的。
如果带了固定头结点,因为插入时都属于结点间的插入操作,所以操作方式和上述第二种情况都是一样的。
也就是说,带固定头结点时,统一了头插和非头插时的插入操作。
同理,第一个位置的删除和其他位置的删除操作也能统一起来。
另外,带头结点还有个好处
参考:深刻理解:带头结点和不带头结点的区别 使用头结点的优势-CSDN博客
优势2:统一空表和非空表的处理
若使用头结点,无论表是否为空,头指针都指向头结点,也就是*LNode类型,对于空表和非空表的操作是一致的。
若不使用头结点,当表非空时,头指针指向第1个结点的地址,即*LNode类型,但是对于空表,头指针指向的是NULL,此时空表和非空表的操作是不一致的。
这一点主要是统一了头指针的指向问题。
更多参考:单链表实现+注释+原码
参考:
关注下结点命名和链表命名的问题,因为链表本身也是用一个结点指针来表示的,链表的类型声明中是否含有node关键词?
接下来参考上述链接,来关注下各功能里的一些注意点,我们以不带头结点的单链表为例。
定义如下:
typedef int SLTDataType; typedef struct SListNode { SLTDataType data; struct SListNode* next;//存放下一个结点的地址 }SListNode;
完整实现
//打印链表 //传递进来的就是头指针,头指针标记了一个链表 void SListPrint(SListNode* phead) { SListNode* cur = phead;//这里不直接操作头指针,是为了不破坏头指针 while (cur)//当数据元素存在时则打印 { printf("%d->", cur->data);//头指针访问的data就是第一个结点的数据 cur = cur->next;//依次往后遍历结点 } printf("NULL\n"); } //申请结点 //申请结点的功能一般都是独立出来给插入结点的函数调用的 //注意,只用传递进入结点的数据 //返回一个结点指针 SListNode* SListBuyNode(SLTDataType x) { SListNode* newnode = (SListNode*)malloc(sizeof(SListNode)); if (newnode == NULL) { perror("malloc:"); exit(-1); } newnode->data = x;//将元素放进去 newnode->next = NULL;//指向的结点指针先置NULL,然后在被调用处根据情况设置 return newnode; } //销毁链表 //为什么这里要传递进入二重指针? //仔细想一想,如果传递的是指针本身,那么,我们可以去操作指针所指向的内容,但是不能改变指针变量本身, //因为这时传递的是指针的一个副本,改变了副本,也不影响原来的指针变量的值。 //如果传入二重指针,也就是指针的指针,那么,就肯定是因为想要改变传入的指针变量本身。 //销毁链表,需要改变指针变量本身吗? void SListDestory(SListNode** pphead) { assert(pphead); SListNode* cur = *pphead;//对二重指针取指针,得到的就是头指针,这里不是传递二重指针的原因 while (cur) { SListNode* next = cur->next; free(cur); cur = next; } *pphead = NULL;//将头指针置NULL,这里是传递二重指针的原因,若传递头指针,就没法改变原指针 }//注意,当定义的链表头指针是个局部变量的时候,就需要传递进入二重指针; //但是如果链表头指针是个全局变量,则无需传递二重指针; //不管怎样,目的都是为了改变头指针本身的值。 //另外,需要传递二重指针的另一个原因是,所实现的链表是不带固定头结点的, //如果带了固定头结点,那么就不用去改变头指针的值了,也就不需要传递二重指针了。 //所以,总结就是,二重指针仅仅是在实现不带头结点链表,并且头指针是以局部变量的形式定义的情况下才需要的, //如果链表带了头结点,或者是以全局变量定义的,都无需传递二重指针。 //尾插 //传递进入头指针的指针,以及结点数据 //为什么尾插需要传递二重指针?尾插哪里会改变头指针的指向?当链表是空表时用到! void SListPushBack(SListNode** pphead, SLTDataType x) { assert(pphead); SListNode* newnode = SListBuyNode(x); if (*pphead == NULL) { *pphead = newnode;//空表处理,传递二重指针的理由 } else { SListNode* cur = *pphead; while (cur->next) { cur = cur->next; } cur->next = newnode; } } //头插 //头插时一定会改变头指针的指向,所以传递进入二重指针 void SListPushFront(SListNode** pphead, SLTDataType x) { assert(pphead); SListNode* newnode = SListBuyNode(x); newnode->next = *pphead;//将当前头结点挂到新结点后面 *pphead = newnode;//新结点成为新的头结点 } //尾删 void SListPopBack(SListNode** pphead) { assert(*pphead && pphead);//断言不是空表 if ((*pphead)->next == NULL)//如果只有一个结点 { free(*pphead); *pphead = NULL;//置为空表 return; } SListNode* cur = *pphead; SListNode* next = (*pphead)->next; while (next->next) { next = next->next; cur = cur->next; } cur->next = NULL; free(next); next = NULL; } //头删 void SListPopFront(SListNode** pphead) { assert(*pphead && pphead); SListNode* next = (*pphead)->next; free(*pphead); *pphead = next; } //查找 SListNode* SListFind(SListNode* phead, SLTDataType x) { while (phead) { if (phead->data == x) { return phead; } phead = phead->next; } return NULL; } //指定位置后面插入 void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x) { assert(pphead && pos); SListNode* newnode = SListBuyNode(x); SListNode* next = pos->next; pos->next = newnode; newnode->next = next; } //指定位置删除 void SListErase(SListNode** pphead, SListNode* pos) { assert(pphead && pos); if (*pphead == pos) { *pphead = (*pphead)->next; free(pos); pos = NULL; } else { SListNode* cur = *pphead; while (cur->next != pos) { cur = cur->next; } cur->next = pos->next; free(pos); pos = NULL; } }
更多待补充。