目录
1.1 单链表的概念
1.2 单链表的结构
1.3 本期链表的结构实现:无头单向非循环链表
2. 单链表的实现
2.1 单链表定义
2.2 动态申请内存
2.3 单链表尾插
2.4 单链表的尾删
2.5 打印单链表
2.6 单链表头插
2.7 单链表头删
2.8 单链表查找
2.9 单链表修改
2.10 单链表在pos位置之前插入x
2.11 单链表删除pos位置的值
2.12 单链表在pos位置之后插入x
2.13 单链表删除pos位置之后的值
3.LeetCode刷题
4.1 LeetCode 反转链表
4.2 Leetcode链表的中间结点
4.3 链表中倒数第k个结点
4.4 Leetcode合并两个有序链表
4.5 Leetcode移除链表元素
1.1 单链表的概念
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,单链表是一种链式存取的数据结构,链表中的数据是以结点来表示的,每个结点由元素和指针构成。
链表通过不连续的储存方式,自适应内存大小,以及指针的灵活使用,巧妙的简化了上述的内容。
链表的基本思维是,利用结构体的设置,额外开辟出一份内存空间去作指针,它总是指向下一个结点,一个个结点通过NEXT指针相互串联,就形成了链表。
1.2 单链表的结构
以结点的序列表示的线性表称作线性链表,也就是单链表,单链表是链式存取的结构。对于链表的每一个结点,我们使用结构体进行设计,其主要内容有
其中,DATA数据元素,可以为你想要储存的任何数据格式,NEXT为一个指针,其代表了一个可以指向的区域,通常是用来指向下一个结点,链表的尾部NEXT指向NULL(空),因为尾部没有任何可以指向的空间了
1.3 本期链表的结构实现:无头单向非循环链表
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 单链表的实现
SList.c | 单链表函数实现 |
SList.h | 创建单链表,单链表申明 |
Test.c | 测试单链表 |
2.1 单链表定义
typedef int SLTDateType;//便于修改数据类型
typedef struct SList
{
SLTDateType Node;//数据
struct SList* next;//结点
}SList;
2.2 动态申请内存
注意事项:
1.动态开辟内存有可能开辟失败
SList* BuySListNode(SLTDateType x)//动态申请一个节点
{
SList* NewNode = (SList*)malloc(sizeof(SList));//动态开辟内存空间
assert(NewNode);//有可能开辟失败
NewNode->Node = x;
NewNode->next = NULL;//指针初始化
return NewNode;
}
2.3 单链表尾插
注意事项:
1.需考虑两种情况:此时单链表上存在结点、此时单链表上不存在结点
2.如果此时需要修改plist,我们传一级指针是否可行?
一级指针传参用一级指针接收,传过来的是形参,接收的只是一级指针的值,我们修改一级指针无法改变plist,所以我们需要传递二级指针pplist来改变一级指针plist
3.遍历找到最后一个结点,使用尾结点连接新开辟的空间
void SListPushBack(SList** pplist, SLTDateType x)//尾插单链表
{
assert(pplist);
SList* NewNode = BuySListNode(x);//开辟新空间
//考虑此时头结点没有数据
if (*pplist == NULL)
{
*pplist = NewNode;
}
else
{
//考虑已经有结点,尾插数据
SList* tail = *pplist;
while(tail->next != NULL)
{
tail = tail->next;//找到尾
}
tail->next = NewNode;//尾插
}
}
2.4 单链表的尾删
注意事项:
1.要改变plist,就得传二级指针
2.此时有一个以上结点
最后再让倒数第二个结点指向NULL
3.尾删多结点有两种写法:我们已经判断链表为空和只有一个的情况,那此时链表一定是两个结点及以上,判断当前结点的next的next是否为NULL即可
void SListPopBack(SList** pplist)//单链表的尾删
{
assert(*pplist);//防止链表为空
assert(pplist);//防止传递过来的是NULL指针
if ((*pplist)->next == NULL)//此时单链表只有一个结点
{
free(*pplist);
*pplist = NULL;
}
else
{
/*SList* tail = *pplist;
SList* tailPre = NULL;
while (tail->next != NULL)
{
tailPre = tail;
tail = tail->next;
}
free(tail);
tailPre->next = NULL;*/
//另一种写法
SList* tail = *pplist;
while(tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
2.5 打印单链表
注意事项:
1.没修改plist,不需要传递二级指针
void SLPrint(SList* pplist)
{
SList* cur = pplist;
while (cur != NULL)
{
printf("%d-> ", cur->Node);
cur = cur->next;
}
printf("NULL");
}
2.6 单链表头插
注意事项:
1.需要改变plist,传递二级指针
2.头插后改变原plist指针
void SListPushFront(SList** pplist, SLTDateType x)//单链表的头插
{
assert(pplist);
SList* NewNode = BuySListNode(x);
NewNode->next = *pplist;
//让pplist指向新开辟节点
*pplist = NewNode;
}
2.7 单链表头删
注意事项:
1.必须得记录plist->next指针的位置,否则free掉后无法找回
void SListPopFront(SList** pplist)//单链表头删
{
assert(pplist);
assert(*pplist);//不等于NULL
/*free(*pplist);
*pplist = (*pplist)->SListNext;*///错误写法,free的是整个结构体
//一个都没有的情况下头删便会出现问题
SList* next = (*pplist)->next;
free(*pplist);
*pplist = next;
}
2.8 单链表查找
注意事项:
1.找到返回该链表结点地址
SList* SListFind(SList* plist, SLTDateType x)//单链表查找
{
SList* cur = plist;
while (cur != NULL)
{
if (cur->Node == x)
return cur;
cur = cur->next;
}
return NULL;
}
2.9 单链表修改
注意事项:
1.配合单链表查找函数来使用
SList*ret = SListFind(plist,4);
if (ret)
{
printf("\n找到了\n");
ret->Node = 30;
}
2.10 单链表在pos位置之前插入x
注意事项:
1.找到pos之前位置时间复杂度为O(N)
2.如果此时pos位置为头,则直接调用头插;如果不是,则记录pos前位置,让前一个位置结点指向NewNode,NewNode指向pos位置
void SListInsert(SList** pplist,SList* pos, SLTDateType x)//单链表在pos位置之前插入x
{
assert(pos);
assert(pplist);
if (pos == *pplist)
{
SListPushFront(pplist, x);//如果为空顺便检查了
}
else
{
SList* prev = *pplist;
while (prev->next != pos)
{
prev = prev->next;
}
SList* NewNode = BuySListNode(x);
prev->next = NewNode;
NewNode->next = pos;
}
}
2.11 单链表删除pos位置的值
注意事项:
1.如果此时pos位置为头,则直接调用头删;如果不是,找到pos前一个结点记录,让其指向pos的下一个结点
void SListErase(SList** pplist, SList* pos)// 单链表删除pos位置的值
{
assert(pos);//如果链表为空,此时pos也不是有效结点,等同于直接判断链表是否为空
assert(pplist);
SList* cur = *pplist;
if (pos == *pplist)
{
SListPopFront(pplist);
}
else
{
while (cur->next != pos)
{
cur = cur->next;
}
cur->next = pos->next;
free(pos);
}
}
2.12 单链表在pos位置之后插入x
注意事项:
1.时间复杂度为O(1)
2.先让NewNode的next指向pos的next,否则会丢失地址
void SListInsertAfter(SList* pos, SLTDateType x)//单链表在pos位置之后插入x
{
assert(pos);
SList* NewNode = BuySListNode(x);
NewNode->next = pos->next;
pos->next = NewNode;
}
2.13 单链表删除pos位置之后的值
注意事项:
1.判断pos和pos的next是否为空,否则删除会出错
2.记录pos的next里所存的结点,free掉后让pos的next重新指向新结点
void SListEraseAfter(SList* pos)//单链表删除pos位置之后的值
{
assert(pos);
if (pos->next == NULL)
{
return NULL;
}
SList* cur = pos;
cur = cur->next->next;
free(pos->next);
pos->next = cur;
}
3.LeetCode刷题
4.1 LeetCode 反转链表
思路:
1.让链表进行头插到新链表即可
2.迭代,让指针倒过来指向,让n1指向NULL,n2指向n1,n1=n2,n2=n3迭代
struct ListNode* reverseList(struct ListNode* head){
if(head==NULL)
return NULL;
struct ListNode*n1=NULL;
struct ListNode*n2=head;
struct ListNode*n3=n2->next;
while(n2)
{
//倒指针
n2->next=n1;
//迭代
n1=n2;
n2=n3;
if(n3)//判断是否到NULL,到NULL n3则不继续
n3=n3->next;
}
return n1;
}
4.2 Leetcode链表的中间结点
思路:
快慢指针,快指针每次走两步,慢指针每次走一步,当快指针走到终点时,慢指针就会停在中间
struct ListNode* middleNode(struct ListNode* head){
//1.快慢指针
struct ListNode*slow=head;
struct ListNode*fast=head->next;
while(fast)
{
if(fast->next!=NULL)
{
fast=fast->next;
}
fast=fast->next;
slow=slow->next;
}
return slow;
}
4.3 链表中倒数第k个结点
思路:
1.快慢指针,快指针走k步,随后一起走,慢指针便是倒数第k个结点
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
// write code here
//1.快慢指针,快指针走k步
if(pListHead==NULL)
{
return NULL;
}
//2.求长度
int count=0;
struct ListNode*slow=pListHead;
struct ListNode*fast=pListHead;
struct ListNode*cur=pListHead;
while(cur)
{
cur=cur->next;
count++;
}
if(k>count)//判断是否超出链表长度
{
return NULL;
}
while(k--)//快慢指针之间保留k步,最后fast到NULL结束
{
if(fast != NULL)
{
fast=fast->next;
}
}
while(fast)
{
slow=slow->next;
fast=fast->next;
}
return slow;
}
4.4 Leetcode合并两个有序链表
思路:
1.我们前面所讲的单链表是没有哨兵位(无参数的头结点)的,做题给的题目大多也是没有哨兵位,但是我们做题,可以新建一个哨兵位来避免复杂问题
其中链表的限制条件也要写清楚
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
struct ListNode* Sentry=(struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode*tail=Sentry;
//判断链表情况
if(!list1 && !list2)
{
tail->next=NULL;
}
else if(!list1)
{
tail->next=list2;
}
else
{
tail->next=list1;
}
//都不为空
while(list1 && list2)
{
if(list1->val < list2->val)
{
tail->next=list1;
list1=list1->next;
tail=tail->next;
}
else
{
tail->next=list2;
list2=list2->next;
tail=tail->next;
}
}
//如果此时还有非空链表
if(list2 !=NULL)
{
tail->next=list2;
}
else if(list1!=NULL)
{
tail->next=list1;
}
return Sentry->next;
}
4.5 Leetcode移除链表元素
思路:
1.双指针均指向头结点,其中pre记录待删除结点的前一个结点,cur记录当前结点,随机遍历链表
struct ListNode* removeElements(struct ListNode* head, int val){
while (head != NULL && head->val == val) {
head = head->next;
}
struct ListNode* cur = head; // 当前节点
struct ListNode* pre = head; // 保存待删除节点的前一节点
while (cur != NULL) {
if (cur->val == val) {
pre->next = cur->next;
} else {
pre = cur;
}
cur = cur->next;
}
return head;
}