顺序表和链表的对比(一)
前言
今天给小伙伴们分享的是在数据结构中顺序表和链表的对比。它们在计算机科学和软件开发中具有广泛的应用,是理解更复杂数据结构(如栈、队列、树、图等)的基础。这次将会给大家从定义初始化,以及功能增删查改上进行详细对比,以便于大家掌握并使用。那让咱们开始吧@
一.基本知识概述
考虑到可能有些小伙伴还没了解到顺序表或链表,就先概述一下关于它们的基本知识。
1.顺序表
顺序表(Sequential List)是用一段物理地址连续的存储单元依次存储数据元素的线性结构。一般是使用数组方式实现 ,它的元素在内存中是连续存储的。顺序表是数据结构中最基础的一种线性表,具有高效的随机访问特性,但在插入和删除操作上效率较低。
2.链表
链表(Linked List) 是一种在物理存储结构上非连续、非顺序的存储结构,又因为在节点结构和操作上不同分有(双链表还细分是否循环与是否带哨兵位头节点):双链表(Doubly Linked List)和单链表(Singly Linked List)。链表与顺序表不同,它是靠指针链接实现的,顺序表的元素在内存中是连续存储的,而链表的元素可以分散在内存的任何位置。因此它在插入和删除上效率比顺序表更高些。
二.定义及初始化
1.顺序表
顺序表通常由以下部分组成:
-
数据存储区:
一个数组(需要考虑数据大小,是否需要自主开辟空间和扩容),用于存储元素。 -
长度信息:
一个变量,用于记录当前顺序表中元素的数量
静态顺序表
(静态的意思是存储空间有限)首先定义一个结构体,然后在结构体里再定义一个数组以及一个整型变量来显示顺序表长度,不知道数据具体大小的时候,使用起来比较麻烦,容易产生数组越界以及空间浪费问题,这里推荐使用动态的顺序表。
#define MAX_SIZE 100 // 定义一个能容纳所有数据的容量,这个属于静态顺序表了,不易把控使用空间
//一般推荐大家使用动态顺序表
typedef struct SequentialList
{
int data[MAX_SIZE]; // 数据存储区
int length; // 当前顺序表的长度
} SL;
静态顺序表的初始化比较简单,只需初始化长度即可
void InitList(SL* ps)
{
L->length = 0; // 初始化长度为 0
}
动态顺序表
动态顺序表则在空间使用上更加灵活,使用数组指针,通过 malloc 函数开辟所需空间,如果不够再使用realloc 函数进行扩容,但是需考虑内存泄漏问题,以及后续使用完空间的释放问题。
typedef int SLDataType; //定义数组类型,方便存储各种类型不同的数据
#define INIT_CAPACITY 4 //定义一个空间容量,方便后续扩容
// 动态顺序表 -- 按需申请
typedef struct SequentialList
{
SLDataType* a;
int size; // 有效数据个数
int capacity; // 空间容量
}SL;
对比于静态顺序表,动态顺序表初始化则较为复杂一点
(扩容函数的使用放在数据插入,不够再扩,节省空间)
void SLInit(SL* ps)
{
assert(ps);
ps->a = (SLDataType*)malloc(sizeof(SLDataType)* INIT_CAPACITY); 通过malloc 函数开辟空间
if (ps->a == NULL)//若开辟失败,返回错误信息
{
perror("malloc fail");
return;
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
2.链表
链表的话则是通过结构体指针来实现,单链表和双链表的定义差不多,主要区别在节点指针的数量及指向方向。双链表较于单链表,支持双向遍历,增删数据更加灵活,但在空间效率上略低于单链表,因为它有两个节点指针,并且在维护时需要调整两个指针,容易出错。
1. 单链表(Singly Linked List)
-
节点结构:
-
每个节点包含两个部分:
-
数据域:存储数据。
-
指针域:指向下一个节点的指针。
-
-
-
链表结构:
-
链表由一系列节点组成,每个节点通过指针连接。
-
链表的最后一个节点的指针域为
NULL
,表示链表的结束。
-
typedef int SLTDataType; 定义数据类型,方便后续适用于存储各种类型数据
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next; 指向下一个节点
}SLTNode;
单链表的初始化通过节点创造函数创建了一个节点并赋初值,然后把节点指针传递回插入函数(下期会详细介绍)
SLTNode* CreatSLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); //创建节点
if (newnode == NULL)
{
perror("malloc fail"); //若开辟失败,则返回错误信息
return NULL;
}
newnode->data = x;
newnode->next = NULL; //避免出现野指针
return newnode; //返回节点指针到插入函数
}
2. 双链表(Doubly Linked List)
-
节点结构:
-
每个节点包含三个部分:
-
数据域:存储数据。
-
前驱指针:指向前一个节点的指针。
-
后继指针:指向下一个节点的指针。
-
-
-
链表结构:
-
链表由一系列节点组成,每个节点通过前驱指针和后继指针连接。
-
链表的第一个节点的前驱指针为
NULL
,最后一个节点的后继指针为NULL
。
-
双链表的定义则比单链表多一个指针,于是便有了访问上一个节点的功能
typedef int LTDataType; 定义数据类型,方便后续适用于存储各种类型数据
typedef struct ListNode
{
struct ListNode* next; 指向下一个节点指针
struct ListNode* prev; 指向上一个节点指针
LTDataType data;
}SL;
双链表的初始化跟单链表差不多,多了一个回访指针的初始化。
SL* CreatSListNode(LTDataType x)
{
SL* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
//return NULL;
exit(-1);//调用 exit 函数以非零状态终止程序。
}
node->next = NULL;
node->prev = NULL;//回访指针,指向上一个节点
node->data = x;
return node;
}
在这里呢,再提一下带哨兵位(sentinel node)头节点的双链表。先解释一下什么是哨兵位节点(sentinel node)吧。
哨兵节点(sentinel node)是一种特殊的结点,它可以简化链表中的操作,减少边界条件的判断和特殊处理,假如用它作头节点,在进行链表增删操作时就不用单独考虑链表是否为空的情况了。
具体实现也很简单,大家可以理解成跟不带哨兵位节点时一样,只不过之前是在头节点中开始放数据,现在是在第二个节点中开始放数据,相当于头节点里面数据是空的,头节点是空指针。
// 创建新节点的函数
SL*CreatSListNode(LTDataType x)
{
SL* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
//return NULL;
exit(-1);//调用 exit 函数以非零状态终止程序。
}
node->next = NULL;
node->prev = NULL;//回访指针,指向上一个节点
node->data = x;
return node;
}
// 初始化双链表的函数
void InitList(SL* L)
{
// 创建哨兵节点
L->sentinel = (Node*)malloc(sizeof(Node));
L->sentinel->prev = L->sentinel; // 哨兵节点的 prev 指向自己
L->sentinel->next = L->sentinel; // 哨兵节点的 next 指向自己
}
void InsertAtHead(SL* L, int x )
{
SL* newNode = CreatSListNode(x);
// 将新节点插入到哨兵节点之后
newNode->next = L->sentinel->next;
newNode->prev = L->sentinel;
L->sentinel->next->prev = newNode;
L->sentinel->next = newNode;
}
以上就是这期的内容,希望能给有需求的小伙伴们提供一些帮助,如果有疏漏的地方,小伙伴们可以直接指出,以便纠正,祝大家天天开心@