赞
踩
目录
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表 中的指针链接次序实现的 。
注意:
- 链表在逻辑上是连续的,但是在物理上不一定连续。
- 现实中的节点一般都是从堆上申请出来的
- 从堆上申请的空间,是按照一定的策略分配的,两次申请的空间可能连续,也可能不连续
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。
这里用typedef重新定义了int类型的名称,方便以后更换类型。
- typedef int SLTDateType;
-
- typedef struct SListNode
- {
- SLTDateType data;
- struct SListNode* next;
- }SListNode;
用malloc在堆上申请一个 SListNode 结构体大小的空间,这里在vs2019上面要对malloc开的空间进行判断,为空就报错误码,退出。不为空就继续,把x的值赋给data,让新节点的next指向空,这样一个新节点就创建好了,返回结构体指针。
- // 动态申请一个节点
- SListNode* BuySListNode(SLTDateType x)
- {
- SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
-
- if (newnode == NULL)
- {
- perror("malloc fail\n");
- exit(-1);
- }
- newnode->data = x;
- newnode->next = NULL;
-
- return newnode;
- }
这里的打印需不需要判空呢?答案是不需要,因为链表如果为空,那么plist肯定会指向空,所以在这里判断了也没用,单链表为空不打印是很正常的。
用一个指针cur指向头节点plist,遍历链表,如果cur不为空就继续,输出链表节点的值,这里的cur = cur->next;类似于++,表示指向下一个节点。
- // 单链表打印
- void SListPrint(SListNode* plist)
- {
- SListNode* cur = plist;
-
- while (cur != NULL)
- {
- printf("%d->",cur->data);
- cur = cur->next;
- }
-
- printf("NULL\n");
- }
这里的参数如果用一级指针,那么对函数内部的链表插入,并不会修改外部的链表。因为这里用一级指针相当于传值传参,因为刚开始链表为空,要想给链表插入第一个节点需要传二级指针。相当于是给一个函数传值,修改函数内部的变量,不会对外部的变量做修改,要想修改必须要传指针。
这里尾插稍微复杂一点,有两种情况,一链表里没有节点,那么创建一个新节点,让头指针指向新节点即可;二链表里有节点,定义一个cur指针,先让它指向头结点,遍历链表,直到cur指向最后一个节点,把新节点连在尾结点上即可,尾插完成。这里不用考虑新节点的next,因为创建的时候就已经让它指向空了。
- // 单链表尾插
- void SListPushBack(SListNode** pplist, SLTDateType x)
- {
- assert(pplist);
-
- SListNode* newnode = BuySListNode(x);
-
- //1.没有节点
- //2.多个节点
- if (*pplist == NULL)
- {
- *pplist = newnode;
- }
- else
- {
- SListNode* cur = *pplist;
- while (cur->next != NULL)
- {
- cur = cur->next;
- }
- cur->next = newnode;
- }
- }

这里二级指针就不过多解释了。二级指针相当于是一级指针的地址,地址不能为空,所以要判断一下,以防对空指针解引用。
头插比较简单,创建一个新节点,把新节点的next指向头结点,把原本指向头结点的指针指向新节点,头插就完成了。
- // 单链表的头插
- void SListPushFront(SListNode** pplist, SLTDateType x)
- {
- assert(pplist);
-
- SListNode* newnode = BuySListNode(x);
-
- newnode->next = *pplist;
- *pplist = newnode;
- }
同样尾删也稍微复杂一些,有三种情况,一链表为空链表,判断链表里是否为空,为空就返回;二链表只有一个节点,直接释放掉指针指向的空间就可以了,把头指针置空,表示为空链表;三链表里有多个节点,创建一个指针del指向头节点,遍历链表让del指针指向倒数第二个节点,释放掉del的下一个节点,这里为什么这样做?是因为如果直接指向最后的节点,释放空间后,那么尾结点的前一个节点还是会指向释放掉的空间,会出现野指针问题,所以要注意!
- // 单链表的尾删
- void SListPopBack(SListNode** pplist)
- {
- assert(pplist);
- if (*pplist == NULL)
- {
- return;
- }
- if ((*pplist)->next == NULL)
- {
- free(*pplist);
- *pplist = NULL;
- }
- else
- {
- SListNode* del = *pplist;
- while (del->next->next != NULL)
- {
- del = del->next;
- }
- free(del->next);
- del->next = NULL;
- }
- }

头删比较简单,首先判断链表里是否为空,为空就返回,因为没有链表可以删了。链表里有节点,就先创建一个指针del指向头节点,让头指针指向头结点的下一个节点(因为不指向下一个节点,释放空间后就找不到下一个节点了),释放del指针指向的空间,养成好习惯给del指针置空,头删结束。
- // 单链表头删
- void SListPopFront(SListNode** pplist)
- {
- assert(pplist);
- if (*pplist == NULL)
- {
- return;
- }
-
- SListNode* del = *pplist;
- *pplist = del->next;
- free(del);
- del = NULL;
- }
这里不需要对链表进行修改,所以传一级指针也没有问题。定义一个指针cur,指向头结点,遍历链表,如果cur里data的值等于x,那么就返回当前的节点,如果找不到返回空。
- // 单链表查找
- SListNode* SListFind(SListNode* plist, SLTDateType x)
- {
- SListNode* cur = plist;
-
- while (cur)
- {
- if (cur->data == x)
- {
- return cur;
- }
- cur = cur->next;
- }
- return NULL;
- }
首先判断pos是不是空指针,然后创建一个新节点,让新节点的next指向pos节点的下一个节点,而后再让pos的next指针指向新节点,插入完成。
- // 单链表在pos位置之后插入x
- // 分析思考为什么不在pos位置之前插入?
- //因为单链表找pos前面的位置比较麻烦,而且如果pos是第一个节点,那么还需要修改plist,
- //所以不建议在pos之前插入,如果非得前插,那么定义两个指针即可。
- void SListInsertAfter(SListNode* pos, SLTDateType x)
- {
- assert(pos);
- SListNode* newnode = BuySListNode(x);
- newnode->next = pos->next;
- pos->next = newnode;
- }
这里如果删除pos位置的节点,前后链接就比较麻烦所以直接删除pos之后的值会简单一些。
如果pos位置后面没有节点,那么就直接返回,如果有节点,先定义一个指针指向pos节点的下一个节点,然后让pos节点的next直接指向下一个节点的下一个节点(跳过pos之后的节点),释放掉del指向的节点就完成了。
- // 单链表删除pos位置之后的值
- // 分析思考为什么不删除pos位置?
- //因为删除pos位置后,pos之前的位置找的话比较麻烦
- void SListEraseAfter(SListNode* pos)
- {
- assert(pos);
-
- if (pos->next == NULL)
- {
- return;
- }
- SListNode* del = pos->next;
- pos->next = del->next;
- free(del);
- }
定义一个指针prve指向头结点,,从头节点开始一个节点一个节点的销毁,定义一个cur指向prve的下一个节点,释放掉prve节点的空间,让prve指向cur节点,重复循环,直至节点释放完毕。
- // 单链表的销毁
- void SListDestroy(SListNode** pplist)
- {
- assert(pplist);
-
- SListNode* prve = *pplist;
- while (prve != NULL)
- {
- SListNode* cur = prve->next;
- free(prve);
- prve = cur;
-
- }
- pplist = NULL;
- }
操作演示:
这里用typedef重新定义了int类型的名称,方便以后更换类型。
- typedef int LTDateType;
-
- typedef struct ListNode
- {
- LTDateType Date;
- struct ListNode* prve;
- struct ListNode* next;
- }DL;
从之前的图中可以看到,整个链表是需要一个哨兵位的头结点作为第一个,它里面一般不存放值。如果只有哨兵位的节点,那么他的prve和next是指向它自己的。(这里为了整体的参数保持一致,而用了返回头结点,当然你也可以用二级指针)
- //创建一个新节点
- DL* BuyDListNode(LTDateType x)
- {
- DL* newnode = (DL*)malloc(sizeof(DL));
- if (newnode == NULL)
- {
- printf("malloc fail");
- exit(-1);
- }
- newnode->Date = x;
- newnode->next = NULL;
- newnode->prve = NULL;
- return newnode;
- }
- // 创建返回链表的头结点.
- DL* DListCreate()
- {
- DL* Guard = BuyDListNode(0);
-
- Guard->prve = Guard;
- Guard->next = Guard;
-
- return Guard;
- }

头结点创建以后先把打印函数写出来,因为这个链表是循环体,所以不能用判空作为终止条件,定义一个cur让它指向头节点的下一个,遍历一遍,如果cur等于头结点就结束,打印完成。(这里把头结点单独写出来,因为头结点一般不放值,没办法显示,而如果只有一个头节点,那么循环就不会进去)
- // 双向链表打印
- void DListPrint(DL* pHead)
- {
- assert(pHead);
-
- DL* cur = pHead->next;
- printf("phead<==>");
-
- while (cur != pHead)
- {
- printf("%d<==>", cur->Date);
- cur = cur->next;
- }
- printf("\n");
- }
有了头结点以后,就可以插入数据啦。首先进行尾插,尾插就是把新节点连接在尾结点的后面,这里找尾就比较好找啦,头结点的prve就是尾结点,把tail指向的节点和新节点链接起来,而后把头结点和新节点再链接起来就好了。
- // 双向链表尾插
- void DListPushBack(DL* pHead, LTDateType x)
- {
- assert(pHead);
-
- DL* tail = pHead->prve;
- DL* NewNode = BuyDListNode(x);
- tail->next = NewNode;
- NewNode->prve = tail;
- NewNode->next = pHead;
- pHead->prve = NewNode;
- }
头插也很方便,定义一个cur指向头结点的下一个节点,而后把pHead,NewNode和cur三个节点链接起来头插就完成了。
- // 双向链表头插
- void DListPushFront(DL* pHead, LTDateType x)
- {
- assert(pHead);
-
- DL* cur = pHead->next;
- DL* NewNode = BuyDListNode(x);
- NewNode->next = cur;
- cur->prve = NewNode;
- pHead->next = NewNode;
- NewNode->prve = pHead;
- }
定义两个指针cur和prve,把pHead和prve的节点链接起来,而后把cur位置申请的空间释放掉,再把cur指针指向空,尾删结束。
- // 双向链表尾删
- void DListPopBack(DL* pHead)
- {
- assert(pHead);
-
- DL* cur = pHead->prve;
- DL* prve = cur->prve;
- prve->next = pHead;
- pHead->prve = prve;
- free(cur);
- cur = NULL;
- }
定义两个指针prve和cur,把pHead和cur的节点链接起来,而后把prve位置申请的空间释放掉,再把prve指针指向空,头删结束。
- // 双向链表头删
- void DListPopFront(DL* pHead)
- {
- assert(pHead);
-
- DL* prve = pHead->next;
- DL* cur = prve->next;
- pHead->next = cur;
- cur->prve = pHead;
- free(prve);
- prve = NULL;
- }
这里把链表遍历一遍,除了头结点其他节点的空间都释放掉,因为先释放头结点循环结束的条件就不那么容易写了,所以先释放其他节点的空间,最后在释放头结点的空间。这里的pHead置不置空都不重要,因为里面置空无法改变外部的指针,如果要在里面置空就需要二级指针,比较麻烦也容易出错,所以不建议,让使用的人用完后再把指针置空,让别人养成良好的习惯。(哈哈哈哈,开个玩笑)
- // 双向链表销毁
- void DListDestory(DL* pHead)
- {
- assert(pHead);
- DL* cur = pHead->next;
-
- while (cur != pHead)
- {
- DL* next = cur->next;
- free(cur);
- cur = next;
- }
- free(pHead);
- pHead = NULL;
- printf("销毁完成\n");
- }

定义一个cur指针,遍历一遍链表,如果x和cur节点里的值相等就返回cur节点,如果找不到就返回NULL。这里的查找可以和后面的插入删除一起使用。
- // 双向链表查找
- DL* DListFind(DL* pHead, LTDateType x)
- {
- assert(pHead);
- DL* cur = pHead->next;
- while (cur != pHead)
- {
- if (x == cur->Date)
- {
- return cur;
- }
- cur = cur->next;
- }
- return NULL;
- }
定义一个prve指针,指向pos的前一个节点,再把新节点链接到prve和pos中间就可以了,相当于在pos位置头插。这里就可以用find查找,找到指定位置,而后用Insert插入一个节点,是不是很方便。
- // 双向链表在pos的前面进行插入
- void DListInsert(DL* pos, LTDateType x)
- {
- assert(pos);
- DL* NewNode = BuyDListNode(x);
- DL* prve = pos->prve;
-
- NewNode->next = pos;
- pos->prve = NewNode;
- prve->next = NewNode;
- NewNode->prve = prve;
- }
定义两个指针,prve指向pos前一个节点,cur指向pos后面的节点,把prve和cur的位置的节点链接起来,再把pos位置的空间释放掉就完成了。这里想,删除哪个节点用find一找,用Erase一删除,简简单单。
- // 双向链表删除pos位置的节点
- void DListErase(DL* pos)
- {
- assert(pos);
- DL* cur = pos->next;
- DL* prve = pos->prve;
-
- prve->next = cur;
- cur->prve = prve;
- free(pos);
- pos = NULL;
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。