单链表

偶然看到,感觉对于我这样首次学习线性表的很好理解,特别是图画的表达,感谢作者。


1. 线性表的链式存储结构

链式存储:用一组任意的存储单元存储线性表中的数据元素。用这种方法存储的线性表简称线性链表。

存储链表中结点的一组任意的存储单元可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。

链表中结点的逻辑顺序和物理顺序不一定相同。

为了正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其直接后继结点的地址(或位置),称为指针(pointer)或链(link),这两部分组成了链表中的结点结构。

 链表是通过每个结点的指针域将线性表的n个结点按其逻辑次序链接在一起的。

每一个结只包含一个指针域的链表,称为单链表。

操作方便,总是在链表的第一个结点之前附设一个头结点(头指针)head指向第一个结点。头结点的数据域可以不存储任何信息(或链表长度等信息)。

(1) 结点的描述

// 线性表的单链表存储结构 
typedef struct LNode {
	ElemType data;
	struct LNode *next; 
}LNode, *LinkList;

(2) 结点的实现

结点是通过动态分配和释放来的实现,即需要时分配,不需要时释放。实现时是分别使用C语言提供的标准函数:malloc(), realloc(),sizeof(),free() 。

动态分配  p=(LNode*)malloc(sizeof(LNode));

函数malloc分配了一个类型为LNode的结点变量的空间,并将其首地址放入指针变量p中。

动态释放 free(p) ;

系统回收由指针变量p所指向的内存区。P必须是最近一次调用malloc函数时的返回值。

(3) 常见的指针操作



2. 单链表基本操作

(1) 建立单链表

动态地建立单链表的常用方法有如下两种:头插入法,尾插入法。

头插入法建表: 即每次插入的结点都作为链表的第一个结点。

// 头插法建立单链表 
void CreateList_L1(LinkList &L, ElemType array[], int n) {
	L = (LinkList) malloc (sizeof (LNode)); 
	L->next = NULL; // 先建立一个带头结点的单链表  
	for (int i = 0; i < n; i++) 
	{
		LinkList p = (LinkList) malloc (sizeof (LNode));  // 生成新结点  
		p->data = array[i]; 
		p->next = L->next; 
		L->next = p;  // 插入到表头  
	} 
}

尾插入法建表:该方法是将新结点插入到当前链表的表尾,使其成为当前链表的尾结点

// 尾插法建立单链表 
void CreateList_L2(LinkList &L, ElemType array[], int n)
{
	LinkList p, q; 
	L = p = (LinkList) malloc (sizeof (LNode)); 
	p->next = NULL; 
	for (int i = 0; i < n; i++) 
	{
		q = (LinkList) malloc (sizeof(LNode)); 
		q->data = array[i]; 
		q->next = p->next;
		p->next = q; 
		p = p->next; 
	} 
}

无论是哪种插入方法,如果要插入建立的单线性链表的结点是n个,算法的时间复杂度均为O(n)。

对于单链表,无论是哪种操作,只要涉及到钩链(或重新钩链),如果没有明确给出直接后继,钩链(或重新钩链)的次序必须是“先右后左”。

(2) 单链表查找

按序号查找 取单链表中的第i个元素。

对于单链表,不能象顺序表中那样直接按序号i访问结点,而只能从链表的头结点出发,沿链域next逐个结点往下搜索,直到搜索到第i个结点为止。因此,链表不是随机存取结构。

设单链表的长度为n,要查找表中第i个结点,仅当1≦i≦n时,i的值是合法的。

// L为带头结点的单链表的头指针 
// 当第i个元素存在时,将其值赋给e,并返回OK,否则返回ERROR
Status GetElem_L1(LinkList L, ElemType &e, int i)
{
	int j = 1; 
	LinkList p = L->next; 
	while (p && j < i)
	{
		p = p->next; 
		j++;
	}
	if (!p || j > i) return ERROR; 
	e = p->data; 
	return OK; 
}

按值查找 按值查找是在链表中,查找是否有结点值等于给定值key的结点? 若有,则将首次找到的值为key的结点的存储位置返回,并返回OK;否则返回NULL。查找时从开始结点出发,沿链表逐个将结点的值和给定值key作比较。

Status GetElem_L2(LinkList L, LNode *p, ElemType key)
{
	LinkList q = L->next; 
	while (q && q->data != key) 
	{
		q = q->next; 
	}
	if (q->data != key) return ERROR;
	else p = q; 
	return OK; 
}

(3) 单链表的插入

插入运算是将值为e的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间。因此,必须首先找到ai-1所在的结点p,然后生成一个数据域为e的新结点q,q结点作为p的直接后继结点。

// 在带头结点的单链线性表L中第i个位置之前插入元素e  
Status ListInsert_L(LinkList &L, int i, ElemType e)
{
	LinkList p = L; 
	int j = 0; 
	while (p && j < i-1) // 寻找第i-1个结点  
	{
		p = p->next; 
		j++; 
	}
	if (!p || j > i-1) return ERROR; 
	LinkList q = (LinkList) malloc (sizeof(LNode));  // 生成新结点 
	q->data = e; 
	q->next = p->next; 
	p->next = q;  // 插入L中  
	return OK; 
}

设链表的长度为n,合法的插入位置是1≦i≦n。算法的时间主要耗费移动指针p上,故时间复杂度亦为O(n)。

(4)单链表的删除

按序号删除

删除单链表中的第i个结点。

为了删除第i个结点ai,必须找到结点的存储地址。该存储地址是在其直接前趋结点ai-1的next域中,因此,必须首先找到ai-1的存储位置p,然后令p–>next指向ai的直接后继结点,即把ai从链上摘下。最后释放结点ai的空间,将其归还给“存储池”。

设单链表长度为n,则删去第i个结点仅当1≦i≦n时是合法的。则当i=n+1时,虽然被删结点不存在,但其前趋结点却存在,是终端结点。故判断条件之一是p–>next!=NULL。显然此算法的时间复杂度也是O(n)。

// 在带头结点的单链线性表L中,删除第i个元素,并由e返回其值 
Status ListDelete_L(LinkList &L, int i, ElemType &e){
	LinkList p = L; 
	int j = 0; 
	while (p->next && j < i-1) { // 寻找第i个结点,并令p指向其前趋  
		p = p->next; 
		j++;
	}
	if (!(p->next) || j > i-1) return ERROR; // 删除位置不合理  
	LinkList q = p->next; 
	p->next = q->next; 
	e = q->data; 
	free(q);  // 删除并释放结点  
	return OK; 
}

按值删除  删除单链表中值为key的第一个结点。

// 删除单链表中值为key的第一个结点,
Status ListDeleteKey_L(LinkList &L, ElemType key){
	LinkList p = L, q = L->next; 
	while (q && q->data != key){
		p = q; 
		q = q->next; 
	}
	if (!q) return ERROR; 
	p->next = q->next; 
	free(q); 
	return OK; 
}

算法的执行与形参key有关,平均时间复杂度为O(n)。

从上面的讨论可以看出,链表上实现插入和删除运算,无需移动结点,仅需修改指针。解决了顺序表的插入或删除操作需要移动大量元素的问题。

变形一:删除链表中所有值为key的结点

基本思想:从单链表的第一个结点开始,对每个结点进行检查,若结点的值为key,则删除之,然后检查下一个结点,直到所有的结点都检查。

void ListDeleteKey_L2(LinkList &L, ElemType key) {
	LinkList p = L, q = L->next; 
	while(q) {
		if (q->data == key) {
			p->next = q->next;
			free(q); 
			q = p->next;
		}
		else {
			p = q; 
			q = q->next; 
		}
	}
}

变形二:删除单链表中所有值重复的结点,使所有结点的值都不相同。

基本思想:从单链表的第一个结点开始,对每个结点进行检查:检查链表中该结点的所有后继结点,只要有值和该结点的值相同,则删除之;然后检查下一个结点,直到所有的结点都检查。

void ListDeleteKey_L3(LinkList &L){
	LinkList p = L->next, q, ptr; 
	while (p){
		q = p;
		ptr = q->next; 
		while (ptr){
			if (ptr->data == p->data) {
				q->next = ptr->next;
				free(ptr); 
				ptr = q->next;  
			} else {
				q = ptr; 
				ptr = ptr->next; 
			}
		}
		p = p->next; 
	}
}

(5)有序单链表合并

    设有两个有序的单链表,它们的头指针分别是La 、 Lb,将它们合并为以Lc为头指针的有序链表。算法说明:算法中pa ,pb分别是待考察的两个链表的当前结点,pc是合并过程中合并的链表的最后一个结点。

// 已知单链表La、Lb的元素按值非递减排列 
// 归并得到新的单链表Lc,Lc的元素也按值非递减排列  
void ListMerge_L(LinkList &La, LinkList &Lb, LinkList &Lc) {
	LinkList pa = La->next, pb = Lb->next, pc = La; 
	Lc = La;  // 用La的头结点作为Lc的头结点  
	while (pa && pb) {
		if (pa->data <= pb->data) {
			pc->next = pa;
			pc = pc->next;
			pa = pa->next;
		}
		else  {
			pc->next = pb;
			pc = pc->next; 
			pb = pb->next; 
		}

	}
	pc->next = pa ? pa : pb;  // 插入剩余段  
	free(Lb);  // 释放Lb的头结点  


  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值