主要内容
链表的基本概念
链表的特点是:用一组任意的存储单元存储某逻辑结构的数据元素(存储单元可连续可不连续)。
因此,为了表示每个数据元素与其直接后继数据元素的逻辑关系,除了要存储数据元素本身的信息外,还需要存储一个指向直接后继数据元素的指针(对于线性表而言)。这两部分信息组成数据元素的存储映像,称为结点。
结点包括两个域:数据域(存储数据元素信息)和指针域(存储直接后继的指针)。
但结点的指针域并不是只能存放一个指针。根据链表结点所含指针数、指针指向和指针连接方式的不同,可以将链表分为(线性表:)单链表、循环链表、双向链表、(树:)二叉链表、(图:)邻接表、十字链表、邻接多重表。
链表的基本术语和存储结构
1)首元结点:链表中存储第一个数据元素的结点。
2)头结点:在首元结点之前的附加结点,其数据域为NULL(也可以存储其他有用的信息),指针域存储指向首元结点的指针。
3)头指针:指向链表中的第一个结点,或是首元结点,或是头结点。
4)末尾结点:链表中存储最后一个数据元素的结点。
5)尾指针:指向链表中的最后一个结点。
typedef struct LNode /*单链表结点*/
{
Elemtype data;
struct LNode *next;
} LNode, *LinkList; /*LNode表示结点内容,LinkList表示单链表的起始存储位置*/
因为链表的存储单元不像顺序表那样固定,所以我们只能用链表的起始存储位置来表示一个链表,而不能像顺序表那样用一个数组来表示。
链表的基本操作
1)初始化链表:
void InitList(Linklist &L)
{
L = new LNode; /*生成新的头结点,返回指针给L*/
L->next = NULL; /*指针域设置为空*/
}
2)根据给定的结点位置取值:
Elemtype GetElem(LinkList L, int i) /*传递指定位置i*/
{
LNode *p = L->next; /*使指针p指向首元结点*/
int timer = 1;
while(p && timer < i) /*当p为非空且timer未到i时循环*/
{
p = p->next;
timer++;
}
if(!p || timer > i) return ERROR; /*如果p为空或timer超过i,报错*/
else return p->data;
}
3)查找结点,返回指向结点的指针:
LNode *SearchNode(LinkList L, Elemtype check)
{
LNode *p = L->next; /*p指向首元结点*/
while(p && p->data != check) /*当p为非空且p->不等于给定值时循环*/
p = p->next;
return p; /*成功则返回地址,失败时p为NULL,返回NULL*/
}
4)在给定位置上插入结点,插入的思想还可推广到创建链表跟合并链表上:
void InsertNode(LinkList &L, int i, Elemtype e)
{
LNode *p = L->next;
int timer = 1;
while(p && timer < (i-1)) /*找到第i-1个结点的位置*/
{
p = p->next;
timer++;
}
LNode insert = new LNode; /*新结点*/
insert->data = e;
insert->next = p->next; /*插入*/
p->next = insert;
}
5)删除给定位置的结点:
在删除结点时,除了修改相连结点的指针外,还需要释放结点的存储空间(通过delete实现)。所以我们要引入一个指针临时保存被删除结点的地址,以备释放。
void DeleteNode(LinkList &L, int i)
{
LNode *p = L->next;
int timer = 1;
while(p && timer < (i-1)) /*找到第i-1个结点*/
{
p = p->next;
timer++;
} /*上面这段代码写好多遍了...*/
if(!p->next || j > (i-1)) return ERROR; /*如果没找到要删除的结点,报错*/
LNode *temp = p->next; /*temp暂存第i个结点的地址*/
p->next = temp->next; /*使第i-1个结点指向第i个结点指向的下一个结点*/
delete temp; /*释放空间*/
}
将上面的代码稍微修改也能得到“删除数据元素等于给定值的结点”的算法,这里就不多写了。
循环链表
使单链表中的最后一个结点指向头结点,整个链表形成一个环,这就是循环链表。从表中的任意一点出发都能找到表中的其他结点。
在某些情况下,若在循环链表中设尾指针(指向最后一个结点)而不设头指针,可以简化一些操作。
例如合并两个线性表,只需要将第二个表的末尾结点指向第一个表的头结点a,第一个表的末尾结点指向第二个的首元结点b,然后释放第二个表的头结点c。
/*first为第一个表的尾指针,second为第二个表的尾指针*/
LNode *temp1 = second->next; /*temp1暂存第二个表的头结点地址*/
LNode *temp2 = temp1->next; /*temp2暂存第二个表的首元结点地址*/
second->next = first->next; /*a*/
first->next = temp2; /*b*/
delete temp1; /*c*/
双向链表
在单链表中,查找直接后继结点的执行时间为O(1),而查找直接前驱的执行时间为O(n)。为克服单链表的这种单向性缺点,可利用双向链表,即增加指向直接前驱的指针域。
typedef struct DuLNode
{
Elemtype data;
struct DuLNode *prior; /*直接前驱*/
struct DuLNode *next; /*直接后继*/
} DuLNode, *DuLinkList;
虽然增加一个指针域后,插入和删除操作需要修改更多的信息,但其实双向链表和单链表的时间复杂度都为O(n)。