线性表的基本概念
线性表是由n(n >= 0)个数据元素(结点)组成的有限序列.
则n为表的长度, 当n = 0时为空表
非空的线性表(n > 0)的表示方式
L = (a1, a1, …, an)
其中a1为起始结点, an为终端结点. 对任意一对相邻的结点ai和ai+1 (1 <= i < n), ai称为ai+1的直接前驱, ai+1称为ai的直接后继.
在这张图片中, 线性表是一个由10个结点组成的有限序列. 表的长度为10, 结点A为起始结点, 结点J为终端结点. 对于任意相邻的两个结点, 例如AB两个结点, 结点A为结点B的直接前驱, 结点B为结点A的直接后继.
特点
- 线性表中只有一个起始结点, 一个终端结点
- 起始结点没有直接前驱, 有一个直接后继
- 终端结点没有直接后继, 有一个直接前驱
- 除了起始结点和终端结点, 每个结点都有且只有一个直接前驱和一个直接后继
线性表的顺序存储
将表中的结点依次存放在计算机内存中一组连结的存储单元中. 用顺序存储方式实现的线性表称为顺序表. 一般使用数组来表示顺序表.
已知a1地址为locate(a1), 每个数据占m个存储单元, 则ai的地址
locate(ai) = locate(a1) + m * (i - 1)
顺序存储线性表需要存储
线性表的大小: maxSize
线性表的长度: length
所存放的数据类型: DataType
注意
- 顺序表是用一维数组实现的线性表, 数组的下标可以看成是元素的相对地址.
- 逻辑上相邻的元素, 存储在物理位置也相邻的单元中
特点
- 线性表的逻辑结构与存储结构一致.
- 可以对数据元素实现随机读取
基本运算在顺序表上的实现
插入
线性表的插入运算是指在表的第 i (1 <= i <= n+1) 个位置上, 插入一个新结点, 使长度为n的线性表变成长度为n + 1的线性表
前提: 线性表的长度为n, 要插入的位置为i
元素的移动次数: 在i位置插入数据时, 要移动 n - i + 1 个数据元素
平均移动元素个数: 我们可以简单理解为最好的时候, 在末尾插入, 移动0个数据元素, 最坏的时候, 在位置为1的地插入, 移动n个数据元素, 则平均移动元素个数为 n / 2, 则时间复杂度为O(n)
注意事项
1. 当表空间已满, 不可再做插入操作
2. 当插入位置为非法位置, 不可做正常插入操作
复制代码
代码
/**
线性表的插入
@param pList 类型为SeqList的结构体指针
@param x 要插入的类型为DataType类型的数据元素
@param i 要插入的位置
*/
void insertSeqList(SeqList * pList, DataType x, int i) {
// 判断线性表是否已满
if (pList->length == maxSize) {
printf("表已满 \n");
return;
}
// 检查插入位置是否合法
if (i < 1 || i > (pList->length) + 1) {
printf("插入位置不合法 \n");
return;
}
// 进行插入操作
// 从表的最后一个位置依次进行后移
for (int j = pList->length; j >= i; j--) {
pList->data[j] = pList->data[j-1];
}
// 后移完成之后, 对空出的位置进行赋值
pList->data[i - 1] = x;
// 表的长度加1
pList->length++;
}
复制代码
删除
线性表的删除运算是指将表的第 i 个结点删去, 使长度为 n 的线性表变成长度为 n-1 的线性表
注意事项
- 当要删除元素的位置 i 不在表长范围内 (i < 1 || i > L -> length) 时, 为非法位置, 不能做正常的删除操作
代码
/**
线性表的删除
@param pList 类型为SeqList结构体的指针
@param i 要删除的位置
*/
void deleteSeqList(SeqList * pList, int i) {
// 检查删除位置是否合法
if (i < 1 || i > pList -> length) {
printf("删除位置不合法");
return;
}
// 进行删除操作, 依次左移
for (int j = i; j < pList->length; j++) {
pList->data[j - 1] = pList->data[j];
}
// 表的长度减1
pList->length--;
}
复制代码
元素的移动次数: 在i位置删除数据时, 要移动 n - i 个数据元素
平均移动元素个数: 我们可以简单理解为最好的时候, 在末尾删除, 移动0个数据元素, 最坏的时候, 在位置为1的地删除, 移动 n-1 个数据元素, 则平均移动元素个数为 (n-1) / 2, 则时间复杂度为O(n)
定位 / 查找
定位运算的功能是求L中值等于x的结点序号的最小值, 当不存在这种结点时结果为0
/**
线性表的定位
@param pList 类型为SeqList结构体的指针
@param name 要查找的姓名
@return 查找到的元素的位置
*/
int locateSeqList(SeqList * pList, char name[]) {
int i = 0;
// 在顺序表中查找值为name的结点
while (i < pList->length && pList->data[i].name == name) {
i++;
}
// 若找到值为name的结点, 则返回结点的序号
if (i < pList->length) {
return i + 1;
} else { // 未找到值为name的结点, 返回0
return 0;
}
}
复制代码
总结
- 插入算法 , 时间复杂度为O(n). 一般情况下, 元素的移动次数为 n-i+1次, 平均移动次数为 n / 2
- 删除算法, 时间复杂度为O(n). 元素的平均移动次数为 (n-1) / 2
- 定位算法, 比较操作的时间复杂度为O(n). 求表长和读表元素算法的时间复杂度为O(1)
顺序表的优点
- 无需为表示结点之间的逻辑关系而增加额外存储空间
- 可以方便地随机存取表中的任一结点
顺序表的缺点
- 插入和删除运算不方便, 必须移动大量的结点
- 顺序表要求占用连结的空间, 当表长变化较大时, 难以确定合适的存储规模
线性表的链接存储
链接方式存储的线性表称为链表. Link List
特点
- 用一组任意的存储单元来存放
- 链表中结点的逻辑次序和物理次序不一定相同. 还必须存储指示其后继结点的地址信息
结点的结构
{% asset_img node.png %}
数据域, 又为data域, 存放结点中的数据
指针域, 又为next域, 存放结点的直接后继的地址
单链表的类型定义
所有结点通过指针链接而组成单链表.
NULL称为为空指针
Head称为头指针, 存放链表中的第一个结点的地址
单链表中第一个结点内一般不存数据, 称为为头结点
常见的单链表的四种形式
单链表的特点
- 起始结点又称为首结点. 没有直接前驱, 故设头指针head指向首结点
- 链表由头指针唯一确定, 单链表可以用头指针的名字来命名.
- 终端结点又称为尾结点. 没有直接后继, 故终端结点的指针域为空, 即NULL
- 除头结点之外的结点为表结点
- 为运算操作方便, 头结点中不存数据
单链表的初始化
// 声明一个结构体类型
typedef struct {
char name[12];
int age;
} DataType;
// 声明一个单链表类型
typedef struct node {
DataType data; // 数据域
struct node * next; // 指针域
} Node, * LinkList;
/**
初始化一个单链表
@return 初始化好的单链表
*/
LinkList initiateLinkList() {
// 头指针
LinkList head;
// 动态创建一个结点, 也就是头结点, 然后让头指针指向这个结点
head = malloc(sizeof(Node));
// 头结点的指针域为NULL
head->next = NULL;
// 返回这个单链表
return head;
}
复制代码
插入
插入运算是将值为x的新结点 插入到表的第i个结点的位置上, 即插入到ai-1与ai之间
/**
插入
@param list 单链表
@param x 要插入的数据元素
@param i 要插入的位置
*/
void insertLinkList(LinkList list, DataType x, int i) {
LinkList s, p;
if (i == 1) {
p = list;
} else {
// 找到第 i-1 个数据元素结点
p = getLinkList(list, i - 1);
}
if (p == NULL) {
// 如果不存在, 返回错误信息
printf("找不到插入的位置");
} else{
// 生成新结点并初始化
s = malloc(sizeof(Node));
s->data = x;
// 新结点的指针域指向第i个元素
s->next = p->next;
// 第 i-1 个结点的指针域指向新结点
p->next = s;
}
}
复制代码
删除
在单链表中删除第i个结点的基本操作为: 找到线性表中第i-1个结点, 修改其指向直接后继的指针
/**
删除
@param list 单链表
@param i 要删除结点的位置
*/
void deleteLinkList(LinkList list, int i) {
LinkList p;
// 找到待删结点的直接前驱
if (i == 1) {
p = list;
} else {
p = getLinkList(list, i-1);
}
// 若直接前驱存在, 且待删结点存在
if (p != NULL && p->next != NULL) {
// q指向待删结点
LinkList q = p->next;
// 移出待删结点
p->next = q->next;
// 释放已经移出的结点
free(q);
} else {
printf("要不到要删除的结点");
}
}
复制代码
求表长
在单链表存储结构中, 线性表的长度等于单链表所含结点的个数(不含头结点)
/**
求表的长度
@param list 单链表
@return 表的长度
*/
int LengthLinkList(LinkList list) {
// 用于计数
int j = 0;
// p指向头指针
LinkList p = list;
// 当下一个结点不为空时, 计数加1, p指向下一个结点
while (p->next != NULL) {
j++;
p = p->next;
}
// 返回表的长度
return j;
}
复制代码
读表元素
/**
读表元素
@param list 单链表
@param i 要读取的位置
@return 如果有返回读取到的元素, 没有则返回NULL
*/
LinkList getLinkList(LinkList list, int i) {
// 用于计数
int j = 1;
// p指向首结点
LinkList p = list->next;
while (j < i && p != NULL) {
j++;
p = p->next;
}
// 如果j等于i, 则p指向的结点为要找的结点
if (j == i) {
return p;
} else { // 否则没有要找的结点
return NULL;
}
}
复制代码
定位
定位运算是给定表元素的值, 找出这个元素的位置
/**
定位
@param list 单链表
@param name 要查找的结点的名字
@return 如果查找到了, 则返回结点的位置, 如果没有, 则返回0
*/
int locateLinkList(LinkList list, char name[]) {
int j = 0;
// p指向首结点
LinkList p = list->next;
// 查找结点
while (p != NULL && !(strcmp(p->data.name, name) == 0)) {
j++;
p = p->next;
}
// 如果有则返回序号, 没有返回0
if (p != NULL) {
return j + 1;
} else {
return 0;
}
}
复制代码
其它链表
循环链表
普通链表的终端结点的next值为NULL
循环链表的终端结点的next指向头结点
特点
在循环链表中, 从任一结点出发都能扫描整个链表
双向循环链表
在链表中设置两个指针域, 一个指向后继结点, 一个指向前驱结点, 这就是双向链表
空双向循环链表
非空双向循环链表
双向链表中结点的删除
设p指向待删结点, 删除*p可这样做
p -> prior -> next = p -> next; // p前驱结点的直接后继指向p的后继结点
p -> next -> prior = p -> prior; // p后继结点的直接前驱指向p的前驱结点
free(p); // 释放*p的空间
双向链表中结点的插入
在p所指后面插入一个新的结点t, 插入可这样做
t -> next = p -> next;
t -> prior = p;
p -> next -> prior = t;
p -> next = t;
顺序实现与链接实现的比较
- 单链表的每个包括数据域和指针域, 指针域需要占用额外空间
- 从整体考虑, 顺序表要预分配存储空间, 如果预先分配得过大, 将造成浪费, 若分配得过小, 又将发生上溢;
- 单链表不需要预先分配空间, 只要内存内存空间没有耗尽, 单链表中的结点个数就没有限制.
顺序表的完整代码
#include <stdio.h>
// 定义一个结构体类型
typedef struct {
char name[10];
int age;
} DataType;
// 顺序表的结构体定义
// 线性表的大小, 也就是最多存储多少个数据元素
const int maxSize = 100;
typedef struct {
// 所存放的数据类型为int, 最多存放100个元素的数组
DataType data[maxSize];
// 当前所存放的数据元素的个数
int length;
} SeqList;
/**
线性表的插入
@param pList 类型为SeqList的结构体指针
@param x 要插入的类型为DataType类型的数据元素
@param i 要插入的位置
*/
void insertSeqList(SeqList * pList, DataType x, int i) {
// 判断线性表是否已满
if (pList->length == maxSize) {
printf("表已满 \n");
return;
}
// 检查插入位置是否合法
if (i < 1 || i > (pList->length) + 1) {
printf("插入位置不合法 \n");
return;
}
// 进行插入操作
// 从表的最后一个位置依次进行后移
for (int j = pList->length; j >= i; j--) {
pList->data[j] = pList->data[j-1];
}
// 后移完成之后, 对空出的位置进行赋值
pList->data[i - 1] = x;
// 表的长度加1
pList->length++;
}
/**
线性表的删除
@param pList 类型为SeqList结构体的指针
@param i 要删除的位置
*/
void deleteSeqList(SeqList * pList, int i) {
// 检查删除位置是否合法
if (i < 1 || i > pList -> length) {
printf("删除位置不合法");
return;
}
// 进行删除操作, 依次左移
for (int j = i; j < pList->length; j++) {
pList->data[j - 1] = pList->data[j];
}
// 表的长度减1
pList->length--;
}
/**
线性表的定位
@param pList 类型为SeqList结构体的指针
@param name 要查找的姓名
@return 查找到的元素的位置
*/
int locateSeqList(SeqList * pList, char name[]) {
int i = 0;
// 在顺序表中查找值为name的结点
while (i < pList->length && pList->data[i].name == name) {
i++;
}
// 若找到值为name的结点, 则返回结点的序号
if (i < pList->length) {
return i + 1;
} else { // 未找到值为name的结点, 返回0
return 0;
}
}
int main(int argc, const char * argv[]) {
// insert code here...
// 创建一个结构体实例, 并进行初始化
DataType dt1 = {"alex", 18};
DataType dt2 = {"kevin", 16};
DataType dt3 = {"jack", 21};
// 创建一个空的线性表
SeqList list = {};
// 创建一个指针指向这个线性表
SeqList * pList = &list;
printf("插入前表的长度: %d \n", list.length);
// 插入操作
insertSeqList(pList, dt1, 1);
insertSeqList(pList, dt2, 1);
insertSeqList(pList, dt3, 1);
printf("插入后表的长度: %d \n", list.length);
// 删除操作
deleteSeqList(pList, 2);
printf("插入后表的长度: %d \n", list.length);
// 定位
int loc = locateSeqList(pList, "alex");
printf("alex在第%d位 \n", loc);
return 0;
}
复制代码
单链表的完整代码
#include <stdio.h>
#include <stdlib.h>
// 声明一个结构体类型
typedef struct {
char name[12];
int age;
} DataType;
// 声明一个单链表类型
typedef struct node {
DataType data; // 数据域
struct node * next; // 指针域
} Node, * LinkList;
/**
初始化一个单链表
@return 初始化好的单链表
*/
LinkList initiateLinkList() {
// 头指针
LinkList head;
// 动态创建一个结点, 也就是头结点, 然后让头指针指向这个结点
head = malloc(sizeof(Node));
// 头结点的指针域为NULL
head->next = NULL;
// 返回这个单链表
return head;
}
/**
求表的长度
@param list 单链表
@return 表的长度
*/
int LengthLinkList(LinkList list) {
// 用于计数
int j = 0;
// p指向头指针
LinkList p = list;
// 当下一个结点不为空时, 计数加1, p指向下一个结点
while (p->next != NULL) {
j++;
p = p->next;
}
// 返回表的长度
return j;
}
/**
读表元素
@param list 单链表
@param i 要读取的位置
@return 如果有返回读取到的元素, 没有则返回NULL
*/
LinkList getLinkList(LinkList list, int i) {
// 用于计数
int j = 1;
// p指向首结点
LinkList p = list->next;
while (j < i && p != NULL) {
j++;
p = p->next;
}
// 如果j等于i, 则p指向的结点为要找的结点
if (j == i) {
return p;
} else { // 否则没有要找的结点
return NULL;
}
}
/**
插入
@param list 单链表
@param x 要插入的数据元素
@param i 要插入的位置
*/
void insertLinkList(LinkList list, DataType x, int i) {
LinkList s, p;
if (i == 1) {
p = list;
} else {
// 找到第 i-1 个数据元素结点
p = getLinkList(list, i - 1);
}
if (p == NULL) {
// 如果不存在, 返回错误信息
printf("找不到插入的位置");
} else{
// 生成新结点并初始化
s = malloc(sizeof(Node));
s->data = x;
// 新结点的指针域指向第i个元素
s->next = p->next;
// 第 i-1 个结点的指针域指向新结点
p->next = s;
}
}
/**
删除
@param list 单链表
@param i 要删除结点的位置
*/
void deleteLinkList(LinkList list, int i) {
LinkList p;
// 找到待删结点的直接前驱
if (i == 1) {
p = list;
} else {
p = getLinkList(list, i-1);
}
// 若直接前驱存在, 且待删结点存在
if (p != NULL && p->next != NULL) {
// q指向待删结点
LinkList q = p->next;
// 移出待删结点
p->next = q->next;
// 释放已经移出的结点
free(q);
} else {
printf("要不到要删除的结点");
}
}
/**
定位
@param list 单链表
@param name 要查找的结点的名字
@return 如果查找到了, 则返回结点的位置, 如果没有, 则返回0
*/
int locateLinkList(LinkList list, char name[]) {
int j = 0;
// p指向首结点
LinkList p = list->next;
// 查找结点
while (p != NULL && !(strcmp(p->data.name, name) == 0)) {
j++;
p = p->next;
}
// 如果有则返回序号, 没有返回0
if (p != NULL) {
return j + 1;
} else {
return 0;
}
}
int main(int argc, const char * argv[]) {
// insert code here...
Node list = {};
LinkList pList = &list;
DataType dt1 = {"jack", 18};
DataType dt2 = {"kevin", 20};
// 插入元素
insertLinkList(pList, dt1, 1);
insertLinkList(pList, dt2, 1);
// 读表元素
LinkList p = getLinkList(pList, 2);
printf("%s \n", p->data.name);
// 删除元素
// deleteLinkList(pList, 2);
// deleteLinkList(pList, 1);
// 读表长
int length = LengthLinkList(pList);
printf("表的长度为: %d \n", length);
// 定位
int loc = locateLinkList(pList, "kevin");
printf("在第%d位 \n", loc);
return 0;
}
复制代码