一、线性表
1、线性表简介
1.1、线性表
简称表,是零个或多个元素的有穷序列,表示为k0,k1,…kn-1:
- 表目:线性表中的元素
- 索引(下标):元素的位置i称为k1的索引或下标表的长度:线性表的所含元素个数n
- 空表:不含任何元素即长度为零的线性表(n=0)
1.2、特点
操作灵活,易于存储,长度可增加、缩短
- 均匀性:同一线性表的元素的数据类型和长度相同
- 有序性:元素直接的相对位置是线性的
1.3、前驱和后继
<ai,ai+1>,ai是ai+1的直接前驱,ai+1是ai的直接后继。(直接前驱和直接后继指相邻的前驱结点和后继结点)
特殊:唯一的开始结点(没有前驱,有唯一的直接后继)和终止结点(没有后继,只有唯一的直接前驱)
表头是所有元素的前驱,表尾是所有元素的后继
特点:
- 反对称性:若<ai,ai+1>即ai+1是ai的直接后继,则ai+1不可能是ai的前驱
- 传递性:若<ai,ai+1>且<ai+1,ai+2>则ai是ai+2的前驱(非直接前驱)
1.4、线性表分类
- 按存储结构分为 数组(顺序表)和链表
- 按运算操作分为线性表(普通,不限制操作)、栈(在一端操作,在深度优先算法良好适用)在和队列(在两端操作,在宽度优先算法良好适用)及字符串.
二、顺序表和链表(存储结构)
根据不同的存储结构把线性表分为顺序表(数组)和链表
1、顺序表(也称向量,数组存储)
- 按 索引值从小到大 存放在一片 相邻的连续区域。
- 紧凑结构,存储密度为1。
2、链表
- 通过指针把链表的一串存储结点链接成一个链
- 存储结节由两部分组成:数据域+指针域(后继地址,即下一个结点,所以是结点类型)
-
- 存储上存在指针开销,存储密度大于1
2.1.单链表
- 整个单链表:head
- 第一个结点:head
- 空表判断:head==null
NOTE:数据结构中带头结点的单链表
- 整个单链表:head
- 第一个结点:head->next ,head≠null(在带头结点的单链表中head永远不可以为空)
- 空表判断:head->next==null
单链表的操作
- 单链表插入新结点
- 创建新结点
- 新结点指向后面的结点
注: 由于链表结点的指针域存储下一个结点(存储位置),若先左边结点指向新结点,会找不到右结点,导致右结点丢失
- 单链表删除结点
- 寻找到要删除结点的直接前驱
- 把前驱结点的直接后继指向删除结点的直接后继(前驱结点的原直接后继结点的直接后继)
- 释放要删除的这个结点
NOTE:单链表操作时间复杂度(几乎为O(n))
- 定位:O(n)
- 插入:O(n)+O(1)
- 删除:O(n)+O(1)
2.2.双链表
- 既有指向后继结点的指针域,又有前驱结点的指针域
注:单链表的操作结点需要定位该结点的前驱,而双链的操作结点仅需要定位到该结点
2.3.循环链表
- 终止结点(tail)的后继指向开始结点 开始结点的前驱指向终止结点(head)
- 当遇到虚头结点判断循环链表循环结束
3、顺序表和链表的比较(存储结构上的比较)、
三、栈
- 后进先出 体现元素之间透明性
- 主要操作:进栈和出栈
1、顺序栈
主要问题:
上溢(压入栈顶)
判断栈是否满(栈上溢出)
若未满,则新元素入栈并修改栈顶指针
下溢(从栈底弹出)
判断栈是否为空(栈下溢出)
若栈不为空我,则弹出最后一个元素,并修改栈顶指针
- 栈主要由栈顶(逻辑结点)控制
2、链式栈
- 用单链表方式存储
- 指针的方向从栈顶(注意,是栈顶)向下链接,即加入新元素是是在原栈顶(开始结点)的前继,即新元素的指针域指向原开始结点(无头结点的链表:head结点,或带头结点的链表:head结点的后一个元素),再修改栈顶的指针;删除则是把栈顶(开始结点)删掉,再修改栈顶的指针。
3、顺序栈和链式栈比较
- 时间效率
所有操作都只需常数时间,即O(1)
顺序栈和链式栈在时间效率上难分伯仲 - 空间效率
顺序栈需定义一个固定的长度
链式栈的长度可变,但增加结构性(指针域)开销 - 由于顺序栈是数组存储,实际应用中一般使用顺序栈
- 由于信息封装性,栈只能进行受限操作,只能操作栈顶元素(增删读)
4、栈的应用
- 递归转非递归
4.1递归调用原理
- n*f(n-1) n≥1
阶乘=
- 1 n = 0
递归规则
- 将原问题划分成子问题
- 保证递归的规模向出口条件靠拢
递归出口 - 递归终止条件,即最小子问题的求解
- 可以允许多个出口
4.2递归到非递归的转换
- 机器的递归转换
四、队列
- 先进先出(FIFO):
由于队列是限制访问点的线性表:按照到达的顺序来释放元素,所有的插入在表的一端进行,所有的删除都在表的另一端进行 - 主要元素:队头(front)和队尾(rear)
1、顺序队列
- 主要问题是 防止假溢出
- 注意:为了区分满队列和空队列,必须在 满队列是存在一个空位置空(虚溢出)
1.1、队列的操作
- 插入:元素往后面插入,rear指针发生改变
- 删除:元素在队列里位置未发生改变,只需移动front指针,保证在时间复杂度O(1)完成
2、链式队列
- 单链表队列
- 指针的方向是从队列的前段向尾端链接
注意:链式栈插入删除均操作开始结点,而链式队列插入操作终止结点,删除操作开始结点
3、顺序队列与链式队列的比较
- 顺序队列:固定的存储空间
- 链式队列:可以满足大小无法估计的情况
注意:顺序队列与链式队列,都不允许访问队列内部元素,只允许访问队头(front)元素
五、字符串
1、简介
- 特殊的线性表,元素为字符 的线性表
- n个字符的有限序列 一般记作S:“c0c1…cn-1”
S是串名
“c0c1...cn-1”是串值
ci是串中的字符
N是串长,即一个字符串S所包含的字符个数
- 空串是长度为0的字符串,包含字符个数为0,与空格串(字符为空格)有区别。
2、普通线性结构和字符串区别
- 普通线性结构的数据对象无特殊限制,字符串的数据对象为字符集
- 普通线性表大多以“单个元素”为操作对象,字符串以“串的整体”为操作对象
- 普通线性表的存储方法同样适用于字符串,注意由于字符串的数据对象为字符,如果用链接形式存储会出现很大的指针结构性开销,而且运算无太大改进,因此一般用顺序存储。
3、字符/符号
- 字符:组成字符串的基本单位
- 取值依赖于字符集(同线性表,结点的有限集合)
- 字符编码常用8bits单字节ASCII码,对于复杂字符一般用UNICODE编码或中文的国标码(均为16bits双字节)
4、字符串的数据类型
- 字符串常量
- 字符串变量
5、子串(substring)
5.1、定义
5.2求子串个数
- 公式(n*(n+1)/2)+1
- 即n+n-1+…+2+1+1(最后一个1为空串)
6、字符串的顺序存储结构和实现
对于串长变化不大的字符串,有三种处理方案
- 用S[0]作为记录串长的存储单元,缺点是限制了串的最大长度不能超过256(即一个字节,2的8次方)
- 为存储串的长度,另辟一个存储的地方,缺点是串的最大长度一般是静态给定的,不是动态申请速空
- 用一个特殊的末尾标记‘\0’(c/c++),且‘\0’的ASCII字符表中编号为0,等价于常量NULL,数字0、常量false
注意:复制线性表时要挨个把当前线性表的元素复制到需要复制结果的表中,如果直接让两表相等会导致原来线性表消失
7、字符串的模式匹配
7.1、模式匹配
- 一个目标对象T(字符串)
- (pattern)P(字符串)
- 在目标T中寻找一个给定的模式P的过程
7.2、字符串的模式匹配
- 用给定的模式P,在目标字符串T中搜索与模式P全同的一个子串,并求出T中第一个和P全同匹配的子串(该子串简称为“配串”),返回 其首字符位置。
- 为使模式P与目标T匹配,必须满足p0p1…pm=titi+1…ti+m-1
7.3 、解决模式匹配问题的算法
7.3.1、朴素模式匹配(穷举法,BF算法)
- 朴素模式匹配算法效率分析
7.3.2、 快速模式匹配(KMP算法)
-
KMP算法的步子可以跨这么大?
因为 KMP算法中的next数组存放了模式串T出现重复字符的信息,利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置,而避免模式串移动到无效位置。 -
KMP算法主要分两步:第一步是求next数组,第二步是进行匹配。
-
next数组是什么?
next顾名思义,是下一个的意思,那么其作用是在回溯时指引指针j回溯到模式串P的下一位置,也就是j=next[j];那么next[j]也就是j下一个要回溯到的位置。那么 ***next数组就是用于存储每一位当发生不匹配时所对应的下一个要移动到的位置。言下之意next数组存的是位置(下标),通过带入j的值就可以获取到j的下一个回溯位置next[j]***。 -
如何得到next数组的值(next数组所存的下标)?
这个问题还得从 “最大长度”—Length数组说起。“最大长度”—Length数组是一个用来存放一个字符串的最长前缀和最长后缀相同的长度。按每一位对应的下标将对应长度值存于数组Length中,如模式串T为“ababaca”,长度是7,所以Length[0],Length[1],Length[2],Length[3],Length[4],Length[5],Length[6]分别计算的是 a,ab,aba,abab,ababa,ababac,ababaca的相同的最长前缀和最长后缀的长度。所以Length[]={0,0,1,2,3,0,1}。 -
那我们的到了这个长度之后有什么用呢?
Length数组的下标与模式串T的下标是一致的(也就是下标 j ),也就是模式串T中每一位都可以对应其下标( j )在Length数组中找到该位当前前缀与后缀的最长匹配长度,那么也就知道了,在当前位置下(也就是后缀)是否与模式串的开头(也就是前缀)有相同的部分、相同的部分是几位。那么我们举个例子当j=4时,此时模式串为“ababa”,那么此时显然有 前缀:“aba”与 后缀:“aba”相匹配,长度是3。那么我们将j=4带入Length[ j ];结果是Length[4]=3。那么此时,当j前走一位,j=5时,如果需要回溯,我们就可以知道回溯位置是Length[4],也就是第四个位置。
有了Length数组,我们只需要知道下标就可以知道,当前的前后缀的最长匹配长度。而当j位发生不匹配,需要回溯时,我们要看前一位也就是j-1位的前后缀匹配情况也就是Length[j-1],可以知道要从j位回溯到Length[j-1]位。 -
为什么通过前缀和后缀的最长匹配长度可以知道回溯位置呢?
假如当j位发生不匹配时,长度为j-1的模式真子串的前缀和后缀的最长匹配长度为i,则从0开始存储即0~i-1位真子串为长度为i的前缀,而从j-i~j-1位为长度为i的后缀,则对应i位与j位的元素不同,且由于最长匹配长度为i,与需回溯到的第i位(最长前缀的后一位元素)对应数字均为i,所以可以通过前缀和后缀的最长匹配长度可以知道回溯位置 -
那么现在问题来了,当我们发现j所指向的“D”此时发生失配的情况时,我们得去前一位 j-1(也就是Length[j-1])找回溯位置,而并不能直接在Length[j]中找到,那么我们此时就引入next数组来代替Length数组来完成查找回溯位置这一工作。
所以next数组只需要在原有Length的基础上,整体向后移一位,即可实现通过next[j]直接找到j当前所需要回溯到的位置。
- next数组优化
- kmp算法效率
kmp算法总结:
- kmp算法通过前缀和后缀最长匹配长度确定next数组,即当模式串j位不匹配是,则求出模式串前j-1位真子串的前缀和后缀最长匹配长度就是j位需要回溯的位置。
- 优化next数组即是当j位元素与需要回溯到位置的元素相同时,则next数组需要回溯的位置可优化为原需要回溯到位置的元素回溯的位置。
单模式算法对比