目录
数据结构的个人笔记,学习自《大话数据结构》和《数据结构与算法基础(青岛大学-王卓)》,BV1nJ411V7bd
定义
线性表(List):零个或多个数据元素的有限序列,元素间具有线性关系,即前驱后继关系
线性表元素的个数n(n >= 0)定义为线性表的长度;当 n=0 时,称为空表
aᵢ 是第 i 个数据元素,称 i 为数据元素 aᵢ 在线性表中的位序
同一线性表中的元素必定具有相同特性,数据元素间的关系是线性关系
线性表的抽象数据类型
ADT List {
数据对象:D = { aᵢ | aᵢ 属于 Elemset,(i = 1,2,3……n,n >= 0)}
数据关系:R = { < aᵢ₋₁,aᵢ > | aᵢ₋₁,aᵢ 属于D,(i = 2,3,4……,n)}
基本操作:
InitList(&L)
操作结果:构造一个空的线性表L
ListEmpty(L)
初始条件:线性表L已存在
操作结果:若线性表L为空表,则返回True;否则返回False
ClearList(&L)
初始条件:线性表L已存在
操作结果:将线性表L重置为空表
ListLength(L)
初始条件:线性表L已存在
操作结果:返回线性表L中的数据元素个数
GetElem(L, i, &e)
初始条件:线性表L已存在,1 <= i <= ListLength
操作结果:用 e 返回线性表L中第 i 个数据元素的值
LocateElem(L, e,compare())
初始条件:线性表L已存在,compare()是数据元素判定函数
操作结果:返回L中第1个与 e 满足 compare() 的数据元素的位序。若这样的数据元素不存在则返回0
ListInsert(&L, i, e)
初始条件:线性表L已存在,i <= i <= ListLength(L)+1 //+1表示插在最后1个元素之后
操作结果:在L的第 i 个位置之前插入新的数据元素e,L的长度+1
ListDelete(&L, i, &e)
初始条件:线性表L已存在,1 <= i <= ListLength(L)
操作结果:删除L的第 i 个数据元素,并用 e 返回其值,L的长度-1
DestroyList(&L)
初始条件:线性表L已存在
操作结果:销毁线性表L
}ADT List
当你传递一个参数给函数的时候,这个参数会不会在函数内被改动决定了使用什么参数形式
- 需要被改动:传递指向这个参数的指针
- 不需要被改动:可以直接传递这个参数
顺序存储结构
用一段地址连续的存储单元依次存储线性表的数据元素
顺序表中元素存储位置的计算
如果每个元素占用8个存储单元,aᵢ存储位置是2000单元,则aᵢ₊₁存储位置是2008单元
顺序表的顺序存储表示
顺序表的特点:以物理位置相邻表示逻辑关系,任一元素均可随机存取
#define MAXSIZE 20 /* 存储空间初始分配量 */ typedef int ElemType; /* ElemType类型根据实际情况而定,这里假设为int */ typedef struct { ElemType data[MAXSIZE]; /* 数组存储数据元素,最大值为MAXSIZE */ int length; /* 线性表当前长度 */ }SqList;
描述顺序存储结构需要三个属性:
- 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置
- 线性表的最大存储容量:数组长度MAXSIZE
- 线性表当前长度:length
线性表的当前长度不能超过存储容量,即数组的长度
- 数组长度:存放线性表的存储空间的长度,一般不可改变
- 线性表的长度:线性表中数据元素的个数,随着表中数据的插入、删除而改变
如果数组长度为100,那线性表只能放99个元素,因为最后1格要放length
数组定义
- 静态分配的数组在什么位置,占多大的空间已经定了。数组的名字当中,存放的是数组当中的首元素data[0],放的是第1个元素的地址,也就是这个数组的首地址(基地址)
- 动态分配的数组,用指针存放数组当中首元素的地址,这个数组的内存可以用动态分配的函数来分配
c语言的内存动态分配
- malloc()函数用来分配内存,开辟堆区内存空间
- sizeof()计算出一个元素需要占多少字节,之后乘以MaxSize,计算出总共需要多少字节
- malloc()前的(ElemType*)强制类型转换,*表示强制类型转换成指向该类型的一个指针(如:这里一共800个字节,使用char类型,800个字节分成800块,使用int类型,800个字节分成200块)
- 动态开辟的内存要释放
线性表的顺序存储结构的实现(部分代码)
算法操作中用到的预定义常量和类型
- Status通常指返回值的类型
- ElemType通常指元素的类型
预定义常量和类型的好处
预定义常量:
语义清晰,提高可读性:
ERROR
和OK
这样的宏定义提供了明确的语义,使得代码更加易于理解。当看到return OK;
时,程序员可以立即知道这个函数调用是成功的,而return ERROR;
则表明有错误发生。相比之下,return 0;
或return 1;
则需要额外的上下文信息才能理解其含义。易于维护,便于重构:如果未来需要添加更多的状态码,使用宏定义可以方便地进行扩展。例如,可以定义
WARNING
、INFO
等状态码,而不需要修改每个返回0
或1
的函数。如果需要改变某个状态码的值,只需要修改宏定义,而不需要在代码中搜索所有的返回值。这减少了出错的可能性。跨平台兼容性:在不同的编译器或平台上,整数
0
和1
的处理可能有所不同。使用宏定义可以确保跨平台的一致性。
预定义类型:
这样,调用者和维护者可以更快地理解函数的返回值是一个状态码,而不是一个普通的整数。
提高可读性:使用
typedef
可以为数据类型定义一个更具描述性的名称。例如,Status
比int
更直观地表达了这个变量是用来表示状态的。它清楚地表明了变量的用途。易于维护:如果未来需要更改底层的数据类型,例如从
int
改为 double,只需要更改typedef
的定义即可,而不需要在代码中搜索和替换所有的int
。减少错误:使用
typedef
可以减少错误,因为编译器会将Status
视为一个类型,如果不小心将Status
与不兼容的类型一起使用,编译器会报错。类型安全,避免混淆:
typedef
可以提高类型安全,因为它确保了变量只能用于特定的用途。在复杂的系统中,使用typedef
可以避免与其他库或系统定义的int
类型混淆。
顺序表L的初始化
// 定义顺序表的结构 typedef struct { ElemType* data; // 存储空间的基地址 int length; // 当前线性表的长度 }SqList; // 顺序表L的初始化 Status InitList(SqList* L) { // 为线性表分配空间 L->data = (ElemType*)malloc(sizeof(ElemType) * MaxSize); // 存储空间分配失败,!L->elem成真,返回OVERFLOW if (!L->data) return OVERFLOW; // 空表长度为0 L->length = 0; return OK; }
顺序表L的插入
算法思路:
- 如果插入位置不合理,抛出异常
- 如果线性表的长度大于数组长度,则抛出异常或者动态增加容量
- 从最后一个元素开始向前遍历到第i个位置,分别将他们向后移动一个位置
- 将要插入元素填入位置i处
- 表长加1
// i是顺序表中的位序,e为要插入的元素 Status ListInsert(SqList* L, int i, ElemType e) { int k = 0; /* 顺序线性表已经满 */ if (L->length == MaxSize) return ERROR; /* 当i比第一个位置小或者比最后一个位置的后一位置还要大时 */ /* 即插入位置不合法 */ if (i<1 || i>L->length + 1) eturn ERROR; /* 若插入数据位置不在表尾 */ if (i <= L->length) { /* 将要插入位置之后的数据元素向后移动一位 */ for (k = L->length - 1; k >= i - 1; k--) // i-1就是最后一个元素,i是位序,不是下标 L->data[k + 1] = L->data[k]; //将前一元素移到后一位置 } /* 将新元素e插入第i个位置 */ L->data[i - 1] = e; /*表长+1*/ L->length++; return OK; }
顺序表L的删除
算法思路:
- 如果删除位置不合理,抛出异常
- 取出删除元素
- 从删除位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置
- 表长减1
Status ListDelete(SqList* L, int i, ElemType* e) { int k = 0; /* 线性表为空 */ if (L->length == 0) return ERROR; /* 删除位置不正确 */ if (i<1 || i>L->length) return ERROR; /*用e返回被删除的第i个元素*/ *e = L->data[i - 1]; /* 如果删除不是最后位置 */ if (i < L->length) { /* 将删除位置后继元素前移 */ for (k = i; k < L->length; k++) L->data[k - 1] = L->data[k]; } /*表长-1*/ L->length--; return OK; }
顺序表L的销毁
void DestroyList(SqList* L) { // 如果数组中有空间,就释放存储空间 if (L->data) free(L->data); // 将指针设置为NULL空指针 L->data= NULL; }
在C语言中,销毁一个动态分配的内存对象后,将指针设置为
NULL
是一种良好的编程习惯。这样做有几个好处:
防止悬挂指针:如果一个指针指向的内存已经被释放,那么这个指针就变成了悬挂指针(dangling pointer)。悬挂指针指向的内存区域可能已经被操作系统回收并分配给其他程序使用,如果程序继续使用这个指针,可能会导致不可预测的行为,如访问违规(segmentation fault)或者数据损坏。将指针设置为
NULL
可以避免这种情况。便于调试:将指针设置为
NULL
可以帮助调试程序。如果指针没有被设置为NULL
,那么即使它指向的内存已经被释放,它仍然保留着原来的地址。这可能会导致混淆,因为程序可能会错误地认为这块内存仍然可用。将指针设置为NULL
可以清楚地表明这块内存已经不再被程序所持有。避免重复释放内存:如果指针没有被设置为
NULL
,那么在程序的其他部分可能会再次调用free()
函数释放同一块内存,这会导致未定义的行为,通常是程序崩溃。将指针设置为NULL
可以防止这种情况,因为如果再次尝试释放这块内存,free(NULL)
是安全的,不会对程序造成任何影响。逻辑清晰:将指针设置为
NULL
可以清楚地表明这块内存已经被释放,指针不再指向任何有效的内存区域。这有助于程序的其他部分逻辑上明确地处理指针的状态。