文章向导
线性表的链式存储结构
单链表的抽象数据类型 (组织数据,设计接口)
一个完整的实例,验证成果!
一、线性表的链式存储结构
线性表的链式存储结构就是用一组任意的存储单元存储线性表中的元素,这组存储单元可以是连续的,也可以是不连续的。同时也就意味着这些数据元素可以存在于内存未被占用的任意位置。
我们习惯将链表(链式表)中的每个元素称之为结点,每个节点包含两部分的组成内容:存储数据元素的信息(数据域) + 存储直接后继(下一个结点)的位置(指针域)。有点懵?希望下面两张图能帮到你!
上图中清晰地描述了链式存储结构中结点间的逻辑关系,说到这儿其实还应该补充一点小内容,然后就可以正式开始接下来的单链表具体实现。
对于线性表来说,必然有头有尾。“头”通常用头结点来表示,而“尾”则是链表的最后一个结点(将其指针域置为NULL,用于表明这是尾部)。虽然一些本科数据结构教材中会把头结点称之为是链表中的第一个结点,但这点对于初学者来说很容易在具体的代码实践中造成混淆,所以我的建议是在理解上干脆就把头结点给独立起来,剩下的部分则是第一个结点到最后一个结点。
二、单链表的抽象数据类型 (组织数据,设计接口)
~~~~ ~~~ 笔者试图应用软件工程中所提到的一些良好准则来组织这部分内容,也就是尽量贴近工程实际中所要求的模块化、可读性、简洁性等问题。
/* list.h */
#ifndef LIST_H
#define LIST_H
#include <stdlib.h>
typedef int ElemType;
typedef struct ListNode
{
ElemType data;
struct ListNode *next;
} ListNode;
typedef struct ListMsg
{
int size;
ListNode *head;
ListNode *tail;
} ListMsg;
/*Public Interface*/
int list_init(ListMsg *list_msg, ListNode **list);
void list_destory(ListMsg *list_msg, ListNode **list);
int list_get_node(ListMsg *list_msg, ListNode *list);
int list_ins_node(ListMsg *list_msg, ListNode *list);
int list_del_node(ListMsg *list_msg, ListNode *list);
#define list_head(list_msg) ((list_msg)->head)
#define list_tail(list_msg) ((list_msg)->tail)
#define list_is_head(list_msg, list) ((list) == (list_msg)->head ? 1 : 0)
#define list_is_tail(list) ((list)->next == NULL ? 1 : 0)
#define list_size(list_msg) ((list_msg)->size)
#endif
~~~~ ~~~ 上面的list.h头文件中,完整地定义了单链表的抽象数据类型,这也是工程实际开发中常用到的方式。 接下来逐一讲解各部分接口是如何设计的。
1. 创建一个单链表
/* 函数名:list_init
*
* 功能:创建一个带头结点的指定结点数目的单向链表
*
* 入口参数:
* > list_msg: 存放链表信息(大小、头尾结点)
* > list: 描述链表结点的内容(数据域、指针域)
*
* 返回值:-1: fail, 0: success
*/
int list_init(ListMsg *list_msg, ListNode **list)
{
ListNode *p, *r;
int i, size;
memset(list_msg, 0, sizeof(ListMsg)); //clean up
printf("Please Enter the size of list: ");
scanf("%d", &(list_msg->size));
size = list_size(list_msg);
srand(time(0));
//*list为整个链表,但形参list为栈变量,注意!!!
*list = (ListNode*)malloc(sizeof(ListNode));
r = *list; //r为指向头结点
/*将新结点插入表尾:尾插法*/
for (i = 0; i < size; i++) {
//生成新结点,总计size个,故在外使用时list->next才是整个链表的第一个节点
p = (ListNode*)malloc(sizeof(ListNode));
if (p == NULL) {
printf("fail to creat a new node!\n");
return -1;
}
p->data = rand()%100 + 1; //生成[1,100]范围内的随机数
r->next = p; //表尾结点指向新结点
r = p; //将新生成的结点p赋值给r, 让r始终保持为名义上的尾结点
}
r->next = NULL; //当前链表结束
return 0;
}
2. 删除一个单链表
~~~~ ~~~ 当我们不打算使用一个链表时,最好将其销毁(也就是在内存中把它释放掉),以便留出空间给其他程序或软件使用。删除整个单链表的思路也比较简单,循环释放每个节点即可。
/* 函数名:list_destory
*
* 功能:创建一个带头结点的单向链表置为空表
*
* 入口参数:
* > list_msg: 存放链表信息(大小、头尾结点)
* > list: 描述链表结点的内容(数据域、指针域)
*
* 返回值:none
*/
void list_destory(ListMsg *list_msg, ListNode **list)
{
ListNode *p, *q;
memset(list_msg, 0, sizeof(ListMsg)); //clean up
p = (*list)->next; //第一个结点赋值给p
while(p) {
q = p->next; //下一个结点赋值给q
free(p); //释放上一个结点
p = q;
}
free(*list); //释放头节点
*list = NULL;
printf("ClearList: *list = %p\n", *list);
}
三、查找、插入、删除链表结点
1. 查找链表结点
/* 函数名:list_get_node
*
* 功能:返回list中第get_loc个位置的数据元素值
*
* 入口参数:
* > list_msg: 存放链表信息(大小、头尾结点)
* > list: 描述链表结点的内容(数据域、指针域)
*
* 返回值:-1: fail, 0: success
*/
int list_get_node(ListMsg *list_msg, ListNode *list)
{
ListNode *p = NULL; //工作指针
int j, get_loc;
int size = list_size(list_msg);
printf("Please Enter get_loc: ");
scanf("%d", &get_loc);
/*变量检查*/
if (get_loc < 1 || get_loc > size) {
puts("get_loc err!");
return -1;
}
p = list->next; //从链表的第一个结点开始
j = 1; //计数器
while (p && j < get_loc) { /*寻找第get_loc个节点*/
p = p->next;
++j;
}
if (!p || j > get_loc) { /*若第get_loc个节点不存在*/
puts("get_loc node is inexistence!");
return -1;
}
printf("Get node: location = %d, value = %d, address = %p\n", j, p->data, p);
return 0;
}
~~~~ ~~~ 从上述程序片段可得出,单链表结构与顺序存储结构在查找能力上相比,前者为O(n),后者为O(1)。另外,该算法主要的核心思想就是“工作指针后移”,这点也是很多算法中的通用技术。
2.插入链表结点
假设想将一结点s插入到结点p和p->next之间,该如何做呢?实际上仅需两个步骤:s->next = p->next; p->next = s; 就可实现,不妨看看下图加深理解。
/* 函数名:list_ins_node
*
* 功能:在list中第ins_loc个结点位置(之前/之后)插入新的结点,且list的长度加1
*
* 入口参数:
* > list_msg: 存放链表信息(大小、头尾结点)
* > list: 描述链表结点的内容(数据域、指针域)
*
* 返回值:-1: fail, 0: success
*
* Notes:实际上是将节点s插入到p与p->next之间
*/
int list_ins_node(ListMsg *list_msg, ListNode *list)
{
ListNode *p, *s; //工作指针与待生成的结点
int j, ins_loc, size = list_size(list_msg);
int element;
printf("Please Enter ins_loc:");
scanf("%d", &ins_loc);
printf("Please Enter element inserted:");
scanf("%d", &element);
/*变量检查*/
if (ins_loc < 1 || ins_loc > size) {
puts("ins_loc err!");
return -1;
}
p = list; //从头节点开始(情形: s插入于头节点与第一个节点之间)
j = 0; //结点计数器(j=0: 在ins_loc之后插入, j=1:在ins_loc之前插入)
/* j=0: 寻找第ins_loc-1个结点, 因在ins_loc之前插入结点
* j=1: 寻找第ins_loc个节点, 因在ins_loc之后插入节点
*/
while (p && j < ins_loc) {
p = p->next;
++j;
}
if (!p || j > ins_loc) { /*若寻找的结点不存在*/
puts("ins_loc node is inexistence!");
return -1;
}
s = (ListNode*)malloc(sizeof(ListNode)); //生成新结点
s->data = element;
s->next = p->next; //将p的后继结点赋值给s的后继
p->next = s; //将s赋值给P的后继
size++;
list_msg->size = size;
return 0;
}
首先谈谈在第i个位置之前插入结点的算法思路:
- 声明工作指针p指向链表头结点,初始化j从1开始;
- 当j < i时让p不断后移指向下一个结点,同时j累加1;
- 若链表末尾p为NULL,则说明第i个结点不存在;
- 否则查找成功,并生成一个空结点s;
- 将element赋值给结点s的数据域;
- s->next = p->next; p->next = s;
- 表长加1,返回成功。
~~~~
~~~
通过上述的步骤分解,读者应该可以顺利理解插入结点的算法思路。但这里需要提及一点针对此类情况的思维优化。一些读者习惯于按照计算机的方式机械的代入式分析,这样既难受也效率低下,如果在面对含递归的程序时恐怕会疯掉。
首先,看一下我们的目标“在i个结点位置之前插入新的结点”,那么这件事两步就可搞定。step1:找到第i-1个结点, step2:利用公式s->next = p->next; p->next = s; 嗯?感觉我在说废话? 我的意思就是不要纠结于如while或for循环真的是不是达成了这件事,而应集中于程序的大框架下某一代码块完成了特定的功能,多个这样的块最后再组合起来即可,这也是程序设计的思考流程。
3.删除链表结点
~~~~
~~~
假设想将第i个结点从链表中删掉又该如何操作呢?好吧,实际上更为简单仅需一条语句:p->next = p->next->next; 这种关系通过图来说明则更加容易让人理解。
/* 函数名:list_del_node
*
* 功能:删除链表list的第del_loc个结点, 且表长减1
*
* 入口参数:
* > list_msg: 存放链表信息(大小、头尾结点)
* > list: 描述链表结点的内容(数据域、指针域)
*
* 返回值:-1: fail, 0: success
*/
int list_del_node(ListMsg *list_msg, ListNode *list)
{
ListNode *p, *q;
int j, del_loc, size = list_size(list_msg);
int element;
printf("Please Enter del_loc:");
scanf("%d", &del_loc);
/*变量检查*/
if (del_loc < 1 || del_loc > size) {
puts("del_loc err!");
return -1;
}
p = list; // 从头结点开始
j = 1;
while (p->next && j < del_loc) { /*寻找第del_loc-1个节点*/
p = p->next;
++j;
}
if (!(p->next) || j > del_loc) { /*若第del_loc个节点不存在*/
puts("del_loc node is inexistence!");
return -1;
}
q = p->next; //待删除的节点
p->next = q->next; //将q的后继赋值给p的后继
printf("Delete node: location = %d, value = %d, address = %p\n", j, q->data, q);
free(q);
return 0;
}
~~~~
~~~
删除链表结点的算法思路与插入链表结点的算法思路大致类似:都是先找到第i-1个结点,然后进行后续的操作。为何是先找到第i-1个结点呢?emm, 如果你这样问,那么你应该再继续翻到前面链式存储结构处好好阅读下,我就不解释了!
最后值得说明的是,单链表的插入和删除结点的时间复杂度都是O(n):查找时为O(n),而插入和删除操作上仅是单纯的赋值移动指针而已,时间复杂度为O(1)。故整体算法时间复杂度为O(n)。因此,对于插入或删除数据越频繁的操作,单链表的效率就远大于顺序存储结构。
四、一个完整的实例,验证成果!
~~~~ ~~~ 以下是一个完整的单链表测试实例,完成了创建、删除、查找、插入、删除结点这几种基本操作,可以用于验证上述算法的正确性。
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "list.h"
/* 函数名:list_init
*
* 功能:创建一个带头结点的指定结点数目的单向链表
*
* 入口参数:
* > list_msg: 存放链表信息(大小、头尾结点)
* > list: 描述链表结点的内容(数据域、指针域)
*
* 返回值:-1: fail, 0: success
*/
int list_init(ListMsg *list_msg, ListNode **list)
{
ListNode *p, *r;
int i, size;
memset(list_msg, 0, sizeof(ListMsg)); //clean up
printf("Please Enter the size of list: ");
scanf("%d", &(list_msg->size));
size = list_size(list_msg);
srand(time(0));
//*list为整个链表,但形参list为栈变量,注意!!!
*list = (ListNode*)malloc(sizeof(ListNode));
r = *list; //r为指向头结点
/*将新结点插入表尾:尾插法*/
for (i = 0; i < size; i++) {
//生成新结点,总计size个,故在外使用时list->next才是整个链表的第一个节点
p = (ListNode*)malloc(sizeof(ListNode));
if (p == NULL) {
printf("fail to creat a new node!\n");
return -1;
}
p->data = rand()%100 + 1;
r->next = p; //表尾结点指向新结点
r = p; //将新生成的结点p赋值给r, 让r始终保持为名义上的尾结点
}
r->next = NULL; //当前链表结束
return 0;
}
/* 函数名:list_destory
*
* 功能:将一个带头结点的单向链表置为空表
*
* 入口参数:
* > list_msg: 存放链表信息(大小、头尾结点)
* > list: 描述链表结点的内容(数据域、指针域)
*
* 返回值:none
*/
void list_destory(ListMsg *list_msg, ListNode **list)
{
ListNode *p, *q;
memset(list_msg, 0, sizeof(ListMsg)); //clean up
p = (*list)->next; //第一个结点赋值给p
while(p) {
q = p->next; //下一个结点赋值给q
free(p); //释放上一个结点
p = q;
}
free(*list); //释放头节点
*list = NULL;
printf("ClearList: *list = %p\n", *list);
}
/* 函数名:list_get_node
*
* 功能:返回list中第get_loc个位置的数据元素值
*
* 入口参数:
* > list_msg: 存放链表信息(大小、头尾结点)
* > list: 描述链表结点的内容(数据域、指针域)
*
* 返回值:-1: fail, 0: success
*/
int list_get_node(ListMsg *list_msg, ListNode *list)
{
ListNode *p = NULL; //工作指针
int j, get_loc;
int size = list_size(list_msg);
printf("Please Enter get_loc: ");
scanf("%d", &get_loc);
/*变量检查*/
if (get_loc < 1 || get_loc > size) {
puts("get_loc err!");
return -1;
}
p = list->next; //从链表的第一个结点开始
j = 1; //计数器
while (p && j < get_loc) { /*寻找第get_loc个节点*/
p = p->next;
++j;
}
if (!p || j > get_loc) { /*若第get_loc个节点不存在*/
puts("get_loc node is inexistence!");
return -1;
}
printf("Get node: location = %d, value = %d, address = %p\n", j, p->data, p);
return 0;
}
/* 函数名:list_ins_node
*
* 功能:在list中第ins_loc个结点位置(之前/之后)插入新的结点,且list的长度加1
*
* 入口参数:
* > list_msg: 存放链表信息(大小、头尾结点)
* > list: 描述链表结点的内容(数据域、指针域)
*
* 返回值:-1: fail, 0: success
*
* Notes:实际上是将节点s插入到p与p->next之间
*/
int list_ins_node(ListMsg *list_msg, ListNode *list)
{
ListNode *p, *s; //工作指针与待生成的结点
int j, ins_loc, size = list_size(list_msg);
int element;
printf("Please Enter ins_loc:");
scanf("%d", &ins_loc);
printf("Please Enter element inserted:");
scanf("%d", &element);
/*变量检查*/
if (ins_loc < 1 || ins_loc > size) {
puts("ins_loc err!");
return -1;
}
p = list; //从头节点开始(情形: s插入于头节点与第一个节点之间)
j = 0; //结点计数器(j=0: 在ins_loc之后插入, j=1:在ins_loc之前插入)
/* j=0: 寻找第ins_loc-1个结点, 因在ins_loc之前插入结点
* j=1: 寻找第ins_loc个节点, 因在ins_loc之后插入节点
*/
while (p && j < ins_loc) {
p = p->next;
++j;
}
if (!p || j > ins_loc) { /*若寻找的结点不存在*/
puts("ins_loc node is inexistence!");
return -1;
}
s = (ListNode*)malloc(sizeof(ListNode)); //生成新结点
s->data = element;
s->next = p->next; //将p的后继结点赋值给s的后继
p->next = s; //将s赋值给P的后继
size++;
list_msg->size = size;
return 0;
}
/* 函数名:list_del_node
*
* 功能:删除链表list的第del_loc个结点, 且表长减1
*
* 入口参数:
* > list_msg: 存放链表信息(大小、头尾结点)
* > list: 描述链表结点的内容(数据域、指针域)
*
* 返回值:-1: fail, 0: success
*/
int list_del_node(ListMsg *list_msg, ListNode *list)
{
ListNode *p, *q;
int j, del_loc, size = list_size(list_msg);
int element;
printf("Please Enter del_loc:");
scanf("%d", &del_loc);
/*变量检查*/
if (del_loc < 1 || del_loc > size) {
puts("del_loc err!");
return -1;
}
p = list; // 从头结点开始
j = 1;
while (p->next && j < del_loc) { /*寻找第del_loc-1个节点*/
p = p->next;
++j;
}
if (!(p->next) || j > del_loc) { /*若第del_loc个节点不存在*/
puts("del_loc node is inexistence!");
return -1;
}
q = p->next; //待删除的节点
p->next = q->next; //将q的后继赋值给p的后继
printf("Delete node: location = %d, value = %d, address = %p\n", j, q->data, q);
free(q);
return 0;
}
int main(int argc, char *argv[])
{
ListMsg list_msg;
ListNode *list;
list_init(&list_msg, &list);
list_get_node(&list_msg, list);
list_ins_node(&list_msg, list);
list_get_node(&list_msg, list);
list_del_node(&list_msg, list);
list_get_node(&list_msg, list);
list_destory(&list_msg, &list);
return 0;
}
测试结果:
上图的测试流程较为明确:先是获取第一个结点的信息,然后在原链表中第一个结点之前插入新结点(在新链表中新结点则成为第一个结点),然后再获取信息看是否成,紧接着删除第一个结点并再次获取信息验证是否成功,最后释放整个单链表。
参阅资料
《大话数据结构》
《数据结构-C语言版》
《算法精解—C语言描述》