线性表的链式结构
存储思想:把任意的存储单元连接起存放数据
特点:
- 逻辑次序和物理存放位置不一定相同,元素之间的指向用指针表示
- 单向列表,只能顺序读取,从前向后,方便增加和删除
头结点:第一个元素(首结点)之前的一个类型相同的结点,头节点不放入数据
头指针:指向了第一个结点的指针
存储密度:结点数据本身占用的空间/结点占用的空间
#include <stdio.h>
#include <malloc.h>
typedef int ElemType;
typedef struct LNode
{
ElemType data;
struct LNode *next; //指向后继结点
} LinkNode,*LinkList; //声明单链表结点类型
- typedef struct Node LNode: 这个声明为名为
Node
的结构体类型创建了一个新的别名LNode
。这意味着之后在程序中你可以使用LNode
代替struct Node
来声明变量
LNode node1; //定义了一个结点变量 - typedef struct Node *LinkList*: 这个声明不仅为结构体类型创建了一个别名,而且是为其指针类型创建的别名。这里
LinkList
成为了指向struct Node
类型的指针类型的别名
LinkList list; 相当于 struct Node * list; //定义了一个指向结点的指针变量 - 同样的struct LNode * next 定义了一个指向这个结构体类型的指针,直接使用“next”的话就是一个地址,使用“ *next ”就是使用指向的结点。前者是指针,指向作用,后者是变量,存放数据的;
问:如果我修改“ next ”的值呢?会发生什么?
答:那next 的指向就发生变化,就像你修改你的收货地址一样的
问:那修改 * next呢?
答:变化的就是指向的那个存储单元的值的变化。
基础代码
前情小知识:
- (LinkNode *&L): 这里的
LinkNode *&L
表示传入的是一个指向LinkNode
结构体的引用。这意味着函数内部对L
的修改会影响到调用者持有的原始指针变量。通常这种形式用于需要改变实参(即链表头指针)的情况,比如当删除操作会更改链表头部时。 - (LinkNode *L): 这里的
LinkNode *L
表示传入的是一个指向LinkNode
结构体的普通指针副本。函数内部可以修改L
指向的内容,但无法直接改变调用者持有的原始指针变量。因此,如果删除操作涉及到更新链表头节点,那么仅凭这个版本的函数是无法将新头节点地址返回给调用者的。
1.1创建
//初始化单链表
void InitList(LinkNode*& L)
{
L = (LinkNode*)malloc(sizeof(LinkNode)); //创建头结点
L->next = NULL;
}
void CreateListF(LinkNode *&L,ElemType a[],int n)
//头插法建立单链表
{
LinkNode *s;
L=(LinkNode *)malloc(sizeof(LinkNode)); //创建头结点
L->next=NULL;
for (int i=0;i<n;i++)
{
s=(LinkNode *)malloc(sizeof(LinkNode));//创建新结点s
s->data=a[i];
s->next=L->next; //将结点s插在原开始结点之前,头结点之后
L->next=s;
}
}
- 头插法:元素序列和结点顺序是相反的,逆序的。
void CreateListR(LinkNode *&L,ElemType a[],int n)
//尾插法建立单链表
{
LinkNode *s,*r;
L=(LinkNode *)malloc(sizeof(LinkNode)); //创建头结点
L->next=NULL;
r=L; //r始终指向终端结点,开始时指向头结点
for (int i=0;i<n;i++)
{
s=(LinkNode *)malloc(sizeof(LinkNode));//创建新结点s
s->data=a[i];
r->next=s; //将结点s插入结点r之后
r=s;
}
r->next=NULL; //终端结点next域置为NULL
}
注:
- 尾插法:元素的顺序和结点的顺序是对应的,顺序的。
- 需要创建一个尾指针r ,来定位链表的尾部,用来插入数据
1.2 插入
bool ListInsert(LinkNode *&L,int i,ElemType e)
{
int j=0;
LinkNode *p=L,*s;
if (i<=0) return false; //i错误返回假
while (j<i-1 && p!=NULL) //查找第i-1个结点p
{ j++;
p=p->next;
}
if (p==NULL) //未找到位序为i-1的结点
return false;
else //找到位序为i-1的结点*p
{ s=(LinkNode *)malloc(sizeof(LinkNode));//创建新结点*s
s->data=e;
s->next=p->next; //将s结点插入到结点p之后
p->next=s;
return true;
}
}
- 要注意:别把已知的结点变成未知,链接断开
1.3删除(重点)
bool ListDelete(LinkNode *&L,int i,ElemType &e)
{
int j=0;
LinkNode *p=L,*q;
if (i<=0) return false; //i错误返回假
while (j<i-1 && p!=NULL) //查找第i-1个结点
{ j++;
p=p->next;
}
if (p==NULL) //未找到位序为i-1的结点
return false;
else //找到位序为i-1的结点p
{ q=p->next; //q指向要删除的结点
if (q==NULL)
return false; //若不存在第i个结点,返回false
e=q->data;
p->next=q->next; //从单链表中删除q结点
free(q); //释放q结点
return true;
}
}
1.4遍历
void DispList(LinkNode *L)
{
LinkNode *p=L->next;
while (p!=NULL) //空链表跳过
{ printf("%d ",p->data);
p=p->next;
}
printf("\n");
}
int ListLength(LinkNode *L)
{
LinkNode *p=L;int i=0;
while (p->next!=NULL)
{ i++;
p=p->next;
}
return(i);
}
1.5 查询
//获取某个位置的元素值
bool GetElem(LinkNode *L,int i,ElemType &e)
{
int j=0;
LinkNode *p=L;
if (i<=0) return false; //i错误返回假
while (j<i && p!=NULL)
{ j++;
p=p->next;
}
if (p==NULL) //不存在第i个数据结点
return false;
else //存在第i个数据结点
{ e=p->data;
return true;
}
}
//获取某个值的位置
int LocateElem(LinkNode *L,ElemType e)
{
LinkNode *p=L->next;
int n=1;
while (p!=NULL && p->data!=e)
{ p=p->next;
n++;
}
if (p==NULL)
return(0);
else
return(n);
}
1.6插入
bool ListInsert(LinkNode *&L,int i,ElemType e)
{
int j=0;
LinkNode *p=L,*s;
if (i<=0) return false; //i错误返回假
while (j<i-1 && p!=NULL) //查找第i-1个结点p
{ j++;
p=p->next;
}
if (p==NULL) //未找到位序为i-1的结点
return false;
else //找到位序为i-1的结点*p
{ s=(LinkNode *)malloc(sizeof(LinkNode));//创建新结点*s
s->data=e;
s->next=p->next; //将s结点插入到结点p之后
p->next=s;
return true;
}
}
1.7删除
bool ListDelete(LinkNode *&L,int i,ElemType &e)
{
int j=0;
LinkNode *p=L,*q;
if (i<=0) return false; //i错误返回假
while (j<i-1 && p!=NULL) //查找第i-1个结点
{ j++;
p=p->next;
}
if (p==NULL) //未找到位序为i-1的结点
return false;
else //找到位序为i-1的结点p
{ q=p->next; //q指向要删除的结点
if (q==NULL)
return false; //若不存在第i个结点,返回false
e=q->data;
p->next=q->next; //从单链表中删除q结点
free(q); //释放q结点
return true;
}
}
//直接销毁整个表
void DestroyList(LinkNode *&L)
{
LinkNode *pre=L,*p=pre->next;
while (p!=NULL)
{ free(pre);
pre=p;
p=pre->next;
}
free(pre); //此时p为NULL,pre指向尾结点,释放它
}
注:
- 删除操作要比增加操作多使用一个工作指针q,用来指向被删除的结点,便于释放空间
- 删除有三种情况,一种是正常的,一种是空表
应用
拆分单链表L,奇数顺序L1,偶数逆序L2
void split(LinkNode *&L,LinkNode *&L1,LinkNode *&L2)
{ LinkNode *p=L->next,*q,*r1; //p指向第1个数据结点
L1=L; //L1利用原来L的头结点
r1=L1; //r1始终指向L1的尾结点
L2=(LinkNode *)malloc(sizeof(LinkNode)); //创建L2的头结点
L2->next=NULL; //置L2的指针域为NULL
while (p!=NULL)
{ r1->next=p; //采用尾插法将结点p(data值为ai)插入L1中
r1=p;
p=p->next; //p移向下一个结点(data值为bi)
q=p->next; //由于头插法修改p的next域,故用q保存结点p的后继结点
p->next=L2->next; //采用头插法将结点p插入L2中
L2->next=p;
p=q; //p重新指向ai+1的结点
}
r1->next=NULL; //尾结点next置空
}
注解:
- 原来的单链表L,被拆分后留作奇数的单链表L1存在
- 整个思想是把偶数项分离出去,剩下的奇数项相互连接起来,用指针r1始终指向单链表的尾部,来实现尾插法保证元素的顺序
- p指针指向的是要操作的元素(被拆出去尾插到L2),而q指针需要始终保持p的后面,方便p指针尾插之后归位
- r1指针需要移动到一个奇数元素项,靠的是p指针尾插完偶数项归位到q位置时,r1移动
删除单链表中元素最大的结点
void delmaxnode(LinkNode *&L)
{
LinkNode *p=L->next,*pre=L,*maxp=p,*maxpre=pre;
while (p!=NULL) //用p扫描整个单链表,pre始终指向其前驱结点
{
if (maxp->data<p->data) //若找到一个更大的结点
{ maxp=p; //更改maxp
maxpre=pre; //更改maxpre
}
pre=p; //p、pre同步后移一个结点
p=p->next;
}
maxpre->next=maxp->next; //删除maxp结点
free(maxp); //释放maxp结点
}
注:
- 删除结点操作需要两个指针, 记录最大结点并删除也需要两个指针
给定两个升序的单链表LA ,LB 合并成一个升序的单链表L
LinkNode* Merge_LinkList(LinkNode* La, LinkNode* Lb) {
LinkNode* Lc, * pa, * pb, * pc, * ptr;
Lc = La; //把b表插入合并进a表
pc = La;
pa = La->next;
pb = Lb->next;
while (pa != NULL && pb != NULL) { //尾插法插入
if (pa->data < pb->data)
{
pc->next = pa;
pc = pa;
pa = pa->next;
}
else if (pa->data < pb->data)
{
pc->next = pb;
pc = pb;
pb = pb->next;
}
else
{
pc->next = pa;
pc = pa;
pa = pa->next;
ptr = pb;
pb = pb->next;
free(ptr);
}
}
if (pa != NULL) pc->next = pa; //把剩下的结点所有都加上去
else pc->next = pb;
free(Lb);
return Lc;
}
注解:
- 升序合成升序,所以要用尾插法
- 列表元素相比较的时候相等,其中一个插入,另一个释放空间
- 在插入之后需要后移元素,然后继续比较,循环
- 直到有一个表已经插入完成了,直接把另一个表直接接上
去除链表中的重复元素
void Delete_Node_value(LinkNode* L) {
LinkNode* p = L->next, * q, * ptr;
while (p != NULL) {
q = p, ptr = p->next;
//检查结点p的所有后继有没有相同的
while (ptr != NULL) {
if (ptr->data == p->data) { //找到了就删除掉
q->next = ptr->next;
free(ptr);
ptr = q->next;
}
else
{
q = ptr;
ptr = ptr->next;
}
}
p = p->next;
}
}
- 时间复杂度为n2
综合
1.已知线性表中的元素以值递增有序的方式排列,并以单链表作存储结构。试设计一个高效的算法,删除表中所有值大于 mink 且小于 maxk 的元素(若表中存在这样的元素),同时释放被删除的结点空间。(注意, mink 和 maxk 是给定的两个参变量,它们的值可以和表中的元素相同,也可以不同。)
bool DeleteMiddleElem(LinkNode* L, int mink, int maxk) {
if (mink > maxk) {
printf("mink和maxk数据输入有误");
return false;
}
LinkNode* p, * q, * pre = NULL;
p = L;
pre = p;
p = p->next;
while (p && p->data < maxk) {
if (p->data <= mink)
{
pre = p;
p = p->next;
}
else {
pre->next = p->next;
q = p;
p = p->next;
free(q);
}
}
return true;
}
2.已知非空线性链表由 list指出,链结点的构造为(data,next)。请写一算法,将链表中数据域值最小的那个链结点移到链表的最前面。要求:不得额外申请新的链结点。
void minMoveFirst(LinkNode* L) {
//创建两对指针,一对指向当前的,一对记录最小的
LinkNode* min, * minpre, * p, * pre;
//空表
if (L == NULL) return;
//寻找
pre = L;
p = L->next;
min = p;
minpre = pre;
while (p != NULL) {
if (p->data < min->data) { //找到了比最小还小的
minpre = pre;
min = p;
}
pre = p;
p = p->next;
}
//把最小的结点放到前面去
minpre->next = min->next;
min->next = L->next;
L->next = min;
}
3.给定两个单链表(假设两个链表均不含有环)的头指针分别为head1和headz,请设计一个算法判断这两个单链表是否相交,如果相交就返回第一个交点,要求算法的时间复杂度为O(lengthl+length2),其中lengthl和length2分别为两个单链表的长度。
注
LinkNode* SearchFirst(LinkNode* L1, LinkNode* L2) {
//定义两个扫描指针
LinkNode* pa, * pb;
pa = L1->next;
pb = L2->next;
if (pa == NULL || pb == NULL) {
return NULL;
}
//让两指针开始从头开始同步走,直到相遇
while (pa != pb) {
if (pa == NULL) {
pa = L2->next;
}
if (pb == NULL) {
pb = L1->next;
}
pa = pa->next;
pb = pb->next;
}
return pa;
}
注:
- 这个题的想法是找到相交点,相交点后面的元素都是相同的,个数也是相同的。只有相交点前面的元素个数不同。
- 两个单链表一般情况下一定:一个长,一个短;而我们的扫描指针是从头部开始的,这就引出思考,如何做才能让扫描指针指向相交点?并确定那就是相交点?
- 想法:如果两个表一样长,那么当粮扫描指针走一样的步数,直到两个指针指向的结点相同,就可以确定那个结点就是相交点。so?如何让他们走一样长的路程呢?
答:短的走完x + L 这段结点路程,然后去走y这段;长的走完y+L这段路程去走 x这段。他们速度相同,路程相同,必定可以相遇,且刚好到达相交点