1 线性表
线性表是最常用的数据结构
除了第一个元素无直接前驱、最后一个元素无直接后继外,其他每个数据元素都有一个前驱和后继
1.1 线性表的表示方式
1.1.1 顺序表示方式
用一组地址连续的存储单元依次存储线性表的数据元素,这种存储结构的线性表称为顺序表。
随机存取:顺序表逻辑上相邻的数据元素,物理次序也是相邻的。每个元素的占用存储空间的大小是确定的,所以只要确定了存储线性表的起始位置,线性表当中的任一数据元素都可以随机存储。随机指的是时间上随机,与其他元素无关。
线性表当中的基本操作
- 初始化
- 取值
- 查找
- 插入
- 删除
1.1.2 链式表示方式
线性表链式存储结构的特点是:用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。为了表示每个元素和其后继之间的逻辑关系,每个元素除了存储自己本身的信息,也需要存储指示其直接后继的信息,由这两部分组成的存储映像称为结点。
- 结点包括两个域,存储元素信息的域称为数据域,存储直接后继的位置的域称为指针域
- 线性表的链式存储结构由n个节点组成
- 单链表或者说线性链表每个节点都只包含一个指针域
- 链表的存储必须从头指针开始进行,头指针指示链表当中的第一个结点(第一个元素,也称首元结点)
- 最后一个元素没有直接后继,单链表当中把最后一个结点的指针置为空
- 使用链表时,关心的是数据元素之间的逻辑相邻的关系,而不是实际存储位置
- 为了处理方便有时会在单链表的第一个结点前设置一个头结点,其数据域可以不存储其他信息,也可以存放长度之类的附加信息,指针域指针指向首元结点 头节点的用处
- 便于首元结点的处理,有了头节点,那么首元结点也和其他结点一样,地址被保存在前驱的指针域
- 便于空表和非空表的处理,有了头节点,空表的指针的指向和非空表一样不为空,指向头节点
顺序存取:单链表要去的第i个元素必须从头指针出发顺链进行寻找,也称单链表为顺序存取的存取结构
单链表的操作
- 初始化
- 取值
- 查找
- 插入
- 删除
单链表添加新的结点可以有前插法和后插法两种
- 前插法将元素添加到链表的首元结点之前
- 后插法设置一个尾指针用来指向最后一个元素,后插法添加元素将其插入到最后一个元素后边
循环链表
最后一个结点的指针指向头结点,整个链表形成一个环,从链表的任一结点出发都能找到链表的其他结点
- 当前指针为p,判断当前的结点是否是尾部,p->next == L
- 只设立尾指针而不设立头指针有时会让一些操作变得简单,比如合并。尾指针指向尾结点,尾结点的指针指向头节点,现有尾指针A、B,合并的操作可以表示为:
p = B -> next -> next ,保存B的第一个元素的地址
B->next = A->next, 将B的最后一个元素指向A的头结点,即新的链表的头部是A的头节点
A->next = p , 将A的最后一个元素指向B的第一个元素
双向链表
单链表不能从某个结点出发寻找到其前驱,要找前驱只能从头节点出发往后找;而循环链表可以顺着链表绕一周找到当前结点的前驱。这些链表在查找前驱的时候都不方便。在查找直接前驱耗费的时间比查找直接后继上耗费的时间多。
双向链表结点中有两个指针域,一个指向其直接前驱,一个指向其直接后继。在带来寻找直接前驱的遍历的同时,也增加了删除、插入等操作的复杂性 双向链表也可以有循环表,其头部的指针指向尾部,尾部的指针指向头部,形成闭环
1.2 顺序表和链表的比较
1.2.1 空间性能的比较
- 存储空间分配
- 顺序表的存储空间必须预先分配,元素个数扩充受限,易造成存储空间浪费或者空间溢出现象
- 链表不需要预先分配空间,只要内存空间充足,链表中的元素个数没有限制
- 当线性表长度变化比较大,难以预估存储规模时,宜采用链表作为数据结构
- 存储密度大小:指元素本身占用的存储量和整个结点占用的存储量之比
- 顺序表的存储密度为1
- 链表的存储密度小于1,链表除了数据域还有指针域,如果数据域过小,就会造成指针域占的比重过大,存储密度减小
- 存储密度越大,存储空间的利用率也就越大,不考虑顺序表中空闲区,顺序表的空间利用率为100%
- 当线性表长度变化不大,易于事先确定大小的时候1,为了节约空间,宜采用顺序表作为存储结构
1.2.2 时间性能的比较
- 存取元素的效率
- 顺序表是随机存取的结构,指定任意一个位置的序号i,都能偶在O(1)时间内直接存取该位置上的元素
- 链表是顺序存取结构,访问链表当中的第i个元素,只能从头开始依次遍历直到第i个元素,时间复杂度为O(n)
- 如果线性表的主要操作是和元素位置紧密相关的取值操作,很少插入或者删除,适合用顺序表
- 插入和删除操作的效率
- 顺序表插入删除平均需要移动表内一半的元素,时间复杂度为O(n)
- 链表确定插入或删除的位置之后,只需要修改指针,时间复杂度为O(1)
- 如果线性表需要频繁进行插入或删除操作,适合使用链表
2 栈和队列
2.1 栈
栈:stack,是限定在表尾进行插入或者删除操作的线性表
栈顶:top,代表栈的线性表的尾端
栈顶:bottom,代表栈的线性表的表头
栈的修改是按照后进先出的原则进行的,类似生活当中的洗好的盘子总是逐个网上叠放在洗好的盘子上面,使用时从上往下逐个取用;一盒羽毛球,放进去时从盒子底部一直放到盒子出口,要取出来用的时候总是先拿到靠近盒子出口的。
栈的相关操作
- 初始化
- 销毁
- 清空
- 判断是否空栈
- 取栈内元素个数
- 入栈
- 出栈
- 取栈顶元素(不出栈)
2.2 队列
队列:queue,只允许在表的一段进行插入,在另一端进行删除操作的线性表
队头: front,允许删除的一端
队尾: rear,允许插入的一端
队列是一种先进先出(First In First Out)的线性表,类似食堂当中排队打饭,位于队伍前靠近窗口的同学先拿到饭先离开,位于后面的同学要等待前面的同学都拿到饭后才能取饭离开,不能插队,想要取饭必须排到队伍的最后边
队列的相关操作
- 初始化
- 销毁
- 清空队列
- 队列是否为空
- 取队列元素个数
- 入队
- 出队
- 取队头元素 (不出队)
3 串、数组和广义表
3.1 串
串是由零个或多个字符组成的有限序列,串的长度为串内包含的字符的个数,零个字符的串称为空串
子串:串中任意个连续字符组成的子序列称为该串的子串
串的相等:仅当两个串长度相等且每个位置对应的字符相等时才称这两个串是相等的
模式匹配:也叫串匹配,用于定位子串的位置,常用于搜索引擎、拼写检查、语言翻译、数据压缩等。著名的模式匹配算法有BF算法和KMP算法。设有字符串S和T,S为主串,T为子串(模式),模式匹配就是查找子串T在字符串S中出现的位置
3.1.1 BF算法
算法思路
- 从指定位置i开始,匹配字符串S和T
- 如果当前位置两个串上的字符相同,比较两个字符串下一位置的字符
- 如果当前位置两个串上的字符不相同,S从i + 1位置开始 、T从头开始重新进行匹配
- 如果成功匹配到T的结尾位置,返回S成功匹配的开始位置,否则匹配失败
int bf(String S,String T,int pos){
int i = pos;
int j = 0;
while(i + T.length() - 1 < S.length()){
if(S.charAt(i) == T.charAt(j)){
if(j == T.length() - 1){
return i - T.length() + 1;
}
i ++;
j ++;
}else{
i = i - j + 1;
j = 0;
}
}
return -1;
}
时间复杂度:O(m * n),m、n分别为S和T的长度,m < n
- 匹配的趟数最多为 m - n + 1
- 每趟匹配匹配的次数最多为n
- 所以时间复杂度为O((m - n) * n) ,即O(m * n)
3.1.2 KMP算法
由Knuth、Morris、Pratt同时设计实现的,简称KMP算法 。在BF算法之上进行少许改进,也就是S的指针i一旦开始匹配就不再回溯。用数组next记录下S和T在T处于j位置时不匹配,j应该移动到哪个位置,继续S和T的匹配
next[j] = k 的实际意义表示 T字符从0到k-1的子串和T字符从j - k到j - 1的子串是相同的,如果此时T与S的对应位置的字符串不匹配:
- 表示S从i开始匹配了一段字符,但下一个字符与T的j号字符不匹配了
- 我们不想放弃已经匹配了一段的字符
- S中这一段匹配的字符的后半段恰好和T的已经匹配过的字符串的前半段相同
- 退而求其次,我们的S依然停在当前位置,让T的指针j回溯一段距离,指向T的已经匹配过的前半段的后一个字符。就是S不动,T往后拉,已经匹配的字符变少了,但好歹保留了一些一些已经匹配的,不用从头找起
求解next数组过程类似于动态规划,以abaabcac为例求对模式T求解next过程如下:
- 求解next数组的过程是模式T和自己匹配的过程,为的是得到自己当前字符前面的部分和T开头部分到哪里是相同的
- next[0] = -1,表明当前已经回溯到了T串的开头,且开头就和i不匹配,是一个特殊的标记,表示不能再回溯了
- next[1] = 0, i == 1的时候,不需要计算,当前是第二个字符,如果不匹配那么直接去匹配第一个
- i == 2, j == 0开始, i == 2 表示当前是 ab* 这个串,而且2这个位置的* 已经和模式中的a不匹配了,next[2]是模式的指针j下一个应该到的地方,i == 2 是主串的指针, j == 0 是模式的指针
- 当前我们比较的是0位置的a和1位置的b,二者不相等
- 不相等情况下,取已经匹配的模式的子串的子串与其比较,也就是j = next[j]
- j = next[j] = -1,表示模式的指针已经到头了,不能再从已经匹配的子串中再找子串,所以next[2] 设为0,表示如果主串匹配到2和模式不一样,模式需要从头开始和主串下一个位置匹配
- j = next[i] != -1,表示模式头部到next[i] - 1和主串当前位置的前面next[i] 个字符是匹配的。下一次的匹配是主串的当前位置和模式的next[i]
- 不相等情况下,取已经匹配的模式的子串的子串与其比较,也就是j = next[j]
- 假设当前串为 aa*, 比较0位置的a和1位置的a,二者相等
- 相等情况下,next[2] = j + 1 = 1; 虽然主串aa* 的2位置不能匹配模式的2位置,但是模式0位置的a和主串1位置的a是相同的串,我们可以把模式的指针设置为next[2]也就是1,主串的指针不动还是2,下一个需要匹配的就是主串的2位置和模式的1位置
- 相等情况下,next[2] = j + 1 = 1; 虽然主串aa* 的2位置不能匹配模式的2位置,但是模式0位置的a和主串1位置的a是相同的串,我们可以把模式的指针设置为next[2]也就是1,主串的指针不动还是2,下一个需要匹配的就是主串的2位置和模式的1位置
- 当前我们比较的是0位置的a和1位置的b,二者不相等
public int[] getNext(String t){
int[] next= new int[t.length()];
next[0] = -1;
next[1] = 0;
int i = 2;
int j = 0;
while(i < t.length()) {
if(t.charAt(i - 1) == t.charAt(j)) {
next[i] = j + 1;
j ++;
i ++;
}else {
j = next[j];
if(j == -1) {
next[i] = 0;
i++;
j++;
}
}
}
}
利用next数组求解的kmp代码如下
int kmp(String S,String T,int pos,int[] next){
int i = pos;
int j = 0;
while(i + T.length() - 1 < S.length()){
if(j == -1 || S.charAt(i) == T.charAt(j)){
if(j == T.length() - 1){
return i - T.length() + 1;
}
i ++;
j ++;
}else{
j = next[j];
}
}
return -1;
}
3.1.3 KMP算法改进版
KMP算法简单来说就是主串S的i和模式T的j不匹配了,只让j回溯,回溯的位置由next[j]记录,表示i之前的一段字符和模式T的0到next[j] - 1的字符是匹配的。但也存在不必要的回溯。如果此时的S的i与模式的j不匹配,而模式的j与模式的next[j]代表的字符又是同一个字符,那么S的i和next[j]必定也不匹配,这个时候回溯到next[j]就是不必要的。
KMP算法的改进版就是计算nextVal作为next的修正值,如果有next[j]和j位置上的字符是相同的,那么nextVal[j] = nextVal[ next[j] ]
3.2 数组
数组是由类型相同的数据元素构成的有序集合,每个元素称为数组元素,每个元素在数组的序号称为下标。数组是线性表的推广,结构中的元素可以是具有某种结构的数据但是类型相同。一维数组可以看作线性表,二维数组可以看作元素是线性表的线性表。
数组一般不做插入或者删除的操作,一旦建立了数组,结构中的数据元素个数和元素之间的关系就不再发送变动,因此,采用顺序表表示数组比较合适.
3.3 广义表
广义表是线性表的推广,也叫列表,其元素由单个数据元素或者广义表组成
- 广义表的元素可以是子表,子表的元素还可以是子表
- 广义表可以递归地写 E =(a,E) 便是一个无穷无尽的广义表
- 广义表有取表头和取表尾两个操作
- 取表头是取非空广义表当中第一个元素,可以是单个数据元素,也可以是广义表
- 取表尾是取广义表当中除了表头的其他元素组成的表,表尾一定是个表
- 广义表通常用链式存储方式,有头尾链表和扩展线性链表的存储结构
- 头尾链表存在表结点和原子结点
- 表结点由标志tag = 1 、元素指针、下一个表结点指针组成
- 原子结点由标志tag = 0、值域组成
- 单个元素的表由一个表结点加一个原子结点组成,单个元素则直接从主链的空表结点链下
- 多个元素的表,每个元素通过表结点连接起来,再通过一个空的表结点指向这个链
- 如果一个表里有元素是表,为这个元素使用一个空的表结点连接作为元素的子表
- 扩展的线性链表存储结构存在原子结点和表结点
- 表结点标志tag = 1、元素指针、下一个结点指针
- 原子结点标志tag = 0、元素指针、下一个结点指针
- 扩展线性链表存储结构同一级使用下一个结点指针连接、不同级使用元素指针连接
- 每个广义表由一个空表头指向元素主链
- 头尾链表存在表结点和原子结点