线性表的链式表示
顺序表达插入删除操作需要移动大量元素,影响了运行效率,故而引出了线性表的链式存储。
在使用链式存储的过程中不需要使用地址连续的存储单元,不要求逻辑上相邻的两个元素在物理上也相邻。用过“链”建立起数据元素之间的逻辑关系。对线性表的插入删除不需要移动元素,只需要修改指针。
单链表的定义
结点类型的描述如下:
typedef struct Lnode {
ElemType data;
struct Lnode *next;
}Lnode,*LinkList;
利用单链表可以解决顺序表需要大量的连续存储空间的缺点,但是单链表附加指针域,也存在浪费存储空间的缺点。单链表是非随机存取的数据结构,不能直接找到表中特定的结点。查找某一个特定结点时,需要从表头开始遍历,依次查找。
通常用头指针来标识一个单链表,头指针为NULL时表示一个空表。
为了操作上的方便,在单链表的第一个结点之前附加一个结点,成为头结点。头结点的数据域可以不设任何信息,也可以记录表长等相关信息。头结点的指针域指向线性表的第一个元素结点。
头结点和头指针的区别:不管带不带头结点,头指针始终指向链表的第一个结点,而头结点是带头结点链表中的第一个结点,结点内不存储信息。
引入头结点的好处:
- 由于开始结点的位置被存放在头结点的指针域中,所以链表的第一个位置上的操作和在表的其他位置上的操作一致,不需要进行特殊处理。
- 无论链表是否为空,其头指针是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也统一了。
单链表各种操作省略,后续习题会有。
双链表
在单链表中,访问后继结点的时间复杂度为O(1),访问前驱结点的复杂度为O(n)。为了克服这点,引入了双链表。双链表结点中有两个指针,prior和next,表示前驱和后继。
结点类型的描述如下:
typedef struct DNode {
ElemType data;
struct Lnode *next,*prior;
}Dnode,*DLinkList;
双链表中执行按值和按位查找的操作和单链表相同。
双链表在插入和删除操作的实现上和单链表有很大不同,因为链变化时也要对前驱指针作出修改,要保证在修改的过程中不断链。
插入和删除结点算法的时间复杂度为O(1)
在p所指的结点后,插入结点*s
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
双链表的删除操作
删除结点p的后继结点q
p->next = q->next;
q->next->prior = p;
free(q);
循环链表
循环链表和单链表的区别在于,最后一个结点的指针不为NULL,指向头结点。
循环链表判空条件就不是原来的判断头结点指针是否为空,而是判断它是否等于头指针。
循环链表的插入删除算法与单链表几乎一样,不同的是若在表尾进行,执行的操作不同。
循环链表可以从表中任意位置开始遍历链表。
若对于链表的操作是在表头和表尾进行的,可对循环链表不设置头指针而仅设置尾指针,这样操作效率更高。因为如果设置为头指针,需要花O(n)才能进行表尾的操作,而如果设置的是尾指针r,可通过r->next找到头指针,对表头和表尾的操作的复杂度都为O(n).
循环双链表
在循环双链表中,某结点*p为尾结点时,p->next = L
若循环双链表为空表时,其头结点的prior和next 都为L
静态链表
静态链表是借助数组来描述线性表的链式存储结构,结点也有data和next。
这里的指针是结点的相对地址(数组下标),又称游标。
和顺序表一样,静态链表也要提前分配一块连续的内存空间。
结构类型的描述如下
#define MAXSIZE 50
typedef struct {
ElemType data;
int next;
} sLinkList[MAXSIZE];
静态链表以next为-1作为结束的标志。插入删除操作与动态链表相同,只需要修改指针而不需要移动元素。
使用起来不够方便。
顺序表和链表的比较(重点)
存取方式
顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。逻辑结构与物理结构
采用顺序存储时,逻辑上相邻的元素,其对应的物理存储位置也相邻。
链式存储时,逻辑上相邻的元素,其物理存储位置不一定相邻,其对应的逻辑关系是通过指针链接来表示的。查找插入和删除操作
对于按值查找,当顺序表无序的情况下,两者都为O(n)。
当顺序表有序时,可以采用二分查找,时间复杂度为O(logn)。
对于按序号查找,顺序表支持随机访问,时间复杂度仅为O(1)。链表的平均时间复杂度为O(n).
顺序表的插入删除平均需要移动半个表长的元素。
链表的插入删除仅需要修改相关结点的指针域。
链表每个结点都带有指针域,故存储空间消耗大于顺序表,存储密度不够大。空间分配
顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新的元素就造成内存溢出,需要预先分配足够大的存储空间。预先分配过大,会导致顺序表后部大量闲置。分配过小,会造成溢出。
动态存储分配虽然存储空间可以扩充,但是需要移动大量元素,倒置操作效率降低,若内存中没有更大块的连续存储空间将导致分配失败。
链式存储的结点空间只在需要的时候申请分配,只要有内存空间就可分配,操作灵活高效。
实际中如何选择存储结构?
- 基于存储的考虑
对线性表长度或存储规模难以估计,不宜采用顺序表。
链表不用事先估计存储规模,但是存储密度低,小于1 - 基于运算的考虑
若常用运算是按序号访问数据元素,顺序表优于链表。
若常用运算是插入删除,链表由于顺序表。 - 基于环境的考虑
顺序表实现简单,链表复杂。
下面是习题里不熟悉的点
- 单链表中,增加一个头结点的目的是为了方便运算的实现
- 对于一个头指针为head的带头结点的单链表,判定该表为空表的条件是head->next = NULL,对于不带头结点的单链表,判定为空的条件为head = NULL
- 一个链表最常用的操作是在末尾插入结点和删除结点,则选用(带头结点的双循环链表)最节省时间。
- 需要分配较大的空间,插入和删除不需要移动元素的线性表是(静态链表)。
编程题将在下一部分列出。