290 likes | 437 Views
第四章 串. 本章内容. 串的数据类型定义; 串的几种存储结构: 顺序存储结构; 单链表存储结构; 堆分配存储结构; 串的各种基本操作的实现及其应用; 串的模式匹配算法。. 本章要点. 熟悉串的 5 种基本操作的定义,并能利用这些基本操作实现串的其它各种操作的方法; 熟练掌握在串的顺序存储结构基础上实现的串的各种操作的方法; 掌握串的分块存储结构以及在其上实现串操作的基本方法; 理解串匹配的 KMP 算法; 了解串操作的应用方法和特点。. 4.1 串类型的定义. 串 是零个或多个字符组成的有限序列;
E N D
本章内容 • 串的数据类型定义; • 串的几种存储结构: • 顺序存储结构; • 单链表存储结构; • 堆分配存储结构; • 串的各种基本操作的实现及其应用; • 串的模式匹配算法。
本章要点 • 熟悉串的5种基本操作的定义,并能利用这些基本操作实现串的其它各种操作的方法; • 熟练掌握在串的顺序存储结构基础上实现的串的各种操作的方法; • 掌握串的分块存储结构以及在其上实现串操作的基本方法; • 理解串匹配的KMP算法; • 了解串操作的应用方法和特点。
4.1 串类型的定义 • 串 • 是零个或多个字符组成的有限序列; • 一般记作S="a0a1a2…an-1" • S 是串名,单引号括起来的字符序列是串值 • ai(0≦i<n)可以是字母、数字或其它字符 • 串的长度:串中所包含的字符个数 • 空串(Empty String):长度为零的串,它不包含任何字符。 • 空白串(Blank String):仅由一个或多个空格组成的串; • 空串和空白串不同: • " "和""分别表示长度为1的空白串和长度为0的空串。
串类型的定义(续) • 子串 • 串中任意个连续字符组成的子序列称为该串的子串; • 主串 • 包含子串的串称为主串。 • 子串在主串中的位置 • 子串在主串中首次出现时,该子串的首字符对应的主串中的位置 • 模式匹配 • 在主串中找到等于给定串的子串的过程,即子串的定位操作; • 结果 • 匹配成功,返回子串第一个字符在主串中的位置 • 匹配失败 • 空串是任意串的子串,任意串是其自身的子串。 • 串的相等:两串的长度及对应位置的字符均相等。
串的基本运算 • 用串变量赋值 assign(s,t) • 用串常量赋值 creat(s,ss) • 判等函数 equal(s,t) • 求长函数 length(s) • 连接函数 concat(s,t) • 求子串函数 substr(s,start,len) • 定位函数 index(s,t) • 置换函数 replace(s,t,v)用v替换s中的t • 插入操作 insert(s,pos,t) • 删除操作 delete(s,pos,len) • 线性表的操作通常以“数据元素”为操作对象; • 串的操作主要以“串的整体”为操作对象。
4.2 串的表示和实现 定长顺序存储表示 堆分配存储表示 串的块链存储表示
串的顺序存储结构 #define MaxLen 256 1. typedef char string[MaxLen]; string s; 2. typedef struct string { char str[MaxLen]; int length; };
高级语言中的字符串表示 • C语言字符串的存储格式 (ASCIIZ) • 字符串尾部存放0 • 举例 • Pascal语言字符串的存储格式(nASCII) • s[0]存放字符串长度 • 举例 • 优缺点对比
Head A S T R I N G ∧ ∧ Head A R I S N T G 串的链式存储结构 (块链结构) #define MaxLen 256 typedef struct node { char data[MaxLen]; struct node *next; };
串的堆分配存储结构 • 串的存储空间在程序执行过程中动态分配而得 • malloc() • free() • 优点 • 具有顺序存储结构的优点,处理方便 • 对串长没有任何限制,灵活 typedef char *string; typedef struct string { char *data; int length; };
4.3 串的模式匹配算法 • 模式匹配 • 模式串:子串 • 在主串中找到模式串的过程,即子串的定位操作 • 结果 • 匹配成功,返回子串第一个字符在主串中的序号 • 匹配失败,返回0 • 算法思想 • 主串s,子串p:Index(s, p) • 将s中的第一个字符与p中的第一个字符进行比较; • 若不同,就将s中的第二个字符与p中的第一个字符进行比较...,直到s的某一个字符和p的第一个字符相同,将它们之后的字符进行比较 • 若相同,将它们之后的字符进行比较 • 当s的某一个字符si与p的字符pj不同时,则s,p回退,即:将s中的第i-j+2个和p的第一个字进行比较,重复上述过程
int Index(string S, string p) { i = j = 1; while (i <= S[0] && j <= p[0]){ if (S[i] == p[j]) { i++; j++; } else { i = i - j + 2; j = 1; } } return j > p[0] ? i - p[0] : 0; } 模式匹配结果 成功,返回子串在主串中第一次出现的位置 失败,返回 0 时间复杂度 最坏:为 O(m·n),其中m和 n 分别为 s 串和 t 串的长度 最好:O(n) 算法特点 匹配过程易于理解 匹配过程中,对主串的访问需要回退 朴素的模式匹配算法
a b c d a b e g k=3 本趟比较失配点:i=7, j=7 (即s7p7) 失配点处p1p2=s5s6=p5p6p1…pk-1=pj-k+1…pj-1 下趟比较:i不变,j=k(此处即3) 模式匹配算法的改进 [改进之处] 在比较过程中,主串的下标i只增不减,不回退,可使算法复杂度提高到O(m+n) [分析] 1 2 3 4 5 6 7 8 9 10 11 12 13 主串s a b c d a b c d a b e g h s1s2…sn 子串t a b c d a b e g p1p2…pm i=7 m<n j=7
{ 不考虑 j=1 Next[j] = Max { k | 1<k<j 且 p1p2…pk-1=pj-k+1pj-k+2 …pj-1 } 1 其它情况 算法改进 int Index(string S, string p) { i = j = 1; while ( i <= S[0] && j <= p[0] ) { if (S[i] == p[j] ) { i++; j++; } else if (j == 1) { i++; j =1; } else j = next[j]; } return j>p[0] ? i - p[0] : 0; }
{ 0 j=1 Next[j] = Max { k | 1<k<j 且 p1p2…pk-1=pj-k+1pj-k+2 …pj-1 } 1 其它情况 算法进一步改进(KMP算法) int Index(string S,string p) { i = j = 1; while ( i <= S[0] && j <= p[0] ) { if (j==0 || S[i] == p[j] ) { i++; j++; } else j = next[j]; } return j>p[0] ? i - p[0] : 0; }
{ 0 当 j=1 时 Next[j] = Max { k | 1<k<j 且 p1p2…pk-1=pj-k+1pj-k+2 …pj-1 } 1 其它情况 KMP算法 由 D.E.Knuth 与 J.H.Morris 和 V.R.Pratt 同时提出; 没有回溯的模式匹配算法; 用 s[i] 与 p[j] 匹配,若相等则往后继续匹配,否则就用s[i]与p[next[j]]匹配,直至匹配成功或失败为止
s k 1 1 k j -k+1 KMP 算法原理 当 s[i] 与 p[j] 不匹配时,s[i] 就与 p[next[j]] 比较 i j p Next[j] = k
模式串的next求值 • next值仅取决于模式串本身,与主串无关 • 设next[i]=k,则next[i+1]=? • 由next[i]=k有:p1…pk-1=pi-k+1…pi-1 • 若pk==pi • p1…pk-1pk=pi-k+1…pi-1pi,next[i+1] =k+1=next[i]+1 • 若pk≠pi • p1…pk-1pk ≠pi-k+1…pi-1pi • 当pk≠pi时,应将模式向右滑动至模式的第next[k]=k’个字符和主串的失败字符继续比较 • 若pk’==pi,则有p1…pk’=pi-k’+1…pi (1<k’<k<i) • next[i+1]=k’+1=next[k]+1 • 若pk’ ≠pi,将模式向右滑动至模式的第next[k’]个字符和主串的第i个字符相比较,依次类推,直至next[i+1]=1
求next函数值的算法演化(1) 循环: /* k记录next[i] */ if (p[i]==p[k]) { next[i+1] = k + 1; i++; k++; /* 为下次循环做好准备*/ } else { /* i不变,以便继续求解next[i+1] */ k = next[k]; }
求next函数值的算法演化(2) 循环: if (k == 0) { next[i+1] = 1; i++; k = 1; } else if (p[i]==p[k]) { next[i+1] = k + 1; i++; k++; /* 为下次循环做好准备*/ } else { /* i不变,以继续求解next[i+1] */ k = next[k]; }
求next函数值的算法演化(3) i = 1; (需要对K赋初值) while (i < p[0]) { if (k == 0 || p[i] == p[k]) { i++; k++; next[i] = k; } else k = next[k]; }
j 1 2 3 4 5 6 7 8 9 10 11 a a b c a a a b c a d 模式串 求next函数值 i = 1; next[1] = 0; k = 0; while (i < p[0]) { if (k == 0 || p[i] == p[k]) { i++; k++; next[i] = k; } else k = next[k]; } next 0 1 2 1 1 2 3 3 4 5 6
Next数组的缺陷 例j 1 2 3 4 5 6 7 8 9 10 11 a b c a b c a b b a c next[j] 0 1 1 1 2 3 4 5 6 1 2 s= a b c a b c a x a b c a b c a b b a c t= a b c a b c a b b a c next[8]=5 a b c a b c a b b a c next[5]=2 a b c a b c a b b a c next[2]=1 a b c a b c a b b a c 存在p8=p5=p2, 因此当s8p8时,s8与p5,p2的比较无意义
KMP 算法的改进 已知next[i],如何求解next[i+1]? 设next[i]=k, 当p[i]=p[k]并且p[i+1]与p[k+1]相同时,就将next[k+1]赋给next[i+1],即为 nextval 数组。
改进的 KMP 算法实现 i=1; k=0; nextval[1]=0; while (i<T[0]) { if (k==0 || p[i] == p[k]) { i++; k++; if (p[i] != p[k]) nextval[i] = k; else nextval[i] = nextval[k]; } else k=nextval[k]; }
j 1 2 3 4 5 6 7 8 9 10 11 a a b c a a a b c a d 模式串 求nextval的例子 nextval 0 0 2 1 0 0 3 2 1 0 6 例j 1 2 3 4 5 6 7 8 9 10 11 a b c a b c a b b a c nextval[j] 0 1 1 0 1 1 0 1 6 0 2
KMP算法时间复杂度分析 • 通常,模式串长度m<<主串长度n • 求next(nextval)数组:O(m) • 主串与模式串匹配: O(n) • 整个匹配算法O(m+n)
作业 1.参阅81页KMP算法中,next数组的定义。 (1)为什么j=1时,next[j]=0? (2)为什么要取max(k),k的最大值是什么? (3)其他情况是什么情况?为什么取next[j]=1? 2.(1)求出下列模式串的next数组值 t1="abcaabbcabcaabdab" t2="ababaaababaa" (2)求出下列模式串的nextval数组值 t1="abcaabbcabcaabdab" t2="ababaabab" 3.写出对串求逆的递归算法。