数据结构与算法之链表
软件设计中,最常用的两种数据存储结构就是顺序存储结构和链式存储结构,顺序存储结构中用的最多的便是数组了,而链式存储结构用的比较多的应该是单链表以及它们的变形。
单链表中只有一个指向下一个结点的指针,并且最后一个元素的next指针为NULL;循环链表与单链表的区别就是最后一个指针指向头结点;双向链表中,每个结点不仅有指向下一个结点的next指针,还有一个指向前一个结点的prior指针,头结点的prior和尾结点的next为NULL。因为都是单链表的变形,所以这里主要分析单链表。
与顺序存储空间不同,链式存储空间并不是连续的,数组中,直接通过索引来访问元素。而单链表中它是通过每个结点中的next指针来找到下一个结点的。
1、 单链表的结构体
单链表的结构体中包含两个元素,一个是数据元素,一个是指向下一个结点的地址。下列结构体中data的数据类型可以是基本数据类型,也可以是复杂数据类型。
typedef struct SingleListNode{ int data; struct SingleListNode *next; }ListNode;
2、单链表的创建
如果返回值为NULL,则创建失败,否则创建成功。
ListNode* allocNode(int data){ ListNode *pNode; pNode = (ListNode*)malloc(sizeof(ListNode)); if(NULL != pNode){ memset(pNode,0,sizeof(ListNode)); pNode->data = data; } return pNode; }
3、单链表查找
如果找到对应的元素,返回结点地址,否则返回NULL。
ListNode* findDataInSingleList(const ListNode *pListNode,int data){ ListNode *pNode; if(NULL == pListNode) return NULL; if(data == pListNode->data) return (ListNode*)pListNode; pNode = pListNode->next; while(pNode){ if(data == pNode->data) return pNode; pNode = pNode->next; } return NULL; }
4、单链表的插入
找到链表的最后一个结点,即next为NULL的结点,然后新建一个结点插入。
bool insertNodeIntoSingleList(ListNode **pListNode,int data){ ListNode *pNode,*ppListNode; if(NULL == pListNode) return false; pNode = createSingleListNode(data); if(NULL == pNode) return false; if(NULL == *pListNode) *pListNode = pNode; else{ ppListNode = *pListNode; while(ppListNode->next) ppListNode = ppListNode->next; ppListNode->next = pNode; } return true; }
5、单链表的删除
找到元素等于data的结点pNode,用指针pPre表示pNode的前一个结点,把pPre的next指向pNode的next结点,然后free pNode.
bool deleteFromSingleList(ListNode **pListNode,int data){ if(NULL == pListNode || NULL == *pListNode) return false; ListNode *pNode = *pListNode->next; if(*pListNode->data == data){ free(*pListNode); *pListNode = pNode; return true; } ListNode *pPre = *pListNode; while(pNode){ if(pNode->data == data){ pPre = pNode->next; free(pNode); return true; } pPre = pNode; pNode = pNode->next; } return false; }
6、释放链表
链表是通过mallic动态创建的,所以每次用完以后,必须free,以免内存泄漏。
void freeSingleList(ListNode *pListNode){ if(NULL == pListNode) return ; ListNode *pNode = NULL; while(pListNode){ pNode = pListNode->next; free(pListNode); pListNode = pNode; } }
关于链表的算法:
最常用的就是快慢指针,第7、8、9用的都是这个思想。
还有就是借助第三指针的概念。其实对链表的操作都是对指针的操作,理解链表首先得理解指针。下面举几个常用的链表算法。
7、找出链表的中间结点
用两个快慢指针,快指针每次走两步,慢指针每次走一步,当快指针走到链表尾部时,慢指针所指向的结点即为中间结点。
ListNode* findMidPositionOfList(const ListNode *pListNode){ ListNode *pQuick = pListNode,*pSlow = pListNode: if(NULL == pListNode) return NULL; while(pQuick){ pSlow = pSlow->next; pQuick = pQuick->next; if(NULL == pQuick) break; pQuick = pQuick->next; } return pSlow; }
8、找出倒数第k个结点
快指针先走k步,然后快慢指针一起走,知道快指针走到链表 末尾,慢指针即为倒数第k个结点。
ListNode* findNodeCountBackwards(const ListNode *pListNode,int k){ if(NULL == pListNode) return NULL; ListNode *pQuick = pListNode,*pSlow = pListNode: while(k--){ if(NULL == pQuick) return NULL; pQuick = pQuick->next; } while(pQuick){ pQuick = pQuick->next; pSlow = pSlow->next; } return pSlow; }
9、判断链表是否有环
快指针每次走两步,慢指针每次走一步,当快指针走到NULL的时候,说明该链表肯定不是循环链表,否则,当快指针==慢指针时,也就是快指针追上了慢指针,说明该链表有环。
bool judgeCircleInList(const ListNode *pListNode){ if(NULL == pListNode) return false; ListNode *pQuick = pListNode,*pSlow = pListNode: while(pQuick){ pQuick = pQuick->next; if(NULL == pQuick) return false; pQuick = pQuick->next; pSlow = pSlow->next; if(pQuick == pSlow) return true; } }
10、判断两个链表是否有公共结点
判断两个链表有没有公共结点的方法有很多,最简单粗暴的就是穷竭搜索,但是时间复杂度为o(n2)。还有就是把一个链表的尾结点与另一个结点的首结点连起来组成一个链表,然后判断这个链表有没有环。当然还有其他方法,这里只写了一种个人认为比较简单方法,既然有公共结点,那它们从某一个结点开始之后的结点都相等,所以它们的最后一个结点肯定相等。所以只需要判断两个链表的最后一个结点是否相等便可。
bool judgeCommonNode(const ListNode *pList1,const ListNode *pList2){ if(NULL == pList1 || NULL == pList2) return false; while(pList1->next) pList1 = pList1->next; while(pList2->next) pList2 = pList2->next; return pList1==pList2; }
11、找出两个链表的首个公共结点
两个链表有公共结点,那他们从最后一个结点到第一个公共结点都是相等的,长度也一样,我们只需要从两个链表离尾结点相同距离处开始搜索,就可以找到第一个公共结点了。
int countSizeOfList(const ListNode *pListNode){ if(NULL == pListNode) return 0; int count = 0; while(pListNode){ count ++; pListNode = pListNode->next; } return count; } ListNode* findFirstCommonNode(const ListNode *pList1,const ListNode *pList2){ if(NULL == pList1 || NULL == pList2) return NULL; int size1 = countSizeOfList(pList1); int size2 = countSizeOfList(pList2); if(size1 > size2){ int index = size1 - size2; while(index--) pList1 = pList1->next; } else if(size1 < size2){ int index = size2 - size1; while(index--) pList2 = pList2->next; } while(pList1){ if(pList1 == pList2) return pList1; pList1 = pList1->next; pList2 = pList2->next; } }
12、链表的反转
链表的反转可以用栈来处理,也就是递归,但是递归本身效率太低,所以这里我们用循环来处理。用一个结点来保存上一个结点,用另一个结点保存下一个结点,然后处理本结点。
ListNode* reverseList(ListNode *pListNode){ ListNode *pGuard = NULL,*pNode = pListNode; if(NULL == pListNode) return NULL; pGuard = pListNode->next; pListNode->next = NULL; pListNode = pGuard; while(pListNode->next){ pGuard = pListNode->next; pListNode->next = pNode; pNode = pListNode; } return pListNode; }
完整代码详见:https://github.com/whc2uestc/DataStructure-Algorithm/tree/master/list
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端