一、数据模型
(一)存储架构
1 顺序结构
什么是顺序结构?故名思意,使用一块连续的内存来存放数据就是顺序结构,我们最常见的就是数组,但是,我们知道,直接定义数组的话,使用的是栈内存空间,而可用的栈内存是非常少的,因此,我们所说的数据结构都是站在堆内存上来说的。
在堆内存上,我们可以使用malloc和calloc来动态分配内存:
malloc(sizeof(int)*40); // 申请一块足够连续存放40个int类型数据的内存
上面就是一个典型的顺序存储结构,在一块连续的内存上,存放数据,优点是使用简单,也可以直接通过下标或者地址偏移来获取数据,但是缺点也很明显,一开始就分配了一大块的内存,很容易造成浪费。例如,我们预先估计需要存储20个数据,就申请了30个地址,但是发现在某些时候,需要存储100个数据,那我们怎么办呢?因为一开始就申请了20个空间了,又不能在运行的时候重新分配空间,因此只能一开始申请100个单位的空间,这就以为着有大量的时间我们都会有大片的空间被浪费,特别是对于内存紧缺的嵌入式开发来说,这是要命的。
因此,我们又想了另一种存储数据的方式,分散式存储,也叫做链式存储。
2 链式存储
什么是链式存储?链式存储又叫做分散式存储,故名思意,就是我们不是把数据放在一块连续的大的内存里,而是分散的,单独为一个个数据分配空间来存储它。
这时候就会有一个问题,对于这些零散的数据,我们是没有办法通过下标或者地址偏移来寻找到他们的,因为他们不一定会在连续的内存上,那我们怎么才能找到这些数据呢。
答案是使用指针
。我们可以在存储数据的时候同时把下一个数据的地址存储进来,就能够通过上一个数据获取到下一个数据的地址了,这就用到了结构体
。
struct node{
int data; // 存储的数据
struct node *next; // 指向下一个结构的指针
}
这样我们就能够得到一个数据节点,我们在内存中存放的都是这样的数据节点,每个数据节点又存放了下一个数据节点的地址,因此,我们只要保存着第一个节点的地址,就能依次遍历到每个节点。
(二)逻辑结构
1 线性逻辑
像一个班级的同学的学号就是一种线性结构,是按照一定顺序排列下来的。如下图:
每个节点都是一对一的关系,就是最多有一个箭头指向下一个。
2 非线性逻辑
像每个同学的人际关系就是非线性逻辑,因为每个同学都和多个不同的人有关系,就是一对多。
在平时使用的非线性逻辑中,又属二叉树数最常使用的。
二叉树和普通树的区别就是二叉树的每个节点有且仅有最多两个子节点。
为什么要使用二叉树呢?这是因为后面的BST和经典的红黑树,都是基于二叉树的。
二、顺序表
(一)定义一个顺序表
用顺序存储结构来存储线性逻辑,就是顺序表。
定义个顺序表
#define MAX_LEN
typedef int datatype;
typedef struct node{
datatype array[MAX_LEN];
int last;
} sql;
上面定义了一个顺序表结构,该结构总共有一个长度为100的存放datatype
数据的数组,同时还有一个记录数组使用的长度的last
。
(二)初始化顺序表
有了顺序表,那么我们要做的第一件事就是初始化它,不然怎么使用呢?
sql * init_sql(void)
{
sql *p = malloc(sizeof(sql));
if (NULL == p) // 如果p为NULL,代表malloc申请内存失败了,返回NULL
{
return NULL;
}
sql->last = -1; // 将数组以使用长度置为-1
}
上述初始化代码里我们并没有对申请到的数组进行清零操作,其实这是没有必要的,因为我们是根据last
来判断顺序表里是否存放了数据,因此只要将last
置为-1,我们就知道这个顺序表里并没有存放数据,即使数组里不为空,我们也知道这些数据不属于我们,从而不会去使用。
为什么要将last
置为-1呢?因为数组下标是从0
开始的,添加一个数据后,last++
刚好变为0。
(三)添加数据
初始化了一个空的顺序表后,我们就需要往里面添加数据了。添加数据的时候可能会碰到两种不同的可能:
- 数组没满,可以插入
- 数组已满,不能插入
我们可以实现一个函数来判断数组是否满了:
bool is_full(sql *list)
{
return list->last == MAX_LEN - 1;
}
然后我们就可以在准备插入数据的时候,先判断数组是否为满,满了就不插入。
如下代码:
int sql_insert(sql *list, datatype data)
{
if (is_full(list))
{
perror("数组已满\n");
return -1;
}
list.last++;
list.array[list.last] = data;
return 0;
}
(四)遍历数据
有了数据之后我们就需要来看看数据长什么样,需要一个遍历函数。
void sql_find(sql *list)
{
int i;
for (i = 0; i <= list->last; ++i)
{
printf("%d ", list->array[i]);
}
}
(五)删除数据
遍历完后,我们就可能要删除数据了,代码如下:
int sql_delete(sql *list, datatype data)
{
int i;
for (i = 0; i <= list->last; ++i)
{
if (list->array[i] == data)
{
memcpy(list->array + i, list->array + i + 1, sizeof(datatype)*(list->last - i));
--list->last;
break;
}
}
}
删除数据,我们找到需要删除的数据后,就把后面的数据全部往前一个单位填充,将i
的数据覆盖掉,就是变相删除了,最后还需要把last
的值置为-1。