

目录
[1.1 线性表](#1.1 线性表)
[1.1.1 核心定义](#1.1.1 核心定义)
[1.1.2. 物理实现方式(存储结构)](#1.1.2. 物理实现方式(存储结构))
[1.2 非线性表](#1.2 非线性表)
[1.2.1 核心定义](#1.2.1 核心定义)
[1.2.2 核心结构](#1.2.2 核心结构)
[2.1 静态顺序表](#2.1 静态顺序表)
[2.2 动态顺序表](#2.2 动态顺序表)
[2.2.1 定义](#2.2.1 定义)
[2.2.2 初始化与销毁](#2.2.2 初始化与销毁)
[2.2.2 头插与尾插](#2.2.2 头插与尾插)
[2.2.3 头删与尾删](#2.2.3 头删与尾删)
[2.2.4 指定位置增删查改](#2.2.4 指定位置增删查改)
[2.3 给予动态顺序表的通讯录项目](#2.3 给予动态顺序表的通讯录项目)

一、前言
1.1 线性表
1.1.1 核心定义
数据元素之间存在 严格的 "一对一" 线性逻辑关系:
- 有且仅有一个 首元素(无前驱)
- 有且仅有一个 尾元素(无后继)
- 中间所有元素:恰好有一个直接前驱(前一个元素)和一个直接后继(后一个元素)
可以类比为 "排队":每个人只跟着前一个人,只被后一个人跟着,队伍呈直线排列。

1.1.2. 物理实现方式(存储结构)
线性表的逻辑结构是 "线性",但物理存储(内存中实际存放方式)有两种:
(1)顺序存储:顺序表(数组实现)
元素连续存储在内存的一块连续地址空间中,通过 "数组下标" 直接访问(O (1) 时间复杂度),但是插入 / 删除需移动元素(O (n))。

(2)链式存储:链表(指针 / 引用实现)
元素分散存储在内存中,每个元素(节点)包含 "数据域" 和 "指针域",通过指针连接成线性结构,插入 / 删除无需移动元素(O (1),只需修改指针),访问元素需遍历(O (n))
例子:单链表、双链表、循环链表。

1.2 非线性表
1.2.1 核心定义
数据元素之间是 "一对多" 或 "多对多" 的逻辑关系,不存在严格的线性顺序,元素的前驱 / 后继个数不唯一。
可以类比为:
- 树:"家族族谱"(一对多,父节点对应多个子节点);
- 图:"社交网络"(多对多,每个人可关联多个好友)。

1.2.2 核心结构
树(Tree):有一个根节点,其余节点分为若干个互不相交的子树,每个节点有且仅有一个父节点(根节点无父节点),可有多个子节点。
顺序存储:堆;

链式存储:二叉链表。
二、顺序表
顺序表是线性表的顺序存储结构,指将线性表中的元素按照逻辑顺序依次存储在内存中一块连续的物理地址空间里的线性结构。简单来说,顺序表就是用数组实现的线性表,是数据结构中最基础、最常用的结构之一。
因为顺序表的实现底层是数组,所以顺序表在物理存储上是连续的(因为数组占据一块连续的内存空间),元素的逻辑顺序(如第 1 个、第 2 个、第 n 个)与内存中的物理存储顺序完全相同,所以在逻辑上也是连续的。
2.1 静态顺序表
定义:使用固定长度的数组实现,存储容量在定义时确定,运行过程中无法修改。
特点:实现简单,但存在内存浪费(数组容量过大)或溢出(数组容量不足)的问题,灵活性差。
cpp
#define MAX_SIZE 100 // 固定容量
typedef int ElemType; // 元素类型(可自定义)
typedef struct {
ElemType data[MAX_SIZE]; // 存储元素的数组
int length; // 顺序表的实际元素个数(≤MAX_SIZE)
} StaticSeqList;
2.2 动态顺序表
定义 :使用动态分配的数组(如 C 语言的malloc/realloc、C++ 的new)实现,存储容量可在运行过程中根据需要动态扩容或缩容。
特点:灵活性高,能有效利用内存,是实际开发中最常用的形式。
2.2.1 定义
cpp
typedef int SqListType;
typedef struct Sqlist
{
//动态数组
SqListType* _arr;
int _size;
int _cpacity;
}SL;
在C语言中我们使用一个结构体来封装顺序表,其中SqListType表示内部数组存储的元素的类型,_size与_cpacity分别表示数组中的有效数据的个数与容量的大小。
2.2.2 初始化与销毁
cpp
void SL_Init(SL* list)
{
list->_arr = NULL;
list->_cpacity = 0;
list->_size = 0;
}
void SL_Destory(SL* list)
{
free(list->_arr);
list->_arr = NULL;
}
SL_Init接口中分别将_arr初始化为NULL,_size与_cpacity初始化为0。因为其中_arr是一个动态数组后期的容量会随需求调用realloc在堆上申请空间来扩充容量,而堆上开辟的空间不会随程序结束自动释放所以需要我们自己调用free函数进行释放以免造成内存泄漏。
2.2.2 头插与尾插
cpp
//头插
void PushFront(SL* list, SqListType in)
{
assert(list);
//进来首先判断空间够不够:
if (list->_cpacity == list->_size)
{
int newcapacity = list->_cpacity == 0 ? 4 : 2 * list->_cpacity;
SqListType* temp = (SqListType*)realloc(list->_arr, newcapacity * sizeof(SqListType));
if (temp == NULL)
{
printf("realloc fail!\n");
return;
}
list->_arr = temp;
list->_cpacity = newcapacity;
}
//首先挪动数据
for (int i = list->_size; i > 0; i--)
{
list->_arr[i] = list->_arr[i - 1];
}
list->_arr[0] = in;
list->_size++;
}
//尾插
void PushBack(SL* list, SqListType in)
{
assert(list);
//进来首先判断空间够不够:
if (list->_cpacity == list->_size)
{
int newcapacity = list->_size == 0 ? 4 : 2 * list->_cpacity;
SqListType* temp = (SqListType*)realloc(list->_arr, newcapacity * sizeof(SqListType));
if (temp == NULL)
{
printf("realloc fail!\n");
return;
}
list->_arr = temp;
list->_cpacity = newcapacity;
}
list->_arr[list->_size] = in;
list->_size++;
}
在头插与尾插的实现中我们需要注意两件事:
- 在插入数据之前先判断空间够不够,依据是顺序表中的有效数据个数_size是否等于空间大小_cpacity,如果相等则说明空间不够了需要我们手动调用realloc扩容。初始时_cpacity为0默认给予4个SqListType大小的空间,之后每次扩容时都扩容原空间大小的2倍。
还有一个小细节就是,我们每次扩容后的新空间的头指针不能第一时间赋值给顺序表的_arr而是用用一个临时变量temp来存储,当temp不为NULL时在赋值给_arr。因为realloc可能会调用失败返回NULL,如果不加判断就赋值给_arr则会永远丢失我们的数据信息。
cpp
//进来首先判断空间够不够:
if (list->_cpacity == list->_size)
{
int newcapacity = list->_cpacity == 0 ? 4 : 2 * list->_cpacity;
SqListType* temp = (SqListType*)realloc(list->_arr, newcapacity * sizeof(SqListType));
if (temp == NULL)
{
printf("realloc fail!\n");
return;
}
list->_arr = temp;
list->_cpacity = newcapacity;
}
- 当我们头插一个数据时必须将原有数据向后移动一个元素大小,因为插入操作默认是一种覆盖操作否则会造成数据的丢失。
2.2.3 头删与尾删
cpp
//尾删
SqListType PopBack(SL* list)
{
assert(list);
assert(list->_size);
SqListType temp = list->_arr[list->_size - 1];
list->_size--;
return temp;
}
//头删
SqListType PopFront(SL* list)
{
assert(list);
assert(list->_size);
SqListType temp = list->_arr[0];
for (int i = 0; i < list->_size - 1; i++)
{
list->_arr[i] = list->_arr[i + 1];
}
list->_size--;
return temp;
}
在这两个接口中,尾删较为简单直接将_size--即可。头删时只需要将_arr[ 0 ]后的数据整体向前移动一个元素的大小,原有数据就会覆盖_arr[0]处的数据。
2.2.4 指定位置增删查改
cpp
//指定位置头删数据
void SLInsert(SL* list, int pos, SqListType in)
{
assert(list->_arr);
assert(pos >= 0 && pos <= list->_size);
//进来首先判断空间够不够:
if (list->_cpacity == list->_size)
{
int newcapacity = list->_size == 0 ? 4 : 2 * list->_cpacity;
SqListType* temp = (SqListType*)realloc(list->_arr, newcapacity * sizeof(SqListType));
if (temp == NULL)
{
printf("realloc fail!\n");
return;
}
list->_arr = temp;
list->_cpacity = newcapacity;
}
//挪动数据:
for (int i = list->_size; i > pos; i--)
{
list->_arr[i] = list->_arr[i - 1];
}
list->_arr[pos] = in;
list->_size++;
}
//删除指定位置的数据
SqListType SLErase(SL* list, int pos)
{
assert(list);
assert(pos >= 0 && pos < list->_size);
SqListType temp = list->_arr[pos];
for (int i = pos; i < list->_size - 1; i++)
{
list->_arr[i] = list->_arr[i + 1];
}
list->_size--;
return temp;
}
//修改指定下标的数据
void Edit(SL* list,int pos, SqListType in)
{
assert(list);
assert(list->_size!=0);
//如果是字符串则使用strcpy
list->_arr[pos] = in;
}
2.3 给予动态顺序表的通讯录项目
cpp
#define NAME_MAX 100
#define SEX_MAX 4
#define TEL_MAX 11
#define ADDR_MAX 100
//通讯录中的个人信息:
typedef struct PersonInfo
{
char name[NAME_MAX];
char sex[SEX_MAX];
int age;
char tel[TEL_MAX];
char addr[ADDR_MAX];
}PI;
typedef struct Sqlist contact;
//初始化通讯录
void InitContact(contact* con);
//添加通讯录数据
void AddContact(contact* con);
//删除通讯录数据
void DelContact(contact* con);
//展⽰通讯录数据
void ShowContact(contact* con);
//查找通讯录数据
void FindContact(contact* con);
//修改通讯录数据
void ModifyContact(contact* con);
//销毁通讯录数据
void DestroyContact(contact* con);
cpp
#include"SqList.h"
#include"string.h"
//初始化通讯录
void InitContact(contact* con)
{
SL_Init(con);
}
//添加通讯录数据
void AddContact(contact* con)
{
assert(con);
PI info;
printf("请输入姓名:\n");
scanf("%s", &info.name);
printf("请输入性别:\n");
scanf("%s", &info.sex);
printf("请输入年龄:\n");
scanf("%d", &info.age);
printf("请输入联系电话:\n");
scanf("%s", &info.tel);
printf("请输入地址:\n");
scanf("%s", &info.addr);
PushBack(con, info);
printf("插入成功!\n");
}
//根据姓名查找该联系人的下标:
int Find_By_Name(contact* con,char name[] )
{
assert(con);
for (int i = 0; i < con->_size; i++)
{
if (strcmp(con->_arr[i].name, name) == 0 )
{
return i;
}
}
return -1;
}
//删除通讯录数据
void DelContact(contact* con)
{
assert(con);
// 定义固定长度的字符数组,对应NAME_MAX等宏定义
char name[NAME_MAX];
printf("请输入要删除的联系人姓名:\n");
scanf("%s", name);
int pos = Find_By_Name(con,name);
if (pos == -1)
{
printf("删除的联系人不存在\n");
return;
}
SLErase(con,pos);
printf("删除成功!\n");
}
//展⽰通讯录数据
void ShowContact(contact* con)
{
assert(con);
printf("%-10s %-4s %-4s %15s %-20s\n", "姓名", "性别", "年龄", "联系电话", "地址");
for (int i = 0; i < con->_size; i++)
{
printf("%-10s %-4s %-4d %15s %-20s\n",
con->_arr[i].name,
con->_arr[i].sex,
con->_arr[i].age,
con->_arr[i].tel,
con->_arr[i].addr);
}
}
//查找通讯录数据
void FindContact(contact* con)
{
assert(con);
char name[NAME_MAX];
printf("请输入要删除的联系人姓名:\n");
scanf("%s", name);
int pos = Find_By_Name(con, name);
if (pos < 0)
{
printf("查询的联系人不存在\n");
return;
}
printf("%-10s %-4s %-4s %15s %-20s\n", "姓名", "性别", "年龄", "联系电话", "地址");
printf("%-10s %-4s %-4d %15s %-20s\n",
con->_arr[pos].name,
con->_arr[pos].sex,
con->_arr[pos].age,
con->_arr[pos].tel,
con->_arr[pos].addr);
}
//修改通讯录数据
void ModifyContact(contact* con)
{
assert(con);
char name[NAME_MAX];
printf("请输入要修改的联系人姓名:\n");
scanf("%s", name);
int pos = Find_By_Name(con, name);
if (pos < 0)
{
printf("要修改的联系人不存在\n");
return;
}
printf("%-10s %-4s %-4s %15s %-20s\n", "姓名", "性别", "年龄", "联系电话", "地址");
printf("%-10s %-4s %-4d %15s %-20s\n",
con->_arr[pos].name,
con->_arr[pos].sex,
con->_arr[pos].age,
con->_arr[pos].tel,
con->_arr[pos].addr);
// 定义固定长度的字符数组,对应NAME_MAX等宏定义
char temp1[NAME_MAX]="", temp2[NAME_MAX]="";
printf("请输入要修改的联系人的属性:\n");
scanf("%s", temp1);
printf("请输入要修改的联系人的属性内容:\n");
scanf("%s", temp2);
// 用strcmp比较字符串
if (strcmp(temp1, "姓名") == 0)
{
//用strcpy复制字符串到字符数组
strcpy(con->_arr[pos].name, temp2);
}
else if (strcmp(temp1, "性别") == 0)
{
strcpy(con->_arr[pos].sex, temp2);
}
else if (strcmp(temp1, "年龄") == 0)
{
con->_arr[pos].age= atoi(temp2);
}
else if (strcmp(temp1, "联系电话") == 0)
{
strcpy(con->_arr[pos].tel, temp2);
}
else if (strcmp(temp1, "地址") == 0)
{
strcpy(con->_arr[pos].addr, temp2);
}
else
{
printf("要修改的联系人的属性不存在:\n");
}
return;
}
//销毁通讯录数据
void DestroyContact(contact* con)
{
SL_Destory(con);
}
cpp
int main()
{
contact con;
InitContact(&con);
int op = -1;
do {
printf("**********************************\n");
printf("*****1、添加用户 2、删除用户******\n");
printf("*****3、查找用户 4、修改用户******\n");
printf("*****5、展示用户 0、退出**********\n");
printf("**********************************\n");
printf("请选择您的操作:\n");
scanf("%d", &op);
switch (op)
{
case 1:
AddContact(&con);
break;
case 2:
DelContact(&con);
break;
case 3:
FindContact(&con);
break;
case 4:
ModifyContact(&con);
break;
case 5:
ShowContact(&con);
break;
default:
printf("输入有误,请重新输入\n");
break;
}
} while (op != 0);
DestroyContact(&con);
return 0;
}
项目演示:
基于顺序表的通讯录项目