一 链表的定义
讨论链表之前,先说线性表。
线性表是一种最常用且最简单的数据结构。一个线性表是n个数据元素的有限集合。对于一个非空的线性表,一般存在几个特征:(1)存在唯一的一个被称作“第一个”的数据元素;(2)存在唯一的一个被称为“最后一个”的数据元素;(3)除第一个之外,线性表中的每个数据元素均只有一个前驱;(4)除最后一个之外,集合中每个数据元素均只有一个后继.
最常见的线性表就是数组了。对于数组而言,每个元素的存储空间相邻,其逻辑关系上相邻的两个元素,在物理位置上也相邻,并且可以通过索引进行访问。这种存储结构的缺点是当进行插入或删除操作时,需要移动大量元素。并且当集合中元素数量很多的时候,需要一整块大的存储空间。
于是就有了链表。
链表的特点就是用一组任意的存储单元去存储线性表中的每个数据元素。那么逻辑上的两个相邻元素之间怎样联系呢?链表的解决方法是对于每一个存储单元来说,不仅要存储该数据元素,还要存储下一个元素的地址。每一个这样的存储空间,称为一个结点。一个结点包括两个部分:数据域存储该元素的值,指针域存储下一个元素的地址。当然,对于双向链表而言,指针域不仅要存储下一个元素的地址,还要存储上一个元素的地址。下图分别为数组和链表的存储结构简图,很容易看出各自的特点:数组为连续地址的存储,链表为分散的存储地址,相邻元素通过指针链接。
数组示意图
链表示意图
二链表的基本操作
1.结点
在讨论链表的操作之前,先看看怎么表示一个链表。我们知道链表的组成单元是结点,因此表示一个结点就基本把链表表示出来了。在C语言当中,可以用结构体来表示结点:
typedef struct node *link; struct node { Item item; link next; };
其中的Item就是表示数据类型,在C语言中,可能是int,char,double等基本类型,也有可能是一个结构体对象甚至是一个链表,总之,它表示结点的数据域。Link则表示指针域,它指向下一个结点。
在实际应用过程中,往往实现并不知道需要多少个这样的结点,所以肯定需要动态申请。需要一个结点的时候,就创建一个新的结点。C语言中一般通过malloc函数来完成。
link x = malloc(sizeof *x);
对应的,释放一个结点用free
free(x);
2.插入操作
在不考虑链表为空的情况下,将一个结点t插入到结点x后面,可以这样表示。
t->next = x->next; x->next = t;
3.删除操作
同样,在不考虑链表为空的情况下,将一个结点x的后面一个结点删除,可以这样表示。
t = x->next; x->next =t->next; free(t);
4.遍历
在链表上执行的最常用的操作之一是遍历操作,即按照顺序遍历链表中的元素,对每个元素都执行某种操作。假如x是一个指向链表首结点的指针,尾结点中的指针为空,visit是一个元素为参量的函数,那么遍历操作可以用如下语句:
for(t = x;t != NULL;t = t->next) visit(t->item);
三 有哨兵的双向循环链表
如图,是一个带哨兵的双向循环链表。哨兵是一个哑元素,也就是其数据部分是没有意义的,只是为了占一个位置,让链表永远不为空,从而简化判断的边缘条件。图中黑色部分即为哨兵,位于链表头和尾之间。不同于上面介绍的单链表,双向链表的指针部分包括指向后继和前驱的地址。同样,我们按照上文中描述单链表的顺序,对双向循环链表的基本操作进行简单介绍。
1.结点
typedef struct node *link; struct node { Item item; link next; link prev; };
2.插入操作
假设nil为指向哨兵元素的指针,x为要插入的结点,插入的位置为表头,则操作步骤如下。
x->next = nil->next; nil->next->prev = x; nil->next = x; x->prev = nil;
3.删除操作
假设要删除的结点地址为x,则删除操作步骤如下:
x->prev->next = x->next; x->next->prev = x->prev;
4.遍历
for(t = nil->next;t != nil;t = t->next) visit(t->item);
带哨兵的循环双向链表涵盖了双向链表和循环链表,掌握了这种链表,也就基本上掌握了所有链表的基本知识。下一篇博客将会给出链表的几个应用。