目录
一、链式存储
链表的定义是递归的,它或者为空null,或者指向另一个节点node的引用,这个节点含有下一个节点或链表的引用。
链表中的第一个节点的存储位置叫做头指针,最后一个节点指针为空(NULL)。
与顺序存储相比,允许存储空间不连续,插入删除时不需要移动大量的元素,只需修改指针即可,但查找某个元素,只能从头遍历整个链表。
二、单链表
单链表是链表的一种。链表由节点所构成,节点内含一个指向下一个节点的指针,节点依次链接成为链表。因此,链表这种数据结构通常在物理内存上是不连续的。链表的通常含有一个头节点,头节点不存放实际的值,它含有一个指针,指向存放元素的第一个节点。
单链表图:
空链表图:
在C语言中可以用结构指针来描述单链表:
typedef struct Node
{
ElemType data; // 数据域
struct Node* Next; // 指针域
} Node;
typedef struct Node* LinkList;
结点由存放数据元素的数据域和存放后继节点地址的指针域组成。
1、单链表的读取
获得链表第i个数据的算法思路
——声明一个结点 p 指向链表的第一个结点,初始化 j 从1开始
——当 j<1 时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j+1;
——若到链表末尾 p 为空,则说明第 i 个元素不存在;
——否则查找成功,返回结点 p 的数据;
/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:用e返回L中第i个数据元素的值 */
Status GetElem( LinkList L, int i, ElemType *e )
{
int j;
LinkList p;
p = L->next;
j = 1;
while( p && j<i )
{
p = p->next;
++j;
}
if( !p || j>i )
{
return ERROR;
}
*e = p->data;
return OK;
}
由于这个算法的时间复杂度取决于i的位置,当i=1时,则不需要遍历,而i=n时则遍历n-1次才可以。因此最坏情况的时间复杂度为O(n)。
2、单链表的插入
如何将S插入到ai、ai+1之间?
s->next = p->next;
p->next = s; (上下两句不能颠倒)
单链表第i个数据插入结点的算法思路:
–声明一结点p指向链表头结点,初始化j从1开始;
–当j<1时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
–若到链表末尾p为空,则说明第i个元素不存在;
–否则查找成功,在系统中生成一个空结点s;
–将数据元素e赋值给s->data;
–单链表的插入刚才两个标准语句;
–返回成功。
/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */
Status ListInsert(LinkList *L, int i, ElemType e)
{
int j;
LinkList p, s;
p = *L;
j = 1;
while( p && j<i ) // 用于寻找第i个结点
{
p = p->next;
j++;
}
if( !p || j>i )
{
return ERROR;
}
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
3、单链表的删除
假设元素a2的结点为q,要实现结点q删除单链表的操作,其实就是将它的前继结点的指针绕过指向后继结点即可。
即可以表示为:p->next = p->next->next;
或者: q=p->next; p->next=q->next;
单链表第i个数据删除结点的算法思路:
–声明结点p指向链表第一个结点,初始化j=1;
–当j<1时,就遍历链表,让P的指针向后移动,不断指向下一个结点,j累加1;
–若到链表末尾p为空,则说明第i个元素不存在;
–否则查找成功,将欲删除结点p->next赋值给q;
–单链表的删除标准语句p->next = q->next;
–将q结点中的数据赋值给e,作为返回;
–释放q结点
/* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) */
/* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度-1 */
Status 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 ERROR;
}
q = p->next;
p->next = q->next;
*e = q->data;
free(q); //释放q节点
return OK;
}
无论是单链表插入还是删除算法,它们其实都是由两个部分组成:第一部分就是遍历查找第i个元素,第二部分就是实现插入和删除元素。从整个算法来说,它们的时间复杂度都是O(n)。
但对于插入或者删除数据越频繁的操作,单链表的料率优势越来越明显,例如从第i个位置来说,插入连续10个元素,对于顺序存储结构意味着,每一次插入都需要移动n-i个位置,所以每次都是O(n)。而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。
三、单链表的整表创建
创建单链表的过程是一个动态生成链表的过程,从“空表”的初始状态起,依次建立各元素结点并逐个插入链表
单链表整表创建的算法思路如下:
–声明一结点p和计数器变量i;
–初始化一空链表L;
–让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
–循环实现后继结点的赋值和插入。
1、头插法
将新节点插入到当前链表的表头,(头结点之后),插入的顺序与链表中的顺序相反,关键点就是记住旧的表头,生成一个新的放到旧表头前面,如图:
/* 头插法建立单链表示例 */
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;
p->next = (*L)->next;
(*L)->next = p;
}
}
2、尾插法
增加一个尾指针,新节点插到链表的尾部,插入的顺序和链表的顺序一致,如图:
/* 尾插法建立单链表演示 */
void CreateListTail(LinkList *L, int n)
{
LinkList p, r;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));
r = *L;
for( i=0; i < n; i++ )
{
p = (Node *)malloc(sizeof(Node));
p->data = rand()%100+1;
r->next = p;
r = p; // 备注:初学者可能很难理解这句,重点解释。
}
r->next = NULL;
}
四、单链表的整表删除
当我们不打算使用单链表时,我们需要把它销毁,即在内存中将它释放,以便于留出空间给其它程序或者软件使用。
–声明结点p和q;
–将第一个结点赋值给p,下一结点赋值给q;
–循环执行释放p和将q赋值给p的操作;
Status ClearList(LinkList *L)
{
LinkList p, q;
p = (*L)->next;
while(p)
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
return OK;
}
五、单链表结构与顺序存储结构的优缺点
分别从存储分配方式、时间性能、空间性能三方面来做对比。
存储分配方式:
–顺序存储结构用一段连续的存储单元依次存储线性表的数据元素。
–单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。
时间性能:
–查找
•顺序存储结构O(1)
•单链表O(n)
–插入和删除
•顺序存储结构需要平均移动表长一半的元素,时间为O(n)
•单链表在计算出某位置的指针后,插入和删除时间仅为O(1)
空间性能:
–顺序存储结构需要预分配存储空间,分大了,容易造成空间浪费,分小了,容易发生溢出。
–单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。
结论:
–若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。
–若需要频繁插入和删除时,宜采用单链表结构
–当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。
–而如果事先知道线性表的大致长度,比如一年12个月,一周就是星期一至星期日共七天,这种用顺序存储结构效率会高很多。