2.5单链表的基本运算
1、求单链表长度操作
在顺序表中,线性表的长度是它的属性,数组定义时就已确定。
在单链表中,整个链表由“头指针”来表示,单链表的长度在从头到尾遍历的过程中统计计数得到。
【算法思路】采用“数”结点的方法求出单链表的长度。即从“头”开始 “数(p=L->next),用指针 p 依次指向各个结点,一直“数”到最后一个结点(p->next= =NULL),从而得到单链表的长度。
- 顺链头开始,计数器 j 初值为 0
- 当前指针 p 指向链表 L 的首元结点
- p=L->next p 依次往后(计数 j++)直到表尾(p==NULL )
【算法描述】
int ListLength(LinkList L) /*求带头结点的单链表 L 的长度*/
{
Node *p;
p=L->next;
j=0; /*用来存放单链表的长度*/
while(p!=NULL)
{
p=p->next; j ++;
}
return j; /*j 为求得的单链表长度*/
} /* ListLength */
【算法分析】若单链表 L 为空表,p 的初值为“NULL”,算法中 while 循环未执行, 则返回链表长度 j 为 0。 若单链表 L 为非空表,算法中 while 循环执行次数为表长度 n,故算法的时间复 杂度为 O(n)。
2、建立空单链表
建立由头指针 L 指向的头结点,头结点的指针域置为空,完成空的单链表 L 的初始化。
InitList(LinkList *L)
{
*L = (LinkList)malloc(sizeof(Node)) ; /*建立头结点*/
(*L)->next = NULL; /*建立空的单链表 L*/
}
3、 建立单链表
建立单链表的方法主要由头插法和尾插法。
1)头插法建表
【算法思想】已知空链表 L,依次读入结点数据头插入,直到读入结
束标志为止。
每插入一个结点到表头的步骤:
- 生成新结点 s 并赋值;
- 将结点 s 插入到首元结点之前,即表头结点之后;
- 重复以上过程,生成并插入第 i个结点,直到读取到结束字符为$时, 结束读取数据,建表完毕。
【算法描述】
void CreateFromHead(LinkList L) /* L 是已经初始化好的空链表的头指针,通过键盘输入表中元素值,利用头 插法建单链表 L */
{
Node *s;
char c;
int flag=1;
while(flag) /* flag 初值为 1,当输入“$”时,置 flag 为 0, 建表结束*/
{
c=getchar();
if(c!=’$’)
{
s=(Node*)malloc(sizeof(Node)); /*建立新结点 s*/
s->data=c; s->next=L->next; /*将 s 结点插入表头*/
L->next=s;
}
else
flag=0;
}
}
注:头插法得到的单链表的逻辑顺序与输入的顺序相反,所以也称为逆序建表法。
2)尾插法建表
头插法建立链表虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。若希望二者次序一致,可采用尾插法建表。该方法是将新节点插到当前单链表的表尾上。为此需增加一个尾指针 r,始终指向当前链表的表尾。
【算法思想】
已知空链表 L,设置尾指针 r 指向当前表尾 r=L
依次读入结点数据尾插入,直到读入结束标志时将表尾结点链域置空
每插入一个结点到表尾的步骤:
- 生成新结点 s;
- 将结点 s 插入到表尾 r 结点之后;该结点作为当前表尾结点
【算法描述】
void CreateFromTail(LinkList L) /* L 是已经初始化好的空链表的头指针,通过键盘输入元素值,利用尾插法建 单链表 L */
{
Node *r, *s;
int flag =1; /*设置一个标志,初值为 1,当输入“$”时,flag 为 0, 建表结束*/
r=L; /* r 指针动态指向链表的当前表尾,以便于做尾插入,其初值指 向头结点*/
while(flag) /*循环输入表中元素值,将建立新结点 s 插入表尾*/
{
c=getchar();
if(c!=’$’)
{
s=(Node*)malloc(sizeof(Node));
s->data=c; r->next=s; r=s;
}
else
{
flag=0;
r->next=NULL; /*将最后一个结点的 next 链域置为空,表示 链表的结束*/
}
} /*while*/
} /*CreateFromTail*/
4、单链表的查找
单链表查找可分为按序号查找和按值查找的两种方式。
1)按序号查找
- 在单链表 L 中查找第 i 个结点,
【基本思想】链表的头指针出发,顺着链域 next 逐个数结点,直
至搜索到第 i 个结点为止。
【算法思想】
- 从头查找,当前指针 p 指向表头 L;
- 顺链计数,表未查完且未找到 则计数器 j 加 1,p 指针后移;继续找下一个.
- 结果判断 如找到第 i 个结点,则返回结点 p; 如表查完未找到,则返回空。
【算法描述】
Node * Get (LinkList L, int i) / * 在带头结点的单链表 L 中查找第 i 个结点,若找到(1≤i≤n),则返回 该结点的存储位置; 否则返回 NULL * /
{
int j;
Node *p;
p=L;
j=0; / * 从头结点开始扫描 * /
while ((p->next!=NULL)&&(j<i)
{
p=p->next; / * 扫描下一结点 * /
j++; / * 已扫描结点计数器 * /
}
if(i= =j)
return p; / * 找到了第 i 个结点 * /
else
return NULL; / * 找不到,i≤0 或 i>n * /
} / * Get * /
【问题】:在单链表中可否直接找到第 i 个结点?
答案:不行。链表中结点只能顺链查找,不能直接随机存取。 因为第 i 个结点的位置信息是在第 i-1 个结点的链域中存放,无法直接随机存取,这是由链表结构的特征决定的,必须从链表的表头开始查找。
2) 按值查找
【算法思想】
- 从表头开始, p 指向链表 L 首元结点 p=L->next;
- 当表未查完时, 若当前结点值不为 key,则指针 p 后移 比较当前结点值为 key,则跳出循环
- 结束返回 p 指针的位置。
【算法描述】
Node *Locate( LinkList L,ElemType key) / * 在带头结点的单链表 L 中查找其结点值等于 key 的结点,若找到则返 回该结点的位置 p,否则返回 NULL * /
{
Node *p;
p=L->next; / * 从表中第一个结点开始 * /
while (p!=NULL)
if (p->data!=key)
p=p->next;
else
break; / * 找到结点值=key 时退出循环 * /
return p;
} / * Locate * /
【算法时间性能分析】按序号查找和按值查找由于都是需要从表头开
始逐个搜索,最多走完整个表,因而这两个算法的平均时间复杂度是
相同的即 O(n)。
5、单链表插入操作
要在带头结点的单链表L中第i个数据元素之前插入一个数据元素e。
【算法思想】
- 确定第 i-1 个结点的位置 (同按序号查找算法)
- 申请新结点 s
- 插入挂链:新结点 s 插至第 i-1 个结点之后
S 结点的后继指向第 i 个结点 s->next=pre->next
第 i-1 个结点的指针指向 s pre->next=s
【算法描述】
void InsList(LinkList L,int i,ElemType e) /*在带头结点的单链表 L 中第 i 个位置插入值为 e 的新结点 s*/
{
Node *pre,*s;
int k;
pre=L;
k=0;/*从“头”开始,查找第 i-1 个结点*/
while(pre!=NULL&&k<i-1) /*表未查完且未查到第 i-1 个时重复,找到 pre 指向第 i-1 个*/
{
pre=pre->next;
k=k+1;
} /*查找第 i-1 结点*/
if(!pre) /*如当前位置 pre 为空表已找完还未数到第 i 个,说明插入位置不合理*/
{
printf(“插入位置不合理!”);
return ERROR;
}
s=(Node*)malloc(sizeof(Node)); /*申请一个新的结点 S */
s->data=e; /*值 e 置入 s 的数据域*/
s->next=pre->next; /*修改指针,完成插入操作*/
pre->next=s;
return OK;
}
6、单链表删除
【算法思想】
- 确定第 i-1 个结点的位置 p(同按序号查找算法)
- 删除并释放第 i 个结点
指针 r 指向被删结点 r=p->next
删除第 i 个结点 r p->next=r ->next
释放 r 结点 free(r )
【算法描述】
int DelList(LinkList L,int i,ElemType *e) /*在带头结点的单链表 L 中删除第 i 个元素,并将删除的元素保存到变量*e 中 */
{
Node *pre,*r;
int k;
pre=L;k=0;
while(pre->next!=NULL&&k<i-1) /*寻找被删除结点 i 的前驱结点 i-1 使 p 指向它*/
{
pre=pre->next;
k=k+1;
} /*查找第 i-1 个结点*/
if(!(pre->next)) /* 即while 循环是因为p->next=NULL 或 i<1 而 跳出的,而是因为没有找到合法的前驱位置, 说明删除位置 i 不合法。*/
{
printf(“删除结点的位置 i 不合理!”);
return ERROR;
}
r=pre->next;
pre->next=r->next; /*修改指针,删除结点 r*/
*e=r->data; free(r); /*释放被删除的结点所占的内存空间*/
return OK;
}
说明:删除算法中的循环条件(pre->next!=NULL && k<i-1)与前插算法中的循 环条件(pre!=NULL &&k<i-1)不同,因为前插时的插入位置有 m+1 个(m 为当 前单链表中数据元素的个数)。i=m+1 是指在第 m+1个位置前插入,即在单链表 的末尾插入。而删除操作中删除的合法位置只有 m 个,若使用与前插操作相同的循环条件,则会出现指针指空的情况,使删除操作失败。