文章目录
前言
字符串(string,简称串)是由字符组成的有限序列,是计算机中常用的一种非数值数据。动态逻辑结构上来看,串是一种特殊的线性表,其特殊性在于线性表中的每一个元素是一个字符。作为一种抽象数据类型,串有自己的一组操作,其操作特点与线性表不同。
本章先介绍字符串的基本概念,描述串抽象数据类型,分析顺序和链式两种存储结构存储串的特点;然后以Java语言的两种字符类String和StringBuffer为例,介绍顺序存储结构的各种操作算法;最后介绍串的模式匹配问题的两种算法:Brute-Force算法和KMP算法。本章的重点是串数据类型的各种操作实现,以及两种串的匹配模式算法。
3.1 串抽象数据类型
3.1.1 串的基本概念
- 串的定义
一个串是由n(n>=0)个字符组成的有限序列,记为 s=“s0s1…sn-1”。其中,s是串名,一双双引号括起来的字符序列s0s1…sn-1是串值,si为特定字符集合中的一个字符。一个串中包含的字符数称为串的长度。长度为0的串称为空串,记为"“。由多个空格组成的字符串” "称为空格串。
一个字符在串中的位置是指该字符在串中的序号(index),用一个整数表示。约定串中的第一个字符的序号是0,-1表示该字符不在指定串中。 - 子串
由串s中任意连续字符组成的一个子序列sub称为s的子串(Substring),s称为sub的主串。特别地,空串是任何串的子串。任意串s都是它自身的子串。除自身外,s的其他子串称为s的真子串。 - 串比较
串的比较规则与字符比较规则有关,字符比较规则由所属字符集的编码决定,通常在字符集中的同一字母的大小写形式有不同的编码。
两个串可比较是否相等,也可比较大小。两个串(子串)相等的充要条件是两个串(或子串)的长度相同,并且各对应位置上的字符也相同。
两个串的大小由对应位置的第一个不同字符的大小决定,字符比较顺序是从头开始依次向后。当两个串长度不等而对应位置的字符都相同时,较长的串定义为较大。
3.1.2 串抽象数据类型
对串的操作要求与线性表不同。串的基本操作主要有:创建一个串,求串的长度,读取/设置字符,求子串,插入,删除,连接,判断相等,查找,替换等。其中求子串,插入,查找等操作以子串为单位,一次操作处理多个字符。
3.2 串的表示和实现
3.2.1 串的存储结构
串的存储结构分为顺序存储和链式存储。
1 顺序存储
串的顺序存储结构采用字符数组将串中的字符序列依次连续存储在数组的相邻单元中。使用字符数组有两种方案,数组容量大于或等于串长度。当数组容量等于串长度时,串不易增加字符,通常用于存储串常量,如图3-1(a)所示;当数组容量大于串长度时,便于增加字符,通常用于存储串变量,如图3-1(b)所示,此时还需要一个整型变量记录串的长度。
顺序存储的串具有随机存取功能,存取指定位置字符的时间复杂度为O(1);缺点时插入和删除时需要移动元素,平均一栋数据量时串长度的一半;当数组容量不够时,需要重新申请一个更大的数组,并复制原数组里面的所有元素。插入和操作的时间复杂度为O(n)。
2 链式存储
串的链式存储结构有单字符链表和块链表两种。单字符链表是每个节点的数据域只包含一个字符的单链表,块链表是每个节点的数据域包含若干字符的单链表,其结构如图3-2所示。
链式存储的串,存取指定位置的字符的时间复杂度为O(n);单字符链表虽然插入和删除操作不需要移动元素,但占用空间太多;块链表的插入和删除需要移动元素,效率较低。
3.2.2 常量字符串String
1. string类声明
java.lang.String 类提供构造串对象、求串长度、取字符、求子串、连接串等操作。String类以串常量方式存储和实现字符串操作,其字符数组容量等于串长,存储结构见图3-1a,并且字符数组声明为最终量,当构造串对象时进行一次赋值,其后不能改变,因此string类不提供插入删除操作。
模拟string类的MyString类部分声明如下,因为string类不可以被继承,所以我们用关键字sealed保证MyString也不可以被继承。
using System;
namespace DataConstructure.String
{
public sealed class MyString: IComparer<MyString>
{
private char [] value; //字符数组,私有变量
// 构造一个空串
public MyString()
{
this.value = new char[0];
}
// 由字符串常量构造串对象
public MyString(string original)
{
this.value = original.ToCharArray();
}
// 由value数组中从begin开始的count个字符构造字符串对象
public MyString(char[] value, int begin, int count)
{
this.value = new char[count];
for (int i = 0; i < count; i++)
{
this.value[i] = value[begin + i];
}
}
// 由value数组构造字符串对象
public MyString(char[] value) :this(value, 0, value.Length)
{
}
//返回字符串长度
public int Length()
{
return this.value.Length;
}
// 返回指定位置的字符
public char CharAt(int i)
{
if (i < 0 || i >= this.value.Length)
throw new IndexOutOfRangeException(); // 越界异常
else
return this.value[i];
}
// 重写tostring方法
public override string ToString()
{
return new string(this.value);
}
public int Compare(MyString? x, MyString? y)
{
return 0;
}
}
}
测试代码如下:
char[] test = { '1', '2', '3', '4', '5' };
MyString myString1 = new MyString(); // 构造一个空串
MyString myString2 = new MyString(test); // 由char数组构建串对象
MyString myString3 = new MyString(test,2,2); // 由char数组的begin和count构建串对象
MyString myString4 = new MyString("mystring4"); // 由字符串常量构建串对象
Console.WriteLine(myString1.ToString());
Console.WriteLine(myString2.ToString());
Console.WriteLine(myString3.ToString());
Console.WriteLine(myString4.ToString());
例题3.1 获得整数和实数字符串表示的数值
2. 连接串
设有s1,s2串,返回将s2连接在s1之后的串s3,不改变s1,s2原串。算法描述如图3-4所示:
在MyString串中增加如下方法,返回当前串和指定串str连接生产的新串,没有改变当前串。
// 返回当前串与其他串连接后的新串
public MyString Contact(MyString str)
{
if (str == null || str.Length() == 0)
return this;
else
{
char[] newstr = new char[this.Length() + str.Length()];
int i;
for (i = 0; i < this.Length(); i++)
{
newstr[i] = this.value[i];
}
for (int j = 0; j < str.Length(); j++)
{
newstr[i + j] = str.value[j];
}
return new MyString(newstr);
}
}
3.求子串
设有s串,求s串的子串的操作是,将s串中序号从begin至end-1的子串组成一个新的串对象返回,不改变s串。算法描述如图3-5所示:
在MyString串中增加如下方法:
// 返回当前串从begin到end-1的子串
public MyString Substring(int begin, int end)
{
if (begin > end)
throw new IndexOutOfRangeException();
else
{
if (begin < 0)
begin = 0;
if (end < this.value.Length)
end = this.value.Length;
if (begin == 0 && end == this.value.Length)
return this;
else
{
char[] sub = new char[end-begin];
for (int i = 0; i < sub.Length; i++)
{
sub[i] = this.value[begin + i];
}
return new MyString(sub);
}
}
}
// 返回当前串从begin开始的子串
public MyString Substring(int begin)
{
return Substring(begin, this.value.Length);
}
4. 比较串的大小
两个字符串变量用==和!=比较的是两者是否引用同一个字符串对象;采用以下equal方法比较两个串是否相等,它覆盖object类的equal方法。
// 比较连个字符串是否相等
public bool Equal(object obj)
{
if (this == obj)
return true;
if (obj is MyString)
{
MyString str = (MyString)obj;
if (this.value.Length == str.value.Length)
{
for (int i = 0; i < str.value.Length; i++)
{
if (this.value[i] != str.value[i])
return false;
else
return true;
}
}
}
return false;
}
MyString类实现接口IComparer<>,给出如下Compare实现方式。
public int Compare(MyString? str, MyString? y)
{
return CompareTo(str);
}
public int CompareTo(MyString str)
{
if (str == null)
throw new ArgumentNullException();
else
{
for (int i = 0; i < this.value.Length && i< str.value.Length; i++)
{
if (this.value[i] != str.value[i])
return this.value[i] - str.value[i]; // 返回两串第一个不同字符的差值
}
}
return this.value.Length - str.value.Length; // 返回两串长度的差值
}
一个类可以在compare方法中约定与equal方法不同的比较两个对象相等的规则。
5. string 串的插入、删除操作
string类没有插入和删除操作,这些功能可通过连接和求子串等操作实现。
1)在s1的i位置插入s2
设有s1串和s2串,在s1的i位置插入s2,返回插入后的s3串,算法描述如图3-6所示,以i为界将s1分为两个子串,用substring()方法可分为求得两个子串,然后用“+”运算符将s2连接在它们之间,构成一个新串s3,而s1和s2不变。调用语句如下:
string s1 = "abcdefg", s2 = "123";
int i = 2;
string s3 = s1.substring(0,2) + s2 + s1.substring(2);