本文收录于算法总结系列专栏
本文从属于算法总结系列,笔者写此篇的(此系列)目的主要用于记录自己的学习过程,和用于对相关知识点的复习。
链表和数组一样同属于线性表,即一种线性存储结构,线性存储结构又可分成两种。其一,为 顺序存储结构(即 数组的存储方式)其特点是是逻辑关系上相邻的两个元素在物理位置上也相邻,即在连续的地址块中存储一系列数据。其二,为链式存储结构(链表的存储方式)其特点为不需要逻辑上相邻的元素在物理位置上也相邻,即在不连续的地址中存储一系列数据 ,所以链表不能向数组一样通过下标随机访问。那么链表之间的元素又是如何相互关联的呢?它是由一系列的存储数据元素的单元通过指针串接来确定元素之间相互关系的,因此每个单元至少都有两个域—数据域和指针域,这样的单元也被称为节点(node)。
链表可大致分为
1.单链表(最普通的链表)
2.双向链表(有两个指针域)
3.循环链表(首尾相接)
4.静态链表(链表的数组表示法)
优点:
(1)插入和删除速度快,保留原有的物理顺序,在插入或者删除一个元素的时候,只需要改变指针指向即可。(2)没有空间限制,存储元素无上限,只与内存空间大小有关。(3)动态分配内存空间,不用事先开辟内存
缺点:
(1)占用额外的空间以存储指针,比较浪费空间,不连续存储,malloc函数开辟空间碎片比较多) (2) 查找速度比较慢,因为在查找时,只能顺序查找,需要循环链表
头指针:指向链表第一个节点的指针(不一定是头节点,因为……链表要是没有头节点呢?),没有实际开辟空间 (即没有用malloc等动态分配内存) 而且必须存在,因为头指针不存在,就不便于对链表进行操作。
头节点:不是必须存在(若存在则为链表的第一个节点)其主要作用是使所有链表(包括空表)的头指针非空,并使对单链表的插入、删除操作不需要区分是否为空表或是否在第一个位置进行(还是挺有用的)。其数据域可以不储存任何数据,若储存则通常是链表的长度啥的。
首元节点:第一个数据域中储存有效数据的节点 若头节点不存在 则其为链表的第一个节点,是一定要存在的(除非是空表)
节点结构默认为:
struct ListNode
{
int val;
struct ListNode *next;
}
//创建链表
struct ListNode* createList()
{
struct ListNode*p=NULL,*tail=NULL,*head=NULL;//p为待开辟的节点指针,tail为指向链表尾部的指针,head为指向链表头部的指针。
//笔者喜欢在创建链表时 创建head tail 两个指针 虽然不一定都用得到
int num; //将指针置空是个好习惯
scanf("%d",&num);
//尾插法 顺序插法
while(num!=-1)//假设以num/val值为-1作为链表的结束标志 或者你用定长条件也行
{
p=(struct ListNode*)malloc(sizeof(struct ListNode));//注意此处用sizeof计算大小时在ListNode前要加struct关键字
p->val=num;
p->next=NULL;
if(head==NULL)//第一次循环时将头指针head指向p
{
head=p;
}
else
{
tail->next=p;
}
tail=p;//此句放else里面也行 笔者更喜欢在第一次循环时就将tail与p与head产生关联
scanf("%d",&num);
}
// 头插法 逆序插法
// while(num!=-1)
// {
// p=(struct ListNode*)malloc(sizeof(struct ListNode));
// p->val=num;
// if(head==NULL)
// {
// head=p;
// }
// else
// {
// p->next=head;
// head=p;
// }
// }
tail->next=NULL//用p->next也行 这是尾插法
return head;//返回链表头指针
}
struct ListNode *insertList(ListNode *head,int index,int num)//index 表示在链表中插入的位置(从1开始)原位置的元素接在其后 num表示要插入的数值
{
struct ListNode *p=NULL,q;//在此q为虚拟节点 主要方便对链表进行操作
int cnt=1;//计数器从1开始计数
if(head==NULL&&head->next==NULL)//链表为空 返回一个空指针
return NULL;
if(index==1)//若插在头结点(在此特指链表的第一个节点)的位置
{
p=(struct ListNode*)malloc(sizeof(struct ListNode));//给p开辟实际空间 用来储存操作存入的值
p->val=num;
p->next=head;
head=p;
}
else
{
q=head;
while(q->next!=NULL&&cnt<=index)//对于链表我们通常对当前节点的下一个节点进行操作
{
if(cnt+1<index)//为什么要加一呢?因为表示下一个位置
{
q=q->next;
}
else
{
p=(struct ListNode *)malloc(sizeof(struct ListNode));
p->val=num;
p->next=q->next;
q->next=q;
}
}
}
if(cnt<index)//如果cnt还是 小于 index 则表明插在链表末尾
{
p=(struct ListNode*)malloc(sizeof(struct ListNode));
p->val=num;
p->next=NULL;
q->next=p;
}
return head;
}
struct ListNode *deleteList(struct ListNode *head,int num)//执行删除节点val值为num的操作
{
struct ListNode *p=NULL,*q=NULL;//p,q均为辅助指针 不给他们开辟实际空间
if(head->val==num)//若删除节点为头结点
{
p=head;
head=head->next;
free(p);
}
else//若删除节点为除头结点外的其他节点
{
p=head;
while(p->next!=NULL)
{
if(p->next->val==num)
{
q=p->next;
p->next=p->next->next;
free(q);
}
else
{
p=p->next;
}
}
}
return head;
}
void printList(struct ListNode *head)
{
struct ListNode *p=head;//p依然为辅助指针
while(p!=NULL)//因为是遍历(打印输出)操作 所以对当前节点进行操作即可
{
printf("%d",p->val);
p=p->next;
}
}
双向链表即单链表由单向的链变成了双向的链(一个指针域(next)变成一前一后两个指针域(prev,next)) 这里只演示双向链表的创建
另:
节点结构为:
struct ListNode
{
int val;
ListNode *prev;
ListNode *next;
};
struct ListNode *createDbList()
{
struct ListNode*p=NULL,*head=NULL,*tail=NULL;
int num;
scanf("%d",&num);
while(num!=-1)//以-1作为创建链表结束的标志
{
p=(struct ListNode*)malloc(sizeof(struct ListNode));
p->val=num;
if(head==NULL)//这里主要演示尾插法
{
head=p;
p->prev=NULL; //就不将双向链表和循环链表相互掺和了
}
else
{
tail->next=p;
p->prev=tail;
}
tail=p;
}
tail->next=NULL;
}
即将链表首尾相接 同样这里只演示循环链表的创建
struct ListNode *createList()
{
struct ListNode*p,*head,*tail;
int num;
scanf("%d",&num);
while(num!=-1)
{
p=(struct ListNode*)malloc(sizeof(struct ListNode));
p->val=num;
if(head==NULL)
{
head=p;
}
else
{
tail->next=p;
}
tail=p;
}
tail->next=head;//创建结束后将链表首尾相接
}
若只定义一个链表指针,想要方便地对循环链表进行操作,应将该指针指向尾结点,因为只要标识了尾结点,链表的首元结点即可用tail->next->next得到。
静态链表的节点通常用结构体数组表示
struct staticListNode
{
int val;
int cur;//游标cur用来储存后继节点的下标
};
例如 我们创建一个node[index]数组来表示链表的各个节点,则node[index].val表示存储的数据,node[index].cur表示下一个节点的地址,index表示当前节点的地址
由于在进行链表操作时,尤其是删除节点时,经常会因为对当前节点进行操作而导致内存或
指针出现问题。有两个小技巧可以解决这个问题:一是尽量处理当前节点的下一个节点而非当前
节点本身,二是建立一个虚拟节点 (dummy node),使其指向当前链表的头节点,这样即使原链表
所有节点全被删除,也会有一个 dummy 存在,返回 dummy->next 即可。
对于链表找环路的问题,有一个通用的解法——快慢指针(Floyd) 。给定两个指针, 分别命名为 slow 和 fast ,起始位置在链表的开头。每次 fast 前进两步, slow 前进一步。如果 fast 可以走到尽头,那么说明没有环路;如果fast 可以无限走下去,那么说明一定有环路,且一定存 在一个时刻 slow 和 fast 相遇。当 slow 和 fast 第一次相遇时,我们将 fast 重新移动到链表开头,并 让 slow 和 fast 每次都前进一步。当 slow 和 fast 第二次相遇时,相遇的节点即为环路的开始点。
假设链表 A 的头节点到相交点的距离是 a,链表 B 的头节点到相交点的距离是 b,相交点到链表终点的距离为 c。我们使用两个指针,分别指向两个链表的头节点,并以相同的速度前进,若到达链表结尾,则移动到另一条链表的头节点继续前进。按照这种前进方法,两个指针会在a + b + c 次前进后同时到达相交节点
未完待续