05. 线性表
一、什么是线性表
线性表是具有 相同 数据类型的 n(n≥0)个 数据元素 的 有限序列,其中 n 为 表长,当 n=0 时,线性表是一个空表。若用 L 命名线性表,则其一般表示为 。
其中, 是线性表中的 “第 i 个” 元素线性表中的 位序, 是 表头元素, 是 表尾元素。除了第一个元素外,每个元素有且仅有一个 直接前驱。除了最后一个元素外,每个元素有且仅有一个 直接后继。
ADT List
{
Data:
线性表L∈List, 整数i表示位置, 元素X∈ElementType;
Operation:
List MakeEmpty(void); // 初始化一个空线性表L
ElementType FindKth(List L, int i); // 根据位序K,返回相应元素
int FindElement(List L, ElementType X); // 在线性表L中查找X的第一次出现位置
void Insert(List L, int i, ElementType X); // 在位序i前插入元素X
void Delete(List L, int i); // 删除位序i的元素
void Length(List L); // 返回线性表L的长度
} ADT List;
二、线性表的顺序存储实现
2.1、线性表的顺序存储
利用数组的 连续存储空间顺序存放 线性表的各元素。逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
#define MAX_SIZE 10
typedef int ElementType;
typedef struct LNode
{
ElementType Data[MAX_SIZE];
int Last;
}LNode, * List;
如果我们要访问下标为 i 的元素,可以通过以下方法:L.Data[i]
或 PtrL->Data[i]
。如果我们要获取线性表的长度,可以通过如下方法:L.Last+1
或 PtrL->Last+1
。
我们还可以通过动态分配的方式实现顺序表。
#define INIT_SIZE 10
typedef int ElementType;
typedef struct LNode
{
ElementType * Data;
int MaxSize;
int Last;
}LNode, * List;
2.2、顺序表的主要操作
2.2.1、初始化
/**
* @brief 创建空的顺序表
*
* @return List 指向顺序表表头的指针
*/
List MakeEmpty(void)
{
List PtrL = (List)malloc(sizeof(LNode));
PtrL->Last = -1;
return PtrL;
}
如果我们使用分配的方式实现顺序表,则它的初始化方法如下:
/**
* @brief 创建空的顺序表
*
* @return List 指向顺序表表头的指针
*/
List MakeEmpty(void)
{
List PtrL = (List)malloc(sizeof(LNode));
PtrL->Data = (ElementType *)malloc(INIT_SIZE * sizeof(ElementType));
PtrL->MaxSize = INIT_SIZE;
PtrL->Last = -1;
return PtrL;
}
采用动态分配实现顺序表时,我们还可以增加动态数组的长度。
/**
* @brief 增加动态数据的长度
*
* @param PtrL 顺序表
* @param Length 增加的长度
*/
void IncreaseSize(List PtrL, int Length)
{
int i = 0;
int * temp = PtrL->Data;
PtrL->Data = (ElementType *)malloc((PtrL->MaxSize + Length) * sizeof(ElementType));
for (i = 0; i <= PtrL->Last; i++)
{
PtrL->Data[i] = temp->Data[i]; // 将原表内容复制到新表
}
PtrL.MaxSize += Length; // 增加表空间
free(temp); // 释放原表空间
}
2.2.2、查找元素
/**
* @brief 按位序查找
*
* @param PtrL 顺序表
* @param position 要查找的位置
* @return ElementType 要查找的元素
*/
ElementType FindKth(List PtrL, int position)
{
if (position < 0 || position > PtrL->Last + 1)
{
printf("查找位置有误!\n");
return NULL;
}
return PtrL->Data[position - 1];
}
由于顺序表的各个元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第 i 个元素,即随机存取特性,时间复杂度为 。
/**
* @brief 查找元素X的位置
*
* @param PtrL 顺序表
* @param X 查找的元素
* @return int 如果找到返回元素的索引,否则返回-1
*/
int FindElement(List PtrL, ElementType X)
{
int i = 0;
if (PtrL == NULL)
{
printf("链表为空!\n");
return NULL;
}
while (i <= PtrL->Last && PtrL->Data[i] != X)
{
i++;
}
return (i <= PtrL->Last) ? i : -1;
}
查找成功的平均次数为 ,平均时间性能为 。
最好情况:目标元素在表头,循环 1 次,最好时间复杂度为 。
最坏情况:目标元素在表尾,循环 n 次,最坏时间复杂度为 。
平均情况:假设目标元素出现在任何一个位置的概率相同,都是 。目标元素在第 1 位,循环 1 次,目标元素在第 2 位,循环 2 次,……,目标元素在第 n 位,循环 n 次。,平均时间复杂度为 。
2.2.3、插入元素
/**
* @brief 插入一个新元素
*
* @param PtrL 顺序表
* @param i 要插入的位置
* @param X 要插入的元素
*/
void Insert(List PtrL, int i, ElementType X)
{
int j = 0;
if (PtrL->Last == MAX_SIZE - 1) // 表空间已满,不能插入
{
printf("表已满\n");
return;
}
if (i < 1 || i > PtrL->Last + 2) // 插入位置不合法
{
printf("位置不合法\n");
return;
}
for (j = PtrL->Last; j >= i - 1; j--)
{
PtrL->Data[j + 1] = PtrL->Data[j]; // 将a[i]~a[n]倒序向后移动
}
PtrL->Data[i - 1] = X; // 新元素插入
PtrL->Last++; // 表长增1
}
最好情况:新元素插入到表尾,不需要移动元素,i=n+1,循环 0 次,最好时间复杂度为 。
最坏情况:新元素插入到表头,需要将原有的 n 个元素全都向后移动,i=1,循环 n 次,最坏时间复杂度为 。
平均情况:假设新元素插入到任何一个位置的概率相同,即 i=1,2,3,...,Last+2 的概率都是 。i=1,循环 n 次,i=2 时,循环 n-1 次,……,i=n+1时,循环 0 次。,平均时间复杂度为 。
2.2.4、删除元素
/**
* @brief 按位置删除表中元素
*
* @param PtrL 顺序表
* @param i 删除元素的位置
*/
void DeleteByPosition(List PtrL, int i)
{
int j = 0;
if (i < 1 || i > PtrL->Last + 1) // 删除位置不合法
{
printf("不存在第%d个元素\n", i);
return;
}
for (j = i; j <= PtrL->Last; j++)
{
PtrL->Data[j-1] = PtrL->Data[j]; // 将a[i+1]~a[n]倒序向前移动
}
PtrL->Last--; // 表长减1
}
/**
* @brief 按元素删除表中元素
*
* @param PtrL 顺序表
* @param X 要删除的元素
*/
void DeleteByElement(List PtrL, ElementType X)
{
int i = 0;
for (i = 0; i < PtrL->Last + 1; i++)
{
if (PtrL->Data[i] == X) // 找到元素X所在的位置
{
break;
}
}
if (i == PtrL->Last + 1)
{
printf("删除元素不存在!\n");
return;
}
DeleteByPosition(PtrL, i + 1);
}
最好情况:删除表尾元素,不需要移动其它元素,i=n,循环 0 次,最好时间复杂度为 。
最坏情况:删除表头元素,需要将后续的 n-1 个元素全都向前移动,i=1,循环 n 次,最坏时间复杂度为 。
平均情况:假设删除任何一个元素的概率相同,即 i=1,2,3,...,Last+1 的概率都是 。i=1,循环 n-1 次,i=2 时,循环 n-2 次,……,i=n时,循环 0 次。,平均时间复杂度为 。
2.2.5、遍历顺序表
/**
* @brief 遍历顺序表
*
* @param PtrL 顺序表
*/
void PrintList(List PtrL)
{
int i = 0;
for (i = 0; i <= PtrL->Last; i++)
{
printf("%d ", PtrL->Data[i]);
}
printf("\n");
}
三、线性表的链式存储实现
3.1、线性表的链式存储
我们可以使用链式存储来实现线性表,链式存储不要求逻辑上相邻的两个元素物理上也相邻。我们可以通过 “链” 建立数据元素之间的逻辑关系。插入和删除不需要移动数据元素,只需要修改 “链” 即可。
链表由一系列不必在内存上相连的结构组成。每个结构均含有表元素和指向包含该元素后继元的结构组成。我们称之为 Next 指针。最后一个单元的 Next 指针指向 NULL。
typedef int ElementType;
typedef struct LNode
{
ElementType Data;
struct LNode * Next;
} LNode, * List;
3.2、链表的主要操作
3.2.1、求表长
/**
* @brief 求链表的表长
*
* @param PtrL 链表
* @return int 链表的长度
*/
int Length(List PtrL)
{
List p = PtrL; // p指向表的第一个节点
int j = 0;
while (p != NULL)
{
p = p->Next;
j++; // 当前p指向的是第j个节点
}
return j;
}
3.2.2、查找元素
/**
* @brief 按序号查找
*
* @param PtrL 链表
* @param K 要查找的序号
* @return List 如果找到返回指向第K个的指针,否则返回NULL
*/
List FindKth(List PtrL, int K)
{
List p = PtrL; // p指向表的第一个节点
int i = 1;
if (PtrL == NULL)
{
printf("链表为空!\n");
return NULL;
}
if (i < 1 || i > Length(PtrL) + 1)
{
printf("查找的位置不合法\n");
return PtrL;
}
while (p != NULL && i < K)
{
p = p->Next;
i++; // 当前p指向的是第j个节点
}
return (i == K) ? p : NULL;
}
/**
* @brief 按值查找
*
* @param PtrL 链表
* @param X 要查找的元素
* @return List 如果找到返回指向X的指针,否则返回NULL
*/
List FindElement(List PtrL, ElementType X)
{
List p = PtrL; // p指向表的第一个节点
while (p != NULL && p->Data != X)
{
p = p->Next;
}
return p;
}
3.2.3、插入元素
- 先构造一个新节点,用 s 执行。
- 再找到链表的第 i-1 个结点,用 p 指向。
- 然后修改指针,插入结点(p 之后插入新结点是 s)。
/**
* @brief 插入一个元素
*
* @param PtrL 链表
* @param i 要插入的位置
* @param X 要插入的元素
*
* @return List 指向添加元素的链表的头指针
*/
List Insert(List PtrL, int i, ElementType X)
{
List p = NULL, s = NULL;
if (i < 1 || i > Length(PtrL) + 1)
{
printf("插入的位置不合法\n");
return PtrL;
}
if (PtrL == NULL)
{
PtrL = (List)malloc(sizeof(LNode));
PtrL->Data = X;
PtrL->Next = NULL;
return PtrL;
}
if (i == 1) // 新节点插入在表头
{
s = (List)malloc(sizeof(LNode)); // 申请新节点
s->Data = X; // 新节点的数据域为表头
s->Next = PtrL; // 新节点指向原来的表头
return s;
}
p = FindKth(PtrL, i - 1); // 找到第i-1个节点
if (p == NULL) // 插入的位置不合法
{
printf("插入的位置不合法\n");
return PtrL;
}
s = (List)malloc(sizeof(LNode)); // 申请新节点
s->Data = X;
s->Next = p->Next; // 新节点指向p的下一个节点
p->Next = s; // p指向新节点
return PtrL;
}
3.2.4、删除元素
- 先找到链表的第 i-1 个节点,用 p 指向。
- 再用指针 s 指向要被删除的节点(p 的下一个节点)。
- 然后修改指针,删除 s 所指节点。
- 最后释放 s 所指节点的空间。
/**
* @brief 删除一个元素
*
* @param PtrL 链表
* @param i 要删除元素的位置
* @return List 指向删除后的链表的头指针
*/
List DeleteByPositon(List PtrL, int i)
{
List p = NULL, s = NULL;
if (PtrL == NULL)
{
printf("链表为空!\n");
return NULL;
}
if (i < 1 || i > Length(PtrL))
{
printf("删除的位置不合法\n");
return PtrL;
}
if (i == 1) // 要删除的节点是表头
{
s = PtrL; // s指向表头
PtrL = PtrL->Next; // 删除表头,表头指向下一个节点
free(s); // 释放s
return PtrL;
}
p = FindKth(PtrL, i - 1); // 找到第i-1个节点
if (p == NULL)
{
printf("第%d个节点不存在!\n", i - 1);
}
else if (p->Next == NULL)
{
printf("第%d个节点不存在!\n", i);
}
else
{
s = p->Next; // s指向第i个节点
p->Next = s->Next; // p指向第i+1个节点
free(s); // 释放s
}
return PtrL;
}
/**
* @brief 按元素删除
*
* @param PtrL 链表
* @param X 要删除的元素
* @return List 指向删除后的链表的头指针
*/
List DeleteByElement(List PtrL, ElementType X)
{
List p = PtrL;
LNode * node = FindElement(PtrL, X);
LNode * next_node = NULL;
if (p == NULL)
{
printf("链表为空!\n");
return NULL;
}
if (node == NULL)
{
printf("要删除的元素不存在!\n");
return PtrL;
}
next_node = node->Next;
if (next_node != NULL)
{
node->Data = next_node->Data;
node->Next = next_node->Next;
free(next_node);
return PtrL;
}
else
{
return DeleteByPositon(PtrL, Length(PtrL));
}
}
3.2.5、遍历链表
/**
* @brief 遍历链表
*
* @param PtrL 链表
*/
void PrintList(List PtrL)
{
List p = PtrL;
while (p != NULL)
{
printf("%d ", p->Data);
p = p->Next;
}
printf("\n");
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理