零个或多个数据元素的有限序列。
线性表元素个数:n (n
≥
\geq
≥ 0),当n=0时,称为空表。
非空表中的每个数据元素都有一个确定的位置:位序。
在比较复杂的线性表中,一个数据元素可以由若干个数据项组成。
当传递参数给函数时,它在函数内是否会被改动决定了使用什么参数形式。
线性表的每个元素类型都相同!
数组长度与线性表长度区别:
一般高级语言可以实现动态分配数组。
数组的下标是从0至n-1,用数组存储顺序表意味着要分配固定长度的数组空间。
// 获取线性表中第i个位置的元素,并用e返回
int GetElem(SqList L, int i, ElemType *e){
bool ret = true;
// 若线性表长度为0 或 位置值小于1 或 线性表长度小于位置i则返回错误
if (L.length == 0 || i < 1 || L.length < i){
ret = false;
} else{
*e = L.data[i - 1];
}
return ret;
}
// 在位置i处插入元素e
int ListInsert(SqList *L, int i, ElemType e){
bool ret = true;
// 线性表满时
// 当i插入位置低于第一位 或 当前插入位置超出线性表长度
if ((L->length == MAXSIZE) || (i < 1 || L->length + 1 < i)){
ret = false;
} else{
// 将要插入位置的所有元素向后移1位
for (int j = L->length - 1; i - 1 <= j; j--) {
L->data[j + 1] = L->data[j];
}
L->data[i - 1] = e;
L->length++;
}
return ret;
}
// 删除位置i上的元素,并用e返回
int ListDelete(SqList *L, int i, ElemType *e){
bool ret = true;
// 若删除位置不合理
if ((L->length == 0) || (i < 1 || L->length < i)){
ret = false;
} else{
*e = L->data[i - 1];
// length: 5 0,1,2,3,4
for (int j = i; j < L->length; j++) {
// 删除i位置的元素,将其余后面的元素向前移
L->data[j] = L->data[j + 1];
}
L->length--;
}
return ret;
}
线性表的链式存储结构:n个节点链结成一个链表 -> 单链表
特点:是用一组任意的存储单元存储线性表的数据元素,可以连续或不连续。
顺序存储结构中,每个数据元素只需要存储数据元素信息
链式存储结构中,除了存储数据元素信息,还需要存储后继元素的存储地址
头指针:链表中第一个节点存储位置
头节点:单链表的第一个节点前附设一个节点
请添加图片描述
头指针与头节点的异同:
若线性表为空,则头节点的指针域为"NULL"
typedef int ElemType;
typedef struct Node{
ElemType data;
struct Node *next;
}Node;
typedef Node *LinkList;
节点由存放数据元素的数据域和存放后继节点的指针域组成
在单链表中,由于第 i 个元素到底在哪没办法一开始就知道,必须得从头开始找。
思路:
// 查找第i个元素,e返回数据值
int GetElem(LinkList L, int i, ElemType *e){
int j = 1;
bool ret = true;
// 声明p节点
LinkList p;
p = L->next;
// 指针p不为空,或j不等于i时
while (p && j < i){
p = p->next;
++j;
}
if (!p || i < j){
ret = false;
} else{
*e = p->data;
}
return ret;
}
只需要让s->next和p->next的指针做一点改变即可。
s-next = p->next;
p->next = s;
表头与表尾的特殊情况是相同的。
思路:
// 插入
int Insert(LinkList *L, int i, ElemType e){
LinkList p, s;
p = *L;
int ret = true, j = 1;
// 当jnext;
++j;
}
if (!p || i < j) {
ret = false;
} else{
s = (LinkList) malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
}
return ret;
}
其实就是将前继节点的指针绕过,指向它的后继节点即可。
思路:
// 删除
int Delete(LinkList *L, int i, ElemType *e){
LinkList p, q;
p = *L;
int ret = true, j = 1;
while (p->next && j < i){
p = p->next;
++j;
}
// 末尾为空的情况,表示当前删除元素为
if (!(p->next) || i < j){
ret = false;
} else{
q = (LinkList) malloc(sizeof(Node));
q = p->next;
p->next = q->next;
*e = q->data;
}
return ret;
}
数组的初始化,声明一个类型和大小的数组并赋值的过程。
创建一个单链表的过程就是一个动态生成链表的过程,从“空表”的初始状态起,依次建立元素节点,并逐个插入链表。
思路
头插法
// 整表创建(头插法) 随机生成n个元素的值
void CreateListHead(LinkList *L, int n){
LinkList p;
// 初始化随机种子
srand(time(0));
*L = (LinkList) malloc(sizeof(Node));
(*L)->next = NULL;
for (int i = 0; i < n; i++) {
p = (LinkList) malloc(sizeof(Node));
p->data = rand() % 100 + 1; // 随机生成100以内的数字
p->next = (*L)->next;
// 插入到表头
(*L)->next = p;
}
}
尾插法
// 整表创建(尾插法)
void CreateLinkTail(LinkList *L, int n){
LinkList p, r;
srand(time(0));
*L = (LinkList) malloc(sizeof(Node)); // 单链表
r = *L; // 指向尾部节点 r: 指向尾节点的变量, 不断变化 L则随着循环增长为一个多节点的链表
for (int i = 0; i < n; i++) {
p = (Node *) malloc(sizeof(Node));
p->data = rand() % 100 + 1;
r->next = p; // 将表尾终端节点的指针指向新节点
r = p; // 将当前的新节点定义为表尾终端节点
}
r->next = NULL;
}
思路
// 整表删除
int ClearList(LinkList *L){
bool ret = true;
LinkList p, q;
p = (*L)->next;
while (p){
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
return ret;
}
用数组表述的链表
数据域data存放数据元素; cur相当于单链表中的next指针,存放该元素的后继在数组中的下标。
第一个与最后一个元素不存数据。
备用链表:未被使用的数据元素
// 初始化
void InitList(StaticLinkList space){
// 从下标 0 至 MAXSIZE - 1 的游标设置为 i + 1 for (int i = 0; i < MAXSIZE; i++) {
space[i].cur = i + 1;
}
// 最后一个下标的游标设置为0,表示数组为空
space[MAXSIZE].cur = 0;
}
如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放。
辨明数组中哪些分量未被使用,解决办法是将所有未被使用过的及已被删除的分量用游标链成一个备用链表,每当插入时,可以从备用链表上取得第一个结点作为待插入的新结点。
// 插入
int ListInsert(StaticLinkList L, int i, ElemType e){
int j, k;
bool ret = true;
// 获得空闲分量的下标
j = Malloc_SSL(L);
// 获得最后一个元素的下标
k = MAXSIZE - 1;
if ((i < 1 || ListLength(L) + 1 < i) || !(j)){
ret = false;
}
switch (ret) {
case false:
break;
case true:
L[j].data = e;
// 找到第i个元素之前的位置
for (int l = 1; l < i; l++) {
k = L[k].cur;
}
// 将第i个元素之间的cur赋值给新元素的cur
L[j].cur = L[k].cur;
// 将新元素的下标赋值给之前元素的cur
L[k].cur = j;
break;
}
return ret;
}
同插入类似
// 删除
int ListDelete(StaticLinkList L, int i) {
bool ret = true;
int j, k;
// 获得最后一个元素的下标
k = MAXSIZE - 1;
// 下标不在范围
if (i < 1 || ListLength(L) < i) {
ret = false;
}
switch (ret) {
case false:
break;
case true:
for (j = 1; j < i; j++) {
k = L[k].cur;
}
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L, j);
break;
}
return ret;
}
静态链表优点:
静态链表只是为了给没有指针的高级语言设计的一种实现单链表能力的方法! 尽管不一定用上,但这样的思考方式是非常巧妙的,应该理解其思想,以备不时之需。
将单链表中终端节点的指针端由空指针改为指向头节点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表——简称循环链表。
为了使空链表与非空链表处理一致,通常设置一个头节点(并非一定要头节点)!
循环链表与单链表的主要差异就在循环的判断条件上。
p -> next
p -> next不等于头节点
双向链表是在单链表的每个节点中,再设置一个指向其前驱节点的指针域。
typedef struct DulNode{
ElemType data;
struct DulNode *prior;
struct DulNode *next;
}DulNode, *DulLinkList;
既然单链表也可以有循环链表,那么双向链表当然也可以是循环表。
双向链表是单链表中扩展出来的结构,很多操作和单链表都是相同的。
s -> prior = p;
s -> next = p -> next;
p -> next -> prior = s;
p -> next = s;
关键在于它们更改的顺序!如何插入理解了,删除也就比较简单了
线性表的两种结构式后面其它数据结构的基础,把它们学明白了对后面的学习有着至关重要的作用!