文章目录
线性表的基本概念
- 线性表:n个同类型数据元素的有限序列,记为 L=(a1,a2,…,ai,an);i为数据元素ai在线性表中的位序;n为线性表的表长;n=0时成为空表。
- 线性表的特点
- 数据元素类型相同
- i位序从1开始
- 除第一个元素外,每个元素都有一个直接前驱
- 除最后一个元素外,每个元素都有一个直接后继
- 线性表的抽象数据类型
ADT List { 1. 数据对象 Data 2. 数据关系 3. 基本操作 Operation 3.1 结构初始化操->线性表的建立 3.2 结构销毁操作->线性表的销毁 3.3 线性表元素的常规操作->求表长,定位数据元素,得到数据元素,对线性表的插入和删除等基本操作。 }
线性表的顺序存储结构
概述
-
线性表的顺序存储结构:指的是用一组连续的存储单元一次存储线性表中的各个元素 (逻辑上相邻的元素物理上也相邻) 。通过数据元素物理存储的连续性来反映数据元素在逻辑上的相邻关系。采用顺序存储结构的线性表通常叫做顺序表。
-
顺序存储结构的优缺点
线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1)。而在插入或删除时,时间复杂度都是O(n)。
这就说明,它比较适合元素个数比较稳定,不经常插入和删除元素,而更多的操作是存取数据的应用。随机存取的理解(逻辑转化到计算机底层去思考)
1)顺序存储结构的地址在内存中是连续的所以可以通过计算地址实现随机存取,与此相对的链式存储结构的存储地址不一定连续,只能通过第个结点的指针顺序存取。
2)线性表的顺序存储结构可以通过线性表的首址加偏移的方法计算出来第i个数据的位置a+i*sizeof(单个结构)而线性表的链式存储结构要访问第i个数据,就必须先访问前面的i-1个数据- 优点:随机存取时间复杂度为O(1),节点之间的逻辑关系没有占用额外的存储空间。
- 缺点:插入和删除操作可能需要移动大量的元素,时间复杂度为O(n)。
顺序存储结构的实现(基础方法)
#include <stdio.h>
#include <stdlib.h>
#define LIST_INIT_SIZE 100// 线性表存储空间的初始分配量
#define LIST_INCREAMENT 10// 线性表存储空间的分配增量
// 状态常量
#define TRUE 1 // 真/是
#define FALSE 0 // 假/否
#define OK 1 // 通过/成功
#define ERROR 0 // 错误/失败
typedef int Status;
typedef int ElemType;
typedef struct SqList
{
ElemType *elem;
int length;// 表长/顺序表中的元素个数
int list_size;// 表的内存所占空间
} SqList;
/*
*
* 初始化
*
* 初始化成功则返回OK,否则结束程序。
* 时间复杂度 O(1)
*/
Status InitList(SqList* L)
{
L->elem = (ElemType*) malloc(sizeof(SqList)*LIST_INIT_SIZE);
if (L->elem == NULL)
exit(-1);
L->length = 0;
L->list_size = LIST_INIT_SIZE;
return OK;
}
/*
* 销毁(结构)
*
* 释放顺序表所占内存。
*/
Status DestroyList(SqList* L)
{
// 确保顺序表存在
if (L == NULL || L->elem == NULL)
return ERROR;
// 释放顺序表内存
free(L->elem);
//释放内存后置空指针
L->elem = NULL;
L->length = 0;
L->list_size = 0;
return OK;
}
/*
* 置空(内容)
*
* 只是清理顺序表中存储的数据,不释放顺序表所占内存。
* 时间复杂度 O(1)
*/
Status ClearList(SqList* L)
{
// 确保顺序表存在
if (L == NULL || L->elem == NULL)
return ERROR;
L->length = 0;
return OK;
}
/*
* 判空
*
* 判断顺序表中是否包含有效数据。
*
* 返回值:
* TRUE : 顺序表为空
* FALSE: 顺序表不为空
* 时间复杂度 O(1)
*/
Status ListEmpty(SqList L)
{
return L.length == 0? TRUE : FALSE;
}
/*
* 计数
*
* 返回顺序表包含的有效元素的数量。
*/
int ListLength(SqList L)
{
return L.length;
}
/*
* 取值
*
* 获取顺序表中第i个元素,将其存储到e中。
* 如果可以找到,返回OK,否则,返回ERROR。
*
*【备注】
* 教材中i的含义是元素位置,从1开始计数,但这不符合编码的通用约定。
* 通常,i的含义应该指索引,即从0开始计数。
* 时间复杂度 O(1)
*/
Status GetElem(SqList L, int pos, ElemType* e)
{
// pos的范围 [1, lenght]
if (pos < 1 || pos > L.length)
return ERROR;
*e = L.elem[pos -1];
return OK;
}
/*
*
* 查找
*
* 返回顺序表中首个与e满足Compare关系的元素位序。
* 如果不存在这样的元素,则返回0。
*
*【备注】
* 元素e是Compare函数第二个形参
* L是一个线性表,e是一个指定数据元素,compare()是数据元素判定函数,
* LocateElem表达的意思是,在L中找到第1个与e元素满足compare()条件的数据元素的位序并返回,
* 若不存在则返回0。compare()可以代表等于、大于、小于等判定关系。
* 时间复杂度 O(n)
*/
int LocateElem(SqList L, ElemType e, Status(Compare)(ElemType, ElemType))
{
int i;
ElemType* p;
if (L.elem == NULL)
return ERROR;
// i表示位序
i = 1;
p = L.elem;
while(i <= L.length &&!Compare(*p++, e))
i++;
if (i <= L.length)
return i;
else
return 0;
}
/*
* 前驱
*
* 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱
* 否则操作失败
* 获取元素cur_e的前驱,
* 如果存在,将其存储到pre_e中,返回OK,
* 如果不存在,则返回ERROR。
*/
Status PriorElem(SqList L, ElemType cur_e, ElemType* pre_e)
{
int i;
// 确保顺序表结构存在,至少包含两个元素
if (L.elem == NULL || L.length < 2)
return ERROR;
// 这里的i初始化为第1个元素的【索引】
i = 0;
// 因为要保证cur_e不能是第一个所以i从第2个元素开始遍历
for(i = 1; i < L.length; i++)
{
if (L.elem[i] == cur_e)
{
*pre_e = L.elem[i-1];
return OK;
}
}
return ERROR;
}
/*
* 后继
* 操作结果:若cur_e是L的数据元素,且不是最后一个,则用pre_e返回它的后继
* 获取元素cur_e的后继,
* 如果存在,将其存储到next_e中,返回OK,
* 如果不存在,则返回ERROR。
*/
Status NextElem(SqList L, ElemType cur_e, ElemType* next_e)
{
int i;
// 确保顺序表结构存在,且最少包含两个元素
if(L.elem == NULL || L.length < 2)
{
return ERROR;
}
// 这里的i初始化为第1个元素的【索引】
i = 0;
// 从第1个元素开始,查找cur_e的位置
while(i < L.length-1 && L.elem[i] != cur_e)
{
++i;
}
// 如果cur_e是最后1个元素(没有后继),或者没找到元素cur_e,返回ERROR
if(i >= L.length-1)
{
return ERROR;
}
// 存储cur_e的后继
*next_e = L.elem[i + 1];
return OK;
}
/*
*
* 插入
*
* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
*【备注】
* 教材中i的含义是元素位置,从1开始计数,插入范围[1,ListLength(L) + 1]
* 为什么length+1,因为要满足从表尾插入;右移操作完全可以用数组来实现,这里按照课本
*/
Status ListInsert(SqList* L, int i, ElemType e)
{
ElemType* newBase;
ElemType* p,* q;
// 确保顺序表结构存在
if (L == NULL || L->elem == NULL)
return NULL;
// 判断插入位置是否合法
if (i < 1 || i > L->length + 1)
return ERROR;
//存储空间已满,则增加新空间
if (L->length >= L->list_size)
{
// 使用realloc函数基于现有空间扩容
newBase = (ElemType*) realloc((L->elem), (L->list_size + LIST_INCREAMENT)*sizeof(SqList));
if (newBase == NULL)
exit(-1);
// 新基址
L->elem = newBase;
L->list_size += LIST_INCREAMENT;
}
// ***右移腾空
/*
for (int j = L->length - 1; j >= (i-1); j--)
{
L->elem[j + 1] = L->elem[j];
}
L->elem[i-1]=e;
*/
// q为插入位置
q = &(L->elem[i-1]);
// 1.右移元素,腾出位置
for(p = &(L->elem[L->length-1]); p >= q; --p)
{
*(p + 1) = *p;
}
// 2.插入e
*q = e;
// 3.表长增1
L->length++;
return OK;
}
/*
* 删除
*
* 删除顺序表第i个位置上的元素,并将被删除元素存储到e中。
* 删除成功则返回OK,否则返回ERROR。
*
*【备注】
* 教材中i的含义是元素位置,从1开始计数
*/
Status ListDelete(SqList* L, int i, ElemType* e)
{
ElemType* p,* q;
// 确保顺序表结构存在
if (L == NULL || L->elem == NULL)
return NULL;
// i值越界
if (i < 1 || i > L->length)
return ERROR;
// p为删除元素的位置
p = &(L->elem[i - 1]);
*e = *p;
// 表尾位置
q = (L->elem) + L->length - 1;
//左移表长自减
for(++p; p <= q; p++)
*(p-1)=*p;
L->length--;
return OK;
}
/*
* 遍历
*
* 用visit函数访问顺序表L
*/
void ListTraverse(SqList L, void (Visit)(ElemType))
{
int i;
for(i = 0; i < L.length; i++)
{
Visit(L.elem[i]);
}
printf("\n");
}
// 判断data=e是否成立
Status CmpEqual(ElemType data, ElemType e)
{
return data == e ? TRUE : FALSE;
}
// 打印元素
void PrintElem(ElemType e)
{
printf("%d ", e);
}
/*
* 测试
*/
int main()
{
SqList L;
int i;
ElemType e;
printf(" InitList \n");
InitList(&L);
printf(" ListEmpty \n");
if(ListEmpty(L) == TRUE)
printf("L 为空!\n");
else
printf("L 不为空!\n");
printf(" ListInsert \n");
for(i = 1; i <= 8; i++)
{
ListInsert(&L, i, i*2);
}
printf(" ListTraverse \n");
ListTraverse(L, PrintElem);
printf(" ListLength \n");
i = ListLength(L);
printf("L的长度为 %d \n", i);
printf(" ListDelete \n");
printf("删除前的元素:L = ");
ListTraverse(L, PrintElem);
printf("尝试删除 L 中第 6 个元素...\n");
if(ListDelete(&L, 6, &e) == OK)
{
printf("删除成功,被删除元素是:\"%d\"\n", e);
}
else
{
printf("删除失败,第 6 个元素不存在!\n");
}
printf("删除后的元素:L = ");
ListTraverse(L, PrintElem);
printf(" GetElem \n");
GetElem(L, 3, &e);
printf("L 中第3个位置的元素为 %d \n", e);
printf(" LocateElem \n");
i = LocateElem(L, 10, CmpEqual);
printf("L 中第一个等于 10 的元素的位置是:%d\n", i);
printf(" PriorElem \n");
ElemType cur_e = 6;
if(PriorElem(L, cur_e, &e) == OK)
{
printf(" 元素 \"%d\" 的前驱为 \"%d\" \n", cur_e, e);
}
else
{
printf("元素 \"%d\" 的前驱不存在!\n", cur_e);
}
printf(" NextElem \n");
{
ElemType cur_e = 6;
if(NextElem(L, cur_e, &e) == OK)
{
printf(" 元素 \"%d\" 的后继为 \"%d\" \n", cur_e, e);
}
else
{
printf(" 元素 \"%d\" 的后继不存在!\n", cur_e);
}
}
printf("ClearList \n");
ClearList(&L);
printf(" 清空 L 后:");
if(ListEmpty(L) == TRUE)
{
printf(" L 为空!!\n");
}
else
{
printf(" L 不为空!\n");
}
printf(" DestroyList \n");
DestroyList(&L);
printf(" 销毁 L 后:");
if(L.elem != NULL) {
printf(" L 存在!\n");
} else {
printf(" L 不存在!!\n");
}
return 0;
}
顺序存储结构的应用一 求并集
/*
* 应用 求并集
* A=A∪B
*
* 计算La与Lb的并集并返回。
* 由于生成的并集会拼接在La上,所以La的入参为指针类型。
*/
void Union(SqList* La, SqList Lb)
{
int La_len = ListLength(*La);
int Lb_len = ListLength(Lb);
ElemType e;
for (int i = 1; i <= Lb_len; i++)
{
GetElem(Lb, i, &e);
// 如果不在La中则插入
if (!LocateElem(*La, e, CmpEqual))
ListInsert(La, ++La_len, e);
}
}
顺序存储结构的应用二 非递减顺序表归并
/*
* 非递减顺序表归并:C=A+B
*
* 归并顺序表La和Lb,生成新的顺序表Lc。
* 其中,La、Lb、Lc均为非递减序列。
*/
void MergeSqList_1(SqList La, SqList Lb, SqList* Lc)
{
InitList(Lc);
int La_len = ListLength(La);
int Lb_len = ListLength(Lb);
ElemType ea, eb;
int i,j,k;
i = j = 1;
k = 0;
while(i <= La_len && j <= Lb_len)
{
GetElem(La, i, &ea);
GetElem(Lb, j, &eb);
if (ea <= eb)
{
ListInsert(Lc, ++k, ea);
i++;
}
else
{
ListInsert(Lc, ++k, eb);
j++;
}
}
// 如果Lb已遍历完,但La未遍历完,将La中剩余元素加入Lc
while(i <= La_len)
{
GetElem(La, i++, &ea);
ListInsert(Lc, ++k, ea);
}
// 如果La已已遍历完,但Lb未遍历完,将Lb中剩余元素加入Lc
while(j <= Lb_len)
{
GetElem(Lb, j++, &eb);
ListInsert(Lc, ++k, eb);
}
}
void MergeSqList_2(SqList La, SqList Lb, SqList* Lc) {
ElemType* pa, * pb, * pc;
ElemType* pa_last, * pb_last;
pa = La.elem; // 指向La第一个元素
pb = Lb.elem; // 指向Lb第一个元素
// 没有使用InitList创建Lc
(*Lc).list_size = (*Lc).length = La.length + Lb.length;
pc = (*Lc).elem = (ElemType*) malloc((*Lc).list_size * sizeof(ElemType));
if(pc == NULL) {
exit(-1);
}
pa_last = La.elem + La.length - 1; // 指向La最后一个元素
pb_last = Lb.elem + Lb.length - 1; // 指向Lb最后一个元素
// 如果La及Lb均未遍历完
while(pa <= pa_last && pb <= pb_last) {
if(*pa <= *pb) {
*pc++ = *pa++;
} else {
*pc++ = *pb++;
}
}
// 如果Lb已遍历完,但La还未遍历完,将La中剩余元素加入Lc
while(pa <= pa_last) {
*pc++ = *pa++;
}
// 如果La已遍历完,但Lb还未遍历完,将Lb中剩余元素加入Lc
while(pb <= pb_last) {
*pc++ = *pb++;
}
}
线性表的链式存储结构
概述
用一组地址任意的存储单元存储数据元素。为了表示每个元素ai与其直接后继ai+1的逻辑关系,链式存储不仅需要存储元素本身,还要存储一个指向其直接后继元素的地址。这种存储结构被称之为结点(node)。存储元素的叫数据域,存储地址的叫指针域,结点=数据域+指针域。结点元素的逻辑顺序称之为线性链表或单链表。
-
链式存储结构的优缺点
- 优点:插入和删除速度快,时间性能O(1),只需要移动指针即可;动态分配存储空间,没有空间限制,存储元素的个数与内存空间大小有关。
- 缺点:存放指针占用了额外的空间(物理地址不连续,空间碎片多);查找速度慢,因为查找时,需要循环链表从头结点开始访问。时间性能:O(n)。
-
头指针和头结点
以线性表中第一个数据元素a1的存储地址作为线性表的地址,称作线性表的头指针。
有时为了操作方便,在第一个结点之前虚加一个“头结点”,并用链表的头指针指向头结点,称为带头结点的单链表。其数据域可以存放如线表长度等信息,而指针域则存放第一个元素结点的地址信息。若该链表为空,则头结点指针域为空。
2.1) 不带头结点的单链表
链表指针存放链表第一个数据元素结点的地址
空链表时该指针域为NULL
2.2) 带头结点的单链表
一个专门的结点,称为头结点
该头结点永远存在该头结点指针域存放第一个数据元素结点的地址
L.next= NULL
链式存储结构的实现(基础方法)
#include <stdio.h>
#include <stdlib.h>
// 状态常量
#define TRUE 1 // 真/是
#define FALSE 0 // 假/否
#define OK 1 // 通过/成功
#define ERROR 0 // 错误/失败
typedef int Status;
/*元素类型,自定义*/
typedef int ElemType;
/*
* 单链表结构
*/
typedef struct LNode {
ElemType data;// 数据域
struct LNode* next;// 指针域
} LNode;
// 指向单链表结点的指针
typedef LNode* LinkList;
/*
* 初始化 只是初始化一个头结点
* !!!参数为二重指针,为什么,很久没有学C了也忘了
* 文末有指出,指针内容存放的是地址,对形参内容的任何变化,都不会影响原值
* 初始化成功则返回OK,否则返回ERROR。
*/
Status InitList(LinkList* L) {
(*L) = (LinkList) malloc(sizeof(LNode));
if (*L == NULL) {
exit(-1);
}
(*L)->next = NULL;
return OK;
}
/*
* 销毁(结构)
* 释放链表所占内存,头结点也会被清理
*/
Status DestroyList(LinkList* L) {
LinkList p;
// 确保链表结构存在
if (L == NULL || *L == NULL) {
return ERROR;
}
p = *L;
// 依次释放每个结点
while(p != NULL) {
p = (*L)->next;
free(*L);
*L = p;
}
*L = NULL;
return OK;
}
/*
* 置空(内容)
* 释放链表中非头结点处的空间
*/
Status ClearList(LinkList L) {
LinkList pre,p;
if (L == NULL){
return ERROR;
}
p = L->next;
// 释放链表所有结点所占内存
while(p != NULL){
pre = p;
p = p->next;
free(pre);
}
L->next = NULL;
return OK;
}
/*
* 判空
* 返回值:
* TRUE : 链表为空
* FALSE: 链表不为空
*/
Status ListEmpty(LinkList L) {
// 链表只有头结点时,认为该链表为空
if (L == NULL || L->next == NULL) {
return TRUE;
} else {
return FALSE;
}
}
/*
* 计数
*
* 返回链表包含的有效元素的数量
*/
int ListLength(LinkList L) {
LinkList p;
int i = 0;
// 确保链表存在且不为空表
if(L == NULL || L->next == NULL) {
return i;
}
p = L->next;
while(p) {
i ++;
p = p->next;
}
return i;
}
/*
* 取值
*
* 获取链表中第i个元素,将其存储到e中。
* 如果可以找到,返回OK,否则,返回ERROR。
*
*【备注】
* 教材中i的含义是元素位置,从1开始计数,但这不符合编码的通用约定。
* 通常,i的含义应该指索引,即从0开始计数。
* 实现方法有很多种,不要闷在别人的想法
*/
Status GetElem(LinkList L, int i, ElemType* e) {
LinkList p;
int j = 0;
// 确保链表存在且不为空表
if(L == NULL || L->next == NULL) {
return ERROR;
}
p = L->next;
while (p && j < i-1) {
p = p->next;
j++;
}
// p为空或者第i个元素位置不合法(不存在)
if (!p || j > i-1)
return ERROR;
*e = p->data;
return OK;
}
/*
* 查找
*
* 返回链表中首个与e满足Compare关系的元素位序。
* 如果不存在这样的元素,则返回0。
*
*【备注】
* 元素e是Compare函数第二个形参
*/
int LocateElem(LinkList L, ElemType e, Status(Compare)(ElemType, ElemType)) {
int i ;
LinkList p;
// 确保链表存在且不为空表
if(L == NULL || L->next == NULL) {
return 0;
}
i = 1; // i的初值为第一个元素的位序
p = L->next;
while(p != NULL && !Compare(p->data, e)) {
p = p->next;
i++;
}
if (p != NULL)
return i;
else
return 0;
/**
p = L->next;
while(p != NULL) {
if (Compare(p->data, e))
return i;
i++;
p = p->next;
}
return 0;
**/
}
/*
* 前驱
*
* 获取元素cur_e的前驱,
* 如果存在,将其存储到pre_e中,返回OK,
* 如果不存在,则返回ERROR。
*/
Status PriorElem(LinkList L, ElemType cur_e, ElemType* pre_e) {
LinkList p,pre;
// 确保链表存在且不为空表
if(L == NULL || L->next == NULL) {
return ERROR;
}
p = L->next;
while(p != NULL) {
if (p->data == cur_e && L->next != p)
break;
pre = p;
p = p->next;
}
/**
// 指向第2个元素
next = pre->next;
// 从第2个元素开始,查找cur_e的位置
while(next != NULL && next->data != cur_e) {
pre = next;
next = next->next;
}
*/
if (p == NULL)
return ERROR;
*pre_e = pre->data;
return OK;
}
/*
* 后继
*
* 获取元素cur_e的后继,
* 如果存在,将其存储到next_e中,返回OK,
* 如果不存在,则返回ERROR。
*/
Status NextElem(LinkList L, ElemType cur_e, ElemType* next_e) {
LinkList pre;
// 确保链表存在且不为空表
if(L == NULL || L->next == NULL) {
return ERROR;
}
pre = L->next;
while(pre != NULL && pre->data != cur_e) {
pre = pre->next;
}
// pre为空 || 元素相等
if (pre == NULL || pre->next == NULL)
return ERROR;
*next_e = pre->next->data;
return OK;
}
/*
* 插入
*
* 在带头结点的单链表L中第i个位置之前插入元素e
*/
Status ListInsert(LinkList L, int i, ElemType e) {
LinkList p,s;
int j;
p = L;
j = 0;
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
// p空,或者i的值不合法
if (p == NULL || j > i - 1)
return ERROR;
// 生成新结点
s = (LinkList) malloc(sizeof(LNode));
if (s == NULL)
exit(-1);
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
/*
*
* 删除
*
* 删除链表第i个位置上的元素,并将被删除元素存储到e中。
* 删除成功则返回OK,否则返回ERROR。
*/
Status ListDelete(LinkList L, int i, ElemType* e) {
LinkList p,q;
int j;
// 确保链表存在且不为空表
if(L == NULL || L->next == NULL) {
return ERROR;
}
j = 0;
p = L;
/** 条件的设置
1)j判断是否到达结点,p->next判断后继 ,p->next比j慢一拍
2)或者用一个变量保存前驱结点
**/
while(p ->next != NULL && j < i-1) {
p = p->next;
j++;
}
if (p->next == NULL || j > i-1)
return ERROR;
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return OK;
}
/*
* 遍历
*
* 用visit函数访问链表L
*/
void ListTraverse(LinkList L, void(Visit)(ElemType)) {
LinkList p;
// 确保链表存在且不为空表
if(L == NULL || L->next == NULL) {
return;
}
p = L->next;
while(p != NULL) {
Visit(p->data);
p = p->next;
}
printf("\n");
}
/*
* 头插法创建链表
*/
Status CreateList_Head(LinkList* L, int n){
int i;
LinkList p;
// 建立头结点
(*L) = (LinkList) malloc(sizeof(LNode));
if ((*L) == NULL)
exit(-1);
(*L)->next = NULL;
printf("请输入%d个降序元素", n);
for (i = 1; i <= n; i++) {
p = (LinkList) malloc(sizeof(LNode));
scanf("%d", &(p->data));
p->next = (*L)->next;
(*L)->next = p;
}
return OK;
}
/*
* 尾插法创建链表
*/
Status CreateList_Tail(LinkList* L, int n) {
int i;
LinkList p,q;
// 建立头结点
(*L) = (LinkList) malloc(sizeof(LNode));
if ((*L) == NULL)
exit(-1);
q = (*L);
printf("请输入%d个升序元素", n);
for (i = 1; i <= n; i++) {
p = (LinkList) malloc(sizeof(LNode));
scanf("%d", &(p->data));
q->next = p;
q = p;
}
q->next = NULL;
return OK;
}
// 判断data>e是否成立
Status CmpGreater(ElemType data, ElemType e) {
return data == e ? TRUE : FALSE;
}
// 测试函数,打印元素
void PrintElem(ElemType e) {
printf("%d ", e);
}
个人小结:
- 链表的初始化或者头插尾插法建立单链表函数形参为二级指针,为什么使用二级指针?
记住一句话,无论函数形参如何变化,实参都不变,如果你要说传指针进去就能改变?我就得打你的脸了,指针的内容是什么?地址是吧,指针所指的地址变了吗?没变啊,所以实参还是没变,但你可以访问这个地址,然后在这个地址上做一些操作。所以回到你那个问题,为什么要用二级指针,是因为要改变一级指针的地址(malloc分配空间的地址),所以才需要用二级指针。
通俗一点的理解是一级指针作为参数时,我们只是改变该指针(地址)里面的值,而二级指针是把它的地址传给参数,我们可以更改它的地址👌
- 头插法尾插法(创建一定长度的链表方法)和向链表中插入元素都是本质都是执行插入新结点的操作,但是三者是有区别的,插入元素很像头插和尾插法的结合体。若向一个空链表中依次递增插入,那么最后执行结束和尾插法创建单链表效果是一样的。
- 我发现其他或者课本的程序在判断某个元素是否存在,或者定位某一个元素,往往是让循环结束,然后根据循环结束的条件再去进一步的处理,而不是再循环中满足相应的条件就返回。
- 删除操作循环条件
while(p->next)
而不是提前让p指针指向第一个结点,目的是找到待删除结点的前一个。
单链表应用 - 非递减链表归并
/*
*
* 非递减链表归并:C=A+B
*
* 将链表A和B归并为C,且保持元素相对位置不变。
* Lc利用La的头结点,Lb中结点均插入新链表Lc中。
*/
void MergeList(LinkList* La, LinkList* Lb, LinkList* Lc) {
LinkList pa, pb, pc;
pa = (*La)->next;
pb = (*Lb)->next;
pc = *Lc = *La;
while(pa && pb) {
if(pa->data <= pb->data) {
pc->next = pa;
pc = pa;
pa = pa->next;
} else {
pc->next = pb;
pc = pb;
pb = pb->next;
}
}
// 插入剩余段
pc->next = pa? pa : pb;
// 释放La,Lb的头结点所占内存
*La = NULL;
*Lb = NULL;
}
静态链表
前面我们了解到顺序表和链表各自的实现以及优缺点,那么是否存在一种存储结构,既能快速访问元素,又能快速增加和删除元素。
静态链表也是线性存储结构的一种,它结合了顺序表和链表的优点于一身。静态链表的数据全部存储在数组中(和顺序表一样),但存储位置是随机的,数据之间“一对一”的逻辑关系通过一个整型变量(游标,和指针功能类似)维持(和链表一样)。
-
备用链表
备用链表的作用是回收数组中未使用或之前使用过(目前未使用)的存储空间,留待后期使用。也就是说,静态链表使用数组申请的物理空间中,存有两个链表,一条连接数据,另一条连接数组中未使用的空间。
通常,备用链表的表头位于数组下标为 0(a[0]) 的位置,而数据链表的表头位于数组下标为 1(a[1])的位置。
静态链表中设置备用链表的好处是,可以清楚地知道数组中是否有空闲位置,以便数据链表添加新数据时使用。比如,若静态链表中数组下标为 0 的位置上存有数据,则证明数组已满。
例如,使用静态链表存储
{1,2,3}
,假设使用长度为 6 的数组 a,则存储状态可能如图所示:
备用链表上连接的依次是 a[0]、a[2] 和 a[4],而数据链表上连接的依次是 a[1]、a[3] 和 a[5]。
-
静态链表存储数据的过程
在数组链表未初始化之前,数组中所有位置都处于空闲状态,因此都被链接在备用链表上,当向静态链表中添加数据时,需要提前从备用链表中摘除结点,以供新数据使用。备用链表中添加和删除结点以a[0]的后继结点为基准
备用链表摘除节点最简单的方法是摘除 a[0] 的直接后继节点;同样,向备用链表中添加空闲节点也是添加作为 a[0] 新的直接后继节点。因为 a[0] 是备用链表的第一个节点,我们知道它的位置,操作它的直接后继节点相对容易,无需遍历备用链表,耗费的时间复杂度为 O(1)。
下面以存储元素1 、 2为例
-
静态链表插入数据的过程
倘若我向"第三个"位置插入元素(注意静态链表底层虽然是数组,但值和索引完全错位),那么我需要从备用链表取空间,即从a[0]的后继取,静态链表插入之所以那么快,就因为这个cur游标(形似指针),只需修改第三个位置的cur和a[0]后继的cur即可完成插入操作。
-
静态链表的实现
#include <stdio.h> #include <stdlib.h> // 状态常量 #define TRUE 1 // 真/是 #define FALSE 0 // 假/否 #define OK 1 // 通过/成功 #define ERROR 0 // 错误/失败 // 宏定义 #define MAXSIZE 1000 // 备用控件最大容量,近似于静态链表的最大长度 typedef int Status; /*元素类型,自定义*/ typedef int ElemType; /* * 静态链表结构 * * 注:静态链表依托于一个数组,该数组包含了已占用空间和空闲空间 */ typedef struct SLinkNode { ElemType data; int cur; // cur是游标,做指针用,用来链接下一个结点(与数组下标区别开来) } SLinkList[MAXSIZE]; // 链表空间类型 /* * 初始化 * * 备用空间为静态链表提供可用的内存。 */ void InitSpace(SLinkList space) { int i; for (i = 0; i < MAXSIZE - 1; ++i) { space[i].cur = i + 1;// 将数组分量链接到一起 } space[MAXSIZE - 1].cur = 0;// 链表最后一个结点的游标值为0 } /* * 申请空间 * * 为静态链表从备用链表申请结点空间, * 如果申请成功,返回可用空间的索引, * 申请失败时,返回0。 */ int Malloc(SLinkList space) { // 若备用链表非空,则返回分配的结点下标,否则返回0 // (当分配最后一个结点时,该结点的游标值为0) int i = space[0].cur; if (i != 0) { // 将申请到的空间从备用链表删除 space[0].cur = space[i].cur; } return i; } /* * 回收空间 * * 回收索引k处的结点空间以供静态链表后续复用, * 回收方式就是将该结点空间从静态链表上移除, * 并将其添加到备用链表中 */ void Free(SLinkList space, int k) { space[k].cur = space[0].cur;// 将释放结点的指针指向头结点所指向的结点值 space[0].cur = k; } /*━━━━━━━━━━━━━━━━━━━━━━ 静态链表操作 ━━━━━━━━━━━━━━━━━━━━━━*/ /* * 初始化 * * 先初始化备用空间,而后从备用空间中申请头结点的空间,进而完成静态链表的初始化。 * 初始化成功则使用S存储头结点索引,且返回OK,否则返回ERROR。 */ Status InitList(SLinkList space, int* S) { int index; // 初始化备用空间 InitSpace(space); // 申请头结点空间 index = Malloc(space); if (index == 0) { return ERROR; } space[index].cur = 0; *S = index; return OK; } /* * 销毁(结构) * * 释放静态链表所占内存,即将静态链表所有结点空间移入备用空间列表中。 */ Status DestroyList(SLinkList space, int* S) { int cur; // 确保静态链表存在 if (S == NULL || *S == 0) return ERROR; while(*S != 0) { // 暂存下一个结点的索引 cur = space[*S].cur; // 回收当前结点所占空间 Free(space, *S); // 指向下一个节点索引 *S = cur; } return OK; } /* * 置空(内容) * * 这里需要释放静态链表中非头结点处的空间。 */ Status ClearList(SLinkList space, int S) { int p,cur; if (S == 0) return ERROR; p = space[S].cur; while(p != 0) { cur = space[p].cur; Free(space, p); p = cur; } space[S].cur = 0;// 将链表头结点的cur置为0 return OK; } /* * 判空 * * 判断静态链表中是否包含有效数据。 * * 返回值: * TRUE : 静态链表为空 * FALSE: 静态链表不为空 */ Status ListEmpty(SLinkList space, int S) { // 只存在头结点的静态链表被称为空表 if (S != 0 && space[S].cur == 0) return TRUE; else return FALSE; } /* * 计数 * * 返回静态链表包含的有效元素的数量。 */ int ListLength(SLinkList space, int S) { int count; if (S == 0 || space[S].cur == 0) return 0; count = 0; S = space[S].cur; while (S != 0) { count++; S = space[S].cur; } return count; } /* * 取值 * * 获取静态链表中第i个元素,将其存储到e中。 * 如果可以找到,返回OK,否则,返回ERROR。 * *【备注】 * 教材中i的含义是元素位置,从1开始计数,但这不符合编码的通用约定。 * 通常,i的含义应该指索引,即从0开始计数。 */ Status GetElem(SLinkList space, int S, int i, ElemType* e) { int j = 0; if (S == 0 || space[S].cur == 0) return ERROR; S = space[S].cur; while(S != 0 && j < i - 1) { S = space[S].cur; j ++; } if (j > i - 1 || S == 0) return ERROR; *e = space[S].data; return OK; } /* * * 查找 * * 返回静态链表中首个与e满足Compare关系的元素位序。 * 如果不存在这样的元素,则返回0。 * *【备注】 * 1.元素e是Compare函数第二个形参 * 2.这里的实现与教材上的算法2.13不相同,原因参见顶部的“注意”信息 */ int LocateElem(SLinkList space, int S, ElemType e, Status(Compare)(ElemType, ElemType)) { int i,p; if (S == 0 || space[S].cur == 0) return ERROR; i = 1;// i的初值为第一个元素的位序 p = space[S].cur;// p的初值为第一个元素的索引 while(p != 0 && !Compare(space[p].data, e)) { i ++; p = space[p].cur; } if (p != 0) return i; else return 0; } /* * 前驱 * * 获取元素cur_e的前驱, * 如果存在,将其存储到pre_e中,返回OK, * 如果不存在,则返回ERROR。 */ Status PriorElem(SLinkList space, int S, ElemType cur_e, ElemType* pre_e) { int pre, next; // 确保静态链表存在且不为空 if (S == 0 || space[S].cur == 0) return ERROR; // 指向第一个元素 pre = space[S].cur; // 第一个元素没有前驱 if (space[pre].data == cur_e) return ERROR; // 指向第二个元素 next = space[pre].cur; while(next != 0 && space[next].data != cur_e) { // 预存上一个元素的索引 pre = next; next = space[next].cur; } if (next == 0) return ERROR; *pre_e = space[pre].data; return OK; } /* * 后继 * * 获取元素cur_e的后继, * 如果存在,将其存储到next_e中,返回OK, * 如果不存在,则返回ERROR。 */ Status NextElem(SLinkList space, int S, ElemType cur_e, ElemType* next_e) { int pre; // 确保静态链表存在且不为空 if (S == 0 || space[S].cur == 0) return ERROR; pre = space[S].cur; // !!!space[pre].cur !=0 而不是pre != 0 // 从第一个元素开始,查找cur_e的位置,且保证该结点的后继存在 while(space[pre].cur !=0 && space[pre].data != cur_e) { pre = space[pre].cur; } // 没有找到或者找到元素,但是是最后一个 if (space[pre].cur == 0) return ERROR; *next_e = space[space[pre].cur].data; return OK; } /* * 插入 * * 向静态链表第i个位置上插入e,插入成功则返回OK,否则返回ERROR。 * *【备注】 * 教材中i的含义是元素位置,从1开始计数 * 时间复杂度 */ Status ListInsert(SLinkList space, int S, int i, ElemType e) { int p, s, j; // 确保静态链表存在 if (S == 0) return ERROR; p = S; j = 0; while(p !=0 && j < i - 1) { p = space[p].cur; j++; } // 如果遍历到头,或者i位置不合法 if (p == 0 || j > i - 1) return ERROR; // 生成新结点 实质就是从备用链表中取出一个结点, // Malloc做了将当前结点从备用链表删除 s = Malloc(space); space[s].data = e; space[s].cur = space[p].cur; space[p].cur = s; return OK; } /* * 删除 * * 删除静态链表第i个位置上的元素,并将被删除元素存储到e中。 * 删除成功则返回OK,否则返回ERROR。 * *【备注】 * 教材中i的含义是元素位置,从1开始计数 */ Status ListDelete(SLinkList space, int S, int i, ElemType* e) { int j; int p, q; // 确保静态链表存在且不为空 if (S == 0 || space[S].cur == 0) return ERROR; j = 0; p = S; // 和链表类似,找的是前驱结点 while (space[p].cur != 0 && j < i - 1) { p = space[p].cur; j++; } // 如果遍历到头,或者i位置不合法 if (space[p].cur == 0 || j > i - 1) return ERROR; // 删除第i个结点 //ERROR,不能像下面这样写改动游标,因为静态链表形似数组,但它的数组下标并 // 不是和值一一对应的,删除第i个位置上的值,这个i位置上的值不一定是实际的值, // 静态链表的数组下标和逻辑值是篡位的(不在同一位置) // space[p].cur = space[i - 1].cur; q = space[p].cur; space[p].cur = space[q].cur; *e = space[q].data; Free(space, q); return OK; } /* * 遍历 * * 用visit函数访问静态链表S */ void ListTraverse(SLinkList space, int S, void(Visit)(ElemType)) { int p; // 确保链表存在 if(S == 0 || space[S].cur == 0) { return; } p = space[S].cur; while(p != 0) { Visit(space[p].data); p = space[p].cur; } printf("\n"); } /*━━━━━━━━━━━━━━━━━━━━━━ 图形化输出 ━━━━━━━━━━━━━━━━━━━━━━*/ // 以图形化形式输出当前结构,仅限内部测试使用 void PrintList(SLinkList space, int S) { int i = 0; printf("==== 备用链表 ====\n"); while(i < 20) { printf("%2d | %2d | %2d |\n", i, space[i].data, space[i].cur); i = space[i].cur; } printf("==== 静态链表 ====\n"); // 静态链表头指针 i = S; while(i>0 && i < 20) { printf("%2d | %2d | %2d |\n", i, space[i].data, space[i].cur); i = space[i].cur; } } // 判断data==e是否成立 Status CmpEqual(ElemType data, ElemType e) { return data == e ? TRUE : FALSE; } // 测试函数,打印元素 void PrintElem(ElemType e) { printf("%d ", e); }
-
静态链表去重
/*
* S = (A-B)∪(B-A)
*
* 对集合A和集合B进行(A-B)∪(B-A)计算,计算结果存入静态链表S
*
*【备注】
* 教材中默认从控制台读取数据。
* 这里为了方便测试,避免每次运行都手动输入数据,
* 因而选择了从预设的文件path中读取测试数据。
*/
void difference(SLinkList space, int *S) {
int size_a,size_b;// 集合A和B中元素数量
int j; // 循环计数器
int R; // 指向静态链表最后一个结点
int b; // 临时存储从集合B读取到的数据
int i,k,p;
// 初始化备用空间
InitSpace(space);
// 获取链表头结点
*S = Malloc(space);
// 让R指向静态链表的最后一个结点
R = *S;
// 录入集合A的值
printf("请输入集合A和集合B中元素的个数:");
scanf("%d%d", &size_a, &size_b);
printf("请输入集合A中的元素(空格隔开):");
for (j = 1; j <= size_a; j++) {
// 分配结点
i = Malloc(space);
// 输入值
scanf("%d", &space[i].data);
// 将新结点插入表尾
space[R].cur = i;
// 指针后移
R = i;
}
// 尾结点置空
space[R].cur = 0;
// 录入集合B的数据
printf("请输入集合B中的元素(空格隔开):");
for (j = 1; j <= size_b; j++) {
scanf("%d", &b);// 暂存于b中
p = *S;
k = space[p].cur;//k指向首元素
// 循环遍历该元素是否存在于A中
while(k != space[R].cur && b != space[k].data) {
p = k;//存放k的前驱
k = space[k].cur;
}
if (k == space[R].cur) {
// 说明A没有该元素,插入该元素
i = Malloc(space);
space[i].data = b;
// 修改指针
space[i].cur = space[R].cur;
space[R].cur = i;
R = i;
} else {
// 说明A包含该元素,移除该元素
space[p].cur = space[k].cur;
Free(space, k);
// 若移除的是最后一个结点,则应该让R指向P(k的前驱)
if (R == k)
R = p;
}
}
}
双向链表
单链表中每个结点都统一指向直接后继结点,通常称这类链表为单向链表(单链表)。双链表中每个结点不仅指向直接后继,还保留了指向直接前驱的指针域。
双向和单向,指的是各结点之间的逻辑关系是双向或单向,但通常头指针只设置一个。
循环链表
表中最后一个结点的指针域指向头结点,整个链表形成一个环就称为循环链表。
特点:从表中任意结点出发均可找到表中其他结点。
双向循环链表示例
#include <stdio.h>
#include <stdlib.h>
// 状态常量
#define TRUE 1 // 真/是
#define FALSE 0 // 假/否
#define OK 1 // 通过/成功
#define ERROR 0 // 错误/失败
typedef int Status;
/* 双向循环链表元素类型定义 */
typedef int ElemType;
/*
* 双向循环链表结构
*
* 注:这里的双向循环链表存在头结点
*/
typedef struct DuLNode {
ElemType data;
struct DuLNode* prior; // 前驱
struct DuLNode* next; // 后继
} DuLNode;
// 指向双向循环链表结点的指针
typedef DuLNode* DuLinkList;
static DuLinkList GetElemP(DuLinkList L, int i);
/*
* 初始化
*
* 初始化成功则返回OK,否则返回ERROR。
*/
Status InitList(DuLinkList* L) {
*L = (DuLinkList) malloc(sizeof(DuLNode));
if (*L == NULL)
exit(0);
(*L)->next = (*L)->prior = *L;
return OK;
}
/*
* 销毁(结构)
*
* 释放双向循环链表所占内存。
*/
Status DestroyList(DuLinkList* L) {
if (*L == NULL)
return ERROR;
ClearList(*L);
free(*L);
*L = NULL;
return OK;
}
/*
* 置空(内容)
*
* 这里需要释放双向循环链表中非头结点处的空间。
*/
Status ClearList(DuLinkList L) {
DuLinkList p, q;
if (L == NULL)
return ERROR;
p = L->next;
while(p != L) {
q = p->next;
free(p);
p = q;
}
L->next = L->prior = L;
return OK;
}
/*
* 判空
*
* 判断双向循环链表中是否包含有效数据。
*
* 返回值:
* TRUE : 双向循环链表为空
* FALSE: 双向循环链表不为空
*/
Status ListEmpty(DuLinkList L) {
if (L == NULL)
return ERROR;
// 保证前驱后继指向自己
if (L->next == L && L->prior == L)
return OK;
else
return FALSE;
}
/*
* 计数
*
* 返回双向循环链表包含的有效元素的数量。
*/
int ListLength(DuLinkList L) {
int i;
DuLinkList p;
if (L == NULL)
return ERROR;
i = 0;
p = L->next;
while (p != L) {
i ++;
p = p->next;
}
return i;
}
/*
* 取值
*
* 获取双向循环链表中第i个元素,将其存储到e中。
* 如果可以找到,返回OK,否则,返回ERROR。
*
*【备注】
* 教材中i的含义是元素位置,从1开始计数,但这不符合编码的通用约定。
* 通常,i的含义应该指索引,即从0开始计数。
*/
Status GetElem(DuLinkList L, int i, ElemType* e) {
int pos;
DuLinkList p;
// 确保双向循环链表存在
if(L == NULL || L->next == L || L->prior == L) {
return ERROR;
}
pos = 0;
p = L->next;
while (p != L && pos < (i -1)) {
p = p->next;
pos ++;
}
if (pos > (i - 1) || p == L)
return ERROR;
*e = p->data;
return OK;
}
/*
* 查找
*
* 返回双向循环链表中首个与e满足Compare关系的元素位序。
* 如果不存在这样的元素,则返回0。
*
*【备注】
* 元素e是Compare函数第二个形参
*/
int LocateElem(DuLinkList L, ElemType e, Status(Compare)(ElemType, ElemType)) {
int i;
DuLinkList p;
// assert not null ...
i = 1;
p = L->next;
while (p != L && !(Compare(p->data, e))) {
p = p->next;
i ++;
}
if (p == L)
return 0;
return i;
}
/*
* 前驱
*
* 获取元素cur_e的前驱,
* 如果存在,将其存储到pre_e中,返回OK,
* 如果不存在,则返回ERROR。
* 【备注】第一个元素没有前驱,最后一个元素没有后继
*/
Status PriorElem(DuLinkList L, ElemType cur_e, ElemType* pre_e) {
// 因为是双向的,所以不需要额外的指针保存前驱
DuLinkList p;
//Assert not null
// 指向第1个元素
p = L->next;
// 第1个元素没有前驱
if(p->data == cur_e) {
return ERROR;
}
// 指向第2个元素
p = p->next;
// 从第2个元素开始,查找cur_e的位置
while (p != L && (p->data != cur_e)) {
p = p->next;
}
if (p == L)
return ERROR;
else {
*pre_e = p->prior->data;
return OK;
}
}
/*
* 后继
*
* 获取元素cur_e的后继,
* 如果存在,将其存储到next_e中,返回OK,
* 如果不存在,则返回ERROR。
*/
Status NextElem(DuLinkList L, ElemType cur_e, ElemType* next_e) {
DuLinkList p;
// Assert not null
p = L->next;
while (p->next != L && (p->data != cur_e)) {
p = p->next;
}
if (p->next == L)
return ERROR;
*next_e = p->next->data;
return OK;
}
/*
* ████████ 算法2.18 ████████
*
* 插入
*
* 向双向循环链表第i个位置上插入e,插入成功则返回OK,否则返回ERROR。
*
*【备注】
* 教材中i的含义是元素位置,从1开始计数
*/
Status ListInsert(DuLinkList L, int i, ElemType e) {
int pos;
DuLinkList p, s;
// 确保双向循环链表存在(但可能为空表)
if(L == NULL) {
return ERROR;
}
// 查找第i个结点位置(引用)
if ((p = GetElemP(L, i)) == NULL)
return ERROR;
// 创建新结点
s = (DuLinkList) malloc(sizeof(DuLNode));
if (s == NULL)
exit(0);
// 将s插入到p的前面,成为第i个结点
s->data = e;
s->prior = p->prior;
p->prior->next = s;
s->next = p;
p->prior = s;
return OK;
}
/*
* ████████ 算法2.19 ████████
*
* 删除
*
* 删除双向循环链表第i个位置上的元素,并将被删除元素存储到e中。
* 删除成功则返回OK,否则返回ERROR。
*
*【备注】
* 教材中i的含义是元素位置,从1开始计数
*/
Status ListDelete(DuLinkList L, int i, ElemType* e) {
DuLinkList p;
// 确保双向循环链表存在
if(L == NULL || L->next == L || L->prior == L) {
return ERROR;
}
// 查找第i个结点位置(引用)
if((p = GetElemP(L, i)) == NULL) {
return ERROR;
}
// 如果p==L,删除头结点,不合规
if (p == L)
return ERROR;
*e = p->data;
// 移除p结点
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
return OK;
}
/*
* 遍历
*
* 用visit函数访问双向循环链表L
*/
void ListTraverse(DuLinkList L, void(Visit)(ElemType)) {
DuLinkList p;
// 确保双向循环链表存在
if(L == NULL || L->next == L || L->prior == L) {
return;
}
p = L->next;
while(p != L) {
Visit(p->data);
p = p->next;
}
printf("\n");
}
/*
* 获取循环链表L上第i个元素的引用
*
* ▓▓▓▓ 注意 ▓▓▓▓
* 1.加static的含义是当前函数只在DuLinkList中使用,不会被别的文件引用
* 2.假设链表长度为len,且需要获取第len+1个元素的引用时,由于这里是循环链表,所以返回的是头结点
*/
static DuLinkList GetElemP(DuLinkList L, int i) {
int pos;
DuLinkList p;
// 确保双向循环链表存在(但可能为空表)
if(L == NULL) {
return NULL;
}
pos = 0;
p = L->next;
// !!!bug点,画图分析
while(p != L && pos < i - 1) {
p = p->next;
pos++;
}
if (pos == i - 1)
return p;
// 至此说明p->next == L,此时需要判断i是否过大
if (pos < i - 1)
return NULL;
// 至此说明需要在len+1的位置上插入元素
return L;
}
// 判断data>e是否成立
Status CmpGreater(ElemType data, ElemType e) {
return data > e ? TRUE : FALSE;
}
// 测试函数,打印元素
void PrintElem(ElemType e) {
printf("%d ", e);
}
当初写这个双向循环链表的基本操作卡在了insert,当时很😠感觉对前驱后继结点处理的不是很恰当,大大归结于不清楚循环条件如何去设置,对双向循环链表“双向”理解的不是很透彻。最后还是画图加结合课本的程序理解了,来个图吧!
单链表的插入,在Header和其后面的结点之间插入node_1,刚开始我困惑当向空表中依次递增插入时,该方式还适用吗?后来经过验证是适用的,若只有一个Header几点,那么可以想象后面有一个空结点(^),不存放东西,那么理解不就easy了,当Header后面存在结点,插入图示更好理解了。
注: 这里的1,不是先执行node_1->next=node_n
而是先进行node_1的前驱赋值操作不是指向操作,因为我们都是找结点都是在插入结点之前,所以我们也不清楚后面就是node_n,所以应该node_1->next=H->next
双向循环链表的插入也很迷惑人,插入到某个结点的前面,这里就不存在空结点一说了,比如往空的双向循环链表中依次递增插入,每次插入之前指针都是位于Header(注意前提:空的双向循环链表、递增插入),这里一定要把握好循环双向的概念,把它看成一个圈去理解。Node_1的前驱是H后继也是H,就这样吧,logic is too weak!
扩展的单链表
#include <stdio.h>
#include <stdlib.h>
// 状态常量
#define TRUE 1 // 真/是
#define FALSE 0 // 假/否
#define OK 1 // 通过/成功
#define ERROR 0 // 错误/失败
typedef int Status;
/* 循环链表元素类型定义 */
typedef int ElemType;
/*
* ████ 注意 ████
*
* 教材中的线性链表命名为LinkList,
* 这里为了与单链表区分,故将其命名为ELinkList。
* 线性链表可以理解成对普通链表的一种扩展。
*/
/*
* 线性链表结构
*
* 注:这里的线性链表存在头结点
*/
typedef struct LNode {
ElemType data;
struct LNode* next;
} LNode, * Link, * Position;
/* 维护线性链表头尾指针及长度信息 */
typedef struct {
Link head, tail; // 分别指向线性链表中的头结点和尾结点
int len; // 指示线性链表中数据元素的个数
} ELinkList;
/*━━━━━━━━━━━━━━━━━━━━━━ 内存操作 ━━━━━━━━━━━━━━━━━━━━━━*/
/*
* 内存分配
*
* 为线性链表申请一个结点,并存入指定的数据e。
*
*【备注】
* static修饰的含义是该函数仅限当前文件内使用
*/
Status MakeNode(Link* p, ElemType e) {
if (p == NULL) {
return ERROR;
}
// 申请空间
*p = (Link) malloc(sizeof(LNode));
if (*p == NULL)
return ERROR;
(*p)->data = e;
(*p)->next = NULL;
return OK;
}
/*
* 内存回收
*
* 释放线性链表中指定的结点。
*
*【备注】
* static修饰的含义是该函数仅限当前文件内使用
*/
void FreeNode(Link* p) {
if (p == NULL || *p == NULL)
return;
free(*p);
*p = NULL;
}
/*━━━━━━━━━━━━━━━━━━━━━━ 链表常规操作 ━━━━━━━━━━━━━━━━━━━━━━*/
/*
* 初始化
*
* 初始化成功则返回OK,否则返回ERROR。
*/
Status InitList(ELinkList* L) {
Link p;
if (L == NULL)
return ERROR;
//创建头结点
p = (Link) malloc(sizeof(LNode));
if (p == NULL)
exit(-1);
p->next = NULL;
// 只有头结点时,首位游标指向自身
L->head = L->tail = p;
L->len = 0;
return OK;
}
/*
* 销毁(结构)
*
* 释放链表所占内存。
*/
Status DestroyList(ELinkList* L) {
// 链表不存在时没必要销毁
if (L == NULL || (*L).head == NULL) {
return ERROR;
}
ClearList(L);
free((*L).head);
L->head = L->tail = NULL;
return OK;
}
/*
* 置空(内容)
*
* 这里需要释放链表中非头结点处的空间。
*/
Status ClearList(ELinkList* L) {
Link p, q;
// 没有有效元素时不需要清理
if(L == NULL || (*L).head == NULL || (*L).len <= 0) {
return ERROR;
}
p = L->head->next;
while (p != NULL) {
q = p;
p = p->next;
free(q);
}
L->head->next = NULL;
L->tail = L->head;
L->len = 0;
return OK;
}
/*
* 判空
*
* 判断链表中是否包含有效数据。
*
* 返回值:
* TRUE : 链表为空
* FALSE: 链表不为空
*/
Status ListEmpty(ELinkList L) {
return L.len == 0 ? TRUE : FALSE;
}
/*
* 计数
*
* 返回链表包含的有效元素的数量。
*/
int ListLength(ELinkList L) {
return L.len;
}
/*
* 查找
*
* 返回链表中首个与e满足Compare关系的元素引用。
* 如果不存在这样的元素,则返回NULL。
*
*【备注】
* 1.元素e是Compare函数第二个形参
* 2.这里的返回值是目标元素的引用,而不是其位序
*/
Position LocateElem(ELinkList L, ElemType e, Status(Compare)(ElemType, ElemType)) {
Position p;
// Assert not null
p = L.head->next;
while(p != NULL && !(Compare(p->data, e))) {
p = p->next;
}
if (p == NULL) {
return NULL;
}
return p;
}
/*
* ████████ 算法2.20 ████████
*
* 插入
*
* 向链表第i个位置上插入e,插入成功则返回OK,否则返回ERROR。
* 【该方法其实是多个子方法(定位、分配、插入)的结合体】
*【备注】
* 教材中i的含义是元素位置,从1开始计数
* 可以看做是算法2.9的改写
*/
Status ListInsert(ELinkList* L, int i, ElemType e) {
Link h, s;
if (L == NULL || L->head == NULL)
return ERROR;
// 确保i值合规 [1, len+1]
if (i < 1 || i > L->len + 1)
return ERROR;
// 查找第i-1个元素的引用,存储在h中
if (LocatePos(*L, (i-1), &h) == ERROR)
return ERROR;
// 分配新结点s
if (MakeNode(&s, e) == ERROR)
return ERROR;
// 将s结点插入到h结点后面,成为h后面的第一个结点
if (InsFirst(L, h, s) == ERROR)
return ERROR;
return OK;
}
/*
* 删除
*
* 删除链表第i个位置上的元素,并将被删除元素存储到e中。
* 删除成功则返回OK,否则返回ERROR。
*
*【备注】
* 教材中i的含义是元素位置,从1开始计数
* 可以看做是算法2.10的改写
*/
Status ListDelete(ELinkList* L, int i, ElemType* e) {
Link h,q;
if (L == NULL || L->head == NULL)
return ERROR;
// 确保i值合规[1, len]
if (i < 1 || i > L->len)
return ERROR;
// 查找第i-1个元素的引用,存储在h中
if (LocatePos(*L, i, &h) == ERROR)
return ERROR;
// 删除h结点后的第一个结点,并用q存储被删除结点的引用
if (DelFirst(L, h, &q) == ERROR)
return ERROR;
// 记下被删除元素的值
*e = q->data;
// 释放被删除结点的空间
FreeNode(&q);
return OK;
}
/*
* 遍历
*
* 用visit函数访问链表L
*/
void ListTraverse(ELinkList L, void(Visit)(ElemType)) {
Link p;
if(L.len <= 0) {
return;
}
// 指向第1个元素
p = L.head->next;
while(p != NULL) {
Visit(p->data);
p = p->next;
}
printf("\n");
}
/*━━━━━━━━━━━━━━━━━━━━━━ 链表扩展操作 ━━━━━━━━━━━━━━━━━━━━━━*/
/*
* 取值
*
* 获取结点p的元素值。
*/
ElemType GetCurElem(Link p) {
if (p == NULL)
return 0;
return p->data;
}
/*
* 设值
*
* 为结点p设置元素值。
*/
Status SetCurElem(Link p, ElemType e) {
if(p == NULL) {
return ERROR;
}
p->data = e;
return OK;
}
/*
* 头结点
*
* 获取头结点引用。
*/
Position GetHead(ELinkList L) {
return L.head;
}
/*
* 尾结点
*
* 获取尾结点引用。
*/
Position GetLast(ELinkList L) {
return L.tail;
}
/*
* 前驱
* 【!!!前驱后继分清】
* 获取结点p的前驱,如果不存在,则返回NULL。
*/
Position PriorPos(ELinkList L, Link p) {
Position pre;
if (L.head == NULL || p == NULL)
return NULL;
pre = L.head;
// 第一个结点无前驱
if (pre->next == p)
return NULL;
while(pre != NULL && pre->next != p) {
pre = pre->next;
}
return pre;
}
/*
* 后继
*
* 获取结点p的后继,如果不存在,则返NULL。
*/
Position NextPos(ELinkList L, Link p) {
// 确保链表(头结点)存在
if(L.head == NULL) {
return NULL;
}
if(p == NULL) {
return NULL;
}
return p->next;
}
/*
* 查找
*
* 查找链表L中第i(允许为0)个结点,并将其引用存入p,且返回OK
* 如果i值不合规,则返回ERROR
* 特别注意,当i为0时,p存储的是头结点的引用
*/
Status LocatePos(ELinkList L, int i, Link* p) {
Link r;
int pos;
// i=0 头结点
if (i < 0 || i > L.len)
return ERROR;
// 保证链表(头结点)存在
if(L.head == NULL) {
return ERROR;
}
// i为0时,取头结点
if(i == 0) {
*p = L.head;
return OK;
}
pos = 0;
r = L.head;//指向头结点
while(r != NULL && pos < i) {
r = r->next;
pos++;
}
if (r == NULL)
return ERROR;
*p = r;
return OK;
}
/*
* 插入
*
* 将s结点插入到h结点后面,成为h后面的第一个结点
*
*【备注】
* 教材中对于该方法的描述有些问题,这里是修正过的版本
*/
Status InsFirst(ELinkList* L, Link h, Link s) {
if (L == NULL || L->head == NULL || h == NULL || s == NULL)
return ERROR;
s->next = h->next;
h->next = s;
// 若h结点为尾结点,则需更新尾结点
if (h == L->tail)
L->tail = s;
L->len++;
return OK;
}
/*
* 删除
*
* 删除h结点后的第一个结点,只是孤立结点,并不释放所占内存,并用q存储被删除结点的引用
*
*【备注】
* 教材中对于该方法的定义略显粗糙,这里是修正过的版本
*/
Status DelFirst(ELinkList* L, Link h, Link* q) {
Link p;
if (L == NULL || L->head == NULL || h == NULL)
return ERROR;
// 如果没有结点可删除,返回错误信息
if (h->next == NULL)
return ERROR;
*q = h->next;
h->next = (*q)->next;
// 将被删除结点变成孤立结点
(*q)->next = NULL;
// 如果h后只有一个结点,更改尾结点指针
if (h->next == NULL) {
L->tail = h;
}
L->len--;
return OK;
}
/*
* 前向插入
*
* 将s结点插入到p结点[已经存在于链表中]之前,并将p指向新结点
*/
Status InsBefore(ELinkList* L, Link* p, Link s) {
Link r;
if (L == NULL || L->head == NULL || *p == NULL || s == NULL)
return ERROR;
r = L->head;
while(r != NULL && r->next != (*p)) {
r = r->next;
}
// 链表不存在结点p
if (r == NULL)
return ERROR;
s->next = r->next;
r->next = s;
*p = s;
L->len++;
return OK;
}
/*
* 后向插入
*
* 将s结点插入到p结点之后,并将p指向新结点
*/
Status InsAfter(ELinkList* L, Link* p, Link s) {
Link r;
if (L == NULL || L->head == NULL || *p == NULL || s == NULL)
return ERROR;
r = L->head;
while (r != NULL && r != (*p)) {
r = r->next;
}
if (r == NULL)
return ERROR;
// 如果p指向最后一个结点则需要更新尾指针
if ((*p) == L->tail) {
L->tail = s;
}
s->next = (*p)->next;
(*p)->next = s;
(*p) = s;
L->len++;
return OK;
}
/*
* 向尾部添加
*
* 将s所指的一串结点链接在链表L后面
*/
Status Append(ELinkList* L, Link s) {
int count;
if(L == NULL || (*L).head == NULL || s == NULL) {
return ERROR;
}
count = 0;
// 确定新的尾结点位置和结点数
L->tail = s;
while(s != NULL) {
L->tail = s;
s = s->next;
count ++;
}
L->len += count;
return OK;
}
/*
* 从尾部移除
*
* 将链表的尾结点移除,并将被移除的结点引用存储在q中
*/
Status Remove(ELinkList* L, Link* q) {
Link p;
if(L == NULL || (*L).head == NULL || q == NULL) {
return ERROR;
}
// 没有元素可供移除
if(L->len == 0) {
*q = NULL;
return ERROR;
}
(*q) = L->tail;
p = L->head;
while(p ->next != L->tail) {
p = p->next;
}
p->next = NULL;
L->tail = p;
L->len --;
return OK;
}
扩展的单链表主要是对链表的结构进行了重新定义,比如求链表长度,宏观上不需要在依次遍历,并且增加了尾指针,使得链表在合并时只需要更改尾指针就可完成。
- 归并非降序链表
/*
* ████ 注意 ████
*
* 为了与之前的归并算法名称区分,这里将算法名称命名为MergeEList(教材中的名称是MergeList_L)
*/
/*
* ████████ 算法2.21 ████████
*
* 非递减链表归并:C=A+B
*
* 将链表A和B归并为C,且保持元素相对位置不变。
* 归并结束后,会销毁La和Lb。
*/
Status MergeEList(ELinkList* La, ELinkList* Lb, ELinkList* Lc, int(Compare)(ElemType, ElemType)) {
Link ha, hb, pa, pb, q;
ElemType a, b;
if(InitList(Lc) == ERROR) {
return ERROR;
}
ha = GetHead(*La); // 指向La头结点
hb = GetHead(*Lb); // 指向Lb头结点
// 保证La和Lb是有效的链表,不过有可能是空表
if (ha == NULL || hb == NULL)
return ERROR;
pa = NextPos(*La, ha);// 指向La第一个元素
pb = NextPos(*Lb, hb);// 指向Lb第一个元素
// 遍历La和Lb,选出较小的元素,一次加入到Lc中
while(pa != NULL && pb != NULL) {
a = GetCurElem(pa);
b = GetCurElem(pb);
if (Compare(a, b) <= 0) {//a≤b
// 删除La中首个结点
DelFirst(La, ha, &q);
// 将被删除的结点链接到Lc中
Append(Lc, q);
// 依然获取La中第一个元素,因为DelFirst删除结点后,改变了头指针
pa = NextPos(*La, ha);
} else {
DelFirst(Lb, hb, &q);
Append(Lc, q);
pb = NextPos(*Lb, hb);
}
}
// 如果La还有结点
if (La != NULL) {
Append(Lc, pa);
}
if (Lb != NULL) {
Append(Lc, pb);
}
// 释放La、Lb的头结点
FreeNode(&(La->head));
FreeNode(&(Lb->head));
La->tail = NULL;
Lb->tail = NULL;
La->len = 0;
Lb->len = 0;
}
/*
* 比较
*
* 比较e1和e2的大小,返回值含义如下:
* < 0:e1 < e2
* = 0:e1 = e2
* > 0:e1 > e2
*/
int Cmp(ElemType e1, ElemType e2) {
return e1 - e2;
}
多项式相加
#include <stdio.h>
#include <stdlib.h>
#define ok 1
#define error 0
typedef int Status;
typedef struct lnode
{
struct lnode *next;
int cof;
int exp;
}lnode;
// 创建包含n项的多项式
Status createlist(lnode *h, int n) {
lnode *pr, *p;
if (h == NULL)
return error;
pr = h;
for (int i = 0; i < n; i++) {
p = (lnode*) malloc(sizeof(lnode));
printf("请输入第%d项的系数和指数",(i + 1));
scanf("%d%d", &p->cof, &p->exp);
// 链接元素
pr->next = p;
pr = p;
}
pr->next = NULL;
return ok;
}
//多项式加法
Status addPloy(lnode* la, lnode* lb, lnode* lc) {
lnode *pa, *pb, *pc;
if (la == NULL || lb == NULL || lc == NULL)
return error;
pa = la->next;
pb = lb->next;
pc = lc;
while(pa && pb) {
switch(cmp(pa->exp, pb->exp)) {
case 0: {
pa->cof += pb->cof;
// 判断相加后系数是否为0
if (pa->cof != 0) {
pc->next = pa;
pc = pa;
pa = pa->next;
pb = pb->next;
} else {
pa = pa->next;
pb = pb->next;
}
break;
}
case 1: {
pc->next = pb;
pc = pb;
pb = pb->next;
break;
}
case -1: {
pc->next = pa;
pc = pa;
pa = pa->next;
break;
}
}
}
if (pa)
pc->next = pa;
if (pb)
pc->next = pb;
return ok;
}
int cmp(int a,int b)
{
if(a>b)
return 1;
if(a<b)
return -1;
else
return 0;
}
void display(lnode *h)
{
lnode *p=h->next;
while(p)
{
printf("%dx^%d+",p->cof,p->exp);
p=p->next;
}
printf("\n");
}
// 次幂大小升序输入