文章目录
0.创作背景与食用方法
东北大学大一萌新路过~
软件学院在大一开设了程序设计基础课,其中有一部分叫做翻转课堂,我们会学习简单的算法与数据结构。为了方便以后新来的学弟学妹进行预习以及让我自己更好地回顾所学知识,我写下了这篇文章。
本文章仅仅初步介绍链表,比较浅显,但对于大一上学期的期末考试来说是远远足够的。
如有不足之处,恳请指正。
1.有关抽象数据类型
抽象数据类型(ADT)是一个非常重要的内容。通俗地讲,就是对用户隐藏了实现细节。当我们把链表创建、删除、插入等等的具体方法写好之后,封装起来,以后直接拿来就用,不再考虑其中的细节。
2.单链表的介绍
2.1数组
大家都知道,数组是在内存中里连续储存的一组数据。它对于遍历与访问指定位置的元素这两个操作以线性时间执行。但是,对于删除元素与插入元素,数组就显得比较费力了,因为在一个位置插入或删除之后,就需要把后边位置的元素移动位置,这就浪费了很多的时间。
其次,有时候我们不知到数组中会出现多少元素,或者数组的元素个数是不断变化的,这样就会导致我们要对数组的最大值进行估值,通常我们会估的大一些,这就会浪费掉许多空间。
2.2单链表
为了避免插入、删除操作对时间上的线性开销,我们便提出了链表。简单地讲,链表就是把一群结构体用指针链接成了一串。每一个结构体叫做一个结点。这些结点在内存中的储存是不必相连的。每一个结点又分为数据域和指针域,数据域用来存储数据,指针域指向下一个结点(或NULL)用来表示各个结点之间的联系。如下图所示为一个结点。
许多这样的节点以指针的方式连接在一起就成了一个链表。为了更好地使用链表,我们可以让链表的第一个结点作为表头(header),也可以叫哑结点(dummy node)。指向头结点的指针称为头指针。头结点是不需要储存数据的,它只是作为一个表头,便于我们使用链表。
为什么表头会便于我们使用链表呢?
第一,并不存在从所给定义出发在表的起始端插入元素的真正显性的方法。第二,从表的起始端实行删除是一个特殊情况,因为它改变了表的起始端,编程中的疏忽将会造成表的丢失。第三个问题涉及一般的删除。虽然指针的移动很简单,但是删除算法要求我们记住被删除元素前面的表元。
不明白也没有关系,只要记住,有一个表头会省下很多麻烦。
3.单链表的实现细节
3.1结点的表示
结点用一个结构体来表示。这个结构体包含两个成员,其中一个是data,也就是数据域,另一个是next,也就是指针域。注意,指针域包含一个指向结点的指针。浮点型是float,指向浮点型的指针用 float* 来声明。字符型是char,指向字符型的指针用 char* 来声明。那么同理,指向结点的指针用struct Node* 来声明。
struct Node
{
int data;
struct Node *next;
};
3.2结点的建立
接下来我们写一个创建结点的函数,其中参数为该结点要储存的数据。默认让这个结点的指针指向空。并将创建好的结点的地址返回。
struct Node* creatNode(int data)
{
struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
newNode->next = NULL;
newNode->data = data;
return newNode;
}
3.2先来一个静态链表试试
头文件stdlib.h包含了malloc与free函数,分别用于分配内存与释放内存。
我们已经知道如何创建结点了,那么我们创建3个结点,并将这3个结点连接起来,就是一个没有表头的单链表。
下面是一段完整的程序。
#include <stdlib.h>
struct Node
{
int data;
struct Node *next;
};
struct Node* creatNode(int data)
{
struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
newNode->next = NULL;
newNode->data = data;
return newNode;
}
int main()
{
struct Node *node1 = creatNode(100);
struct Node *node2 = creatNode(120);
struct Node *node3 = creatNode(140);
node1->next = node2;
node2->next = node3;
return 0;
}
建立一个三结点的链表用了5行代码,那要建立一个100结点的链表难道需要写199行代码吗?这显然不是我们想要的!静态链表没有什么太大用处,我们需要的是动态链表。
3.3动态链表的创建
一个新生的链表空空如也,只有一个表头。所以我们只要建立一个表头就就OK了。
struct Node* creatList()
{
struct Node* list = (struct Node *)malloc(sizeof(struct Node));
//list->data = -1;
list->next = NULL;
return list;
}
已经介绍过,表头的数据域不需要储存数据,所以是否初始化都无所谓。
3.4链表的插入
插入有很多种方式:头插法、尾插法、指定位置插入等…
3.4.1头插法
头插头插,顾名思义。就是紧挨着表头插入一个元素。虚线表示原来的指针。
插入之后,表头的指针指向了新结点,新结点的指针指向了原本的第一个结点。
那么要如何具体操作呢。
图中①②标注的两个指针要 先②后① 而不是先①后②。
每一个结点在内存中的具体位置我们是不知道的,要想访问其中一个结点,就必须从头开始顺着指针逐一访问。
如果先②后①就会造成原本的第一个结点丢失,我们就没有办法访问到它了。
代码如下
void insert(struct Node* headNode, int data)
{
struct Node *newNode = creatNode(data);
newNode->next = headNode->next; // 先②
headNode->next = newNode; // 后①
}
3.5链表的遍历
遍历就很简单了,创建一个指针pMove从头走到尾。
void printList(struct Node* headNode)
{
struct Node* pMove = headNode->next;
while(pMove)
{
printf("%d\t", pMove->data);
pMove = pMove->next;
}
printf("\n");
}
3.6链表的删除
给定一个数据,我们要删除链表中第一次出现这个数据的结点。
删除可以通过修改一个指针来实现。
首先判断链表是否为空链表,如果是空链表,则跳出函数。
删除操作需要两个指针,posNode与posNodeFront。posNode从第一个节点开始,posNodeFront从表头开始,两者同步向后移动。直到posNode移动到待删除的节点上,posNodeFront移动到待删除结点的前驱。
接下来让posNodeFront的指针域指向posNode->next,再将posNode释放掉。
void deleteByAppointData(struct Node* headNode, int data)
{
struct Node *posNode = headNode->next;
struct Node *posNodeFront = headNode;
//判断链表是否为空链表
if(posNode == NULL)
{
printf("No data!\n");
return;
}
//查找所要删除的结点
while(posNode->data != data)
{
posNodeFront = posNode;
posNode = posNode->next;
if(posNode == NULL){
printf("no right data!\n");
return;
}
}
//删除结点
posNodeFront->next = posNode->next;
free(posNode);
}
4.常见的问题
4.1何时使用malloc,何时不使用malloc
必须记住,声明指向一个结构的指针并不创建该结构,而只是给出足够的空间容纳结构可能会使用的地址, 创建尚未被声明过的记录的唯一方法是使用malloc库函数。malloc (HowManyBytes)奇迹般地使系统创建一个新的结构并返回指向该结构的指针。另一方面,如果你想使用一一个指针变量沿着一个表行进,那就没有必要创建新的结构,此时不宜使用malloc命令。非常老的编译器需要-一个类型转换(type cast)使得赋值操作符两边相符。C库提供了malloc的其他形式,如calloc. 这两个例程都要求包含stdlib.h头文件。
当有些空间不再需要时,你可以用free命令通知系统来回收它。free( P)的结果是:P正在指向的地址没变,但在该地址处的数据此时已无定义了。
如果你从未对一个链表进行过删除操作,那么调用malloc的次数应该等于表的大小,若有表头则再加1。少一点儿你就不可能得到一个正常运行的程序;多点儿你就会浪费空间并可能要浪费时间。偶尔会出现下列情况:当你的程序使用大量空间时,系统可能不能满足你对新单元的要求。此时返回的是NULL指针。
5.双链表
有时候以倒序扫描链表很方便。标准实现方法此时无能为力,然而解决方法却很简单。只要在数据结构上附加一个域,使它包含指向前一个单元的指针即可。其开销是一个附加的链,它增加了空间的需求,同时也使得插人和删除的开销增加一倍,因为有更多的指针需要定位。另外,它简化了删除操作,因为你不再被迫使用一个指向前驱元的指针来访问一个关键字,这个信息是现成的。
下图表示一个双链表(doubly linked list)。
6.循环链表
让最后的单元反过来直指第一个单元是一种流行的做法。它可以有表头,也可以没有表头(若有表头,则最后的单元就指向它),并且还可以是双向链表(第一个单元的前驱元指针指向最后的单元)。这无疑会影响某些测试,不过这种结构在某些应用程序中却很流行。
下图表示一个双向循环链表。
7.有关期末考试
根据2020届大一上程序设计基础期末考试行文,仅供参考。
7.1考试形式
去机房上机测试,一共有十道选择,四道大题。选择题为英文叙述,考察学科理解、头文件、指针的指向、控制语句等基础知识。大题为补全程序(让你写一个函数),考察控制语句、函数、排序查找算法、链表。
排序算法至少要会一个。链表要好好学,明白原理,一定要明白原理。
7.2考试范围
冒泡排序、选择排序、二分查找。链表的遍历和插入(删除不考)。
8.参考资料
- 数据结构与算法分析——C语言描述(原书第二版)(典藏版)
- C和指针
- C Primer Plus (第6版)中文版
- C Primer Plus (6th Edition)