写在前面,复习数据结构期末考试,感觉自己复习没效率啊,所以还是把自己的一些复习知识点分享出来吧。我们学校用的教材是《数据结构C语言版》(清华大学出版社)
一、基础知识
1、基本概念和术语
数据:数据是对客观事物的符号表示,在计算机科学中指所有能输入到计算机中并且被计算机程序处理的符号的总称。
数据元素:数据元素是数据的基本单位。
数据对象:是性质相同的数据元素的集合。
数据结构:是相互之间存在一种或者多种特定关系的数据元素的集合。
结构:集合,线性结构,树形结构,图状结构,网状结构。
2、算法
特性:
有穷性,确定性,可行性,输入和输出。
要求:
正确性,可读性,健壮性,效率与低存储量需求。
然后,我们介绍主要知识之前先介绍一下两个比较常用的函数,malloc()和realloc()。malloc()函数主要是为一个新的数据元素分配新的内存空间,这个空间内存指的是堆空间的内存。返回的是一个该类型的指针。
realloc(),就是重新分配一定的内存给某一个指针。
这两个函数在以后用的很常见,所以务必熟悉。
如:
L=(int *)malloc(100*sizeof(int)); //申请100个int型的数据,返回一个int型的指针赋给L。
L=(int *)realloc(L,100*sizeof(int));//重新分配100个内存给L,可能之前分配的不够
线性表
线性表主要分为顺序表(SqList),单链表(LinkList),双链表(DuList),循环链表。接下来,我们来分析这几种链表的一些基本的定义和使用。
typedef struct Sqlist{
ElemType *elem; //数据值,是一个数组元素的首地址
int length; //顺序表目前存储的元素的个数
int listsize; //顺序表的长度,也就是该顺序表可以最多存放几个元素
}SqList;
这个定义是顺序表的定义。主要是有三个变量,分别用来存放数据值,数据个数,表长。顺序表,顾名思义,就是表里面数据的各个元素的位置关系是相邻关系,一个紧挨一个。我们取元素可以直接通过下标进行定位。
typedef struct LNode{
ElemType data; //数据元素
struct LNode *next; //该结点的下一个结点,也叫结构指针
}LNode;
这个是单链表的定义,对于链式存储的结构,不同于顺序表,链式结构的最小单元是结点,并不要求结点之间是相邻的,注意,这个相邻,指的是内存的相邻,顺序表的相邻指的是内存的相邻,因此可以用下标进行定义,但是单链表不是这样的,单链表的内存不一定是相邻的,所以要定位某一个具体的结点,就只能从头结点进行遍历。这里,单链表又分为有头结点和没有头结点的,这个看题目具体的要求。我们查找某一个元素的时候,是通过从最开始的那个结点进行遍历,然后再执行相关的操作。
循环链表就不再进行详细解释了,顾名思义,就是我们普通链表的最后一个结点的next是指向NULL的,而循环链表的最后一个结点指向的是链表的头节点。
typedef struct DuList{
ElemType data;
struct DuList *next; //前驱结点
struct DuList *prior; //后继结点
}DuList;
双向链表,是从普通链表扩展出来的一种链表,就是我们链表的每一个结点,不仅只有指向该节点后面的结点指针,还有就是指向该结点前一个结点的前驱指针。对于单链表,我们如果想删除某一个结点的话,那么我们后面的结点就要纷纷往前移动一个单位。但是,如果我们要执行很多个操作的时候,也就是我们要进行移动指针的操作是很多的,对于数据量很庞大的链表,这是不可取的,所以我们就想一个结构,可以是移动的操作尽可能地少,并且保持链表的结点的相对位置不变。所以,我们就想到了双链表结构,我们对于删除操作的时候,我们只需要找到要删除的结点。然后让这个结点的前驱结点的后继指向这个结点的车后继。让这个结点的后继结点指向这个结点的前驱结点。这样说可能很绕。我给出一个图来吧。
大概的结构就是这样,代码如下:
p->prior->next=p->next;
p->next->prior=p->prior;
其实,对于单链表也不是不可以进行这样的删除操作,我们可以用一个单独的结点记录遍历的某一个结点的前驱结点。然后,一旦找到该结点,我们就将它的前一个结点的后继结点修改为该结点的后继结点。这样说感觉很绕,一开始学就这样,理解不了的话自己在纸上多模拟即可。
队列和栈
栈,栈是一种十分常见的一种数据结构,它的规律是先进后出的一种结构,这个结构,可以比喻为一个箱子,我们可以将一本一本书放进这个箱子,但是我们拿出来的时候,我们只能拿最上面的那一本书。
顺序栈
//顺序栈的结构定义
typedef struct {
SElemType *base; //栈底
SElemType data; //数据值
SElemType *top; //栈顶
int stacksize; //栈大小
}SqStack;
顺序栈有栈顶和栈底,当我们有一个新的元素插入栈里面的时候,top指针++,当有元素从栈里面清除的时候,我们的base++。栈的应用主要是后面的二叉树的遍历和图的遍历的相关的操作。
然后就是队列,队列是一种先进先出的结构,就像我们日常生活中排队一样,先来的,优先级肯定是更高的,所以对于一些操作它也是要优先进行的。
队列
//链队列的结构定义
typedef struct QNode{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct {
QueuePtr *next;
QueuePtr *rear;
}LinkQueue;
循环队列:和顺序栈类似的是,对于循环队列,我们一样有两个指针front和rear,这两个指针一开始都指向0的,当我们往队列里面添加元素的时候,rear指针自增1,当从链表里面删除元素的时候,我们的frontr指针就自增1,这样下来,我们就会遇到一个问题,我们怎么判断此时队列里面是否满的情况,因为在队列满的时候,我们是不能进行入队的操作的。显然, 我们不能用front和rear相等的条件进行判断了。解决这个问题,我们主要有两种方法:
1、我们空出一个空间不进行任何使用,当我们的对位指针的下一个位置是队首指针的时候,我们此时的队列就是满的情况了。
2、我们新增一个tag标志位,当我们的rear自增1后达到了front的位置时,我们就可以判断此时的队列是满的情况。当我们的rear自增1达到了front的时候,我们的tag标志位就记为1,说明此时队列是满的情况;反之,当我们的front+1达到了tag的时候,我们的tag的标志就记为0。这样下来,我们入队操作的时候,可以先查看tag位的数值情况,然后就可以判断操作是否合法了。
对于无法判断队列的长度的情况,我们推荐使用链队列实现动态处理。
树和二叉树
树是数据结构中十分重要的一个模块,树是后面学的图的一种特殊的形式。树的一些基本的概念我就不仔细介绍了。我就置介绍一些二叉树的性质。
性质1 在二叉树的第i层上至多有2^(i-1)个结点,每一层结点的个数依次为1,2,4,…
性质2 深度为k的二叉树至多有2^k-1个结点,对于一个完全二叉树,前i层的结点的个数依次为1,3,7,…
性质3 对任何一棵二叉树T,如果其终端的结点个数为n0,度为2的结点的个数为n2,则n0=n2+1.我们假设n1为度为1的结点的个数,则图中结点的总的个数为n=n1+n0+n2.对于一棵树,我们知道每一条分支出来对应于一个结点,当然,根节点不是由某一个分支出来的,结果为n=n1+2n2+1,两个方程联立可以得到n0=n2+1。
性质4 具有n个结点的完全二叉树的深度为log2n(向下取整)+1.
二叉树的存储结构有顺序存储和链式存储两种结构,个人倾向于后一种,灵活性更高,也更加直观。
二叉树的遍历
先序遍历, 根节点->左子树->右子树
中序遍历,左子树->根节点->右子树
后序遍历,左子树->右子树->根节点
层序遍历,第一层->第二层->第三层
我们可以发现,其实这个先后中,其实是对根节点而言的。所以可以凭借这个进行记忆。
但是,这里有一个用栈模拟的中序遍历还是比较值得记忆的,在一些企业的招聘面试也经常被问及。
Status InOrderTraverse(){
InitStack(S);Push(S,T);
while(!StackEmpty(S)) {
while(Gettop(S)&&p) Push(S,p->lchild); //向左走到尽头
Pop(S,p); //空指针退栈
if(StackEmpty(S)){
Pop(S,p); Visit(p->data); //访问这个结点的数据
Push(S,p->rchild); //将右结点入栈
}
}
}
哈夫曼树
哈夫曼树也叫做最优二叉树,构造一棵最优二叉树的方法是每次选取集合里面最小的两个数字合并成一个新的结点,最终就可以构造出一棵最优二叉树了。在算法竞赛里面,可以使用优先队列来进行处理,产生一棵最优二叉树。
图
图有几个概念还是比较重要的,比如什么是强连通分量,什么是强连通图等等。
图,可以说是数据结构最最重要的一个模块。但是我感觉学校老师在有限的时间里面很难讲清楚这个模块,所以,想学好的建议自己去刷题提升。
图的存储
我们知道图的存储主要有四种结构,邻接矩阵,邻接表,十字链表和邻接多重表这四种结构。
图的遍历
图的遍历主要深度优先遍历和广度优先遍历两种方式,深度优先遍历类似于我们对二叉树的线序遍历,广度优先遍历类似于我们学习的层序遍历。这里广度优先遍历可以用一个队列来进行模拟操作。
至于一些最小生成树,拓扑排序,最短路径算法,个人觉得考试应该不会考这些,主要是难度有点大。
查找
概念:
查找表是由同一类的数据元素构成的集合。
对表进行不更改的操作叫做静态查找,而加入要修改元素,位置,信息等的操作叫做动态查找。
关键字是数据元素中某一个数组项的值。
至于一些查找方法就不再介绍了,这里主要介绍一个概念,叫做平均查找长度。
平均查找长度:为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值称为查找算法在查找成功是的平均查找长度。
A
S
L
=
∑
i
=
1
n
p
i
c
i
ASL=\sum_{i=1}^{n}{p_i}{c_i}
ASL=i=1∑npici
p表示每一个位置的概率,c表示要走过的预期的长度。对于顺序查找,平均查找长度为
n
+
1
2
\frac{n+1}{2}
2n+1,但是我们要考虑到查找成功和查找不成功的情况,所以最后的式子应该是
A
S
L
=
3
(
n
+
1
)
4
ASL=\frac{3(n+1)}{4}
ASL=43(n+1)
折半查找的ASL=log2(n+1)-1.
分块查找的平均查找长度为
A
S
L
=
1
b
∑
j
=
1
b
j
+
1
s
∑
i
=
1
s
i
=
b
+
1
2
+
s
+
1
2
=
1
2
(
n
s
+
s
)
+
1
ASL=\frac{1}{b}\sum_{j=1}^{b}j+\frac{1}{s}\sum_{i=1}^{s}i=\frac{b+1}{2}+\frac{s+1}{2}=\frac{1}{2}(\frac{n}{s}+s)+1
ASL=b1j=1∑bj+s1i=1∑si=2b+1+2s+1=21(sn+s)+1
我们可以知道当s=sqrt(n)的时候,这个取值最小,所以最终的结果为
A
S
L
=
log
2
(
n
s
+
1
)
+
s
2
ASL=\log2(\frac{n}{s}+1)+\frac{s}{2}
ASL=log2(sn+1)+2s
动态查找表
动态查找表主要分为二叉排序树和平衡二叉树。
二叉排序树: 对于每一个结点,该节点的左子树上的值均小根结点的值,该结点右子树上的值均大于根结点的值。根据这个特性,我们发现,只要对二叉排序树进行一个先序遍历即可实现一个有序的序列了。二叉排序树的时间复杂度和折半查找的时间复杂度是类似的。注意的是二叉排序树并不是唯一的,树的高度可能影响查找的效率,所以我们接下来介绍的平衡二叉树就可以很好的降低树的高度。
平衡二叉树:平衡二叉树又称AVL树,它的性质主要是该二叉树的左右子树的深度之差不会超过1,平衡因子指的是该结点的左子树减去该节点的右子树,所以我们可以知道,平衡因子可能为1,0,-1这三个值。
哈希表
对于上面的直接查找,折半查找,或者是构造一棵二叉树进行查找的操作,我们都是要经过若干次的比较才能得出结果。那么我们就想,有没有更加简便的方法呢?就比如我们的单点函数,我们一个x对应于一个f(x),我们只需要一次就可以找到对应的值了。那么我们可以使用类似的方法,就是构造一个哈希函数进行存储。
对于不同的关键字可能得到同一个哈希地址。这种现象叫做冲突。具有相同函数值的关键字对该哈希函数来说称作同义词。然而,我们在构造哈希函数的时候,应该尽可能地减少冲突,但是却不能完全避免。
将一组关键字映像到一个有的连续的地址上,并以关键字在地址集中的像作为记录在表中的存储位置,这种表叫做哈希表。这一映像过程叫做哈希造表或者散列。所得的存储位置称做哈希地址或者散列地址。
哈希函数的构造方法
1、直接定址法 取关键字或者关键字的某一个线性函数值为哈希地址,即:
H(key)=key或者H(key)=akey+b
2、数字分析法 假设关键字是以r为基的数,并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。
3、平方取中法 取关键字平方后的中间几位为哈希地址。
4、折叠法 将关键字分割为位数相同的及部分,然后取这几部分的叠加和作为哈希地址。
5、除留余数法 取关键字被某一个不大于哈希表长m的数p出后所得的余数为哈希地址。对于这个数p,可以选择子树或者不包含小于20的质因数的合数。
6、随机数法 选择一个随机函数,取关键字的随机函数值为它的哈希地址。
哈希函数要考虑的因素:
1、计算哈希函数所需要的时间
2、关键字的长度
3、哈希表的大小
4、关键字的分布情况
5、记录的查找频率
处理冲突的方法
1、开放地址法
2、再哈希法
3、链地址法
4、建立一个公共溢出区
哈希表的查找过程中需和给定值进行比较的关键字的个数取决于三个因素:哈希函数,处理冲突的方法和哈希表的装填因子。其中,装填因子的定义为:a=表中填入的记录数/哈希表的长度。
排序
1、直接插入排序 2、选择排序 3、快速排序 4、堆排序 5、归并排序直接插入排序:
void InsertSort(int arr[],int lenght) {
for(int i=2;i<=length;i++){
if(arr[i]<arr[i-1]){
int temp=arr[i];
for(int j=i-1;j>0;j--){ //寻找适合的位置安放这个数
if(arr[j]<temp) break;
arr[j+1]=arr[j];
}
arr[j+1]=temp;
}
}
}
选择排序法
void SelectSort(int arr[],int length) {
for(int i=1;i<=length;i++) {
int mi=i;
for(int j=i+1;j<=length;j++){
if(arr[j]<arr[mx]) mi=j;
}
int temp=arr[mi];
arr[mi]=arr[i];
arr[i]=temp;
}
}
快速排序法
void QuickSort(int arr[],int l,int r) {
if(l<r){
int i=l,j=r,x=s[l];
while(i<j) {
while(i<j&&&s[j]>=x) j--;
if(i<j) s[i++]=s[j];
while(i<j&&s[i]<x) i++;
if(i<j) s[j--]=s[i];
}
s[i]=x;
QuickSort(arr,l,i-1);
QuickSort(arr,i+1,r);
}
}
后面两种算法考试不考,所以就不再复习了。但是对于自身的发展还是有用的,所以建议还是要去掌握。
最后,说点废话,数据结构确实是我们计算机学科中十分重要的一个模块,可能说可能会影响我们的未来的发展,所以我觉得还是不能仅仅局限于学校的课程学习,自己还需继续探索,深挖。我想,这可能也是为什么我一直坚持着去打ACM的原因之一吧。