目录
1.链表概念及结构
在我顺序表的文章里说了,顺序表是逻辑连续,物理也连续的一段空间,而链表只有逻辑是连续的,物理是不连续的,是通过指针来实现逻辑连续的
在代码实现中,我们会运用动态内存函数在堆上申请空间,每次申请的空间可能恰好连续,也可能不连续。(内存空间分为堆、栈等,具体区别,我改天写篇文章(我现在还有点糊涂))
这是单链表的结构,地址是内存地址,运用指针解引用,我们就能按顺序找到不同的节点
2.单链表的实现
2.1头文件
#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> //定义链表节点结构 typedef int SLDataType; //跟顺序表类似,这也是为了方便改变存储的数据类型 typedef struct SListNode//这就是链表的节点 { SLDataType data;//存储数据 struct SListNode* next;//存储下一个节点的地址 }SLNode; //同样,加快写代码速度 //打印链表 void SLNPrint(SLNode* phead); //申请一个链表节点 SLNode* SLBuyNode(SLDataType x); //尾插 void SLPushBack(SLNode** pphead, SLDataType x); //头插 void SLPushFront(SLNode** pphead, SLDataType x); //尾删 void SLPopBack(SLNode** pphead); //头删 void SLPopFront(SLNode** pphead); //指定位置插入删除 // //指定位置之前插入 void SLInsert(SLNode** pphead, SLNode* pos,SLDataType x); //指定位置之后插入 void SLInsertAfter(SLNode* pos, SLDataType x); //查找节点 SLNode* SLFind(SLNode** pphead, SLDataType x); //删除pos节点 void SLErase(SLNode** pphead, SLNode* pos); //删除pos之后节点 void SLEraseAfter(SLNode* pos); //链表销毁 void SLDesTroy(SLNode** pphead);
2.2接口具体实现
2.2.1打印链表
void SLNPrint(SLNode* phead) { //循环打印 SLNode* pcur = phead; while (pcur) { printf("%d ->", pcur->data); pcur = pcur->next; } printf("NULL\n"); } 链表按顺序打印,就是不停的对当前节点存储的下一节点地址解引用 所以我们用一个pcur接收第一个节点的地址 (为什么要额外创建,因为指针的改变是具有全局性的,phead我们 要求一直指向第一个节点地址,但我们如果打印的时候将phead不停的改变 我们就找不到原来的第一个节点了) pcur==NULL的时候说明已经打印完了,最后打印一个NULL即可
2.2.2申请节点
SLNode* SLBuyNode(SLDataType x) { SLNode* node = (SLNode*)malloc(sizeof(SLNode)); node->data = x; node->next = NULL; return node; } 范围类型用链表节点的地址形式 运用动态内存函数,申请一块空间链表节点的空间 这里没有判断是否开辟失败,你们自己试的时候可以加上判断 接下来,把x赋值给data 因为只是申请,具体怎么关联别的节点在外面实现,所以这里next置NULL即可
2.2.3链表尾插
void SLPushBack(SLNode** pphead, SLDataType x) { assert(pphead); SLNode* node = SLBuyNode(x); if (*pphead == NULL) { *pphead = node; return; } SLNode* pcur = *pphead; while (pcur->next) { pcur = pcur->next; } pcur->next = node; } 断言空指针,防止野指针引用和引用空指针 调用申请节点的函数,将新节点赋给一个节点指针, 要注意,我们这里用的是二级指针,因为我们要知道,函数 调用的时候是创建一个新的变量来接受值,那么如果我们在外面创建了一个节点指针 plist,并且赋值了NULL,然后传参进去,我们要把一个节点地址赋给他,但我们如果用的是 一级指针,我们只是改变这个函数里面的phead指向的空间,事实上,外面的plist 还是指向NULL,那么我们就没有实现我们的目标,所以用二级指针 可以把plist指针的地址传进来,这样我们就能直接修改plist指向的空间了 二级指针存的是节点指针的地址,解引用一层 就是节点地址,再解引用就是节点本身 如果第一个节点是空(这个其实跟我们外面传参究竟是传一个空指针还是一个节点指针 有关),那么我们直接把申请的节点赋给这个解引用一层的二级指针即可 不为空,接下来,我们用一个指针接收第一个节点的地址, 注意循坏条件写pcur->next,因为我们要找到最后一个节点 然后再把新的节点插入到尾
2.2.4链表头插
void SLPushFront(SLNode** pphead, SLDataType x) { assert(pphead); SLNode* node = SLBuyNode(x); node->next = *pphead;//plist *pphead = node; } 断言空指针 申请节点 因为是头插,所以新节点的next应该指向原先的第一个节点, 也就是*pphead,然后再改变*pphead指向的空间为 新节点 因为我们要修改main函数中定义的节点指针指向的空间,所以我们要用二级指针
2.2.5链表尾删
void SLPopBack(SLNode** pphead) { assert(pphead); assert(*pphead); if ((*pphead)->next == NULL) { free(*pphead); *pphead = NULL; return; } SLNode* prev = NULL; SLNode* ptail = *pphead; while (ptail->next != NULL) { prev = ptail; ptail = ptail->next; } prev->next = ptail->next; free(ptail); ptail = NULL;//防止创建链表出问题 } 断言两处,一个是传参时是否是空指针,一个是空链表 如果链表此时只有一个节点,那么我们只需要free掉这个节点即可 记得置空,以免野指针 接下来prev指向ptail的上一个节点, ptail->next==NULL时,说明ptail指向最后一个节点 ,prev指向倒数第二个节点 而我们要删除的是最后一个节点,所以把prev的next置为NULL 再free掉ptail,并且置空,防止野指针
2.2.6链表头删
void SLPopFront(SLNode** pphead) { assert(pphead); assert(*pphead); SLNode* del = *pphead; *pphead = (*pphead)->next; free(del); del = NULL;//代码规范 } 断言两处,空指针和空链表 把第一个节点地址给一个节点指针 让*pphead指向第二个节点 也就是让外面plist指向第二个节点 然后free掉del,del置空即可
2.2.7链表指定位置前插入
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x) { assert(pphead); assert(pos); assert(*pphead); SLNode* node = SLBuyNode(x); //if ((*pphead)->next == NULL || pos == *pphead)也可以,只是如果是第一个节点 //那么pos也就是第一个节点,所以直接写即可 if ( pos==*pphead) { node->next = *pphead; *pphead = node; return; } SLNode * prev = *pphead; while (prev->next != pos) { prev = prev->next; } node->next = pos; prev->next = node; } 断言指定位置是否合法和空指针、空链表 申请新节点,如果是指定位置是第一个节点,那么就是头插即可 如果不是,那么定义一个节点指针,指向第一个节点, 循坏条件说明,结束的时候,正好prev是pos位置的前一个节点 让新节点next指向pos位置节点,prev位置节点的next指向node, 这样就关联起来了
2.2.7链表寻找节点
SLNode* SLFind(SLNode** pphead, SLDataType x) { assert(pphead); SLNode* pcur = *pphead; while (pcur) { if (pcur->data == x) { return pcur; } pcur = pcur->next; } return NULL; } 断言空指针, 定义节点指针指向第一个节点,遍历链表, 找到返回地址,找不到返回NULL
2.2.8链表指定位置之后插入数据
void SLInsertAfter(SLNode* pos, SLDataType x) { assert(pos); SLNode* node = SLBuyNode(x); node->next = pos->next; pos->next = node; } 断言空指针 申请新的节点,让node的next指向pos的next, 再把pos的next指向node,形成一个逻辑顺序即可
2.2.9链表删除指定节点
void SLErase(SLNode** pphead, SLNode* pos) { assert(pphead); assert(*pphead); assert(pos); if (pos == *pphead) { *pphead = (*pphead)->next; free(pos); return; } SLNode* prev = *pphead; while (prev->next != pos) { prev = prev->next; } prev->next = pos->next; free(pos); pos = NULL; } 断言空指针和空链表 如果位置是第一个节点,直接头删即可 定义节点指针,指向第一个节点 遍历链表,循环结束时,prev是pos位置的前一个节点 让prev的next指向pos位置的next,再free掉pos即可, 形成逻辑顺序
2.2.10链表删除指定位置之后的节点
void SLEraseAfter(SLNode* pos) { assert(pos && pos->next); SLNode* del = pos->next; pos->next = del->next; free(del); del = NULL; } 断言两处,因为可能pos是最后一个节点 定义节点指针,指向pos的下一个节点 把pos的next指向del的next,让从逻辑上 先删除,然后再free掉del,即物理上取消这片 空间
2.2.11链表销毁
void SLDesTroy(SLNode** pphead) { assert(pphead); SLNode* pcur = *pphead; while (pcur) { SLNode* next = pcur->next; free(pcur); pcur = next; } *pphead = NULL; } 链表的销毁,要从第一个节点开始,逐个free掉 因为*pphead也就是main函数里的plist指针还可能被其他接口调用, 所以要置空
3.链表例题
3.1移除链表元素
typedef struct ListNode ListNode; struct ListNode* removeElements(struct ListNode* head, int val) { ListNode* newHead, * newTail; newHead = newTail = NULL; ListNode* pcur = head; while(pcur) { if (pcur->val != val) { if (newHead == NULL) { newHead = newTail = pcur; } else { newTail->next = pcur; newTail = newTail->next; } } pcur = pcur->next; } if(newTail) { newTail->next=NULL; } return newHead; } 定义newHead和newTail节点指针,置空 定义指针pcur指向第一个节点 如果pucr的val不等于val,且newhead还是NULL的时候,把两个指针都指向pcur, 也就是第一个检查通过的节点,也是返回的时候的头结点 之后一旦pcur的val不等于val,把newtail的next指向pcur,nexttail再变成pcur 保持newtail总是在我们要返回的链表的最后一个节点 不管什么情况,pcur都进入下一个节点 循环结束后,如果newtail不是空,把newtail指向空
3.2反转链表
typedef struct ListNode ListNode; struct ListNode* reverseList(struct ListNode* head){ if(head==NULL) { return NULL; } ListNode*n1,*n2,*n3; n1=NULL; n2=head; n3=head->next; while(n2) { n2->next=n1; n1=n2; n2=n3; if(n3) n3=n3->next; } return n1; }
这题光靠文字还有点吃力,下面是图解
每次循坏都是进行类似的步骤,n3如果是NULL,就不用继续往下走了,此时n2=n3,n2也等于空,在此之前已经完成了整个链表的反转,所以结束循坏
3.3寻找链表倒数第k个节点
链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)
#include <string.h> #include<assert.h> struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) { struct ListNode* fast=pListHead; struct ListNode* cur=pListHead; while(k--) { if(fast==NULL) { return NULL; } fast=fast->next; } while(fast) { fast=fast->next; cur=cur->next; } return cur; } 首先用k--条件循环,如果fast==NULL,说明k>链表的长度 此时直接return NULL 接下来,我们简单推理下,一共10节点,求倒数第3个节点, 那么我们先循环3次,fast指向第4个节点,如果从第4个节点遍历到最后一个节点的next即NULL, 需要循坏几次呢,7次,那么如果此时同时cur指针从第一个节点开始循环,那么循坏7次 ,正好就循环到了第8个节点,也就是倒数第3个节点。然后return cur即可, 如果一共10个节点,倒数第10个节点,正好是第一个节点,第一个while会循坏10次, 循坏结束,fast正好指向NULL,第二个while循坏就不会进入,此时cur还是指向第一个节点, 直接返回即可。
3.4寻找中间节点
typedef struct ListNode ListNode; struct ListNode* middleNode(struct ListNode* head){ if(head==NULL) { return NULL; } ListNode*slow,*fast; slow=fast=head; while(fast && fast->next) { slow=slow->next; fast=fast->next->next; } return slow; }
这个我们用图解
3.5合并有序链表
typedef struct ListNode ListNode; struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){ if(list1==NULL) { return list2; } if(list2==NULL) { return list1; } //断言两个链表是否是空链表 ListNode*cur1=list1; ListNode*cur2=list2; //让cur1和cur2指向两个链表的头结点 //带头不循环链表 ListNode*newHead,*newTail; newHead=newTail=(ListNode*)malloc(sizeof(ListNode)); //定义一个头结点指针和尾节点指针,并且直接申请一个节点 //这个节点不存数据 while(cur1 &&cur2) { if(cur1->val <cur2->val) { newTail->next=cur1; newTail=newTail->next; cur1=cur1->next; } //两个节点的值比对,小的放进新的链表里,直接尾插 //newtail指向新加进来的,记得让cur1继续走 else { newTail->next=cur2; newTail=newTail->next; cur2=cur2->next; } //同理,等于或大于,放cur2的 } if(cur1) { newTail->next=cur1; } if(cur2) { newTail->next=cur2; } //每次可能有其中一个还没循环到空,如果有的话,就直接继续尾插 ListNode*retHead=newHead->next; free(newHead); 因为返回的时候要有效头结点,所以做一下处理,返回第一个有数据的节点地址 return retHead; }
3.6链表分割
class Partition { public: ListNode* partition(ListNode* pHead, int x) { struct ListNode* head1,*tail1,*head2,*tail2; head1=tail1=(struct ListNode*)malloc(sizeof(struct ListNode)); head2=tail2=(struct ListNode*)malloc(sizeof(struct ListNode)); ListNode*cur=pHead; while(cur) { if(cur->val<x) { tail1->next=cur; tail1=tail1->next; } else { tail2->next=cur; tail2=tail2->next; } cur=cur->next; } tail1->next=head2->next; tail2->next=NULL; pHead=head1->next; free(head1); free(head2); return pHead; } }; 这个格式不要在意,是c++的,因为牛客网在这题里没有设置给c语言的 定义两个新的链表,原链表里小于x或大于等于x的各自放入两个链表, 最后把小的链表跟大的链表互相连接起来就可以
3.7链表回文结构
链表的回文结构_牛客题霸_牛客网 (nowcoder.com)
class PalindromeList { public: bool chkPalindrome(ListNode* A) { struct ListNode* head = (struct ListNode*)malloc(sizeof(struct ListNode)); head->next = A; struct ListNode* slow = head->next; struct ListNode* fast = head->next; if (A == NULL)return false; //定义3个新的节点指针,一个是哨兵位,不存数据,剩下两个指向第一个节点A while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } //利用快慢指针,快速找到中间节点 struct ListNode* n1 = slow; struct ListNode* n2 = head->next; struct ListNode* n3 = head->next->next; while (n2 != slow) { n2->next = n1; n1 = n2; n2 = n3; n3 = n3->next; } head->next = n1; //将slow前面的节点反转,并且让第一个节点变为n1 struct ListNode* cur = head -> next; struct ListNode* cur1 = NULL; if (fast == NULL) { cur1 = slow; } else if (fast->next == NULL) { cur1 = slow->next; } 让cur指向第一个节点,根据奇数偶数节点情况,把slow或slow下一个 节点的地址给cur1 while (cur != slow) { if (cur->val == cur1->val) { cur = cur->next; cur1 = cur1->next; } else { return false; } } 两边同时开始遍历,如果都相同,就返回true,如果有一个不一样,就返回false return true; } };
3.8相交链表
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { struct ListNode*curA=headA,*curB=headB; int lenA=1,lenB=1; while(curA->next) { lenA++; curA=curA->next; } while(curB->next) { lenB++; curB=curB->next; } if(curA!=curB) { return NULL; } //先遍历一遍,计算两个链表有多少个节点 //因为遍历条件是curB->next,所以按理来说此时两个 //指针指向的是同一个空间,否则就是不相交的 int n=abs(lenA-lenB); struct ListNode*longlist =headA,*shortlist=headB; if(lenB>lenA) { longlist=headB; shortlist=headA; } while(n--) { longlist=longlist->next; } //定义一个长指针和短指针,运用假设,让两个指针 //指向正确位置 //然后让长的指针先遍历n遍,n是两个单链表相差的节点数 while(longlist!=shortlist) { longlist=longlist->next; shortlist=shortlist->next; } //然后同时开始遍历,直到地址相同,即是所求的节点 return longlist; }
3.9环形链表
bool hasCycle(struct ListNode *head) { struct ListNode*slow=head,*fast=head; while(fast&& fast->next) { slow=slow->next; fast=fast->next->next; if(slow==fast)return true; } return false; } 思路很简单,就是利用快慢指针,因为每次都是多走一步, 那么当slow和fast都在环中,fast每走一步,跟slow的距离就会缩减1, 直到相遇,如果能够相遇,那么就说明有环, 如果不是环,那么fast->next会等于NULL,那么就退出, 然后返回false,如果fast是NULL指针,那么最开始就不会进入循环,直接返回NULL。
3.10环形链表2
首先要证明一个公式的成立与否。
假设,一个指针从起始点到入口点的距离是L,环的长度是C,(这里直接使用slow走一步,fast走2步,可以最快相遇,因为进入环后,每次fast和slow的距离都会缩减1,这样就不会错过)快慢指针在环中相遇的点跟入口的距离是x,那么相遇点到入口点的距离就是C-X。当slow从起始点走到相遇点,就是L+X,fast从起始到相遇就是L+X+N*C(因为slow进环前,fast可能经历了很多圈)。而由于fast的路程是slow的两倍,所以2(L+X)=L+N*C+X,所以L=N*C-X,所以如果有一个指针从相遇点开始移动,一个指针从起始点开始移动,那么第一个指针完成运转后,最终会跟第二个指针在入口点相遇,那么这时,就是我们要求的入口点了
struct ListNode *detectCycle(struct ListNode *head) { struct ListNode*slow=head,*fast=head; while(fast&& fast->next) { slow=slow->next; fast=fast->next->next; if(slow==fast) { struct ListNode*meet=slow; while(head!=meet) { head=head->next; meet=meet->next; } return meet; } } return NULL; }
3.11随机链表的复制
typedef struct Node nd; struct Node* copyRandomList(struct Node* head) { if(head==NULL) { return NULL; } //断言空指针 for(nd*node=head;node!=NULL;node=node->next->next) { nd *nodeNew=malloc(sizeof(nd)); nodeNew->val=node->val; nodeNew->next=node->next; node->next=nodeNew; } for(nd*node=head;node!=NULL;node=node->next->next) { nd*nodeNew=node->next; nodeNew->random=(node->random!=NULL)?node->random->next:NULL; } nd*Newhead=head->next; for(nd*node=head;node!=NULL;node=node->next) { nd*nodeNew=node->next; node->next=node->next->next; nodeNew->next=(nodeNew->next!=NULL)?nodeNew->next->next:NULL; } return Newhead; } 将原节点的拷贝节点跟原节点相连,第一个for循环是拷贝,同时将拷贝节点的next指向原节点的next节点,原节点的next节点指向拷贝节点。 第2个for循环是将拷贝节点的random指向原节点的random节点的next节点,如果是NULL,则给空 第3个for循环是将原节点的next指向原链表的相应的下一个节点,再讲拷贝节点的下一个节点变成下一个节点(下一个原节点)的next节点(即下一个原节点的拷贝节点),从而将两个链表分开。
4.双链表
链表根据单向、双向,带头、不带头,循环不循环,可以分很多种。
我们前面实现的单链表就是不带头,单向,不循环链表,在oj题中比较常见
双向链表则是双向、带头、循坏,在实际运用比较常见
4.1双链表头文件
#pragma once #include<assert.h> #include<stdlib.h> #include<stdio.h> typedef int LTDataType; //方便改数据类型 typedef struct ListNode { struct ListNode* next;//指向下一个节点 struct ListNode* prev;//指向前一个节点 LTDataType data; }LTNode; //链表节点 //尾插 void LTPushBack(LTNode*phead,LTDataType x); //打印 void LTPrint(LTNode* phead); //申请节点 LTNode* CreateLTNode(LTDataType x); //初始化 LTNode* LTInit(); //尾删 void LTPopBack(LTNode* phead); //销毁 void LTDestory(LTNode* phead); //头插 void LTPushFront(LTNode* phead, LTDataType x); //头删 void LTPopFront(LTNode* phead); //寻找指定节点 LTNode* LTFind(LTNode* phead, LTDataType x); //指定位置插入 void LTInsert(LTNode* pos, LTDataType x); //指定位置删除 void LTErase(LTNode* pos);
4.2双链表具体实现
4.2.1尾插
void LTPushBack(LTNode* phead, LTDataType x) { assert(phead); LTNode* tail = phead->prev; LTNode* newnode = CreateLTNode(x); tail->next = newnode; newnode->prev = tail; newnode->next = phead; phead->prev = newnode; } 首先,我们要注意,这里不用二级指针,因为我们后面 在初始化中,会直接开辟一个节点空间返回给外面的节点指针, 所以就直接引用一级指针就可以了,增删查改。 因为是带头,且循环,所以头结点的前一个节点就是尾节点 申请一个新的节点 让tail的next不指向头结点,指向新的节点 新节点的next指向头结点,prev指向tail 头节点的prev指向新的尾节点
4.2.2申请节点
LTNode* CreateLTNode(LTDataType x) { LTNode* newnode = (LTNode*)malloc(sizeof(LTNode)); if (newnode==NULL) { perror("malloc fail"); exit(-1); } newnode->data = x; newnode->next = NULL; newnode->prev = NULL; return newnode; } 返回类型采用,节点指针 申请节点空间,判断开辟是否失败 给data复制,next和prev置空,返回即可。
4.2.3初始化(重点!巧妙改变,不用二级指针了)
LTNode* LTInit() { LTNode *phead = CreateLTNode(-1); phead->next = phead; phead->prev = phead; return phead; } 申请节点 因为带头、循坏、双向,所以prev和next都是指向自己 注意,这个函数使用后是返回一个有效链表节点的地址 那么我们在外面使用时,节点指针会指向一个有效的空间 那么我们后续增删查改,引用一级指针即可,无需二级指针
4.2.4打印
void LTPrint(LTNode* phead) { assert(phead); LTNode* cur = phead->next; while (cur != phead) { printf("%d<=>", cur->data); cur = cur->next; } printf("\n"); } 没什么好说的,就是链表遍历打印
4.2.5尾删
void LTPopBack(LTNode* phead) { assert(phead); assert(phead->next != phead); LTNode* tail = phead->prev; phead->prev = tail->prev; tail = tail->prev; tail->next = phead; } 断言空指针和空链表情况 tail指向尾节点,让头结点的prev指向尾节点的prev 让尾节点变成倒数第二个节点 让新尾节点的next指向头结点,完成
4.2.6链表销毁
void LTDestory(LTNode* phead) { assert(phead); LTNode* cur = phead->next; while (cur != phead) { LTNode* next = cur->next; free(cur); cur = cur->next; } free(phead); } 断言空指针 cur指向存储数据的第一个节点 遍历链表,逐级free空间 因为这里是一级指针,所以如果为了防止野指针, 规范写法的话,我们要在外面手动给外面的节点指针置空
4.2.7头插
void LTPushFront(LTNode* phead, LTDataType x) { assert(phead); LTNode* newnode = CreateLTNode(x); LTNode* next = phead->next; phead->next = newnode; newnode->prev = phead; newnode->next = next; next->prev=newnode; } 断言空指针 申请节点,定义节点指针next接收第一个有效节点的next节点 然后让头结点的next指向新节点,新节点的prev指向头结点 新节点的next指向next节点,next节点的prev指向新节点
4.2.8头删
void LTPopFront(LTNode* phead) { assert(phead); assert(phead->next != phead); LTNode* next = phead->next->next; LTNode* del = phead->next; phead->next = next; next->prev = phead; free(del); del = NULL; } 断言空指针 断言空链表的情况 next指针指向第二个有效节点 del指向第一个有效节点(要被删的节点) 头节点指向next,next的prev指向头结点 free掉del指向空间,指针置空
4.2.9寻找位置
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; } 断言空指针 cur指向第一个有效节点 遍历链表 找到值返回地址,找不到返回NULL
4.2.10指定位置前插入
void LTInsert(LTNode* pos, LTDataType x) { assert(pos); LTNode* newnode = CreateLTNode(x); newnode->next = pos; newnode->prev = pos->prev; pos->prev->next = newnode; pos->prev = newnode; } 断言空指针 申请新节点 新节点的next指向pos节点,prev指向pos节点的prev pos的prev节点的next指向新节点 pos的prev节点指向新节点
4.2.11指定位置删除
void LTErase(LTNode* pos) { assert(pos); assert(pos->next != pos); LTNode* posnext = pos->next; LTNode* posprev = pos->prev; posprev->next = posnext; posnext->prev = posprev; free(pos); pos = NULL; } 断言空指针和空链表 posnext指向pos节点的下一个节点 posprev指向pos节点的上一个节点 让posprev的next指向posnext posnext的prev指向posprev free掉pos pos置空
5.顺序表和链表的异同
5.1存储
顺序表物理和逻辑都连续,链表逻辑连续,物理不一定连续
5.2访问
顺序表根据下标,可以直接以O(1)的时间复杂度访问,链表不可以
必须遍历一遍,复杂度O(N)
5.3指定位置增删
顺序表要往前或往后大规模挪数据,链表只需改变指针指向
5.4插入
顺序表要考虑空间是否够用,要扩容。链表不需要考虑容量(内存够用情况下)
5.5应用
顺序表适用于频繁访问,高效存储,链表适用于增删频繁的情况
5.6缓存
数据加载的内存的高速缓存中,命中率高就意味着数据是已经放在缓存里的,命中率低,就说明事先不在,需要临时加载进缓存,而加载,每次都会加载一片空间,而链表在物理上不一定是连续的,顺序表是一定连续的,显而易见,顺序表在一次加载会放进更多的数据在缓存中。因此每次cpu寻找数据时,很多数据只需要访问缓存就可以了,对缓存里的数据利用率就高了,相反,链表一次加载大概率只会放很少的数据在缓存里,cpu找数据的时候,要频繁访问内存中其他部件,这对缓存中的数据利用率就低了