线性表的链式存储
1线性表的链式存储结构
链式存储:用一组任意的存储单元存储线性表中的元素。这种方法的线性存储表称为线性链表。
存储链表中节点的一组人一的存储单元可以是连续的也可以是不连续的,甚至可以零散分布在内存任意位置。
链表中节点的逻辑顺粗和物理顺序不一定相同。
链表是通过每个结点的指针域将线性表的n个节点按照逻辑次序连接在一起的。
每一个节点只包含一个指针域的链表,称之为单链表。
为了操作方标,总是在链表的第一个节点之前附设一个(头指针)head指向第一个节点。
单链表是由表头唯一确定,因此单链可以用头指针的名字命名。
1.1 节点的描述和实现
1.1.1 节点的表示
typdedef struct Lnode
{
Elemetype data; // 数据域,保存节点的值
struct Lnode *next; //指针域
}LNode; // 节点类型
1.1.2 节点的实现:
节点是通过动态分配和释放来实现的。(需要时分配,不需要时释放)。用到以下C标准函数:malloc()
,realloc()
,sizeof()
,free()
。
动态分配:
p = (LNode *)malloc(sizeof(LNode));
malloc函数分配了一个类型为LNode的节点变量的空间,并将其首地址赋值给指针变量p中。
动态释放:
free(p);
系统回收指针变量p所指向的内存区。p必须是最近一次调用malloc函数是的返回值。
1.1.3 (单链表)常用的基本操作
1.1.3.1 节点的赋值
LNode *p;
p = (LNode *)malloc(sizeof(LNode));
p->datda = 20;
p->next = NULL;
1.1.3.2 常见的指针操作
1.2 单链表基本操作
1.2.1 建立单链表
头插法、尾插法
1.2.1.1 头插法建表
从一个空表开始,重复读入数据生成新节点,将读入数据存放到新结点的数据域中,然后将新节点插入到当前链表的表头上,直到读取结束。每次插入的节点都作为链表的第一个节点。
这种方法生成链表的特点是:结点的顺序和输入的方向相反。
LNode *create_LinkList(void) //头插法创建单链表,链表头节点head作为返回值
{
int data;
LNode *head,*p;
head = (LNode *)malloc(sizeof(LNode));
head->next = NULL; // 创建链表的表头结点,此时head指向空节点(链表为空,通过下面的扫描输入加入新节点)
while(1){
scanf("%d",&data);
if(data==32767) // 退出条件
break;
p = (LNode *)malloc(sizeof(LNode));
p->data = data; // 数据域赋值
p->next = head->next;
head->next = p; // 勾链,新创建的节点总是作为第一个节点
}
return head;
}
1.2.1.2 尾插法建表
如想链表的节点次序和输入相同,需要使用尾插法:
LNode *create_LinkList(void) //尾插法创建单链表,链表头节点head作为返回值
{
int data;
LNode *head,*p, *q;
head = p = (LNode *)malloc(sizeof(LNode));
p->next = NULL; // 创建链表的表头结点,此时head指向空节点(链表为空,通过下面的扫描输入加入新节点)
while(1){
scanf("%d",&data);
if(data==32767) // 退出条件
break;
q = (LNode *)malloc(sizeof(LNode));
q->data = data; // 数据域赋值
q->next = p->next;
p->next = q; // 勾链,新创建的节点总是作为最后一个节点
p = q;
}
return head;
}
算法的事件复杂度为O(n)。
如果没有明确给出直接后继, 勾链(或重新勾链)的次序必须是“先右后左”。
1.2.2 单链表的查找
1.2.2.1 按序号查找
查找单链表第i各元素:对于单链表,只能先哦嗯链表的头节点触发,沿链域next逐个节点往下搜索,直到搜索到第i个结点为止。
设单链表长度为n,要查找表中第i个节点,仅当1<=i<=n,i的值是合法的。
ElemTYpe Get_Elem(LNode *L, int i)
{
int j;
LNode *p;
p = L->next;
j = 1; 使p指向第一个节点
while(p!=NULL&&j<i)
{
p = p->next;
j++; // 移动指针p,q计数
}
if(j!=i)
return (-32768);
else
return (p->data); // p为NULL表示i太大;j>i表时i为0
}
移动指针p的频度:
i<1时:0次;i在[1,n]之间时:i-1次; i>n: n次。
时间复杂度:O(n)
1.2.2.2 按值查找
按值查找是否有节点值等于给定值key的节点。若有返回首次找到的值为key的节点的存储为位置;否则返回NULL。
// 在以L为头节点的单链表中找值为key的第一个节点
LNode *Locate_Node(LNode *L, int key)
{
LNode *p = L->next;
while(p!=NULL&&p->data!=key)
{
p = p->next;
if(p->data==key)
return p;
else{
printf("The Node has value key is not exist!\n");
return NULL;
}
}
}
算法执行与key有关,平均时间复杂度为O(n)。
1.2.3 单链表插入
将值为e的新节点插入到表的第i个节点的位置上。
void Insert_LNode(LNode *L, int i, ElemTYpe e)
{
int j = 0;
LNode *p,*q;
p = L->next;
while(p!=NULL&&j<i-1)
{
p = p->next;
j++;
}
if(j!=i-1)
printf("i is too big or i equal to 0");
else{
q = (LNode *)malloc(sizeof(LNode));
q->data = e;
q->next = p->next;
p->next = q;
}
}
设链表长度为n,合法的插入位置是i<=i<=n。算法值要花费时间在移动指针p。时间复杂度为O(n).
1.2.4 单链表删除
1.2.4.1 按序号删除
为删除第i个结点,必须找到节点的存储地址。该存储地址其直接前驱地址域next中。因此必须找到ai-1的存储位置p,然后让p->next指向ai的直接后继结点,从而把ai从链表中摘下。
设链表长度为n,合法的删除位置是i<=i<=n。当i=n+1时,虽然被删除的节点不存在,但是其前驱节点存在(终端节点)。故判断条件之一是:p->next !=NULL。
时间复杂度为O(n).
void Delete_LinkList(LNode *L, int i)
{
int j = 1;
LNode *p,*q;
p = L;
q = L->next;
while(p->next!=NULL&&j<i)
{
p = q;
q = q->next;
j++;
}
if(j!=i)
printf("i太大或者i=0\n");
else{
p->next = q->next;
free(q);
}
}
1.2.4.2 按值删除
删除单链表值为key的第一个节点:
首先确定查找值为key的节点是否存在;
若存在则删除;否则返回NULL;
void Delete_LinkList(LNode *L, int key)
{
LNode *p,*q;
p = L;
q = L->next;
while(q!=NULL&&q->data!=key)
{
p = q;
q = q->next;
}
if(q->data==key)
{
p->next = q->next;
free(q);
}
else
printf("要删除节点不存在!\n");
}
删除单链表值为key的所有节点:
基本思想:从单链表的第一个节点开始,对每个节点进行检查,若节点的值为key,删除之,然后检查下一个节点,直到所由节点都被检查。
void Delete_LinkList(LNode *L, int key)
{
LNode *p,*q;
p = L;
q = L->next;
while(q!=NULL)
{
if(q->data==key)
{
p->next = q->next;
free(q);
q = p->next;
}
else{
p = q;
q = q->next;
}
}
}
删除单链表所有重复值的节点:
基本思想:
从链表的第一个节点开始,对每个节点进行检查:检查链表中该节点的所有后继节点,只要有和该节点值相同,则删除之;然后检查下一节点,直到多有的节点都被检查
void Delete_Node_value(LNode *L) //删除单链表所有重复值的节点
{
LNode *p = L->next;
Lnode *q,*ptr;
while(p!=NULL) // 检查链表中所由节点
{
q = p;
ptr = p->next;
while(ptr!=NULL){ // 检查p的所有后继节点ptr
if(ptr->data==p->data)
{
q->next = ptr->next;
free(ptr);
ptr = q->next;
}
else{
q = ptr;
ptr = ptr->next;
}
}
p = p->next;
}
}
1.2.5 单链表合并(并合并值相同的节点)
设由两个有序的单链表,他们的头指针分别是La、Lb,将它们合并成以Lc为头指针的有序链表。
LNode *Merge_LinkList(LNode *La, LNode *Lb)
{
LNode *Lc, *pa, *pb,*pc, *ptr;
Lc = La;
pc = La;
pa = La->next;
pb = Lb->next;
while(pa!=NULL && pb!=NULL) // pa,pb是待考察的两个链表的当前节点
{
if(pa->data < pb->data)
{
pc->next = pa;
pc = pa;
pa = pa->next;
}
//将pa所指的节点合并,pa指向下一个节点
if(pa->data>pb->data)
{
pc->next=pb;
pc = pb;
pb = pb->next;
}
// 将pa所指的节点合并,pa指向下一个节点
if(pa->data==pb->data)
{
pc->next=pa;
pc = pa;
pa = pa->next;
ptr = pb;
pb = pb->next;
free(ptr); // 将pa所指的节点合并,pb指向节点删除
}
}
if(pa!=NULL)
pc->next = pa;
else // 剩余结点链上
pc->next = pb;
free(Lb);
return Lc;
}
算法分析:
若La、Lb两个链表的长度分别为m,n则链表合并的时间复杂度为O(m+n)
1.3 循环链表
循环链表是一种头尾相接的链表。其特点是最后一个节点的指针域指向链表的头节点,整个链表的真真与接成一个环。
从循环链表任一个节点触发都可以找到链表中的其他接待年,使得表处理更加灵活。
循环链表的操作:
对于单循环链表,除链表的合并外,其他的操作和单线性链表基本一致,仅需要在单线性链表操作算法上做以下简单修改
判断是否空链表:head->next ==head;
判断是否是表尾节点:p->next ==head;
2 双向链表
双向链表指的是构成链表的节点设立两个指针域:一个指向其前驱节点pre,一个指向其后继结点next。这样构成的链表有两个不同方向的链。
2.1 双向链表的节点及其类型定义
typedef struct Dulnode
{
ElemType data;
struct Dulnode *pre, *next;
}DulNode;
双向链表的对称性:
(p->pre)->next=p=(p->next)->pre;
2.2 双向链表的基本操作
2.2.1 双向链表的插入
插入时仅仅指出前驱节点,勾链时必须注意前后次序:先右后左
S = (Dulode *)malloc(sizeof(DulNode));
S->data = e;
S->next = p->next;
p->next->pre = S;
p->next = S;
S->prev = p;
插入时同时指出前驱节点p和直接后继节点q,链钩时无需注意先后次序。
S=(DulNode *)malloc(sizeof(DulNode));
S->data = e;
p->next = S;
S->next = q;
S->pre = p;
q->pre = S;
2.2.2 双向节点的删除
设要删除的节点为p,删除时可以不引入新的辅助指针变量,可以先断链,再释放节点。
p->pre->next = p->next;
p->next->pre = p->pre;
free(p);
注意:
与单链表插入和删除操作不同的是,在双向链表中插入和删除必须同时修改两个方向上的指针域的指向。