线性表的顺序表示
文章目录
- 线性表的顺序表示
- 线性表的链式表示
-
- 单链表(LinkList)
-
- 表示方法
- 基本操作
-
- 初始化单链表 `InitList(LinkList &L)`
- 求表长 `getListLen(LinkList L)`
- 查找:按“位序”查找元素并返回LNode *结点 `GetElem(LinkList L, int i)`
- 查找:按值查找 `LocateElem(LinkList L, ElemType e)` 返回结点
- 插入:💯 在指定 p 结点之后插入元素 e,后插结点 `InsertNextNode(LNode *p, ElemType e)`
- 插入:在指定 p 结点之前插入元素 e,前插结点 `InsertPriorNode(LNode *p, ElemType e)`
- 插入:💯 按“位序”插入数据 `ListInsert(LinkList &L, int i, ElemType e)`
- 初始化 & 插入:头插法 `List_HeadInsert(LinkList &L)`
- 初始化 & 插入:尾插法 `List_TailInsert(LinkList &L)`
- 逆置:逆置单链表 `ListReverse(LinkList &L)`
- 删除:删除单链表中位序为 i 的元素,并用 e 返回 `ListDelete(LinkList &L, int i, ElemType &e)`
- 删除:删除指定 p 结点,并返回数据 e 的值 `DeleteNode(LNode *p)`
- 循环单链表
- 双链表(DLinkList——Double Link List)
- 循环双链表
- 静态链表(SLinkList——Static Link List)
- Conclusion
🔸 随机访问,通过首地址和元素符号可以在 O(1)内找到指定的元素
🔸 存储密度高,每个节点只存储数据元素。
🔸 逻辑上相邻的元素物理上也相邻,所以插入和删除操作需要移动大量元素。
静态分配(SqList)
表示方法
使用结构体表示静态分配的顺序表
结构体内容包括:
- 静态数组——存储数据元素,根据需要的 MaxSize 初始化,为其开辟存储空间。
- length——存储当前顺序表的长度
struct SqList {
ElemType data[MaxSize];
int length; // 顺序表当前长度
};
typedef struct SqList {
ElemType data[MaxSize];
int length; // 顺序表当前长度
}SqList;
基本操作
初始化静态分配的顺序表 InitList(SqList &L)
🔸 初始化时,必须将当前顺序表长度置为 0。
🔸 没必要给每个元素赋值。
void InitList(SqList &L){
// 引用调用 & ,带回改变的内容
L.length = 0; // 初始化时,必须将当前顺序表长度置为 0。
// 其他初始化操作
}
动态分配(SeqList)
表示方法
使用结构体表示动态分配的顺序表
静态分配的顺序表使用静态数组初始化,当数据长度大于事先设置好的 MaxSize 后,只能自暴自弃了,而动态分配可以解决这一问题。
动态分配使用的依旧是数组的思想:开辟一片新的更长的连续空间,再将之前的部分复制过来,最后释放先前的存储空间,基本操作中需要增加一个 IncreaseSize(SeqList &L, length)
来实现该操作。
结构体内容包括:
- 数据指针——指示动态分配数组的指针。它指向的是数组的首地址。
- length——存储当前顺序表的长度。
- 顺序表的最大容量
typedef struct SeqList {
ElemType *data;
int MaxSize; // 动态增加长度需要更改此值
int length; // 插入、删除元素需要更改此值
}SeqList;
基本操作
除特殊说明的操作外,以下操作静态动态均可使用。
初始化动态分配的顺序表 InitList(SeqList &L)
与静态顺序表不同,静态分配时在 struct 中通过数组初始化形式开辟了存储空间,而动态分配用指针指示数据,所以需要使用 malloc 或者 new 关键字来开辟存储空间。
InitList(SeqList &L){
L.data = (ElemType *)malloc(sizeof(ElemType) * InitSize);
// 在C++ 中也可以使用 new 关键字开辟存储空间。
L.data = new ElemType[InitSize];
L.length = 0;
L.MaxSize = InitSize;
}
增加动态顺序表长度 IncreaseSize(SeqList &L, int len)
🔹 len 是扩展长度还是增加的长度需要具体问题具体看待。
🔸 步骤:
- 根据需要增加的长度 len 和当前长度 L.length,开辟一块新的连续存储空间,长度为 (len + length) * sizeof(ElemType)。
- 将先前 L 中存储的数据(长度为 L.length)全部复制到新开辟的空间上。
- L.MaxSize 最大存储数据长度更新。第二步和第三步可更改顺序。
- 释放先前的存储空间。
void IncreaseSize(SeqList &L, int len) {
ElemType *p = L.data; // 用 p 指针指向先前数据首地址
L.data = (ElemType *)malloc( (len + L.length) * sizeof(ElemType) ); // step 1
//或者使用 new 开辟空间: L.data = new ElemType[len + L.length];
for (int i = 0; i < L.length; i++) {
// step 2
L.data[i] = p[i]; // 或者采用指针操作方式: *(L.data + i) = *(p + i);
}
L.MaxSize = L.MaxSize + len; // step 3
free(p); // step 4
// 或者使用 delete 释放空间: delete(p);
p = NULL;
}
插入:按位序插入数据元素 ListInsert(SeqList &L, int i, ElemType e)
在顺序表某个“位序”插入数据元素,应当将该位序上及该位序以后的元素全部后移 1 位,然后给该位序元素赋值。
🔸 为保证程序鲁棒性,需要考虑:
- 指定位序是否在有效范围(1~L.length即在当前长度范围内)内。
- 顺序表已满,无法继续插入元素时,继续插入元素会报错。
🔸 步骤:
- 判断指定的位序是否在有效范围。
- 判断顺序表是否已满。
- 将该位序上及该位序以后的元素全部后移 1 位,然后给该位序元素赋值(下标为 i - 1)。
- L.length ++;
🔸 从某个下标开始,全部元素后移一位,需要从末尾开始循环,将前面一个元素赋值给它的后继元素,L.length 作为下标恰好是末尾元素的后继元素,i 即位序,i 作为下标是位序元素的后继元素。
- 站在被赋值的后面元素角度上看:从末尾元素的后继元素(下标为 L.length )到 位序元素的后继元素(下标为 i)均需要其前驱元素给其赋值,所以循环下标需要从 L.length 至 i。
- 站在前面赋值元素的角度上看:同理,从末尾元素(下标为 L.length - 1)到“位序元素”(下标为 i - 1)都需要给后继元素赋值。
bool ListInsert(SeqList &L, int i, ElemType e) {
if(i < 1 || i > L.length)
return flase;
if(L.Length == L.MaxSize)
return flase;
for(int index = L.length; index >= i; index --) {
data[index] = data[index - 1];
}
data[i - 1] = e;
L.length ++;
return true;
}
删除:按位序删除数据元素,并用 e 返回 ListDelete(SeqList &L, int i, ElemType e)
🔹 i 位序,e 被删除的元素——靠指针或者引用传递来“带回”。
与插入同理,删除只需将“位序”后的元素全部前移 1 位即可。
🔸 为保证程序鲁棒性,需要考虑:
- 指定位序是否在有效范围(1~L.length即在当前长度范围内)内。
- 顺序表为空,无法删除数据。
🔸 步骤:
- 判断鲁棒性
- “位序”后元素前移 1 位。
- L.length–;
【注】前移 1 位,操作起来应该从前往后。i 为下标对应“位序”元素的后继。(两种写法)
- 站在前面被赋值元素的角度看:从“位序元素”(下标为 i - 1)开始,至末尾元素的前驱(下标为 L.length - 2),每次循环中元素的后继赋值给它。
- 站在后面给前面赋值的元素角度看:从“位序元素”的后继元素(下标是 i )到末尾元素(下标 L.length - 1),每次循环中给前驱元素赋值。
bool ListDelete(SeqList &L, int i, Elemtyep &e) {
if(i < 1 || i > L.length)
return flase;
if(L.length == 0)
return flase;
e = data[i - 1];
for (int index = i - 1; index <= L.length - 2; index ++) {
data[index] = data[index + 1];
}
// 或
for (int index = i; index <= L.length -1; index ++) {
data[index - 1] = data[index];
}
L.length--;
}
查找:按“位序”查找元素并返回 GetElem(SeqList L, int i)
🔸 返回类型是 ElemType,i 是位序,对应下标是 i - 1。
ElemType GetElem(SeqList L, int i) {
return L.data[i - 1];
}
查找:按值查找,返回其位序 LocateElem(SeqList L, ElemType e)
🔸 注意返回的是位序还是下标
int LocateElem(SeqList L, ElemType e) {
for (int i = 0; i < L.length; i++) {
if (L.data[i] == e)
return i + 1; // 位序 = index + 1
}
return 0; // 没找到与 e 相等的元素,即返回 0
}
线性表的链式表示
🔸 对每个链表结点,除存放元素自身的信息外,还需要存放指向其前驱、后继的指针。
🔸 增 删结点方便。
🔸 查找结点需遍历,麻烦。
单链表(LinkList)
表示方法
使用多个内部带有指向后继结点指针的结构体构成一个单链表
单链表的数据元素由结构体构成,其组成:
// c 写法:
typedef struct LNode {
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// cpp 写法:
typedef struct LNode {
ElemType data;
LNode *next;
}*LinkList; // 无需 typedef LNode 了,因为 C++ 中 struct 类名即可当做类型来声明。
❗ 注意,typedef struct LNode {...;} *LinkList;
将结构体类型和结构体指针类型分别命名为 LNode 和 LinkList,这就意味着:struct LNode *p
等价于 LinkList p
。
这么做并非无理取闹、哗众取宠,而是旨在让代码更具有可读性——LNode * elem
代表指向每个结点的指针,而 LinkList L
则代表指向单链表首地址的指针。
🔸 因此不要忘记,LinkList 声明的是一个 struct LNode 类型的指针,因此在取结构体内容时务必使用 ->
!
那么我们就有了单链表的一些规范:
为结点开辟存储空间,我们用 LNode 配合 malloc 或者 new。
LNode * node = (LNode *)malloc(sizeof(LNode));
LNode * node