线性表的链式表示:链表详解

(一)单向链表

(1)定义

线性表的链式存储结构即链表,其特点是用一组任意的存储单元存储线性表的数据元素,这组在内存中的存储单元可以是连续的,也可以是不连续的。链表,顾名思义,是由各个节点链接。节点的信息包含数据域指针域。数据域存储元素信息,指针域用来存储直接后继节点的地址(两节点间的箭头)。n个节点链接成一个链表,每个元素节点都只有一个指针域,又称为单链表

(2)示意图表示

我们以(A,B,C,D)四个英文字母作为数据域的四个节点,每个节点的数据域存储该元素(字母),其指针域还要存储下一个字母的地址,同时每个节点在内存中也有专属的地址,即存储地址。
链表的存储必须从头指针开始。头指针也是一个元素节点,只是它并不存储数据元素,只存储第一个元素节点的地址。同时最后一个元素D没有直接后继元素,故指针域为空,我们赋值为NULL。示意图如下图所示:

头指针
20
存储地址 数据域 指针域
29 B 46
4 D NULL
20 A 29
46 C 4

1.首先从头指针开始找到第一个元素的地址20,我们找到该地址并遍历了A字母,同时该A字母节点又记录着B节点的地址29,所以我们找到地址29遍历,获取到B字母…如此进行下去,找到D字母时候,由于它的指针域是NULL,故链表遍历终止。如此,我们可以逐步获取到ABCD四个字母。

2.这个过程我们可以类比于寻找宝藏的游戏。一张藏宝图只能记录一个宝贝的位置,假如你是一个有着第一张藏宝图(头指针/头节点)的商人(暗暗自喜),所以你按照藏宝图的指引来到了藏有第一个宝贝A(数据域)的20号洞穴(指针域)并将它取出来。此时,你惊奇地发现这个宝贝上还记录着下一个宝贝(B)的藏宝地(29),于是事不宜迟,你马上赶到了29号洞穴…当找到D宝贝之后,上边并没有下一个宝贝的地址。于是,我们一共获得了ABCD四件宝贝(还不错哈)。

将四个元素节点转换成线性图示:
在这里插入图片描述
图解:
1.L为头节点(头指针),指针域记录A元素的地址,数据域不记录元素。相当于L有一条到A的箭头
2. ABCD事实上在内存中不连续,所以只需要记录下一个节点的地址,可以最大限度节省内存空间。

(3)程序实现

上边说了一些链表的基础内容,那么真正程序实现是什么样的呢。我们下面我们以C++语言为例,实现链表的关键程序代码。

3.1 节点定义

一个链表节点需要指针域于数据域,所以首先定义一个结构体。

typedef int ElemType
typedef struct LNode
{
    
    
	ElemType data;//数据域
	LNode* next;//下一个节点地址 指针域
}LNode,*LList;
  1. LNode结构体类型,同时对于LNode的指针我们定义为LList,避免后期声明指针时需要LList *a,*b。较为繁琐。
  2. data记录数据元素,next记录下一节点地址

3.2 链表的创建

void ListCreate(LList& L, ElemType n, ElemType a[])
{
    
    
	LList p;
	L = new LNode; L->next = NULL;//头节点
	for (int i = 0; i < n; i++)
	{
    
    
		//头插法 1.
		p = new LNode;//声明新的一个节点,用来记录新的元素
		p->data = a[i];//将该元素赋值到指针域
		p->next = L->next;//2.
		L->next = p;//3.

	}
}
  1. 对链表的元素的插入操作分为头插法和尾插法。头插法是在头节点之后,第一个节点之前进行插入,这种方法会将元素序列逆序。尾插法是在链表最后一个节点之后插入,可以保持原序列的原有顺序不变。
  2. 将该新建节点的后继赋值为第一个节点的地址,而第一个节点的地址是由头节点记录的,所以应该用p->next=L->next;
  3. 将该节点地址赋值给头节点,即头插法中将新来的节点作为第一个节点。

3.3链表的遍历输出

该程序实现对与链表的遍历,同时对每一个遍历到的元素进行最简单的输出至控制台。读者可以尝试手动画表执行程序,会让理解更加深刻。

void Print(LList L)
{
    
    
	LList p = L->next;//L表示头节点,无数据元素,故先让p指向第一个节点
	while (p)//当p不是NULL时 执行循环
	{
    
    
		cout << p->data << " ";
		p = p->next;//指向下一节点
	}
		cout << endl;
}

3.4删除链表的重复元素

已经对链表有些掌握的我们,可以试着做一做应用题。
Problem:删除链表的重复元素,假设有线性表(4,3,3,1,1,1),经过算法遍历后成为线性表(4,3,1)。
Analysis: 删除重复的元素,可以遍历原有链表,声明两个指针p,q,p指向前一个遍历的节点(可能会准备删除),q指向当前节点。如果该数据域元素和其直接前驱的元素相同我们将p指向q的后继(因为q会被删除),同时删除q节点。
时间复杂度可以控制在O(n),且不需要额外申请内存空间
代码实现为:

bool ListDelete(LList& L)
{
    
    
	LList p,q; p = L->next;
	LList r;//记录删除节点
	if (p == NULL)return false;//空表
	else if (p->next == NULL)return true;//一个节点
	else 
	{
    
    
		while (p->next)//P的后继存在
		{
    
    
			q = p->next;//暂存p后继
			if (q->data == p->data)
			{
    
    
				p->next = q->next;
				delete(q);
			}
			else 
				p = p->next;
		}
	}
	return true;
}

(二)循环链表

特点

  1. 循环链表是另一种形式的链式存储结构,其特点为链表最后一个节点的指针域不再是NULL,而是指向头节点L。整个链表形成一个闭合的环。故从表中任一节点出发都可以找到其他的节点。
  2. 其基础操作域单向链表基本一致,差别在于算法中循环条件不是p是否为NULL,而是p是否和头指针相同。

(三)双向链表

(1)特点

  1. 我们前面所讨论的链表,其节点都只有一个指向直接后继的指针域,故从某个节点出发只能使指针逐步后移去遍历其他节点。如果想要找到某节点的前驱,则需要从头节点出发。
  2. 顾名思义,在定义节点类型时,我们还需要增加一个结构体成员,即指向直接前驱的指针。故一个节点拥有两个指针域。克服了单向链表单向性的缺点。

(2)节点定义

与单向链表类似,只不过多一个前驱指针罢了。

typedef int ElemType
typedef struct DuLNode
{
    
    
	ElemType data;//数据域
	DuLNode* next;//下一个节点地址 指针域
	DuLNode* prior;//前一个节点的地址
}DuLNode,*DuList;

(四)总结

  1. 线性表包含顺序表(数组)和链表,是最常用最简单 的一种数据结构。其中顺序表为线性表的顺序存储结构,即动态数组。
  2. 链表分为单向链表、双向链表、循环链表等多种结构。其优点在于不浪费任何内存空间、插入节点较为方便,不需要像顺序表一样移动元素、但是获取对应节点不能像顺序表一样依靠索引,而是需要利用头指针逐步后移来获取。
  3. 顺序表以元素在计算机内”物理位置“相邻来表示线性表中数据元素之间的逻辑关系。而链表不要求逻辑上相邻的元素在物理位置上也相邻,故失去了顺序表随机选取的优点。

(五)参考文献

【1】严蔚敏,吴伟民. 数据结构(C语言版). 北京:清华大学出版社,1997.

说明:该文是小白第一次写的blog,错误在所难免,同时受限于知识水平与写作能力,敬请大家批评指正!

猜你喜欢

转载自blog.csdn.net/ln_acmc/article/details/115028259