为了解决顺序存储不足:用线性表另外一种结构-链式存储。
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。在顺序结构中,每个数据元素只需要存数据元素信息就行了,而在链式结构中,除了存储数据元素信息外,还要存储它的后继元素的存储地址。所以一般结点包括两个信息:数据和指针。链表就是n个节点组成的,如何每个结点只包含一个指针,那么就是单链表。
有头有尾:我们把链表中第一个结点的存储位置叫作头指针,那么整个链表的存取就必须是从头指针开始进行的。而线性链表的最后一个结点指针为空(NULL)。
有时,为了更方便对链表进行操作,会在单链表的第一个结点前加一个头结点。头结点的数据域可以不存储任何信息,也可以存储如线性表长度等附加信息,头结点的指针域存储指向第一个结点的指针。
(ps:大话数据结构-P58页,图3-6-4,3-6-6有误,当初理解这里花了好长时间,图文表现的不是一个意思,无语!)
头指针和头结点的异同
头指针
(1)指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
(2)头指针具有标识作用,所以常用头指针冠以链表的名字。
(3)无论链表是否为空,头指针均不为空。头指针是链表的必要元素。
头结点
(1)头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(也可存放链表的长度)
(2)有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了。
(3)头结点不一定是链表必须的要素。
//线性表的单链表存储结构
typedef struct Node
{
ElemType data;
struct Node *next;
} Node;
typedef struct Node *LinkList;
单链表的读取
获取链表第i个数据的算法思路:
(1)声明一个指针p指向链表的第一个结点,初始化j从1开始;
(2)当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
(3)若到链表末尾p为空,则说明第i个结点不存在;
(4)否则查找成功,返回结点p的数据。
//初始条件:链表L已存在,且1≤i≤ListLength(L)
//操作结果:用e返回L中第i个数据元素的值
bool GetElem(LinkList L,int i,ElemType *e)
{
int j = 1;
LinkList p;
p = L->next; //指向第一个结点,这里是有头结点的情况
while( p && j<i )
{
p = p -> next;
++j;
}
if( !p || j>i )
{
return FALSE;
}
*e = p->data;
return TRUE;
}
单链表的读取时间复杂度是O(n)。
单链表的插入和删除
若将结点插入结点p和p->next结点之间,只需要做如下变换:
s->next = p->next;
p->next = s;
这两句的顺序是不能改变的。
单链表第i个数据插入结点的算法思路是:
(1)声明一个指针p指向链表的第一个结点,初始化j从1开始;
(2)当j< i 时,遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
(3)若到链表末尾p为空,说明第i个元素不存在;
(4)否则查找成功,在系统中生成一个空结点s;
(5)将数据元素e赋值给s->data;
(6)单链表的插入标准语句 s->next = p->next; p->next = s;
(7)返回成功。
/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
bool ListInsert(LinkList *L,int i ,ElemType e)
{
int j;
LintList p,s;
p = *L;
j = 1;
while (p && j<i) /*寻找第i个结点*/
{
p = p->next;
++j;
}
if (!p || j >i)
return FALSE; /*第i个元素不存在*/
s = (LinkList)malloc(sizeof(Node)); /*生成新的结点*/
s->data = e;
s->next = p->next; /*将p的后继结点赋值给s的后继*/
p->next = s; /*将s赋值给p的后继*/
return TRUE;
}
单链表的删除
实际上就是一步,p->next = p->next->next. 用p来取代p->next;
q=p->next;
p->next = q->next;
单链表第i个数据删除结点的算法思路:
(1)声明一个指针p指向链表的第一个结点,初始化j从1开始;
(2)当j< i 时,遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
(3)若到链表末尾p为空,说明第i个元素不存在;
(4)否则查找成功,将欲删除的结点p-next赋值给q;
(5)单链表的删除标准语句 p->next = q->next;
(6)将q结点中的数据赋值给e,作为返回;
(7)释放q结点;
(8)返回成功。
bool ListDelete(LinkList *L,int i ,ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while(p->next && j <i)
{
p = p->next;
++j;
}
if(!(p->next) || j>i)
return FALSE;
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return TRUE;
}
对于基本的插入与删除操作,它们其实都是两部分组成:
1.遍历查找第i个结点;
2.插入和删除结点。
从整个算法中,时间复杂度都是O(n)。如果在我们不知道第i个结点的指针位置,单链表数据结构在插入和删除操作上,与线性表的存储结构没有太大的优势。但是如果我们希望从第i个位置,插入10个结点,对于顺序存储结构来说,每一次插入都需要移动n-i个结点,每次都是O(n)。而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单通过赋值移动指针而已,时间复杂度都是O(1)。显然,对于插入和删除数据越频繁的操作,单链表效率优势越明显。
单链表的整表创建
单链表是一种动态结构。
对于每个链表来说,它所占用的空间大小和位置是不需要预先分配的。
所以创建单链表的过程,就是从“空表”的初始状态,一次建立各元素结点,并插入链表中、
单链表的整表创建的算法思路:
(头插法)
1.声明以指针p和计数器变量i;
2.创建一个空链表L;
3.让L的头结点的指针指向NULL;
4.循环:
生成以新结点赋值给p
随机生成以数字赋值给p->data;
将p插入到头结点与前一新结点之间。
头插法,示意图:
/*随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)*/
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); /*当前时间种下随机数种子*/
*L = (LinkList)malloc(sizeof(Node));
(*L) ->next = NULL; /*建立带头结点的链表*/
for (i=0,i<n,i++)
{
P = (LinkList)malloc(sizeof(Node)); /*生成新结点*/
p->data = rand()%100+1; /*随机生成100以内的数字*/
p->next = (*L)->next;
(*L)->next = p; /*插入到表头*/
}
}
尾插法:把每次新结点都插在终端结点的后面
/*随机产生n个元素的值,建立带表头结点的单链线性表(尾插法)*/
void CreateListTail(LinkList *L,int n)
{
LinkList p,r;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node))
r =*L /*r为指向尾部的结点*/
for(i=0;i<n;i++)
{
p = (Node *)malloc(sizeof(Node)) /*生成新结点*/
p->data = rand() %100 +1;
r->next = p; /*将表尾终端结点的指针指向新结点*/
r = p; /*将当前的新结点定义为表尾终端结点*/
}
r->next = NULL; /*表示当前链表结束*/
}
单链表的整表删除
当我们不打算使用这个单链表时候,我们需要把它销毁,其实就是在内存中将它释放掉。
思路:
1.声明结点p和q;
2.将第一个结点赋值给p;
3.循环:
- 将下一结点赋值给q;
- 释放p;
/*初始条件:顺序线性表L已存在,操作结果:将L重置为空表*/
Status ClearList(LinkList *L)
{
LinkList p,q;
p = (*L)->next; /*p指向第一个结点*/
while(p) /*没到表尾*/
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL; /*头结点指针域为空*/
return OK;
}
6.总结
单链表结构与顺序存储结构优缺点
时间性能
顺序存储结构: 查找为O(1),因其是随机存取结构;插入与删除需要平均移动表长一半的元素,故为O(n);
单链表:查找为O(n),查找算法的时间复杂度取决于i的位置,当i=1时,则不需要遍历,第一个就取出数据了,而当i=n时则遍历n-1次才可以。因此最坏情况为O(n);单链表在确定出某位置的指针后,插入和删除时间仅为O(1);
空间性能
顺序存储结构:需要预分配存储空间;
单链表:不需要分配存储空间,只要有就可以分配,元素个数也不受限制。
若线性表需要频繁查找,很少进行插入与删除操作时,宜采用顺序存储结构;若需要频繁插入和删除时,宜采用单链表结构。
当线性表中的元素个数变化较大或未知时,最好使用单链表。如果实现知道线性表的大致长度则使用顺序存储结构效率会高很多。