数据结构研究的是:我们如何把现实中大量而复杂的问题以特定的数据类型和特定的存储结构保存到主存储器(内存)中,以及在此基础上为实现某个功能(比如查找、删除某个元素)而执行的相应操作(这个操作也叫做算法)
总得来说,数据结构研究的是数据的存储和对数据的操作。
其中,数据的存储包括数据个体的存储、个体间关系的存储,我们认为个体关系的存储更为重要,系数据结构所要研究的一个重点。
既然要研究数据的存储,那么我们就需要创造一些存储结构,这些存储结构既要能够满足存储需求,又要方便存储数据之间的关系。
我们将存储结构分为线性结构和非线性结构,划分的依据为结构是否为一维的。其中,线性结构包括数组、链表,非线性结构包括树和图。对于线性结构,其常见应用为栈和队列。这里我主要详细学习了链表、栈、队列,粗略学习了树。
在正式学习数据结构之前,需要预备和巩固一些C语言的知识,如指针、结构体、动态内存的分配:
而结构体则是为了表示一些复杂的数据(普通的基本变量类型无法满足需求)。能熟悉结构体的基本操作就行了,诸如访问其成员变量之类的。
研究动态内存分配有以下几个原因
可以通过malloc得到一块内存空间,并以此方法得到大量(许多块)离散的空间。那么我们就可以通过malloc来模拟一些离散结构或者连续结构。
函数调用中,子函数生成的变量存在作用域,所生成的变量是静态的,有生存期,而malloc生成的内存空间只要没有free掉,就是一直存在的,动态的。那么我们就可以通过malloc生成一些不被自动清除的存储结构。
有了这些简单的理解就可以开始学习数据结构了。
这个没啥好说的,都已经会了。
由于有时候我们无法得到一块较大且连续的内存空间,但是我们可以得到同样内存大小只不过是离散的空间。于是有了链表。
链表简单来说就是先定义一个个"节点"的存储结构,然后用指针把这些结构依次联系起来,得到一条线性的链,即为链表。具体的定义为:
n个节点离散分配,彼此通过指针相连,每个节点只有一个前驱节点和一个后续节点,首节点没有前驱节点,尾结点没有后续节点
于是我们就要研究如何去构建一个"节点"的存储结构:定义一个结构体,其中包含一个数据域和一个指针域,数据域可以是简单的一个int型的数据,也可以是复杂的。指针域可以是两个指针,用于指向该节点的前后两个节点,也可以只用一个指针,指向该节点的后续节点。(两个指针的那种称为双向链表,一个指针的为单向链表,这里我们只以单向链表为例)
//创建一个结构体来表示单链表的节点
typedef struct Node
{
int data; //数据域
struct Node * pNext; //指针域
}NODE,*pNODE; //NODE等价于 struct Node,pNODE等价于struct Node *
有了单个"节点"结构,怎么把整个链表链接起来呢?就用指针,每个节点的指针指向下一个节点,以此类推,就实现了链表的链接。这里不展开,详细代码见附带文件。
这里要提到一点,在首节点前,还有一个头节点,不存放有效数据,只是为了方便链表的一些操作。
于是通过指向链表头节点的指针,我们就可以获取到整个链表。但是,由于是单链表,我们要对链表进行某些操作(插入节点、删除节点)时,必须从头开始往后遍历。
具体代码见附带文件。
栈是在链表的基础上形成的一种存储结构,可以理解为一种容器,一种可以实现“先进后出”的存储结构,类似于子弹弹夹。
栈这种存储类型简单来说就是定义两个指针,用来表示栈顶和栈底,然后用节点去填充这个栈,每填充一个节点,该节点的指针指向先前的栈顶的节点,同时,指向栈顶的指针跟着移位指向新的栈顶。
具体代码定义如下:
typedef struct Stack
{
pNODE pTop; //栈的顶部
pNODE pBottom; //栈的底部
}STACK,*pSTACK; //STACK等价于 struct Stack,pSTACK等价于struct Stack *
这里可以这样理解,其实栈只是提供了两块铁片,实际存储单元还是节点,我们只不过用这两块铁片分别放在这些节点的顶部和底部,"方便"我们操作而已。所以我们可以把栈看做是操作受限的链表(只能从栈顶对其进行操作),诸如压栈、出栈其实就是链表的增加一个节点和删除一个节点的操作,只不过多了两个栈指针,所以稍微复杂了一些。
栈遵循"先进后出",或者说"后进先出",具体应用有诸如函数调用、中断、内存分配等。
具体代码见附带文件。
队列是一种可以实现“先进先出”的存储结构。
这里以静态队列为例,用数组实现。(链式队列——用链表实现,这里不谈,但是队列是链表的常用应用)
静态队列为了避免内存空间的浪费,通常都采用循环队列。于是我们可以把队列想象成一个轮盘、一个左轮手枪的子弹槽。与栈类似,我们有两个指针,一个用于指向新增添的元素的下一位,一个用于指向新删去的元素的下一位。
一开始,两个指针同时指向起始位置(数组下标设为零)。每增加一个元素(即增加节点),存放在名为rear的指针所指向的位置,且rear指向下一位。每删除一个元素(即删除节点),删除名为front的指针所指向位置的节点,且front指向下一位。则front和rear就在元素增减的过程中追赶。由于是循环队列,所以整个"转盘"一直转圈,内存空间恒定,不会存在内存泄露或者浪费的问题。
同时,我们也能发现,我们只能从rear所在位置增加元素,只能从front所在位置删除元素,先进入的节点最接近front最先被删除,是一种"先进先出"的结构。而且它其实也是一种操作受限的链表。
由于是先进先出,所以所有和时间有关的操作都有队列的影子,根据时间顺序先干嘛后干嘛…
其定义是一个函数直接或间接调用自己。
递归的思想个人总结为:任务复杂,能力不足,任务下推
即,尽管任务很庞大,我只管处理我所能够处理的最小一部分,其余部分交给"另一个未来的我"去处理。
或者说,一个任务实现的前提,是完成一个难度稍微降低的同一任务,且存在一个最底层的任务能够轻易完成。
递归的应用:树和森林就是以递归的方式定义的,树和图的很多算法都是以递归来实现的
非线性结构包括树和图,只学了树,这里只讨论二叉树。
其实和前面的链表差不多的是,基础结构还是节点,只不过这个节点的指针域需要有两个指针,用来指向左孩子和右孩子节点。然后类似链表,通过指针链接起来就行了。
这里,我们回顾对比一下之前的链表、栈、队列。
我们之前提到,数据存储中,我们认为个体关系的存储更为重要,系数据结构所要研究的一个重点。
这里树是非线性结构,各个节点之间的关系不是一维的,不是单纯的线性。这样的话,树的存储会有什么不同吗?或者说,我们又如何处理各个节点的关系呢?
其实这个问题我们在生成树的基本单元——节点——的时候,就已经解决了。每个节点都存放着两个指针,指向该节点的孩子节点。只要指针指好了,个体关系就确定好了。那么虽然说各个节点是离散的,只管存放就是了,节点的关系已经保存在每个节点中了。
也就是说,尽管我们每个节点可能是按照随意顺序存放的,但是只要拿出来,还是一整棵完整的树,关系通过存放在节点中的指针保存。
接下来又有一个问题,个体间关系是保存起来了,我们怎么表现出来呢?如果我们罗列出树的所有元素,我们怎么把关系也同时展示出来呢?
我们知道树是非线性的结构,如果我们把这些节点拍成一排,变成线性的一维结构,就要想办法用一种顺序来确定他们的关系。于是就有了先序遍历、中序遍历和后序遍历。通过以上三种遍历方式的其中一种,可以把整棵树转换成一维线性的结构输出,通过以上三种便利方式的中序遍历以及剩余任意一种,可以将一维线性的这种结构还原成原本的非线性的树的结构(关系)。
以上。
学习途径:
个人根据教程敲的一些代码以及整理的一些笔记: