数据的存储结构是指数据在计算机中的表示方法,
是数据的逻辑结构在计算机中的存储实现,
因此在存储时应包含两方面的内容——
数据元素本身 及数据元素之间的关系。
一般来说,一种数据结构的逻辑结构根据需要可以表示成多种存储结构,
常用的存储结构有顺序存储、链式存储、索引存储和散列存储等.
1. 顺序存储
顺序存储是指将数据元素存放在地址连续的存储单元里,
数据间的逻辑关系与物理关系一致,通常用数组来实现。
数组中的每个元素都是按物理顺序排列的,
如果你想访问那个单元你可以根据提供的指针等直接访问到需要的东西。
优点:
- 随机存取表中元素,访问速度快
缺点:
- 插入和删除操作需要移动元素,效率低下
而数组可分为两种:静态数组 和 动态数组
-
静态数组在定义时就已经确定了数组的大小,
-
而动态数组则可以在程序运行时动态地分配内存空间。
1.1 静态数组
优点:
- 访问速度快,不需要额外的内存空间
缺点:
- 不能改变大小,浪费内存空间
用C语言实现静态数组
#define MaxSize 10
typedef int ElemType;
ElemType arr[MaxSize];
用C++实现静态数组:与C语言描述静态数组一致
1.2 动态数组
优点:
- 可根据需要动态地分配内存空间,节省内存空间
缺点:
- 访问速度慢,需要额外的内存空间
用C语言实现动态数组
#define MaxSize 10
typedef int ElemType;
ElemType* pArr = (ElemType*)malloc(MaxSize*sizeof(ElemType));
free(pArr);
用C++实现动态数组
#define MaxSize 10
typedef int ElemType;
ElemType* pArr = new ElemType[MaxSize];
delete[] pArr;
2. 链式存储
链式存储是指将数据元素存放在任意的存储单元里,
这组存储单元可以是连续的,也可以是不连续的,
数据间的逻辑关系用指针来表示。
链式存储结构可用链表来实现,
链表中的每个元素都包含一个指向下一个元素的指针,
这样就可以通过指针来访问下一个元素。
优点:
- 插入和删除操作不需要移动元素,效率高
缺点:
- 不能随机存取表中元素,访问速度慢
而链表可分为两种:静态链表 和 动态链表
-
静态链表
静态链表是用类似于数组方法实现的,是顺序的存储结构,
在物理地址上是连续的,而且需要预先分配地址空间大小。
所以静态链表的初始长度一般是固定的,
即结点的个数是固定的。
-
动态链表
动态链表则不需要预先分配地址空间大小,
可根据实际情况动态地申请和释放空间。
动态链表中的每个结点都是动态地分配内存空间来实现的。
- 单向链表
- 双向链表
- 循环链表
2.1 静态链表
静态链表是用数组来实现链式存储结构,一种用数组描述的链表,
目的是方便在不设指针类型的高级程序设计语言中使用链式结构,
给没有指针的高级语言设计的一种实现单链表功能的方法
兼顾顺序表和链表的优点,是顺序表和链表的升级;
静态链表的数据全部存储在数组中(顺序表),但存储的位置是随机的,
数据直接的一对一关系是通过一个整型变量(称为 “游标”,类似指针的功能)维持。
优点:
- 快速访问元素
- 增加 或 删除数据元素,不需要移动大量元素
缺点:
- 容量固定不变,需提前分配较大的空间
- 不能随机存取
用C语言来实现静态链表
#define MaxSize 10 //静态链表的最大长度
#define End -1 //表尾
#define Blank -2 //此数据空间为空
typedef int ElemType
typedef struct Node{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标 也成为游标cursor
} StaticLinkList;
//静态链表 实际上就是一个结构体数组
StaticLinkList sL1[MaxSize];
用C++来实现静态链表:与C语言描述静态链表一致
适用场景:
- 不支持指针的低级语言
- 数据元素数量固定不变的场景
2.2 动态链表
动态链表是一种数据结构,它是由若干个结点组成的。
每个结点包含两部分:数据域和指针域。
其中,数据域存储数据,指针域存储下一个结点的地址。
动态链表与上面的存储结构不同,它是一种动态结构,不需要预先设定空间大小
整个可用存储空间可为多个动态链表共同使用,
每个链表占用的空间不需预先分配划定,而是系统按需即时生成的。
2.2.0 链表中的常规操作
-
如何表示/描述结点
-
C语言
//如何定义一个链表, 每个结点至少有两个部分, 结点的类型一致 typedef struct Node { int date;//数据域,存储数据本身 struct Node * pNext;//指针域,pNext指向一个和它本身存储指向下一个结点的指针 } NODE, *PNODE; //NODE 等价于 struct Node //PNODE 等价于 struct Node * //动态分配的新结点的地址赋给p PNODE p = (PNODE)malloc(sizeof(Node)); free p; //删除p指向结点所占的内存,不是删除p本身所占的内存 p->pNext; //p所指向结构体变量中的pNext成员本身
-
C++
/* 语法上, 对于动态内存分配的操作有些不同 */ PNODE p = new Node; delete p; /* 其他部分类似, 只不过 利用面向对象的思想 可能会进行封装 */
-
-
如何删除一个结点
/* 删除p所指节点的后面节点 */ //先临时定义一个指向p后面结点的指针r r = p->pNext; //r指向p后面的那个结点 p->pNext = r->pNext; free(r); //只需要p指针变量
-
如何插入一个结点
/* 把q所指向的结点插入到p所指向的结点后面 */ //【法一】:先临时定义一个指向p后面结点的指针r r = p->Next; //r指向p后面的那个结点 p->pNext = q; q->pNext = r; //每个时刻、语句指针变量会发生变化 //【法二】 q->pNext = p->pNext; p->pNext = q; //注意:这两行代码的顺序不可以颠倒
2.2.1 单向链表
单向链表是一种常见的基础数据结构,是一种线性表,
但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。
一个单向链表包含两个值: 当前节点的值 和 一个指向下一个节点的指针。
如果要访问链表中的某个元素,需要从第一个元素开始遍历整个链表,
直到找到所需的元素为止。
创建单向链表的方式:
- 头插法
- 尾插法
- 指针交换法
- 快慢指针法
- 递归法
从空表的初始状态起,依次建立各结点,并逐个插入链表。
根据插入位置的不同,链表的创建方法可分为 前插法 和 后插法。
- 前插法
前插法是通过将新结点逐个插入链表的头部(头部之后)来创建链表,
每次申请一个新结点,读入相应的数据元素值,然后将新结点插入到头结点之后。
因为每次插入在链表的头部,
所以应该逆位序输入数据,输入顺序和线性表中的逻辑顺序是相反的。
算法步骤:
- 创建一个只有头结点的空链表
- 根据待创建链表包括的元素个数n,循环 n 次执行以下操作:
- 生成一个新结点pNew
- 输入元素值赋给新结点pNew 的数据域
- 将新结点pNew 插入到头结点之后
typedef struct Node {
int date;
struct Node * pNext;
} NODE, *PNODE;
void CreatList_Head(PNODE pHead, int len) {
int i, val;
//创建一个带头结点的空链表
pHead = (PNODE)malloc(sizeof(NODE));
if (NULL == pHead) {
exit(-1);
}
pHead->pNext = NULL;
for (i = 0; i < len; ++i) {
//生成新结点pNew
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL == pNew) {
exit(-1);
}
//输入元素值赋给新结点pNew 的数据域
scanf("%d", &val);
pNew->data = val;
//将新结点pNew 插入到头结点之后
//确定后驱结点
pNew->pNext = pHead->pNext;
pHead->pNext = pNew;
}
}
- 尾插法
后插法是通过将新结点逐个插入链表的尾部来创建链表。
同前插法一样,每次申请一个新结点,读入相应的数据元素值。
不同的是,为了使新结点能够插入到表尾,需要增加一个尾指针 pTail 指向链表的尾结点。
因为每次插入在链表的尾部,所以读入数据的顺序和线性表中的逻辑顺序是相同的。
算法步骤:
- 创建一个只有头结点的空链表
- 尾指针 pTail 初始化,指向头结点
- 根据待创建链表包括的元素个数n,循环 n 次执行以下操作:
- 生成一个新结点pNew
- 输入元素值赋给新结点pNew 的数据域
- 将新结点pNew 插入到尾结 pTail 之后
- 尾指针pTail 指向新的尾结点pNew
typedef struct Node {
int date;
struct Node * pNext;
} NODE, *PNODE;
void CreatList_Rear(PNODE pHead, int len) {
//创建一个只有头结点的空链表
pHead = (PNODE)malloc(sizeof(NODE));
if (NULL == pHead) {
exit(-1);
}
pHead->pNext = NULL;
//尾指针 pTail 初始化,指向头结点
PNODE pTail = pHead;
for (i = 0; i < len; ++i) {
//生成一个新结点pNew
PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL == pNew) {
exit(-1);
}
//输入元素值赋给新结点pNew 的数据域
scanf("%d", &val);
pNew->data = val;
//将新结点pNew 插入到尾结 pTail 之后
//确定前驱结点
pTail->pNext = pNew;
pNew->pNext = NULL;
//尾指针pTail 指向新的尾结点pNew
pTail = pNew;
}
}
总结:头插法和尾插法的区别在于插入新节点的位置不同。
具体来说,头插法是将新节点插入到链表的头部,而尾插法则是将新节点插入到链表的尾部。
2.2.2 双向链表
双向链表是一种链表,它的每个节点除了存储数据外,
还有两个指针记录上一个节点和下一个节点的地址,
分别是前驱指针 prev
和 后继指针 next
。
双向链表在节点中除了指向下一节点的 next
指针外,还有指向前一节点的 prev
指针,
这使得双向链表可以在任意节点从头尾两个方向进行遍历,是 “双向” 的。
typedef struct DuLNode {
ElemType data;
struct DuLNode *prior, *next;
} DuLNode, *DuLinkList;
//DuLNode 是双向链表的节点结构体
//DuLinkList 是指向双向链表的指针
Status InitList(DuLinkList &L) {
L = (DuLinkList)malloc(sizeof(DuLNode));
if (!L) {
return ERROR;
}
L->prior = NULL;
L->next = NULL;
return OK;
}
创建双向链表的方式:
-
头插法:从头节点开始,依次将新节点插入到头节点后面,直到所有节点都插入完毕。
Status ListInsertHead(DuLinkList &L, int i, ElemType e) { DuLinkList p = GetElem(L, i - 1); if (!p) { return ERROR; } DuLinkList s = (DuLinkList)malloc(sizeof(DuLNode)); if (!s) { return ERROR; } s->data = e; s->prior = p; s->next = p->next; p->next->prior = s; p->next = s; return OK; }
-
尾插法:从尾节点开始,依次将新节点插入到尾节点后面,直到所有节点都插入完毕。
Status ListInsertTail(DuLinkList &L, ElemType e) { DuLinkList p = L; while (p->next) { p = p->next; } DuLinkList s = (DuLinkList)malloc(sizeof(DuLNode)); if (!s) { return ERROR; } s->data = e; s->prior = p; s->next = NULL; p->next = s; return OK; }
-
按序号插入:在双向链表的第 i 个位置插入一个新节点。
Status ListInsert(LinkList &L, int i, ElemType e) { LinkList p = GetElem(L, i - 1); if (!p) { return ERROR; } LinkList s = (LinkList)malloc(sizeof(LNode)); if (!s) { return ERROR; } s->data = e; s->next = p->next; p->next = s; return OK; }
2.2.3 循环链表
循环链表是一种特殊的链表,它的尾节点指向头节点,形成一个环形结构。
单向循环链表中,每个节点只有一个指针域,指向下一个节点;
双向循环链表中,每个节点有两个指针域,分别指向前驱节点和后继节点。
因此,循环链表可以是单向循环链表,也可以是双向循环链表。
3. 索引存储
索引存储是在记录文件之外建立一个索引表,
索引表中的每个索引项对应文件中的一个记录,
索引项由两部分组成:一个是索引项值,另一个是指向该记录的指针。
索引的存储原理大致可概括为一句话:以空间换时间。
一般来说索引本身也很大,不可能全部存储在内存中,因此 索引往往是存储在磁盘上的文件中的(可能存储在单独的索引文件中,也可能和数据一起存储在数据文件中)。
优点:
- 大大提高数据查询速度。
- 可以提高数据检索的效率,降低数据库的IO成本,类似于书的目录。
- 通过索引列对数据进行排序,降低数据的排序成本降低了CPU的消耗。
- 被索引的列会自动进行排序,包括【单例索引】和【组合索引】,只是组合索引的排序需要复杂一些。
- 如果按照索引列的顺序进行排序,对order 不用语句来说,效率就会提高很多。
缺点:
- 索引会占据磁盘空间。
- 索引虽然会提高查询效率,但是会降低更新表的效率。
- 维护索引需要消耗数据库资源。
综合索引的优缺点:
- 数据库表中不是索引越多越好,而是仅为那些常用的搜索字段建立索引效果最佳 !
实现索引存储的方式:
- STL - C++
map
unorder_map
- B+树
4. 散列存储
散列存储是根据记录的关键字 直接计算出 该记录的存储地址,以加快查找速度
关键字 --散列函数--> 存储地址
它是一种通过关键字直接访问记录的数据结构,
可以通过下标来直接访问,下标(也就是关键字和地址相关)
实现散列存储的方式:
-
链式法
链式法是将散列表中的每个元素都指向一个链表,
当多个键映射到同一个数组索引上时,它们会被添加到同一个链表中。
-
开放寻址法
开放寻址法是在散列表中寻找空闲的位置来存储冲突的元素,
当多个键映射到同一个数组索引上时,它们会被添加到下一个空闲位置上。
散列存储与哈希存储的关系:
- 散列存储和哈希存储是同一概念。
- 散列存储是一种将数据元素的存储位置与关键码之间建立确定对应关系的查找技术
- 而哈希表(散列表)是一种采用散列技术将记录存储在一块连续的存储空间中的数据结构。
- 哈希表中,地址会通过哈希算法来运算成一个相同长度的哈希值,
然后存放这个哈希值,而不是直接存放地址。
在C++中,可以使用 STL
中的 unordered_map
来实现散列存储。
unordered_map
是基于哈希表实现的,
它可以根据各元素的值来确定存储位置,然后将位置保管在散列表中,从而实现高速搜索。
记录的存储地址**,以加快查找速度
关键字 --散列函数--> 存储地址
它是一种通过关键字直接访问记录的数据结构,
可以通过下标来直接访问,下标(也就是关键字和地址相关)
实现散列存储的方式:
-
链式法
链式法是将散列表中的每个元素都指向一个链表,
当多个键映射到同一个数组索引上时,它们会被添加到同一个链表中。
-
开放寻址法
开放寻址法是在散列表中寻找空闲的位置来存储冲突的元素,
当多个键映射到同一个数组索引上时,它们会被添加到下一个空闲位置上。
散列存储与哈希存储的关系:
- 散列存储和哈希存储是同一概念。
- 散列存储是一种将数据元素的存储位置与关键码之间建立确定对应关系的查找技术
- 而哈希表(散列表)是一种采用散列技术将记录存储在一块连续的存储空间中的数据结构。
- 哈希表中,地址会通过哈希算法来运算成一个相同长度的哈希值,
然后存放这个哈希值,而不是直接存放地址。
在C++中,可以使用 STL
中的 unordered_map
来实现散列存储。
unordered_map
是基于哈希表实现的,
它可以根据各元素的值来确定存储位置,然后将位置保管在散列表中,从而实现高速搜索。