数据结构初阶(用C语言实现简单数据结构)-- 顺序表和链表

✨✨欢迎来到T_X_Parallel的博客!!
      🛰️博客主页:T_X_Parallel
      🛰️社区 : 海南大学社区
      🛰️专栏 : 数据结构初阶
      🛰️欢迎关注:👍点赞🙌收藏✍️留言

请添加图片描述


线性表

线性表是一种数据结构,它是由n个具有相同特性的数据元素组成的有限序列。线性表中的数据元素之间有一对一的关系,也就是说除了第一个和最后一个元素之外,每个元素都有一个唯一的前驱和后继。
线性表有以下几个特点

线性表是一种线性结构,也就是说它的数据元素之间只有前后的关系,没有其他复杂的关系。
线性表的长度是可变的,也就是说它可以增加或删除数据元素,但是它的总数是有限的。
线性表可以用不同的方式来存储,例如数组或链表,但是它们都具有相同的逻辑结构和操作。

常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

在这里插入图片描述


💕顺序表

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
它的特点是:

🎀顺序表在内存中占用一整块连续的空间,可以通过下标直接访问元素。 顺序表的长度可以动态调整,但是需要预先分配足够大的空间,否则可能会发生溢出。
🎀顺序表在插入或删除元素时,需要移动后续的元素,这会影响效率。

顺序表一般分为动态顺序表静态顺序表

1.静态顺序表

静态顺序表顾名思义是采用静态数组来存储
下面是静态顺序表的结构体的定义
在这里插入图片描述
静态链表的实现

//定义静态顺序表的最大长度 
#define MAXSIZE 100 
//定义静态顺序表的数据类型 
typedef int ElemType; 
//定义静态顺序表的结构体 
typedef struct 
{ 
	ElemType data[MAXSIZE]; //存储数据元素的数组 
	int length; //存储当前顺序表的长度 
} SqList; 

//初始化一个空的静态顺序表 
void InitList(SqList *L) 
{ 
	L->length = 0; //将长度设为0 
} 

//判断静态顺序表是否为空 
bool ListEmpty(SqList L) 
{ 
	return L.length == 0; 
	//如果长度为0,返回真,否则返回假 
}

//判断静态顺序表是否已满 
bool ListFull(SqList L) 
{ 
	return L.length == MAXSIZE; 
	//如果长度等于最大长度,返回真,否则返回假 
} 

//获取静态顺序表中指定位置的元素 
bool GetElem(SqList L, int i, ElemType *e) 
{ 
	if (i < 1 || i > L.length) 
		return false; //如果位置不合法,返回假 

	*e = L.data[i - 1]; //将元素赋值给e指向的变量 
	return true; //返回真 
} 

//在静态顺序表中查找指定元素的位置 
int LocateElem(SqList L, ElemType e) 
{ 
	for (int i = 0; i < L.length; i++) 
	{ //遍历顺序表中的元素 
		if (L.data[i] == e) 
			return i + 1; //如果找到相等的元素,返回其位置 
	} 
	return 0; //如果没有找到,返回0 
} 

//在静态顺序表中指定位置插入一个元素 
bool ListInsert(SqList *L, int i, ElemType e) 
{ 
	if (i < 1 || i > L->length + 1) 
		return false; //如果位置不合法,返回假 

	if (ListFull(*L)) 
		return false; //如果顺序表已满,返回假 

	for (int j = L->length - 1; j >= i - 1; j--) 
	{ //从后往前移动元素,为插入位置腾出空间 
		L->data[j + 1] = L->data[j]; 
	} 

	L->data[i - 1] = e; //将元素插入到指定位置 
	L->length++; //将顺序表长度加一 
	return true; //返回真 
} 

//在静态顺序表中删除指定位置的元素,并返回其值 
bool ListDelete(SqList *L, int i, ElemType *e) 
{ 
	if (i < 1 || i > L->length) 
		return false; //如果位置不合法,返回假 

	if (ListEmpty(*L)) 
		return false; //如果顺序表为空,返回假 

	*e = L->data[i - 1]; //将要删除的元素赋值给e指向的变量 
	for (int j = i; j < L->length; j++) 
	{ //从前往后移动元素,覆盖被删除的位置 
		L->data[j - 1] = L->data[j]; 
	} 

	L->length--; //将顺序表长度减一 
	return true; //返回真 
} 

//打印静态顺序表中的所有元素 
void PrintList(SqList L)
{
	for (int i = 0; i < L.length; i++)
	{//遍历顺序表中的元素 
		printf("%d ", L.data[i]);
	}
}

注:bool为布尔类型,可自己用枚举等方法实现或者引头文件<stdbool.h>
从上面代码的实现来看静态顺序表有一定的局限性,因为是用静态数组来存储数据的,静态数组的大小是提前设置好的,所以可能用户的数据超出设置好的数组最大容量。因此,就有了动态顺序表来解决上面的问题。

2.动态顺序表

动态顺序表顾名思义就是用动态数组来存储数据的顺序表,可以实现容量不够可以扩容的功能
下面是动态顺序表结构体的定义
在这里插入图片描述
接下来是代码实现动态顺序表的增删查改

//定义动态顺序表的初始长度 
#define INIT_SIZE 10 
//定义动态顺序表的增长因子 
#define INCREMENT 5 
//定义动态顺序表的数据类型 
typedef int ElemType; 
//定义动态顺序表的结构体
typedef struct {
    ElemType* data; //指向存储数据的数组
    int size; //当前表中元素的个数
    int capacity; //当前表的最大容量
} SeqList;

//创建一个空的动态顺序表,返回指向它的指针
SeqList* createSeqList(SeqList* plist) 
{
    plist->data = (ElemType*)malloc(INIT_SIZE * sizeof(ElemType)); //申请初始大小的空间 
	if (!plist->data)
	{
		perror("malloc failed");
		return;
	}//如果申请失败,退出程序 
    plist->capacity = 0; //将长度设为0 
    plist->size = INIT_SIZE; //将容量设为初始大小 
}

//销毁一个动态顺序表,释放内存空间
void destroySeqList(SeqList* list) 
{
    if (list == NULL) 
        return; //如果指针为空,直接返回
    if (list->data != NULL) 
        free(list->data); //如果数据指针不为空,释放数据数组空间
}

//在动态顺序表的末尾插入一个元素,如果成功返回1,如果失败返回0
int insertSeqList(SeqList* list, int value) 
{
    if (list == NULL) 
        return 0; //如果指针为空,返回0
    if (list->size == list->capacity) 
    { //如果当前表已满,需要扩容

		ElemType *newbase = (ElemType *)realloc(list->data, (list->size + INCREMENT) * sizeof(ElemType)); //申请新的空间,并将原来的数据复制过去 
		if (!newbase) 
		{
			perror("malloc failed");
			return;
		}//如果申请失败,退出程序  
		list->data = newbase; //将数据指针指向新的空间 
		list->size += INCREMENT; //将容量增加一定值 
    }
    list->data[list->size] = value; //在末尾插入元素
    list->size++; //更新元素个数
    return 1; //返回1表示成功
}

//在动态顺序表中删除一个元素,如果成功返回1,如果失败返回0
int deleteSeqList(SeqList* list, int index) 
{
    if (list == NULL || list->data == NULL) 
        return 0; //如果指针为空,返回0
    if (index < 0 || index >= list->size) 
        return 0; //如果索引越界,返回0
    for (int i = index + 1; i < list->size; i++) 
    { //从索引位置开始,将后面的元素向前移动一位
        list->data[i - 1] = list->data[i];
    }
    list->size--; //更新元素个数
    return 1; //返回1表示成功
}

//在动态顺序表中查找一个元素,如果找到返回其索引,如果没找到返回-1
int searchSeqList(SeqList* list, int value) 
{
    if (list == NULL || list->data == NULL) 
        return -1; //如果指针为空,返回-1
    for (int i = 0; i < list->size; i++) 
    { //遍历数据数组,查找元素
        if (list->data[i] == value) 
            return i; //如果找到,返回其索引
    }
    return -1; //如果没找到,返回-1
}

//打印动态顺序表中的所有元素
void printSeqList(SeqList* list) 
{
    if (list == NULL || list->data == NULL) 
        return; //如果指针为空,直接返回
    printf("SeqList: size = %d, capacity = %d\n", list->size, list->capacity); //打印表的大小和容量
    printf("Data: ["); //打印数据数组的开始符号
    for (int i = 0; i < list->size; i++) 
    { //遍历数据数组,打印每个元素
        printf("%d", list->data[i]); //打印元素值
        if (i < list->size - 1) printf(", "); //如果不是最后一个元素,打印逗号和空格
    }
    printf("]\n"); //打印数据数组的结束符号
}

小结

✨动态顺序表是根据需要动态分配和释放存储空间的,它的物理地址不一定是连续的,而且可以根据实际情况调整大小。
✨静态顺序表是用类似于数组的方法实现的,它的物理地址是连续的,而且需要预先分配地址空间大小,一旦分配就不能改变。
✨动态顺序表的优点是可以节省存储空间,避免溢出和浪费,适合于数据量不确定或变化较大的情况。
✨动态顺序表的缺点是需要额外的指针来维护存储空间的分配和释放,增加了时间和空间开销,而且可能产生内存碎片。
✨静态顺序表的优点是存取速度快,操作简单,不需要额外的指针。

静态顺序表的缺点是需要预先确定存储空间大小,可能造成溢出或浪费,不适合于数据量不确定或变化较大的情况。


💕链表

上面的顺序表还有很多缺点,比如插入和删除效率低,而链表一定程度上的弥补了顺序表的缺点,下面就来介绍链表的结构和具体实现

1.链表的概念及结构

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
在这里插入图片描述
其实链表的结构很简单,就像一列火车一样,一节节车厢相互连接而成

在这里插入图片描述

注意:
🖼️✨从上图可以看出,链式结构在逻辑上是连续的,但在物理上不一定连续
🖼️✨现实中的节点一般都是从堆上申请出来的
🖼️✨从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

2.链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
❤️1.单向或者双向
在这里插入图片描述

❤️2.带头或者不带头
带头其实就是带哨兵位的头节点,不带头就是不带哨兵位的头节点,下面是对两者的简单介绍

🎭带哨兵位的链表是在链表的头部添加一个哑元结点,该结点不存储任何数据,只是为了操作的方便而引入的。
🎭不带哨兵位的链表是没有哑元结点的,链表的第一个结点就是存储数据的结点。
🎭带哨兵位的链表的优点是可以简化边界条件,避免对空链表和非空链表进行不同的处理,统一了插入和删除操作的代码。
🎭带哨兵位的链表的缺点是需要额外的空间来存储哑元结点,而且可能造成一些逻辑上的混淆,比如链表的长度是否包含哑元结点。
🎭不带哨兵位的链表的优点是节省了空间,不需要额外的哑元结点,而且逻辑上更清晰,比如链表的长度就是结点的个数。
🎭不带哨兵位的链表的缺点是需要对边界条件进行特殊处理,比如空链表和非空链表的插入和删除操作可能不一样,代码更复杂

在这里插入图片描述

❤️3.循环或者非循环
在这里插入图片描述
由上面三种不同的结构两两组合可以得到八种不同结构的链表,但是我们常用的链表结构就两种–无头单向非循环链表带头双向循环链表
在这里插入图片描述

✨无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
✨带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

3. 链表的实现

无头+单向+非循环链表增删查改实现

//定义链表的数据类型
typedef int SLTDateType;
typedef struct SListNode
{
	SLTDateType data;//数据域
	struct SListNode* next;//指针域
}SListNode;

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x) 
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));//分配新结点的空间
	if (newnode == NULL)
	{
		perror("malloc failed");
			return;
	}//如果分配失败就退出本函数
	newnode->data = x;//设置新结点的数据域
	newnode->next = NULL;//设置新结点的指针域为NULL
	return newnode;
}

// 单链表打印
void SListPrint(SListNode* plist)
{
	SListNode* cur = plist;// 用一个临时指针cur遍历链表
	while (cur != NULL)
	{ // 遍历链表中的每个结点
		printf("%d -> ", cre->data);
		cur = cur->next;// 向后移动指针
	}
	printf("NULL\n");
}

// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);//创建一个新结点
	if (*pplist == NULL)//检查头结点是否为空
	{
		*pplist = newnode;
	}
	else
	{
		SListNode* tail = *pplist;
		while (tail->next != NULL)
		{ //遍历找尾
			tail = tail->next;
		}
		tail->next = newnode;//链接新结点
	}
}

// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);//创建一个新结点
	//链接新结点
	newnode->next = *pplist;
	*pplist = newnode;
}

// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
	
	if (*pplist == NULL)//判断链表是否初始化
	{
		return;
	}
	if ((*pplist)->next == NULL)//判断是否只有一个结点,防止对空指针引用
	{
		free(*pplist);
		*pplist = NULL;
		return;
	}
	SListNode* prev_tail = *pplist;
	while (prev_tail->next->next != NULL)//找到尾结点的前一个结点
	{
		prev_tail = prev_tail->next;
	}
	free(prev_tail->next);//释放掉尾结点
	prev_tail->next = NULL;//将尾结点和前一个结点的链接断开,防止访问到该释放掉的尾结点
}

// 单链表头删
void SListPopFront(SListNode** pplist)
{
	SListNode* first = *pplist;
	*pplist = (*pplist)->next;
	free(first);//释放头结点
	first = NULL;
}

// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	SListNode* cur = plist;
	while (cur != NULL)//遍历整个链表
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;//遍历完没有找到返回空
}

// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	if (pos == NULL)//判断给的结点指针是否为空
		return;
	SListNode* newnode = BuySListNode(x);//创建一个新结点
	//链接新结点
	newnode->next = pos->next;
	pos->next = newnode;
}

// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
	SListNode* nextnode = pos->next;
	pos->next = nextnode->next;
	free(nextnode);
	nextnode = NULL;
}

// 单链表的销毁
void SListDestroy(SListNode* plist)
{
	SListNode* p, *q = plist;
	while (q != NULL)
	{
		p = q;
		q = q->next;
		free(p);
		p = NULL;
	}
}

🎀在上面的实现过程中,有几个函数需要传递二级指针,这是因为形参的改变不影响实参。
🎀还有一个问题是为什么不写一个在pos前加删或者删pos处的结点的函数呢?答案很简单,就是因为这是无头非循环单链表的局限性,只给pos结点,无法找到pos前的结点,则就不能写出以上功能的函数。(想要做到这些功能只需要把链表的头节点传给函数即可)

4.经典链表oj题

力扣(LeetCode) - 138. 复制带随机指针的链表

🎭给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点
🎭构造这个链表的深拷贝。 深拷贝应该正好由 n 个全新节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和random
🎭指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
🎭例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有x.random --> y 。
🎭最后返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

val:一个表示 Node.val 的整数。 random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。 你的代码只接受原链表的头节点 head 作为传入参数。

注:直接返回原链表的头结点会被检测出来,别问我怎么知道的,问就是试过了
在这里插入图片描述
下面看几个例子
在这里插入图片描述
在这里插入图片描述
不难看出random指向的结点是随机的,所以random的拷贝是这个题的难点。

简单粗暴点的解法是创建一个新的头结点先依次创建拷贝原链表val值,先不管random。然后用两个指针数组存储两个链表的每个结点的地址,再对比原链表的random指针遍历找到相应的数组下标,从而可以在拷贝链表中找到每个结点的random,进而完成深度拷贝

这个方法虽然可以写出来,但是时间复杂度为O(n*n),那么要优化到O(n),就要用到下面的方法

原理:可以先在原链表的每个结点后面创建一个新的结点,并且新结点的val值与前面结点一致,并且与后面链接起来,这样的处理就很容易解决random难处理的问题

假设原链表其中一个结点为prev,下一个拷贝结点是next,则要找next->random就可以通过prev->random来找,即next->random = prev->random->nxet,就可以实现每个random的深拷贝

完成对random的深拷贝后,将每个新结点从链表中拿出来并连接起来,同时将原链表复原,返回新链表的头结点即可完成本题

下面用图来解释过程帮助了解解题过程
1️⃣第一步,根据遍历到的原节点创建对应的新节点,每个新创建的节点是在原节点后面,比如下图中原节点1不再指向原原节点2,而是指向新节点1

在这里插入图片描述
2️⃣第二步是最关键的一步,用来设置新链表的随机指针
在这里插入图片描述
上图中,我们可以观察到这么一个规律

原节点1的随机指针指向原节点3,新节点1的随机指针指向的是原节点3的next
原节点3的随机指针指向原节点2,新节点3的随机指针指向的是原节点2的next 也就是,原节点i的随机指针(如果有的话),指向的是原节点j

那么新节点i的随机指针,指向的是原节点j的next

3️⃣第三步就简单了,只要将两个链表分离开,再返回新链表就可以了
在这里插入图片描述
代码实现

 struct Node {
    int val;
    struct Node *next;
    struct Node *random;
 };//带random的结构体的定义


struct Node* copyRandomList(struct Node* head) {
	if (!head)
    {
        struct Node* newhead = NULL;
        return newhead;
    }
    struct Node* cur;
    struct Node* prev;
    cur = prev = head;
    //在原链表每个结点后创建一个新结点
    while(cur)
    {
        cur =cur->next;
        struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
        newnode->val = prev->val;
        newnode->next = cur;
        prev->next = newnode;
        prev = cur;
    }
    cur = head;
    //深拷贝random指针
    while (cur)
    {
        if (cur->random == NULL)
        {
            cur->next->random = NULL;
        }
        else
        {
            cur->next->random = cur->random->next;
        }
        cur = cur->next->next;
    }
    struct Node* newhead = head->next;
    struct Node* newtail = newhead;
    prev = cur = head;
    //将新结点链接起来
    while (cur)
    {
        cur = cur->next->next;
        prev->next = cur;
        if (!cur)
        {
            newtail->next = NULL;
        }
        else
        {
            newtail->next = cur->next;
        }
        newtail = newtail->next;
        prev = cur;
    }

    return newhead;
}

此题还有一种解法是用哈希表,博主没了解,感兴趣可以参考以下两个题解

题解一(LeetCode官方题解)
题解二(推荐)


💕总结

简单总结一下,顺序表分动态和静态的,一般会使用动态顺序表,因为静态顺序表具有一定的局限性。链表一共有三种大分类(单或双、循环或非循环、带头或不带头),分别组合链表就一共有八种结构,但是我们一般常用两种结构(无头单向非循环链表带头双向循环链表),而带头双向循环链表看起来难实现,但是增删查改的实现非常方便。
下面是顺序表与链表的大致对比图
在这里插入图片描述

以上就是对顺序表和链表的大概讲解,这些内容是数据结构初阶的开始,可以细细品尝,为后面的内容打好基础。
最后再来一张美图,放松一下疲惫的眼睛
请添加图片描述
是心动的感觉在这里插入图片描述


请添加图片描述

专栏:数据结构初阶
都看到这里了,留下你们的珍贵的👍点赞+⭐收藏+📋评论吧

  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 23
    评论
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

T_X_Parallel〆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值