文章目录
链表
0 链表
- 组成
- 结点:数据域 + 指针域(指向后一个结点的位置)
- 头指针:用来指向链表的起始位置。
- 头结点:在链表中的第一个结点前插入一个结点,其数据域是不存数据或者用来存储链表长度,指针域指向第一个结点。头结点可有可无。
- 首元结点:没有实际的意义,表示存储数据的第一个结点。
- 其他结点:注意:最后一个结点的指针域是指向NULL,表示链表结束。
1 单链表
1.定义
* 链表的每一个结点只包含一个指针域。
2.基本操作
- (1)插入元素
- 步骤1:先遍历找到要插入的位置,使Temp指针一步一步移动到要插入位置,同时需要判断此时的Temp是否为空,为空则表示插入的位置无效(说明插入位置大于链表长度)。
- 步骤2:将新结点的next指针指向插入位置后的结点。
- 步骤3:将插入位置前结点的next指针指向插入结点。
- 步骤4:链表长度加1,返回链表的头指针。
- (2)删除元素
- 步骤1:同插入元素的步骤1。
- 步骤2:将待删除的结点从链表中摘除,即找到该结点的直接先驱Temp的指针域指向该结点的后驱。Temp->next = Temp->next->next;。
- 步骤3:手动释放被删除的结点内存,防止内存泄漏。
- 步骤4:链表长度减1,返回链表的头指针。
- (3)查找元素
- 从表头遍历链表中的结点,每个结点的数据域与被查数据对比,直到遍历相等(即成功找到,返回此时的位置序号)或者遍历至链表的末端NULL,则未找到。
- 由于链表可能不知道长度,所以用while循环更好。
- (4)更新元素
- 步骤1:遍历链表,找到待更新元素的位置。可以采用for循环。
- 步骤2:直接更改该结点的数据域。
- (5)单链表的反转(翻转、逆置链表)
- 方法1:迭代反转法
- 步骤1.创建3个结点类型的指针,分别初始化。
- *不带头结点 *beg = NULL; *mid = head; end = head->next;
- *带头结点:*beg = head->next;*mid = beg->next;end = NULL;(end用在循环中未反转的链表)
- 步骤2.开始遍历,先将中间结点指向前一个结点实现反转(mid->next = beg;),然后判断遍历终止条件(end = NULL);并将上面3个指针向后移动一个结点(采用迭代:beg = mid; mid = end; end = end->next);
- 步骤3:遍历退出后:修改头指针指向,head = mid;返回头指针。
- 方法2:就地逆置法
- 思想:依次将链表的结点摘除移动到头部。不增加新链表,但需要两个指针实现。
- 步骤1. 两个指针初始化为:beg = head; end = head->next;
- 步骤2. 循环调换:beg->next = end->next;(相当于摘除了end结点)end->next = head; head =end; end = beg->next;
- 步骤3. return head;
- 方法3:头插法
- 思想:依次将链表的头部摘下,然后采用从头部插入的方式新生成一个链表。
- *步骤1. 创建1个新链表,1个临时结点。link *pNewHead = NULL; link pTemp = NULL;
- 步骤2. 遍历循环:将原链表的第一个结点摘下存在pTemp中,再插入到新链表中。pTemp = head; head = head->next; (摘得一个结点)pTemp->next = pNewHead; pNewHead = pTemp;(存在新链表中)
- 步骤3. return pNewHead;
2 静态链表
-
创建
- 静态链表的结点组成是由数据域和整形游标组成。
- 备用链表:是指将静态链表中未存放的数据连接起来的链表,其头指针指向a[0]。
- 数据链表:是指静态链表中存放数据的结点连接起来的链表,其头指针指向a[1]。
- 静态链表的特点:a[0]的结点位置上是不存放数据的。
-
创建步骤
- 步骤1.在静态链表未初始化前,数组中的位置都是空闲态,因此全连接在备用链表上。
- 步骤2.逐渐添加数据,将备用链表中的a[0]后继结点摘除,作为数据链表的当前最后一个结点。即备用链表的a[0]的后继结点向后移动一个。
-
代码实现
#include<stdio.h>
#define MAXSIZE 6
//定义一个结点
typedef struct Link {
int data;
int cur;
}link;
//1.创建备用链表
void RsvLink(link* arr)
{
for (int i = 0; i < MAXSIZE; i++)
{
arr[i].data = 0xFF; //初始化为脏值
arr[i].cur = i + 1;
}
arr[MAXSIZE - 1].cur = 0; //链表最后一个指向游标0
}
//2.从备用链表中摘除被分配的结点
int MallocArr(link* arr)
{
int i = arr[0].cur; //记录备用链表中第二结点对应的游标,当分配到链表最后一个结点时,该值为0.
if (arr[0].cur) //如果不是最后一个结点
{
arr[0].cur = arr[i].cur; //备用链表删除一个结点后,首结点中存的游标向下一个结点移动。
}
return i;
}
//3.初始化静态链表
int InitArr(link* arr)
{
RsvLink(arr);
int Head = MallocArr(arr); //空静态链表的头结点中存在的游标
int Tail = Head;//空链表,所以头尾一样
for (int i = 1; i < 4; i++)
{
int j = MallocArr(arr); //从备用链表拿出结点的游标
arr[Tail].cur = j;
arr[j].data = i; //数据域初始化
Tail = j; //指向链表后一个结点的游标后移
}
arr[Tail].cur = 0; //新的链表最后结点游标为0
return Head;
}
//4.打印链表
void DisPlayArr(link* arr, int body)
{
int Temp = body;
while (arr[Temp].cur)
{
printf("%d,%d\t", arr[Temp].data, arr[Temp].cur);
Temp = arr[Temp].cur;
}
printf("%d,%d\t", arr[Temp].data, arr[Temp].cur); //打印最后一个结点,游标是0的结点
}
//创建静态链表
void main()
{
link arr[MAXSIZE];
int Head = InitArr(arr);
printf("静态链表是:\n");
DisPlayArr(arr, Head);
}
- 基本操作
- 添加
- 删除
- 查找
- 更改
静态链表和动态链表的区别
静态链表 | 动态链表 | |
---|---|---|
内存 | 预先申请足够空间 | 需要新增结点时开空间 |
链表数量 | 2条(备用链表,数据链表) | 1条 |
3 双向链表
1.背景
- 单链表的指针域只有一个指向后继元素,因此适合从前往后找数据。因此想从后往前找数据就需要双链表。
2.定义
- 双向链表是存在两个指针域的链表,各个结点之间的关系是双向的,但头指针一般设置1个,除非实际情况需要。
3.组成
- 指针域(prior):指向当前结点的前驱结点;
- 数据域(data):当前结点的数据元素;
- 指针域(next):指向当前结点的后驱结点;
4.创建思想
- 与单链表相似,只是多了如下步骤:
- 将当前的新结点的prior指针指向前驱结点;
- 同时将当前新结点的直接前驱结点next指针指向当前新结点。
5.代码实现
#include<stdio.h>
#include<stdlib.h>
//1.创建结点
typedef struct Link
{
struct Link *prior;
int data;
struct Link *next;
}link;
//2.创建双向链表
link* InitDoubLink(link *pHead, int len)
{
pHead =(link*)malloc(sizeof(link));
if(!pHead)
{
return 0;
}
//对首元结点初始化
pHead->prior = NULL;
pHead->next = NULL;
pHead->data = 1;
if (len < 1)
{
return pHead;
}
link *pTemp = pHead; //声明一个指针指向链表最后一个结点
for (int i = 2; i <= len; i++)
{
//创建其他结点并赋值
link *pBody = (link*)malloc(sizeof(link));
if (!pBody)
{
return 0;
}
pBody->prior = NULL;
pBody->next = NULL;
pBody->data = i;
pTemp->next = pBody; //前驱结点指向当前结点
pBody->prior = pTemp; //当前结点指向前驱结点
pTemp = pTemp->next; //最后一个结点向后移动
}
return pHead;
}
//3.打印双向链表
void Diaplay(link* pHead)
{
link *pTemp = pHead;
while (pTemp)
{
if (!pTemp->next)
{
printf("%d\n", pTemp->data);
}
else
{
printf("%d <-> ", pTemp->data);
}
pTemp = pTemp->next;
}
}
int main()
{
int len = 5;//链表长度
link* pLink = NULL;//创建一个头指针
pLink = InitDoubLink(pLink,len); //调用链表创建函数
Diaplay(pLink);//打印链表
printf("链表中第4个结点直接的前驱是:%d", pLink->next->next->next->prior->data);
return 0;
}
6.基本操作
1.添加结点
- (1)添加至表头
- 步骤1.将新头结点temp的next指向表头结点head,head的prior指针指向temp。
- 步骤2.将head移至temp,重新指向新的表头。
- (2) 添加在表中间
- 步骤1.新结点与其前驱结点建立双层逻辑;
- 步骤2.新结点与其后继结点建立双层逻辑。
- (3)添加在表的末尾
- 步骤1.找到双链表中最后一个结点;
- 步骤2.让新结点与最后一个结点进行双向逻辑关系。
link* insertlink(link *head, int data, int add)
{
//新建temp结点
link *temp = (link*)malloc(sizeof(link));
temp->data = data;
temp->prior = NULL;
temp->next = NULL;
//插入到链表头,要特殊考虑
if(add == 1)
{
temp->next = head; //新结点的后继指针指向原先的头指针
head->prior = temp; //原先头指针的前驱指针指向新结点
head = temp; //将原头结点移至新的temp结点(首结点)
}
else
{
link *body = head; //创建一个临时指针指向头结点
for (int i = 1; i < add - 1; i++) //找到要插入位置的前一个结点
{
body = body->next;
}
//判断条件为真,说明插入位置为链表尾
if (body->next == NULL)
{
body->next = temp;
temp->prior = body;
}
else //即插入的位置不是链表的尾部,而是中间位置
{
body->next->prior = temp;
temp->next = body->next;
body->next = temp;
temp->prior = body;
}
}
return head;
}
2.删除结点
- 实现思想:遍历找到该结点然后将结点删除,并将该结点的内存释放。
link *DelLink(link *pHead, int data)
{
link *pTemp = pHead;
//遍历链表,找到该值
while(pTemp)
{
if(pTemp->data == data)
{
pTemp->prior->next = pTemp->next;
pTemp->next->prior = pTemp->prior;
free(pTemp);
return pHead;
}
pTemp = pTemp->next;
}
printf("链表中没有该元素");
return pHead;
}
3.查找结点
- 实现思想:因为双链表和单链表一样,只有一个头指针,所以查找需要从头结点开始遍历查找。
*代码实现
int FindElemLink(link *pHead,int data, int location)
{
link *pTemp = pHead;
location = 1; //第一个结点位置为索引1
while(pTemp)
{
if(pTemp->data == data)
{
return location;
}
location++;
pTemp = pTemp->next;
}
return -1;
}
4.修改链表的结点元素
- 实现思想:先查找到该结点的位置,直接将数据域改写
- 代码实现
link *PlaceElemLink(link *pHead, int Loc,int data)
{
link *pTemp = pHead;
for(int i = 1; i < Loc; i++) //提前知道位置,所以用for循环
{
//将pTemp指针移至到要被修改的数据的位置
pTemp = pTemp->next;
}
temp->data = data;//修改结点上的数据
return pHead;
}
4 循环链表
1.定义
- 链表中最后一个结点的指针域指向头结点,整个链表形成一个环。
2.特点
- 从链表中的任一结点出发均可以找到其他结点。
- 循环链表成环状,但本质上还是链表,因此在循环链表中,依然能够找到头指针和首元节点等。
3.实例
- 题目:已知 n 个人(分别用编号 1,2,3,…,n 表示)围坐在一张圆桌周围,从编号为 k 的人开始顺时针报数,数到 m 的那个人出列;他的下一个人又从 1 开始,还是顺时针开始报数,数到 m 的那个人又出列;依次重复下去,直到圆桌上剩余一个人。
- 解题分析
- 这是一个循环的过程,当出列其中一个人后,还得继续报数,依次出列。相当于一个循环链表,找到特定位置后,删除该结点,然后继续查找。
- 代码实现
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
//1.创建结点
typedef struct Node
{
int number;
struct Node* next;
}Person;
//2.创建循环链表并初始化
Person* InitNodeLink(int len)
{
//创建首元结点
Person *pHead = (Person*)malloc(sizeof(Person));
if (!pHead)
{
return 0;
}
pHead->number = 1;
pHead->next = NULL;
Person* pTail = pHead;//创建一个指针指向头指针
for (int i = 2; i < len; i++)
{
//创建一个新结点
Person* pTemp = (Person*)malloc(sizeof(Person));
if (!pTemp)
{
return pHead;
}
pTemp->number = i;
pTemp->next = NULL;
//将新结点作为最后一个结点
pTail->next = pTemp;
pTail = pTail->next;//此时未结点为刚才的新结点pTemp
}
//首尾相连
pTail->next = pHead;
return pHead;
}
//3.查找链表中的元素并删除
void FindDelElemLink(Person* head, int k, int m)
{
Person *tail = head;
if (!tail)
{
return;
}
//找到链表第一个结点的上一个结点,即尾结点,为删除操作做准备
while (tail->next != head)
{
tail = tail->next;
}
//找到编号为k的人,记为p指针;编号为k的前1个人的位置记为tail
Person *p = head;
while (p->number != k) {
tail = p;
p = p->next;
}
//从编号为k的人开始,只有符合p->next==p时,说明链表中除了p结点,所有编号都出列了,
while (p->next != p)
{
//找到从p报数1开始,报m的人,并且还要知道数m-1的人的位置tail,方便做删除操作。
for (int i = 1; i < m; i++)
{
tail = p;
p = p->next;
}
tail->next = p->next;//从链表上将p结点摘下来,即报m的人出列。
printf("出列人的编号为:%d\n", p->number);
p = tail->next;//继续使用p指针指向出列编号的下一个编号,游戏继续
}
printf("出列人的编号为:%d\n", p->number);
free(p);
}
//4.主函数
int main()
{
printf("圆桌上一共有:");
int PersonNum;
scanf_s("%d", &PersonNum);
Person* pHead = InitNodeLink(PersonNum);
int k;
printf("输入从第k人开始报数(k>1且k<%d):", PersonNum);
scanf_s("%d", &k);
int m;
printf("输入数到m的人出列:");
scanf_s("%d", &m);
FindDelElemLink(pHead, k, m);
return 0;
}
4.给定一个单链表,如何判断是否为有环链表?
- 有环链表不一定是循环链表,可能是中间的位置出现环状;循环链表一定是首尾相连的。所以有环链表则指的是单链表中存在一个循环子链表。
- 单链表:每一个结点的指针域只能指向一个结点。
- 解析
- 方法1
- 实现思想:从链表的第一个节点开始遍历,每遍历至一个节点,都将其和所有的前驱节点进行比对,如果为同一个节点,则表明当前链表中有环;反之,如果遍历至链表最后一个节点,仍未找到相同的节点,则证明该链表中无环。
- 时间复杂度:O(n^2)
- 方法2
- 实现思想:设置2个指针均指向链表头指针,让两个指针分别按照移动2个结点和1个结点的速度移动。当移动第一次,两者相差一个结点;第二次移动,则相差2次,依次类推,移动n-1次,就会有相差n-1个结点,如果存在环,则两指针重合,如果不重合,则是单链表中无环。
- 时间复杂度为:O(n)