0. 数据结构
1. 线性表(逻辑结构)
1.1 定义 具有相同数据类型的 n个数据元素 组成的 有限序列。
除第一个元素,其余元素都有唯一直接前驱;除最后一个元素,其余元素都有唯一直接后继
【注意】线性表是一种逻辑结构,表中元素在逻辑上有顺序性 。顺序表和链表指的是存储结构。
1.2 基本操作
InitList(&L) //初始化表。构造一个空的线性表;
Legnth(L) //求表长度。返回线性表L的长度,即L中数据元素的个数;
LocateElem(L,e) //按值查找操作。在表L中查找具有给定关键字值的元素;
GetElem(L,i) //按位查找操作。获取表L中第i个位置的元素的值;
ListInsert(&L,i,e) //插入操作。在表L中第i个位置上插入制定元素;
ListDelete(&L,i,e) //删除操作。删除表L中第i个位置上的元素;
PrintList(L) //输出操作。按前后顺序输出线性表L的所有元素的值;
Empty(L) //判空操作。
DestroyList(&L) //销毁操作。
2. 线性表的顺序表示(顺序表 -- 存储结构)
2.1 定义
用一组地址连续的存储单元依次存储线性表中的数据元素,故各元素逻辑上相邻,物理上也相邻,支持随机存取。顺序表的存储结构可以描述为:
2.1.1 静态分配:
弊端:由于数组大小空间已经固定,一旦空间占满,会出现上溢。
#define MaxSize 50
typedef struct {
ElemType data[MaxSize];
int length;
} SqList;
2.1.2 动态分配:
注意:不是链式存储,依旧是顺序存储!只是在程序执行过程中,一旦数据空间占满,就会另外开辟一个更大的存储空间,去替换原来的存储空间。
#define InitSize 100
typedef struct {
ElemType *data; //指向数组中的第一元素
int MazSize, length; // 因为长度可变,所以需要设置一个最大容量
} SqList;
// 动态分配一整片连续的存储空间
L.data = (ElemType *)malloc(sizeof(ElemType) * InitSize);
// 销毁/销毁
free(L.data);
2.1.3顺序存储的特点
->顺序表最重要的特点是随机访问,即通过首地址和元素序号可以在时间O(1)内找到指定的元素。Loc() = Loc(L) + sizeof(ElemType) * (i - 1)
->顺序表的存储密度高,每个结点只存储数据元素。(链式存储还需要存放指针)
->顺序表逻辑上相邻,物理上也相邻。删除和插入需要移动大量元素。
->顺序表扩展容量不方便。
2.2 基本操作
2.2.1 插入操作
最好情况:插入最后一个位子 O(1)
最坏情况:插入第一个位置 O(n)
平均情况: 所以为: O(n)
// i是位序,从1开始
bool ListInsert( SqList &L, int i, ElemType e ) {
// i输入不合法
if ( i < 1 || i > L.length +1 ) {
return false;
}
// 存储空间已满,不能继续插入
if ( L.length >= MaxSize) {
return false;
}
// i后元素均后移一位
for ( int j = L.length; j <= i; j-- ) {
L.data[j] = L.data[j-1];
}
L.data[i-1] = e;
L.length++;
return truel;
}
2.2.2 删除操作
最好情况:删除最后一个元素 O(1)
最坏情况:删除第一个位置 O(n)
平均情况: 所以为O(n)
bool ListDelete( SqList &L, int i, ElemType &e) {
if( i < 1 || i > L.length + 1 ) {
return false;
}
e = L.data[i - 1]; // 记录删除的元素
for ( int j = i; j < L.length; j++ ) {
L.data[j - 1] = L.data[j];
}
L.length--;
return true;
}
2.2.3 按值查找
最好情况:元素在第一个位置 O(1)
最坏情况:元素在不存在(或在表尾) O(n)
平均情况: 所以为O(n)
int LocateElem ( SqList L, ElemType e) {
for ( int i = 0 ; i < L.length; i++ ) {
if ( L.data[i] == e ) {
return i + 1;
}
}
}
3. 线性表的链式表示(链表 -- 存储结构)
3.1 单链表
3.1.1 定义
结点描述如下:
typedef struct LNode {
ElemType data; // 数据域
struct LNode *next; // 指针域
}LNode, *LinkList;
3.1.2 基本操作
头插法(链表的逆置): 每个结点的插入为O(1),设链表长n,则时间复杂度为O(n)
LinkList List_headInsert(LinkList &L) {
LNode *s;int x;
L = (LinkList)malloc(sizeof(LNode)); // 创建头结点,成为一个单链表
L->next = null; // 单链表初始化
scanf("%d", &x);
while(x != 9999) {
s = (LNode *)malloc(sizeof(LNode)); // 分配结点空间
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d", &x);
}
return L; // 返回单链表
}
尾插法:由于设置了一个尾指针p,故每个结点插入为O(1),链表长度为n,时间复杂度为O(n)
LinkList List_tailInsert( LinkList &L ) {
LNode *s, *p; int x;
L = (LinkList)malloc(sizeof(LNode));
L->next = null;
p = L;
scanf("%d" , &x);
while(x != 9999) {
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
s->next = p->next;
p->next = s;
p = s; // p结点往后移
scanf("%d", &x);
}
free(p); // 释放指针p
return L;
}
按序查找结点值:时间复杂度为O(n)
LNode *GetElem(LinkList &L, int i) {
LNode *p = L->next;
int j = 1; // 计数,初始为1
if ( i == 0) return L; // i=0时 => 头结点
if ( i < 1) return null;
// 这里用for不够完美 无法判别p是否为null
while ( p && j < i) {
p = p->next;
j++;
}
return p;
}
按值查找表结点:时间复杂度为O(n)
LNode *LocateElem(LinkList *L, ElemType e) {
LNode *p = L->next;
while ( p && p->data != e) p = p->next;
return p;
}
插入结点操作:将s结点插入到第i个位置上
该算法主要的时间开销为查找第i-1个结点,时间复杂度为O(n)。若给定p结点,在p结点后插入结点,时间复杂度为O(1)。
p = GetElem(L, i-1); // 找到第i个结点的前驱结点
s-next = p->next;
p->next = s;
若给定p结点,在p结点前插入结点s:我们可以先后插,再交换数据值。
s->next = p->next;
p->next = s;
temp = p->data;
p->data = s->data;
s->data = temp;
删除结点操作:将第i个位置的结点删除。和插入操作类似,时间开销在查找前驱结点上,时间复杂度为O(n),若给定p结点,删除p结点的后一结点,时间复杂度为O(1)。
p = GetElem(L, i-1);
q = p->next;
p->next = q->next;
free(q);
若要删除p结点,可以找到p结点的前驱结点,再删除p结点,但是这样需要花费查找的时间复杂度O(n);所以可以采取另一种算法:删除p的直接后继结点,把后继结点的数据赋给p,这样时间复杂度为O(1)。
q = p->next;
p->data = q->data;
p->next = q->next;
free(q);
求表长操作:计算单链表中(不包含头结点)的结点个数,时间复杂度为O(n)。
3.2 双链表
由于在单链表中,访问后继结点的时间复杂度为O(1),而访问前驱结点只能从头遍历,时间复杂度为O(n),故引入双链表。
3.2.1 定义
双链表结点结构的描述:
typedef struct DNode {
ElemType data;
struct DNode *prior, *next; // 前驱和后继指针
}DNode, *DlinkList;
3.2.2 基本操作
按位查找和按值查找与单链表无异,插入和删除操作与单链表有区别。
插入(后插)操作:前插操作可以转化为后插操作
// 在p结点后插入结点s
s->next = p->next;
p->next->prior = s;
p->next = s;
s->prior = p;
第二行代码在p是最后一个结点时会报错,需要做进一步判别!
// 在p结点后插入s结点
bool InsertNextDNode(DNode *p, DNode *s) {
if ( p == null || s == null ) return false;
s->next = p->next;
if ( p->next != null )
p->next->prior = s;
s->prior = p;
p->next = s;
return true;
}
删除操作:
// 删除p的后继结点q
p->next = q->next;
q->next->prior = p;
free(q);
第二行代码在q是最后一个结点是会出错,所以需要进一步判别!
// 删除p结点的后继结点
bool DeleteNextDNode(DNode *p) {
if ( p == null ) return false;
BNode *q = p->next;
if ( q == null ) return false;
p->next = q->next;
if ( p->next != null )
q->next->prior = p;
free(q);
return true;
}
3.3 循环链表
3.3.1 循环单链表
初始化:L->next = L;
3.3.2 循环双链表
初始化:L->next = L; L->prior = L;
基本操作:其插入和删除与双链表的区别是不需要判别p是否为最后一个结点。
3.4 静态链表
借助数组来描述线性表的链式存储结构,这里的指针是结点的相对地址(数组下标),又称游标。和顺序表一样,静态链表也需要预先分配一块连续的内存空间。
适用于操作系统的文件分配表FAT。
结构类型描述如下:
#define MaxSize 50
typedef struct {
ElemType data;
int next;
} SLinkList[MaxSize];
[0] 头 | 2 |
[1] b | 6 |
[2] a | 1 |
[3] d | -1 |
[4] | |
[5] | |
[6] c | 3 |
【注意】 0号结点充当头结点,游标-1表示已经达到表尾
->初始化静态链表:a[0]的next设为-1;
->查找:从头结点出发挨个往后遍历结点,时间复杂度为O(n);
->插入位序为i的结点:
a. 找一个空的结点存放元素
b. 找位序为i-1的结点
c. 修改新结点的next
d. 修改i-1号结点的next
->删除某个结点:
a. 找前驱
b. 修改前驱结点的游标
c. 被删除结点的next改为-2,表示空闲
【优点】增删操作不需要移动大量元素
【缺点】不能随机存取,只能从头结点开始,依次往后找;容量固定不变。
4. 顺序表和链表的比较
顺序表 | 链表 | |
存取方式 | 随机存取 | 顺序存取 |
逻辑结构与物理结构 | 逻辑相邻,物理也相邻 | 逻辑相邻,物理不一定相邻 |
查找、插入和删除操作 | 顺序表无序: 按值查找:O(n) 顺序表有序(折半查找): 按值查找:O() 按位查找:O(1) 插入、删除:O(n),用于移动元素 存储密度大 | 按值查找:O(n) 按位查找:O(n) 插入、删除:O(n),用于查找元素,但相较于顺序表,链表的时间开销更低 存储密度小 |
空间分配 | 大片连续空间分配不方便,改善容量不方便 | 离散的小空间分配方便,改变容量方便 |
之后有新的知识点会继续补充!