链表与顺序表一样,也是线性表的一种,今天就接着上次线性表中的知识点来整理链表。
3.1初识链表
基本概念
链表由N个结点链组成,第一个结点储存的位置叫头指针,最后一个节点的指针为“空”,节点包括数据域和指针域。和昨天我们讲的顺序表有些差异,这是我们昨天的定义:
我们随便举个例子:
typedef struct Monster{
int id;
char *name;
//指向下一个节点
struct Monster *next;
}Monster;
这样就可以实现链表结构了,我们来尝试写一个看一下:
typedef struct Monster{
int id;
char *name;
struct Monster *next;
}Monster;
void test(){
Moster monster1 = {1, "史莱克"};
Moster monster2 = {2, "哥斯拉"};
Moster monster3 = {3, "黑熊精"};
Moster monster4 = {4, "金角大王"};
Moster monster5 = {5, "银角大王"};
//monster1就是头指针
monster1.next = &monster2;
monster2.next = &monster3;
monster3.next = &monster4;
monster4.next = &monster5;
monster5.next = NULL;
}
链表相较于顺序表的优点:
- 不用存储时规定长度
- 存储的元素不受限制
- 插入和删除元素时,不用移动其他元素
链表的头指针和头结点
头指针:链表中第一个结点的储存位置;
头结点:在单链表的第一个结点前附设的一个结点。
头指针 | 头结点 |
若链表有头结点,这指向头结点的指针; 若没有则是链表指向第一个结点的指针。 | 头节点是为了操作和统一方便设立的,放在第一个结点之前; 其数据域一般无意义(可存储链表长度)。 |
头指针具有表示作用,所以经常使用头指针表示链表名字。 | 有了头结点,在第一个结点前插入,删除第一个结点的时候,操作与其他结点就统一了。 |
无论链表是否为空,头指针必定不为空; 头指针是链表的必要元素。 | 头节点不一定是链表的必要元素。 |
3.2单链表
单链表基本概念
链表中每一个结点中只包含一个指针域:
typedef struct Monster{
int id;
char *name;
//指向下一个节点
struct Monster *next;
}Monster;
单链表的插入
要在第 i 个结点后插入一个结点时,需要以下步骤:
- 创建一个空结点,分配内存空间,设置内存数据;
- 获取第 i 个结点,设置新结点的后继结点为该结点的后继结点;
- 设置第 i 个结点的后继结点为新结点。
这时候我们使用代码来实现一下插入算法,别忘了初始化链表,我们在昨天的工程中先新建一个LinkList.h,然后编写如下代码:
#ifndef LINKLIST_H_INCLUDED
#define LINKLIST_H_INCLUDED
#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
/**定义链表的结点,包含数据域和指针域*/
typedef struct Node{
ElementType data; //数据域
struct Node *next; //指针域,指向下个结点
}Node;
/**定义头结点,以便于统一链表结点的插入与删除操作*/
typedef struct LinkList{
Node *next; //头指针(如果链表有头节点,就指向头结点;没有就指向第一个结点)
int length; // 链表的长度初始为零
}LinkList;
/**初始化链表*/
void InitLinkList(LinkList *linkList, ElementType *dataArray, int length);
/**在指定位置pos处插入元素element,这里注意是指定位置,不是下标*/
void InsertLinkList(LinkList *linkList, int pos, ElementType element);
/**再写一个Print*/
void PrintLinkList(LinkList *linkList);
#endif // LINKLIST_H_INCLUDED
这里要注意,在链表定义的时候我们同时定义好了头结点LinkList,这样就方便处理链表结点的插入与删除了。现在我们来实现一下,首先新建一个.c文件LinkList.c:
#include <stdio.h>
#include <stdlib.h>
#include "LinkList.h"
/**初始化链表*/
void InitLinkList(LinkList *linkList, ElementType *dataArray, int length){
}
/**在指定位置pos处插入元素element,这里注意是指定位置,不是下标*/
void InsertLinkList(LinkList *linkList, int pos, ElementType element){
}
/**再写一个Print*/
void PrintLinkList(LinkList *linkList){
}
然后编写代码:
#include "LinkList.h"
/**初始化链表*/
void InitLinkList(LinkList *linkList, ElementType *dataArray, int length){
for(int i = 0; i < length; i++){
InsertLinkList(linkList, i + 1, dataArray[i]);
}
}
/**在指定位置pos处插入元素element,这里注意是指定位置,不是下标*/
void InsertLinkList(LinkList *linkList, int pos, ElementType element){
//1、创建空结点并为数据域赋值
Node *node = (Node*)malloc(sizeof(Node));
node->data = element;
node->next = NULL;
//2、找到要插入的节点
if(pos == 1){ //插入的是第一个元素
linkList->next = node;
node->next = linkList->next;
linkList->length ++;
return;
}
//如果不是,需要通过循环找到要插入的位置
Node *currNode = linkList->next;
for(int i = 1; currNode && i < pos - 1; i++){
currNode = currNode->next;
}
//3、将结点插入并对接前面的结点
if(currNode){
node->next = currNode->next;
currNode->next = node;
linkList->length ++;
}
}
/**再写一个Print*/
void PrintLinkList(LinkList *linkList){
Node *node = linkList->next;
if(!node){
printf("链表为空!\n");
linkList->length = 0;
return;
}
for(int i = 0; i < linkList->length; i++){
printf("%d\t%s\n", node->data.id, node->data.name);
node = node->next;
}
}
这里要注意我们在循环找到插入位置的时候需要用一个新的指针currNode,同时要保证currNode不为空,这时我们的插入算法就写好了,我们在main.c文件中实现一下:
#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
#include "LinkList.h"
ElementType dataArray[] = {
{1, "钢铁侠"},
{2, "美国队长"},
{3, "紫薇一号"},
{4, "紫薇二号"},
{5, "紫薇三号"}
};
void TestLinkList();
int main(){
TestLinkList();
return 0;
}
void TestLinkList(){
LinkList linkList;
linkList.length = 0;
InitLinkList(&linkList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
printf("插入前:\n");
PrintLinkList(&linkList);
ElementType element;
element.id = 11;
element.name = (char*)malloc(10);
strcpy(element.name, "新增者");
InsertLinkList(&linkList, 2, element);
printf("插入后:\n");
PrintLinkList(&linkList);
}
我们一定要h记得在初始化之后将长度设为零,编译通过,运行结果如下:
插入前:
1 钢铁侠
2 美国队长
3 紫薇一号
4 紫薇二号
5 紫薇三号
插入后:
1 钢铁侠
11 新增者
2 美国队长
3 紫薇一号
4 紫薇二号
5 紫薇三号
Process returned 0 (0x0) execution time : 0.029 s
Press any key to continue.
这就是插入的全部过程。
单链表的查找
要获取第 i 个结点的数据,我们需要以下步骤:
- 声明一个结点指针P,让其指向链表的第一个结点a1,初始化一个变量 j ;
- 当 j < i 时,遍历链表,让P的指针向后移动,不断指向下一个结点,同时 j 累加 1 ;
- 当链表末尾P为空时,这说明第 i 个元素不存在;否则查找成功,返回结点P的数据。
同时我们还可以将昨天整理的几个辅助方法都写进去,是否为空,得到长度都写进去,首先在LinkList.h中添加操作:
/**判断是否为空*/
int IsLinkEmpty(LinkList *linkList);
/**返回链表大小*/
int GetLinkLength(LinkList *linkList);
/**得到pos位置的元素*/
ElementType GetLinkListElement(LinkList *linkList, int pos);
然后在LinkList.c中添加操作:
/**判断是否为空*/
int IsLinkEmpty(LinkList *linkList){
return (GetLinkLength(&linkList) == 0 ? 1 : 0);
}
/**返回链表大小*/
int GetLinkLength(LinkList *linkList){
if(linkList->length == NULL){
return 0;
}
return linkList->length;
}
/**得到pos位置的元素*/
ElementType GetLinkListElement(LinkList *linkList, int pos){
Node *node = linkList->next;
for(int i = 1; node && i < pos; i++){
node = node->next;
}
return node->data;
}
这时候在main.c中实现一下:
#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
#include "LinkList.h"
ElementType dataArray[] = {
{1, "钢铁侠"},
{2, "美国队长"},
{3, "紫薇一号"},
{4, "紫薇二号"},
{5, "紫薇三号"}
};
void TestLinkList();
int main(){
TestLinkList();
return 0;
}
void TestLinkList(){
LinkList linkList;
linkList.length = 0;
InitLinkList(&linkList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
printf("插入前:\n");
PrintLinkList(&linkList);
ElementType element;
element.id = 11;
element.name = (char*)malloc(10);
strcpy(element.name, "新增者");
InsertLinkList(&linkList, 2, element);
printf("插入后:\n");
PrintLinkList(&linkList);
printf("链表是否为空:%d,长度为:%d\n",IsLinkEmpty(&linkList), GetLinkLength(&linkList));
printf("寻找第三个元素:%d\t%s\n", GetLinkListElement(&linkList, 3).id, GetLinkListElement(&linkList, 3).name);
}
编译通过,运行结果如下:
插入前:
1 钢铁侠
2 美国队长
3 紫薇一号
4 紫薇二号
5 紫薇三号
插入后:
1 钢铁侠
11 新增者
2 美国队长
3 紫薇一号
4 紫薇二号
5 紫薇三号
链表是否为空:0,长度为:6
寻找第三个元素:2 美国队长
Process returned 0 (0x0) execution time : 0.028 s
Press any key to continue.
单链表的删除
要删除第 i 个结点的数据,我们要进行以下步骤:
- 获取第 i 个结点,若该结点不是第一个结点,则获取第 i - 1 个结点;
- 将第 i - 1 个结点的后缀结点设为第一个结点的后缀结点;
- 删除第 i 个结点,并释放内存空间,记录并返回删除的数据元素的值。
首先我们在 LinkList.h 中添加操作:
/**删除pos位置上的结点*/
ElementType DeleteLinkListElement(LinkList *linkList, int pos);
然后在LinkList.c中添加操作:
/**删除pos位置上的结点*/
ElementType DeleteLinkListElement(LinkList *linkList, int pos){
ElementType element; //被删除的元素
element.id = -999; //用一个不可能的值来判断是否成功
Node *node = NULL;
if(pos == 1){ //如果是第一个结点
node = linkList->next;
if(node){
element = node->data;
linkList->next = node->next;
free(node);
linkList->length --;
}
return element;
}
//如果不是第一个结点
//1、找到要删除结点和他的前缀节点
//2、要删除的结点的next赋值给前缀结点的next
//3、释放找到的结点
Node *preNode; //前缀结点
node = linkList->next;
for(int i = 1; node && i < pos; i++){
preNode = node;
node = node->next;
}
if(node){
element = node->data;
preNode->next = node->next;
free(node);
linkList->length --;
}
}
这里要注意,如果删除的是第一个结点,那么直接将第一个结点的next给LinkList的next,然后free就可以了;如果不是第一个结点,那么就要使用前缀结点来查找结点,查找到后将删除结点的next给前缀结点的next,然后free。我们在main.c中实现一下:
void TestLinkList(){
LinkList linkList;
linkList.length = 0;
InitLinkList(&linkList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
printf("插入前:\n");
PrintLinkList(&linkList);
ElementType element;
element.id = 11;
element.name = (char*)malloc(10);
strcpy(element.name, "新增者");
InsertLinkList(&linkList, 2, element);
printf("插入后:\n");
PrintLinkList(&linkList);
printf("链表是否为空:%d,长度为:%d\n",IsLinkEmpty(&linkList), GetLinkLength(&linkList));
printf("寻找第三个元素:%d\t%s\n", GetLinkListElement(&linkList, 3).id, GetLinkListElement(&linkList, 3).name);
printf("删除第三个元素:\n");
DeleteLinkListElement(&linkList, 3);
PrintLinkList(&linkList);
}
编译通过,运行结果如下:
插入前:
1 钢铁侠
2 美国队长
3 紫薇一号
4 紫薇二号
5 紫薇三号
插入后:
1 钢铁侠
11 新增者
2 美国队长
3 紫薇一号
4 紫薇二号
5 紫薇三号
链表是否为空:0,长度为:6
寻找第三个元素:2 美国队长
删除第三个元素:
1 钢铁侠
11 新增者
3 紫薇一号
4 紫薇二号
5 紫薇三号
Process returned 0 (0x0) execution time : 0.035 s
Press any key to continue.
单链表的清空
如何清空单链表呢?大家可能会觉得先顺序表一样直接将长度变为0就行了,但是链表是很多个不同地址的数据域与指针域连接起来的,所以还要释放内存空间,而且要一个一个释放,所以下面这种写法是是错误的:
/**清除链表*/
void ClearLinkList(LinkList *linkList){
if(linkList == NULL){
return;
}
linkList->length = 0;
free(linkList);
}
这样子乍一看很符合大家的想法,其实只不过是将长度的数值变为零了,然后释放了头结点的内存空间罢了,后面的内存空间都没有释放,所以正确清除链表的步骤如下:
- 声明结点P、Q;
- 将第一个结点赋值给P;
- 循环将下一个结点赋值给Q,然后释放P,再将Q赋值给P。
我们首先在LinkList.h中进行操作:
/**清除链表*/
void ClearLinkList(LinkList *linkList);
然后在LinkList.c中进行操作:
/**清除链表*/
void ClearLinkList(LinkList *linkList){
Node *node = linkList->next;
Node *nextNode;
while(node){
nextNode = node->next; //先记录当前节点的下个结点,以便释放当前结点
free(node);
node = nextNode;
}
linkList->next = NULL;
linkList->length = 0;
}
接下来我们实现一下:
void TestLinkList(){
LinkList linkList;
linkList.length = 0;
InitLinkList(&linkList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
printf("插入前:\n");
PrintLinkList(&linkList);
ElementType element;
element.id = 11;
element.name = (char*)malloc(10);
strcpy(element.name, "新增者");
InsertLinkList(&linkList, 2, element);
printf("插入后:\n");
PrintLinkList(&linkList);
printf("链表是否为空:%d,长度为:%d\n",IsLinkEmpty(&linkList), GetLinkLength(&linkList));
printf("寻找第三个元素:%d\t%s\n", GetLinkListElement(&linkList, 3).id, GetLinkListElement(&linkList, 3).name);
printf("删除第三个元素:\n");
DeleteLinkListElement(&linkList, 3);
PrintLinkList(&linkList);
printf("清除链表!打印如下:\n");
ClearLinkList(&linkList);
PrintLinkList(&linkList);
}
编译通过,运行结果如下:
插入前:
1 钢铁侠
2 美国队长
3 紫薇一号
4 紫薇二号
5 紫薇三号
插入后:
1 钢铁侠
11 新增者
2 美国队长
3 紫薇一号
4 紫薇二号
5 紫薇三号
链表是否为空:0,长度为:6
寻找第三个元素:2 美国队长
删除第三个元素:
1 钢铁侠
11 新增者
3 紫薇一号
4 紫薇二号
5 紫薇三号
清除链表!打印如下:
链表为空!
Process returned 0 (0x0) execution time : 0.065 s
Press any key to continue.
单链表与顺序表的对比
3.3循环链表
循环链表的结构与单链表完全相同,唯一有一点不同的是尾结点不是指向NULL,而是指向第一个结点,这里要注意,带有头结点的循环链表,尾结点的指针域依然指向第一个结点,而不是头结点。
我们先新建一个头文件CircularList.h,在里面定义循环链表与操作:
#ifndef CIRCULARLIST_H_INCLUDED
#define CIRCULARLIST_H_INCLUDED
#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
/**定义链表的结点,包含数据域和指针域*/
typedef struct CircularNode{
ElementType data; //数据域
struct Node *next; //指针域,指向下个结点
}CircularNode;
/**定义头结点,以便于统一链表结点的插入与删除操作*/
typedef struct CircularLinkList{
CircularNode *next; //指向第一个结点
int length; // 链表的长度初始为零
}CircularLinkList;
/**初始化链表*/
void InitCricularLinkList(CircularLinkList *clList, ElementType *dataArray, int length);
/**在指定位置pos处插入元素element,这里注意是指定位置,不是下标*/
void InsertCircularLinkList(CircularLinkList *clList, int pos, ElementType element);
/**再写一个Print*/
void PrintCircularLinkList(CircularLinkList *clList);
/**删除pos位置上的结点*/
ElementType DeleteCircularLinkListElement(CircularLinkList *clList, int pos);
/**根据元素内容返回对应的结点指针*/
CircularNode *GetCircularLinkListNode(CircularLinkList *clList, ElementType element);
/**根据结点 来遍历*/
void PrintCircularLinkListByNode(CircularLinkList *clList, CircularNode *node);
#endif // CIRCULARLIST_H_INCLUDED
这里我们就主要说一下几个比较重要的操作,比如插入、删除、遍历这些操作,其他的操作大同小异,大家可以自己实现一下。
循环链表的插入
这里我们要知道,循环链表的最大特点就是尾指针指向第一个结点,所以当插入的是第一个位置的时候有两种情况:
- 如果插入时链表长度为零:那么就要将插入元素的指针域指向自身;
- 如果插入时链表长度不为零:那么就要将List的next变为插入的元素,然后将最后一位元素的指针域指向插入元素。
其他的时候插入我们与单链表插入无异。
现在我们在CricularList.c的文件中编写操作:
#include "CircularList.h"
/**初始化链表*/
void InitCricularLinkList(CircularLinkList *clList, ElementType *dataArray, int length){
for(int i = 0; i < length; i++){
InsertLinkList(clList, i + 1, dataArray[i]);
}
}
/**在指定位置pos处插入元素element,这里注意是指定位置,不是下标*/
void InsertCircularLinkList(CircularLinkList *clList, int pos, ElementType element){
//创建一个新结点
CircularNode *node = (CircularNode*)malloc(sizeof(CircularNode));
node->data = element;
node->next = NULL;
if(pos == 1){
node->next = clList->next;
if(!node->next){
node->next = node;
}else{
//长度不为零
CircularNode *lastNode = clList->next;
for(int i = 1; i < clList->length; i++){
lastNode = lastNode->next;
}
lastNode->next = node;
}
clList->next = node;
clList->length ++;
return;
}
//插入的不是第一个结点,需要通过循环找到要插入的位置
CircularNode *currNode = clList->next;
for(int i = 1; currNode && i < pos - 1; i++){
currNode = currNode->next;
}
//将结点插入并对接前面的结点
if(currNode){
node->next = currNode->next;
currNode->next = node;
clList->length ++;
if(pos == clList->length){
node->next = clList->next;
}
}
}
/**再写一个Print*/
void PrintCircularLinkList(CircularLinkList *clList){
if(clList->length == 0 || !clList->next){
printf("链表为空!\n");
clList->length = 0;
return;
}
CircularNode *node = clList->next;
for(int i = 0; i < clList->length; i++){
printf("%d\t%s\n", node->data.id, node->data.name);
node = node->next;
}
}
然后我们在main.c中实现一下:
#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
#include "LinkList.h"
#include "CircularList.h"
ElementType dataArray[] = {
{1, "钢铁侠"},
{2, "美国队长"},
{3, "紫薇一号"},
{4, "紫薇二号"},
{5, "紫薇三号"}
};
void TestCircularList();
int main(){
TestCircularList();
return 0;
}
void TestCircularList(){
CircularLinkList *clList = (CircularLinkList*)malloc(sizeof(CircularLinkList));
clList->length = 0;
clList->next = NULL;
InitCricularLinkList(clList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
PrintCircularLinkList(clList);
}
简单的执行一下,编译通过,运行结果如下:
1 钢铁侠
2 美国队长
3 紫薇一号
4 紫薇二号
5 紫薇三号
Process returned 0 (0x0) execution time : 0.043 s
Press any key to continue.
循环链表的删除
在删除的时候我们也要考虑在删除第一个结点的时候,需要将最后一个结点的next先给到第一个元素的next,然后删除,同样的,删除其他的结点与单链表相同。在CricularList.c的文件中编写操作:
/**删除pos位置上的结点*/
ElementType DeleteCircularLinkListElement(CircularLinkList *clList, int pos){
ElementType element; //被删除的元素
element.id = -999; //用一个不可能的值来判断是否成功
if(pos == 1){ //如果是第一个结点
CircularNode *node = clList->next;
if(node){
element = node->data;
//找到最后一个元素,该百年其指针域的指向
CircularNode *lastNode = clList->next;
for(int i = 1; i < clList->length; i++){
lastNode = lastNode->next;
}
clList->next = node->next;
lastNode->next = clList->next;
free(node);
clList->length --;
}
return;
}
CircularNode *preNode; //前缀结点
CircularNode *node = clList->next;
for(int i = 1; node && i < pos; i++){
preNode = node;
node = node->next;
}
if(node){
element = node->data;
preNode->next = node->next;
free(node);
clList->length --;
}
return element;
}
然后在main.c中实现一下:
void TestCircularList(){
CircularLinkList *clList = (CircularLinkList*)malloc(sizeof(CircularLinkList));
clList->length = 0;
clList->next = NULL;
InitCricularLinkList(clList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
PrintCircularLinkList(clList);
printf("删除第三个元素:\n");
DeleteCircularLinkListElement(clList, 2);
PrintCircularLinkList(clList);
}
编译通过,运行结果如下:
1 钢铁侠
2 美国队长
3 紫薇一号
4 紫薇二号
5 紫薇三号
删除第三个元素:
1 钢铁侠
3 紫薇一号
4 紫薇二号
5 紫薇三号
Process returned 0 (0x0) execution time : 0.028 s
Press any key to continue.
循环链表的遍历
因为是循环链表,所以我们可以通过任意一个节点来遍历链表,首先我们来写一个根据元素返回对应的结点指针,这样可以方便遍历,先在CricularList.c的文件中编写操作:
/**根据元素内容返回对应的结点指针*/
CircularNode *GetCircularLinkListNode(CircularLinkList *clList, ElementType element){
CircularNode *node = clList->next;
if(!node){
return NULL;
}
//使用循环来判断现在的结点与初始节点是否相等
do{//因为第一次判断是相等的
if(element.id == node->data.id && strcmp(element.name, node->data.name) == 0){
return node;
}
node = node->next;
}while(node != clList->next);
return NULL;
}
然后我们再根据这个节点返回的指针来遍历:
/**根据结点 来遍历*/
void PrintCircularLinkListByNode(CircularLinkList *clList, CircularNode *node){
if(!node){
printf("链表为空!\n");
return;
}
if(!clList->next){
printf("链表为空!\n");
return;
}
//记录初始指针
CircularNode *origNode = node;
do{
printf("%d\t%s\n", node->data.id, node->data.name);
node = node->next;
}while(node != origNode);
}
现在我们在main.c中实现一下:
void TestCircularList(){
CircularLinkList *clList = (CircularLinkList*)malloc(sizeof(CircularLinkList));
clList->length = 0;
clList->next = NULL;
InitCricularLinkList(clList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
PrintCircularLinkList(clList);
printf("删除第三个元素:\n");
DeleteCircularLinkListElement(clList, 2);
PrintCircularLinkList(clList);
printf("从元素:紫薇一号 开始遍历:\n");
ElementType element;
element.id = 3;
element.name = "紫薇一号";
CircularNode *node = GetCircularLinkListNode(clList, element);
PrintCircularLinkListByNode(clList, node);
}
编译通过,运行结果如下:
1 钢铁侠
2 美国队长
3 紫薇一号
4 紫薇二号
5 紫薇三号
删除第三个元素:
1 钢铁侠
3 紫薇一号
4 紫薇二号
5 紫薇三号
从元素:紫薇一号 开始遍历:
3 紫薇一号
4 紫薇二号
5 紫薇三号
1 钢铁侠
Process returned 0 (0x0) execution time : 0.036 s
Press any key to continue.
3.4静态链表
用数组描述的链表,即称为静态链表,在C语言中,静态链表的表现形式就是结构体数组,结构体变量包括数据域与游标。
优点:
- 在插入与删除时只需要改变游标,不需要修改元素;
- 还有优秀的思考方式。
缺点:
- 没有解决空间动态分配长度的问题;
- 与顺序表相比,并没有带来本质的效率提升。
我们来举一个例子看一下:
typedef struct{
DataElement data;
int next;
}StaticLinkList[10];
现在我们就定义了一个静态链表,在它的表现形式中,有数据与与游标,就像数据域与指针域一样,不同的是:
- 数组的第一个元素的next游标用来存放第一个空闲结点的下标;
- 数组的最后一个元素next用来存放第一个插入元素的下标,初始时为0;
- 数组的第一个元素与最后一个元素不能使用;
- 初始化时,每个元素的next都指向下一个元素。
![](https://img-blog.csdnimg.cn/20200222200138861.png)
这时候如果我们向静态链表中插入一个元素data:
- 这个元素首先被保存在第二个数组下标的数据域中;
- 然后由于下标为2的数据域中有了数据,所以现在第一个空闲结点的下标为3;
- 而由于2中有了数据,其他数据域都没有数据,那么它的游标就不能指向别人,变为了0;
- 下标为9的结点由于要存放第一个插入元素的下标,next变为1;
- 那么8的next就应该变为0。
这样子看起来是不是非常的烧脑呢哈哈哈哈,我们来定义一下试试看:
静态链表的插入
首先建立Static.h与Static.c,在Static.h中编写:
#ifndef STATICLIST_H_INCLUDED
#define STATICLIST_H_INCLUDED
#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
#define MAX_SIZE_SSL 10
#define OK 1
#define ERROR 0
typedef struct{
ElementType data; //数据域
int next; //游标,如果为0则无指向
}StaticLinkList[MAX_SIZE_SSL];
//初始化静态链表
void InitStaticLinkList(StaticLinkList slList);
//在pos位置进行插入
int InsertStaticLinkList(StaticLinkList slList, int pos, ElementType element);
//为静态链表分配内存
int mallocSSL(StaticLinkList slList);
//获得静态链表的长度
int GetStaticLinkList(StaticLinkList slList);
//打印方法
void PrintStaticLinkList(StaticLinkList slList);
#endif // STATICLIST_H_INCLUDED
然后在Static.c中编写:
#include "StaticList.h"
void InitStaticLinkList(StaticLinkList slList){
for(int i = 0; i < MAX_SIZE_SSL; i++){
slList[i].next = i + 1;
}
//将最后一个节点置空
slList[MAX_SIZE_SSL - 1].next = 0;
slList[MAX_SIZE_SSL - 2].next = 0;
}
//打印方法
void PrintStaticLinkList(StaticLinkList slList){
for(int i = 0; i< MAX_SIZE_SSL; i++){
printf("i:%d\tnext:%d\n",i,slList[i].next);
}
}
现在我们尝试一下在main.c中打印一个静态链表:
#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
#include "LinkList.h"
#include "CircularList.h"
#include "StaticList.h"
ElementType dataArray[] = {
{1, "钢铁侠"},
{2, "美国队长"},
{3, "紫薇一号"},
{4, "紫薇二号"},
{5, "紫薇三号"}
};
void TestSequenceList();
void TestLinkList();
void TestCircularList();
void TestStaticList();
int main(){
//printf("Hello world!\n");
//TestSequenceList();
//TestLinkList();
//TestCircularList();
TestStaticList();
return 0;
}
void TestStaticList(){
StaticLinkList slList;
InitStaticLinkList(slList);
PrintStaticLinkList(slList);
}
编译通过,运行结果为:
i:0 next:1
i:1 next:2
i:2 next:3
i:3 next:4
i:4 next:5
i:5 next:6
i:6 next:7
i:7 next:8
i:8 next:0
i:9 next:0
Process returned 0 (0x0) execution time : 0.052 s
Press any key to continue.
现在我们来讨论插入的问题,首先我们模拟一个场景:
![](https://img-blog.csdnimg.cn/20200222203207267.png)
现在我们假设要往2号位置插入一个数据element,那么要进行的过程是这样的:
首先通过第一个元素游标获得空结点下标5;
本来插入后5号结点的游标就变成了0,同时4号结点的next就变成了5;
但现在因为要插入到2号结点,所以将5的next变为2号结点的next:3,同时将2号结点的next变为5;
现在就变成了:
![](https://img-blog.csdnimg.cn/2020022220400926.png)
其实这种插入方法与单链表的插入方法大同小异,只不过因为静态链表是数组类型,数据结点不能随意更改位置,所以需要画图来说明。
我们在StaticList.c中编写操作:
#include "StaticList.h"
void InitStaticLinkList(StaticLinkList slList){
for(int i = 0; i < MAX_SIZE_SSL; i++){
slList[i].data.id = -99;
slList[i].data.name = "NULL";
slList[i].next = i + 1;
}
//将最后一个节点置空
slList[MAX_SIZE_SSL - 1].next = 0;
slList[MAX_SIZE_SSL - 2].next = 0;
}
//在pos位置进行插入
int InsertStaticLinkList(StaticLinkList slList, int pos, ElementType element){
if(pos < 1 || pos > GetStaticLinkList(slList) + 1){
return ERROR;
}
int cursor = MAX_SIZE_SSL - 1;
//需要判断cursor范围是否合法
//分配内存
int newIndex = mallocSSL(slList);
if(newIndex){
slList[newIndex].data = element;
//找到newIndex的前缀结点
for(int i = 1; i <= pos - 1; i++){
cursor = slList[cursor].next;
}
}
slList[newIndex].next = slList[cursor].next;
slList[cursor].next = newIndex;
}
//为静态链表分配内存
int mallocSSL(StaticLinkList slList){
//拿到第一个空闲结点下标(备用链表下标)
int cursor = slList[0].next;
if(cursor){
slList[0].next = slList[cursor].next;
}
return cursor;
}
//获得静态链表的长度
int GetStaticLinkList(StaticLinkList slList){
int count = 0;
int cursor = slList[MAX_SIZE_SSL - 1].next;
while(cursor){
//p = p->next
cursor = slList[cursor].next;
count ++;
}
return count;
}
//打印方法
void PrintStaticLinkList(StaticLinkList slList){
for(int i = 0; i< MAX_SIZE_SSL; i++){
printf("i:%d\tnext:%d\tid:%d\tname:%s\n", i, slList[i].next, slList[i].data.id, slList[i].data.name);
}
}
我们通过malloc方法来给新增元素结点进行操作,然后插入,稍微修改了一下输出方式和初始化方式,这样看起来就比较直接了,我们在main.c文件中实现一下插入:
void TestStaticList(){
StaticLinkList slList;
InitStaticLinkList(slList);
PrintStaticLinkList(slList);
ElementType element;
element.id = 11;
element.name = "小丑";
InsertStaticLinkList(slList, 1, element);
printf("插入元素后:\n");
PrintStaticLinkList(slList);
}
编译通过,运行结果为:
i:0 next:1 id:-99 name:NULL
i:1 next:2 id:-99 name:NULL
i:2 next:3 id:-99 name:NULL
i:3 next:4 id:-99 name:NULL
i:4 next:5 id:-99 name:NULL
i:5 next:6 id:-99 name:NULL
i:6 next:7 id:-99 name:NULL
i:7 next:8 id:-99 name:NULL
i:8 next:0 id:-99 name:NULL
i:9 next:0 id:-99 name:NULL
插入元素后:
i:0 next:2 id:-99 name:NULL
i:1 next:0 id:11 name:小丑
i:2 next:3 id:-99 name:NULL
i:3 next:4 id:-99 name:NULL
i:4 next:5 id:-99 name:NULL
i:5 next:6 id:-99 name:NULL
i:6 next:7 id:-99 name:NULL
i:7 next:8 id:-99 name:NULL
i:8 next:0 id:-99 name:NULL
i:9 next:1 id:-99 name:NULL
Process returned 0 (0x0) execution time : 0.062 s
Press any key to continue.
这样就可以看到,我们插入之后数据被保存在了第二个元素的数据域内,同时最后一个元素的游标也改变了,符合我们静态链表的特征。
静态链表的删除
我们首先在Static.h中添加操作:
//删除pos位置的元素
int DeleteStaticLinkList(StaticLinkList slList, int pos);
//回收原始数组中的指定下标index位置的空间
void FreeStaticLinkList(StaticLinkList slList, int index);
然后在Static.c中添加操作:
//删除pos位置的元素
int DeleteStaticLinkList(StaticLinkList slList, int pos){
if(pos < 1 || pos > GetStaticLinkList(slList) + 1){
return ERROR;
}
int cursor = MAX_SIZE_SSL - 1;
//找到newIndex的前缀结点
for(int i = 1; i <= pos - 1; i++){
cursor = slList[cursor].next;
}
int delIndex = slList[cursor].next;
slList[cursor].next = slList[delIndex].next;
FreeStaticLinkList(slList, delIndex);
return OK;
}
//回收原始数组中的指定下标index位置的空间
void FreeStaticLinkList(StaticLinkList slList, int index){
//将下标为index的空闲结点回收到备用链表
slList[index].next = slList[0].next;
//0号元素的next结点指向备用链表的第一个结点,表示index的节点空闲
slList[0].next = index;
}
这样就能删除了,我们多插入几个元素试一下:
void TestStaticList(){
StaticLinkList slList;
InitStaticLinkList(slList);
PrintStaticLinkList(slList);
ElementType element1;
element1.id = 11;
element1.name = "小丑";
InsertStaticLinkList(slList, 1, element1);
ElementType element2;
element2.id = 12;
element2.name = "蝙蝠侠";
InsertStaticLinkList(slList, 2, element2);
ElementType element3;
element3.id = 13;
element3.name = "蜘蛛侠";
InsertStaticLinkList(slList, 3, element3);
printf("插入元素后:\n");
PrintStaticLinkList(slList);
printf("删除第二个后:\n");
DeleteStaticLinkList(slList, 2);
PrintStaticLinkList(slList);
}
编译通过,运行结果如下:
i:0 next:1 id:-99 name:NULL
i:1 next:2 id:-99 name:NULL
i:2 next:3 id:-99 name:NULL
i:3 next:4 id:-99 name:NULL
i:4 next:5 id:-99 name:NULL
i:5 next:6 id:-99 name:NULL
i:6 next:7 id:-99 name:NULL
i:7 next:8 id:-99 name:NULL
i:8 next:0 id:-99 name:NULL
i:9 next:0 id:-99 name:NULL
插入元素后:
i:0 next:4 id:-99 name:NULL
i:1 next:2 id:11 name:小丑
i:2 next:3 id:12 name:蝙蝠侠
i:3 next:0 id:13 name:蜘蛛侠
i:4 next:5 id:-99 name:NULL
i:5 next:6 id:-99 name:NULL
i:6 next:7 id:-99 name:NULL
i:7 next:8 id:-99 name:NULL
i:8 next:0 id:-99 name:NULL
i:9 next:1 id:-99 name:NULL
删除第二个后:
i:0 next:2 id:-99 name:NULL
i:1 next:3 id:11 name:小丑
i:2 next:4 id:12 name:蝙蝠侠
i:3 next:0 id:13 name:蜘蛛侠
i:4 next:5 id:-99 name:NULL
i:5 next:6 id:-99 name:NULL
i:6 next:7 id:-99 name:NULL
i:7 next:8 id:-99 name:NULL
i:8 next:0 id:-99 name:NULL
i:9 next:1 id:-99 name:NULL
Process returned 0 (0x0) execution time : 0.077 s
Press any key to continue.
这里可以看到,下标为零的游标变为2,所以2号就是空闲的结点,已经被删除了,这里我们看不到被删除,但只要再添加一个元素,就会从2号开始添加,所以就可以理解为2号元素被删除了。
3.5双向链表
双向链表就是在原有链表的基础上添加了一个前置指针,所以在指向的时候一个元素的前置指针必须指向前面的元素,后继指针必须指向它后面的元素,同时它前面的元素的后继指针必须指向它,它后面的元素的前置指针也必须指向它。同时因为他有前置指针,所以它也可以是循环链表。
双向链表插入
我们依旧来看一下如何实现插画与删除,老样子,我们先建立一个DoublyLinkList.h与DoublyLinkList.c的文件,然后在.h的文件中添加定义和操作:
#ifndef DOUBLYLINKLIST_H_INCLUDED
#define DOUBLYLINKLIST_H_INCLUDED
#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
typedef struct DoublyNode{
ElementType data; //数据域
struct DoublyNode *prev; //指向前缀结点
struct DoublyNode *next; //指向后继结点
}DoublyNode;
typedef struct DoublyLinkList{
int length;
DoublyNode *next;
};
//插入元素
void InsertDoublyLinkList(DoublyLinkList *dlList, int pos, ElementType element);
//打印内容
void PrintDoublyLinkList(DoublyLinkList *dlList);
#endif // DOUBLYLINKLIST_H_INCLUDED
这时候我们就要分析一下,在插入的时候,我们要将插入元素的前置指针指向被插入的元素之前的元素上,后继指针要指向被插入元素,同时被插入元素的前置指针要指向插入元素,被插入元素的前一个元素的后继指针要指向插入元素。说的有点绕口哈,我们来看一下操作,在.c文件中添加操作:
#include "DoublyLinkList.h"
//插入元素
void InsertDoublyLinkList(DoublyLinkList *dlList, int pos, ElementType element){
//创建空结点
DoublyNode *node = (DoublyNode*)malloc(sizeof(DoublyNode));
node->data = element;
node->prev = NULL;
node->next = NULL;
//在第一个位置插入节点
if(pos == 1){
if(dlList->length == 0){
dlList->next = node;
dlList->length ++;
return;
}
node->next = dlList->next;
dlList->next = node;
node->next->prev = node;
dlList->length ++;
return;
}
DoublyNode *currNode = dlList->next;
for(int i = 1; currNode && i < pos - 1; i++){
currNode = currNode->next;
}
if(currNode){
node->prev = currNode;
if(currNode->next){//如果前缀结点非空(空就表示没有后继结点了)
//将插入位置处的前置指针指向新的结点
currNode->next->prev = node;
}
node->next = currNode->next;
currNode->next = node;
dlList->length ++;
}
}
//打印内容
void PrintDoublyLinkList(DoublyLinkList *dlList){
DoublyNode *node = dlList->next;
if(!node || dlList->length == 0){
printf("链表为空!\n");
dlList->length = 0;
return;
}
for(int i = 0; i < dlList->length; i++){
printf("%d\t%s\n", node->data.id, node->data.name);
node = node->next;
}
}
在main.c中实现一下:
#include <stdio.h>
#include <stdlib.h>
#include "DataElement.h"
#include "LinkList.h"
#include "CircularList.h"
#include "StaticList.h"
#include "DoublyLinkList.h"
ElementType dataArray[] = {
{1, "钢铁侠"},
{2, "美国队长"},
{3, "紫薇一号"},
{4, "紫薇二号"},
{5, "紫薇三号"}
};
void TestSequenceList();
void TestLinkList();
void TestCircularList();
void TestStaticList();
void TestDoublyList();
int main(){
//printf("Hello world!\n");
//TestSequenceList();
//TestLinkList();
//TestCircularList();
//TestStaticList();
TestDoublyList();
return 0;
}
void TestDoublyList(){
DoublyLinkList *dlList = (DoublyLinkList*)malloc(sizeof(DoublyLinkList));
dlList->length = 0;
dlList->next = NULL;
InsertDoublyLinkList(dlList, 1, dataArray[0]);
InsertDoublyLinkList(dlList, 2, dataArray[1]);
PrintDoublyLinkList(dlList);
}
插入两个元素,编译通过,运行结果如下:
1 钢铁侠
2 美国队长
Process returned 0 (0x0) execution time : 0.019 s
Press any key to continue.
双向链表删除
删除与插入相同,都要考虑双指针的问题,首先在.h的文件中添加操作:
//删除pos位置的元素
ElementType DeleteDoublyLinkList(DoublyLinkList *dlList, int pos);
在.h中实现操作:
//删除pos位置的元素
ElementType DeleteDoublyLinkList(DoublyLinkList *dlList, int pos){
ElementType element;
element.id = -999;
if(pos == 1){
DoublyNode *node = dlList->next;
if(node){
element = node->data;
dlList->next = node->next;
if(node->next){
//如果有第二个结点,那么设置第二个节点的前缀为NULL
node->next->prev = NULL;
}
free(node);
dlList->length --;
}
return element;
}
DoublyNode *node = dlList->next;
for(int i = 1; node && i < pos; i++){
node = node->next;
}
if(node){
element = node->data;
if(node->next){
node->next->prev = node->prev;
}
node->prev->next = node->prev;
free(node);
dlList->length --;
}
return element;
}
在main.c文件中实现一下:
void TestDoublyList(){
DoublyLinkList *dlList = (DoublyLinkList*)malloc(sizeof(DoublyLinkList));
dlList->length = 0;
dlList->next = NULL;
InsertDoublyLinkList(dlList, 1, dataArray[0]);
InsertDoublyLinkList(dlList, 2, dataArray[1]);
PrintDoublyLinkList(dlList);
printf("删除第二个位置的元素:\n");
DeleteDoublyLinkList(dlList, 2);
PrintDoublyLinkList(dlList);
}
编译通过,运行结果如下:
1 钢铁侠
2 美国队长
删除第二个位置的元素:
1 钢铁侠
Process returned 0 (0x0) execution time : 0.029 s
Press any key to continue.
3.6链表的比较
这一部分整理了关于链表的知识点,确实有很多,今天整理的挺晚了,下次会整理栈的知识点,我们下次见👋