链表的特点
- 根据线性表元素多少,长度可变化。
- 动态申请内存空间:元素删除或插入时,对应申请新的存储空间或释放原来占有的存储空间。
- 使用指针来链接各个结点,按照线性表的前驱关系将结点连接起来,结点之间内存地址不必相邻。
- 链表的访问:不能像数组那样根据下标直接访问某一个结点
i
,而需要从头结点开始,沿着link指针一个一个地计数(遍历),才能找到。 - 通常有一个first指针变量和一个last指针遍历,用来存储头结点地址和末尾结点的地址(便于快速在链表末尾添加结点)。
单链表(single link list)
结点数据结构
单链表结点的数据结构通常分为两部分:数据和指向后继结点的指针。
struct ListNode {
ELEM data;
ListNode * link;
};
ListNode *first, *last;
空链表的表示:
为了表示空链表,引入一个特殊的“首结点”,该节点中data的值被忽略,该节点不被看做列表中的实际元素,只是为了方便空链表操作和表头操作。当链表为空时,first和last均指向首结点,如下:
ListNode * headNode = new ListNode;
first = headNode;
last = headNode;
相关算法
查找结点
查找位置为i的结点。
ListNode * find(const int i) {
// 返回“首结点”,
if (-1 == i) {
return first;
}
// 指向第一个结点;
ListNode * p = first->link;
int j=0;
// 遍历到链表末尾或j==i时停止。
while(p != NULL && j<i) {
p = p->link;
j++;
}
// 当链表结点数量小于i时,返回NULL
return p;
}
新增结点
关键点:
- 找到前序结点,注意前序结点为NULL的情况;
- 先将新增结点的data和link设置好,新增结点的link指向前序结点的link;
再将其前序结点的link指向新增结点。 - 注意要考虑到新增结点在链首、中、尾这三种情况的处理,插入到链表末尾时,last指针指向该新增结点。
ListNode * insert(ELEM value, int i) {
ListNode * preNode;
// 查找到前序结点,因为特殊“首结点”的存在,使i==0的情况不用特殊处理。
preNode = find(i - 1);
if (NULL == preNode) {
return NULL;
}
ListNode * newNode;
newNode = new ListNode;
newNode->link = preNode->link;
newNode->data = value;
preNode->link = newNode;
// 处理插入到链表尾部的情况
if (NULL == newNode->link) {
last = newNode;
}
return newNode;
}
删除结点
删除某一个结点的后续结点。
关键点:
- 在真正删除节点之前,安排好它的“后事”:将其前序结点的link指向待删除结点的link;
void removeAfter(ListNode * preNode) {
// 暂存待删除结点
ListNode * delLink = preNode->link;
if (delLink != NULL) {
preNode->link = delLink->link;
delete delLink;
}
}
链表长度
int length() {
ListNode * curNode = first->link;
int count = 0;
while (curNode != NULL) {
curNode = curNode->link;
count++;
}
return count;
}
单链表的缺点
- 可以方便地查询某个结点的后续结点,但不能直接查询其前驱结点。
双链表(double link list)
顾名思义,它是双向列表。主要在其每个结点中增加了一个指向前驱的指针rlink,指向后续结点的指针为llink;
结点结构
struct DblListNode {
ELEM data;
DblListNode * preLink;
DblListNode * nextLink;
};
DblListNode *first, *last;
空链表表示和单链表一样,使用一个特殊“首结点”,其preLink为NULL。
相关算法
新增结点
在某个结点的后面新增一个结点。
关键点:
- 修改旧有结点的preLink或nextLink之前,一定要确定它的旧值是否已经利用完了?
DblListNode * insertAfter(ELEM value, dblListNode * node) {
DblListNode * preNode = node->preLink;
DblListNode * nextNode = node->nextLink;
// 设置新增结点的各字段
DblListNode * newNode = new DblListNode;
newNode->data = value;
newNode->nextLink = node->nextLink;
newNode->preLink = node;
node->llink->rlink = newNode;
node->llink = newNode;
return newNode;
}
删除结点
删除某个指定的结点。
关键点:
- 删除前处理好“后事”:如待删除结点的前驱和后续结点是否已安排妥当?
- 待删除结点的preLink和nextLink置为空。
void deleteNode(DblListNode * node) {
node->preLink->nextLink = node->nextLink;
node->nextLink->preLink = node->preLink;
node->preLink == NULL;
node->nextLink == NULL;
delete node;
}
链表 vs 数组
数组
数组的优点
- 没有使用指针,节省存储空间(因为指针需要2或4字节存储);
- 可直接访问指定下标的元素,简洁便利,程序更易懂;
数组的缺点
- 不可动态改变长度,必须事先确定数组长度。
- 往数组中插入、删除某个元素时,可能需要移动O(k)个元素。
- 需要连续的一大块存储空间,以存储所有的元素。
链表
链表的优点
- 无需事先确定长度,可事先动态增、删元素。
- 删除、插入元素时不需要移动O(k)个元素。
- 结点间的存储空间不需要连续,因而可以利用一些碎片空间。
应用场合
- 当线性表中需要经常删除、插入元素时,不使用数组,使用链表。
当线性表长度不确定时,不能使用数组,使用链表,否则可能为了预留足够的空间而定义一个很大的数组,造成资源浪费。
当读取操作频繁(特别是按位读取操作频繁),插入、删除操作不频繁的时候,可以使用数组。
- 当存储的数据占的存储空间与指针占有的空间相当时(1:1),要慎重考虑使用链表是否值得。
- 当元素内容经常更新,但删除、插入操作并不常见的时候,可以考虑使用数组作为一个“索引”,即数组中元素为一个指针,指针指向内容真正存储的地址,这样就很容易找到内容存储地址,并进行更新。