现在流行写心得, 俺也来一篇。 如何高效地操作字符串。 给初学者一点帮助。 (鲜花和板砖都欢迎)(0分)

  • 主题发起人 Another_eYes
  • 开始时间
A

Another_eYes

Unregistered / Unconfirmed
GUEST, unregistred user!
字符串操作应当说应用相当广泛, 如何高效地操作字符串,将极大影响程序的运行效率。 操作字符串无非就是搜索和修改(包括插入/删除)。
先说说如何高效地搜索字符串。
我们先看一个例子:
要求写一个计算子串出现次数的函数。
方法一:
function CountSubStr(SubStr, Source: string): Integer;
begin
result := 0;
while pos(SubStr, Source)>0do
begin
inc(result);
Source := copy(Source, pos(SubStr, Source)+Length(SubStr), $7FFFFFFF);
end;
end;

方法二:
function CountSubStr(const SubStr, Source: string): Integer;
var
i, len1, len2: Integer;
begin
result := 0;
len1 := length(SubStr);
len2 := length(Source)-len1+1;
i := 1;
while i<=len2do
if (Source=SubStr[1])
and CompareMem(@(Source), Pointer(SubStr), len1) then
begin
inc(result);
inc(i, len1);
end
else

inc(i);
end;

方法一代码非常简洁直观, 三句搞定
方法二有点晦涩。看上去毫无必要地定义了两个变量(len1和len2), 而且用了一句很奇怪的判断语句(and两边的值肯定是一样的,要么都是true,要么都是false,看起来像一个生手写的)。
您会选哪一种方法?
答案是:选第二种。
为什么? 这里就牵涉到一个字符串操作效率的问题。
让我慢慢道来:
我们先看方法一:
>> while pos(SubStr, Source)>0do

查字串位置,如果找不到循环结束
>> begin

>> inc(result);
>> Source := copy(Source, pos(SubStr, Source)+Length(SubStr), $7FFFFFFF);

找到了则计数加1,并且截去前面已经搜索过的字符串,只留下未搜索过的等待下一次循环。问题就在这句,让我们看看这时到底干了些什么:
1. 调用pos函数从头重新搜索Source字符串以查找字串SubStr的位置。
2. 调用Length函数计算SubStr的长度
3. 重新申请内存并复制Source中剩余的字符串
然后每循环一次就重复一次上述步骤直到循环结束。
仔细看看其实上述步骤中没有一步是有用的。
1. 循环开始时已经调用pos取得位置了。 此时又调用一次纯属浪费, 如果子串位于字符串的后面将重新扫描大部分无用的字符串。
2. substr在这个过程中属于一个常量, 不会改变, 其长度完全可以在进入循环前保留下来作为常量使用, 没有必要每次重复调用子过程再去计算一遍(不过由于length函数的实现只有3句汇编指令(call和ret不占cpu时间), 所以这点开销可以忽略)
3. 这是最恐怖的一步, 假设Source有几兆之多, 那么每次复制的数量就非常可观了。造成最后过程运行的大部分时间都在复制字符串了。这步完全是不需要的,只要我们能记录下本次查找到的位置,然后下一次从这个位置继续查找就能达到目的了。 为什么要毫无必要地复制几兆数据呢(而且是循环里每次复制)?
根据这个思路,就有了方法二。
方法二:
>> len1 := length(SubStr);

将SubStr的长度保留做一个常数,供循环中使用。 不过你也可以不用这个变量而在循环中直接写成函数形式, 因为开销很小。
不过我喜欢这么用。length函数尽管开销小,但也是开销呀。 如果循环几千几万次那每用一个length函数就等于多执行了循环次数*2条无效语句。
>> len2 := length(Source)-len1+1;
控制循环次数。
>> i := 1;
>> while i<=len2do
>> if (Source=SubStr[1]) and CompareMem(@(Source), Pointer(SubStr), len1) then
为什么要这么写?这是有原因的,如果source不等于substr[1],那么后面的comparemem就根本不会执行。如果查找的是一个很长的子串的话,那么节约的时间就很可观了。 为什么不用copy(source, i, len1)=substr判断而要用comparemem呢? 这样做的话是将子串复制到一块临时内存然后和substr逐字节比较,既然这样,为什么不直接逐字节地比较呢?由于每次找到substr第一个字符时就会进行比较,那么copy的浪费就太大了。
begin
inc(result);
inc(i, len1);
end
else

inc(i);
可以发现,方法二对source串头到底搜索一次就完成任务了。 没有额外的开销, 不运行无效的代码, 因此效率是相当高的。
通过例子可以发现, 要想高效地处理字符串,就是尽可能地减少循环次数(字符串操作就是一个循环)与提高循环体的执行效率(一个有效的办法就是尽可能将不改变的部分提到循环体外先执行掉)。 如果能顺序搜索一次便达到目的的就绝不重复操作已经搜索过的字符串(包括复制啦(这是最浪费的,除非你的目的就是复制),从头再搜索字符串啦等等)。

根据上面的原则, 现在我们可以改造一下方法一,使其也具备高效率。
代码如下:
function CountSubStr(const SubStr, Source: string): Integer;
// 注意,必须加const或者var关键字
var
i, n, len1, len2: Integer;
begin
result := 0;
i := 1;
len1 := Length(SubStr);
len2 := Length(Source)-len1+1;
while (i <=len2)do
begin
n := pos(SubStr, string(@Source));
// 这里的技巧是高效的关键, 直接将上一次找到的位置作为字符串的起始传入pos函数
if n > 0 then
begin
inc(result);
inc(i, n+len1-1);

end
else
break;
end;
end;

现在让您选择呢?
两种方式效率几乎一样。选哪种都不错。

***小知识:为什么必须加const关键字:
我们知道,字符串参数有三种定义方式:
var s: string;
表示这个参数在函数/过程中可以修改,并且修改会影响原始字符串。
const s: string;
表示这个参数在函数/过程中是只读的。不能修改。
s: string;
表示这个参数在函数/过程中可以修改, 但修改不会影响源串。
在delphi中, 这三种定义传递参数时都是将源串的地址传入函数/过程的。
但怎么实现三种不同的功能呢? 这全是编译器的功劳。
对var s: string;
编译器不做什么判断。
对const s: string;
编译器分析您写的代码。发觉如果有试图修改s字符串的代码时,编译时将弹出一个错误而使编译失败。
对s: string;
这种编译器分析您的代码, 发觉如果有修改,或者有修改可能时, 它会生成代码在修改前自动先复制一份,然后让您修改这个备份字符串。
所以如果你的参数定义成s: string并且你在代码中试图获取它的地址时, 编译器便认为你有可能修改这个字符串而先复制了一份。 显然对我们的程序来说这个复制是多余的。 为了防止编译器自动复制, 我们必须在参数定义时加上const或var关键字。

下面将要讲讲怎样高效地插入/删除子串
待续...
 
很好....
 
可以替你推荐发表么?
如果可以请 jiachun@bjn2ms.net
不行就算了
 
楼上的算法有问题,你用
SubStr = '11';
Source = '1111';
测试一下, 正确结果应该是 3, 楼上的算法结果是2
 
嗯, 没错,我要的就是2
因为1111只包含 前11 和 后11 两个子串。
不能考虑 1 11 1 这种情况, 这种情况下结果就只有1个匹配的子串了, 而显然实际不是这样。
所以结果是2
 
都称不上高效!!不过第一种的改进还可以
 
收藏先,等着给一个Replace 所有的字符串为新的子串的高效算法。
 
呵呵,看我功劳大吧,把 Another_eYes 都“气”出来了:)
待会儿再看你的代码。
 
我认为方法1的效率之所以低下,是因为Delphi的字符串处理过程都是针对String的,我在
实际使用中遇到需要高速处理的地方一般都用入口参数是 PChar 和 剩余长度 的过程,非常
方便。
 
>> len1 := length(SubStr);
我最常用的一句,看来你也喜欢啊:)
呵呵,还是给你挑点刺吧:
你这个办法要是遇上 MBCS 就有可能出问题哦,因为你没有判断 LeadByte :)
还有,看你的 方法二,当 SubStr 为空的时候(Len1 = 0),正常进入 while 循环体,
然后在 if (Source=SubStr[1]) 的时候将会发生异常:SubStr[1] 非法访问!
你需要一个判断:)
不过思路方面是没有问题的啦,值得大家好好学习:)
 
// 需要高速处理的地方一般都用入口参数是 PChar 和 剩余长度 的过程
同意!(搞不好一会儿又有人出来“不能一概而论”云云……不管了[:D])
 
偶会这么写,哪位大侠给分析一下,嘻嘻。
function TForm1.SubStrCnt(const aSubStr, aStr: string): Integer;
var
pTemp: PChar;
begin
Result := 0;
pTemp := StrPos(PChar(aStr), PChar(aSubStr));
while pTemp <> nildo
begin
Inc(pTemp, Length(aSubStr));
Inc(Result);
pTemp := StrPos(pTemp, PChar(aSubStr));
end;
end;
 
方法一:
function CountSubStr(SubStr, Source: string): Integer;
begin
result := 0;
while pos(SubStr, Source)>0do
begin
inc(result);
// Source := copy(Source, pos(SubStr, Source)+Length(SubStr), $7FFFFFFF);
Delete(Source,1,pos(SubStr, Source)+Length(SubStr);
end;
end;

这样不好?为什么要Copy呢
 
to beta: 谨受教
to SOHOMO: 这个方法还有可商榷的地方:
StrPos函数中它要先做一步查找字符串结尾的工作(换句话说, 它要计算字符串的长度), 而这一步显然是浪费的。 假设Source很长的时候这步将是极大的浪费(我原来也喜欢用StrScan, 但后来做过一个程序, 循环在15M字符串中查找, 结果发觉极其缓慢, 和事先设想的完全是两回事, 后来分析汇编码发觉它查找字符串结尾的工作对我的程序而言完全是不必的---我当然已经知道字符串的长度了, 而这步的消耗就占了整个过程的绝大部分时间,
后来我改成直接用循环一个一个字符扫描, 结果整个过程的运行时间就由近30分钟下降到不到300ms)
to xianghb: delete和copy做的工作是一样的。都是复制子串。 而这和copy一样都是完全不必要的。
 
这种文章好。非常非常感谢。前辈十年功,我辈三分钟,再也没有比这个更珍贵的了。
不要骂我,我还是做了测试。使用的测试文本是 creation_zy 的 "DNA 播种机" 建立的
文件。这个程序可以到他的主页取得,文本文件大小 100KB 。
NO. 测得字数 Time (ms) 例程作者
1 25587 210-220 Another_eYes 文中第一个
2 25587 170-180 Another_eYes 文中第二个
3 26415 170-180 Another_eYes 文中第三个
4 25587 190-200 SOHOMO
5 20480 200 xianghb
测试文本总字母数为 102400 个.
为了不跑题,简述一下: NO.3 :分中文和英文测试,其结果都和其他不同。NO.5 :中文测试正常,
英文测试和其他例程不同.如果参考上面的例程的话请留意这些情况.
 
100K 的测试看不出差距,请用超过 10M 的文件再次测试!!!
要快的,我这里有一个,先声明,这个 传说中的 世界上最快的 Pos 函数不是我写的,
我仅仅是修正了一个 bug 而已,对汇编有兴趣的朋友可以看一下。
function FastPos(
const aSourceString, aFindString : String;
const aSourceLen, aFindLen, StartPos : Integer
) : Integer;
begin
// Next, we determine how many bytes we need to
// scan to find the "start" of aFindString.
// Remove by SunLujiang
{
SourceLen := aSourceLen;
SourceLen := SourceLen - aFindLen;
if (StartPos-1) > SourceLen then
begin
Result := 0;
Exit;
end;
SourceLen := SourceLen - StartPos;
SourceLen := SourceLen +2;
}
// Remove end
// The ASM starts here.
asm
// Delphi uses ESI, EDI, and EBX a lot,
// so we must preserve them.
push ESI
push EDI
push EBX
// Add by SunLujiang
Mov ECX, aSourceLen
Mov EAX, aFindLen
// Add by beta begin
//make sure that aFindLen > 0, or it may cause a exception
Cmp EAX, 0
Jle @Result0
// Add by beta end
Sub ECX, EAX
JL @Result0
Mov EAX, StartPos
Dec EAX
Sub ECX, EAX
JL @Result0
Inc ECX
// Add end
// Get the address of sourceString[1]
// and Add (StartPos-1).
// Wedo
this for the purpose of finding
// the NEXT occurrence, rather than
// always the first!
mov EDI, aSourceString
add EDI, StartPos
Dec EDI
// Get the address of aFindString.
mov ESI, aFindString
// Note how many bytes we need to
// look through in aSourceString
// to find aFindString.
// Remove by SunLujiang
// mov ECX, SourceLen
// Remove end
// Get the first char of aFindString;
// note how it isdo
ne outside of the
// main loop, as it never changes!
Mov Al, [ESI]
// Now the FindFirstCharacter loop!
@ScaSB:
// Get the value of the current
// character in aSourceString.
// This is equal to ah := EDI^, that
// is what the [] are around [EDI].
Mov Ah, [EDI]
// Compare this character with aDestString[1].
cmp Ah,Al
// If they're not equal wedo
n't
// compare the strings.
jne @NextChar
// If they're equal, obviously wedo
!
@CompareStrings:
// Put the length of aFindLen in EBX.
mov EBX, aFindLen
// We DEC EBX to point to the end of
// the string;
that is, wedo
n't want to
// add 1 if aFindString is 1 in length!
dec EBX
// add by ShengQuanhu
// If EBX is zero, then
we've successfully
// compared each character;
i.e. it's A MATCH!
// It will be happened when aFindLen=1
Jz @EndOfMatch
//add end
//Here’s another optimization tip. People at this point usually PUSH ESI and
//so on and then
POP ESI and so forth at the end–instead, I opted not to chan
//ge ESI and so on at all. This saves lots of pushing and popping!
@CompareNext:
// Get aFindString character +
// aFindStringLength (the last char).
mov Al, [ESI+EBX]
// Get aSourceString character (current
// position + aFindStringLength).
mov Ah, [EDI+EBX]
// Compare them.
cmp Al, Ah
Jz @Matches
// If theydo
n't match, we put the first char
// of aFindString into Al again to continue
// looking for the first character.
Mov Al, [ESI]
Jmp @NextChar
@Matches:
// If they match, we DEC EBX (point to
// previous character to compare).
Dec EBX
// If EBX <> 0 ("J"ump "N"ot "Z"ero), we
// continue comparing strings.
Jnz @CompareNext
//add by Shengquanhu
@EndOfMatch:
//add end
// If EBX is zero, then
we've successfully
// compared each character;
i.e. it's A MATCH!
// Move the address of the *current*
// character in EDI.
// Note, we haven't altered EDI since
// the first char was found.
mov EAX, EDI
// This is an address, so subtract the
// address of aSourceString[1] to get
// an actual character position.
sub EAX, aSourceString
// Inc EAX to make it 1-based,
// rather than 0-based.
inc EAX
// Put it into result.
mov Result, EAX
// Finish this routine!
jmp @TheEnd
@NextChar:
//This is where I jump to when I want to continue searching for the first char
//acter of aFindString in aSearchString:
// Point EDI (aFindString[X]) to
// the next character.
Mov Ah, [EDI]//先把第一个字符移到Ah中,后面判断是否中文
Inc EDI
// Dec ECX tells us that we've checked
// another character, and that we're
// fast running out of string to check!
dec ECX
// If EBX <> 0, then
continue scanning
// for the first character.
//add by shengquanhu
//if ah is chinese char,jump again
jz @Result0
cmp ah, $80
jb @ScaSB
Inc EDI
Dec ECX
//add by shengquanhu end
jnz @ScaSB
//add by shengquanhu
@Result0:
//add by shengquanhu end
// If EBX = 0, then
move 0 into RESULT.
mov Result,0
// Restore EBX, EDI, ESI for Delphi
// to work correctly.
// Note that they're POPped in the
// opposite order they were PUSHed.
@TheEnd:
pop EBX
pop EDI
pop ESI
end;
end;

// 计数函数:
function BetaCount(substr, str: string): Integer;
var
Start: Integer;
begin
Result := 0;
Start := 1;
repeat
Start := FastPos(str, substr, Length(str), Length(substr), Start);
if Start > 0 then
begin
Inc(Result);
Inc(Start, Length(substr));
end
else
Break;
until Start = 0;
end;

 
有关字符串操作的效率我真的是感慨万分呀!因为我自己写了个XML解析器这里边几乎全是
字符串操作.我现在的解析器真的比第一个版本快了100倍以上.完全是因为字符串操作上
的问题.
大家看看我的一篇心得:
问题:解析一个串,生成一个SQL语句的条件子句,假定下限与上限之间用“~”或“-”分开,并列条件间用“,”或“;”分开,例如:
3~12,18,22-32,120~300可解析为:
((id>=3) and (id<=12)) or (id=18) or ((id>=22) and (id<=32)) or ((id>=120) and (id<=300))
现有两种算法,第一种先分解成一个串表,然后一行行解析;第二种算法使用递归。
functiondo
ParseInteger(const Title: string;
Value: PChar): string;
var
Lines: TStrings;
I, J, X0, X1: Integer;
S: string;
BR: Boolean;
begin
Lines := TStringList.Create;
try
Lines.CommaText := Value;
for I := 0 to Pred(Lines.Count)do
begin
S := Lines;
if Length(S) > 0 then
begin
X0 := 0;
X1 := 0;
BR := False;
for J := 1 to Length(S)do
if S[J] in ['0'..'9'] then
if BR then
X1 := X1 * 10 + Ord(S[J]) - Ord('0')
else
X0 := X0 * 10 + Ord(S[J]) - Ord('0')
else
if (S[J] = '~') or (S[J] = '-') then
BR := True;
if X1 = 0 then
Lines := Format('%s=%d', [Title, X0])
else
Lines := Format('(%s>=%d) and (%s<=%d)', [Title, X0, Title, X1]);
end;
end;
for I := 0 to Pred(Lines.Count)do
begin
if I > 0 then
Result := Result + ' or ';
Result := Result + Format('(%s)', [Lines]);
end;
finally
Lines.Free;
end;
end;

functiondo
ParseInteger1(const Title: string;
Value: PChar): string;
proceduredo
Parse(Source: PChar;
var Res: string);
var
X0, X1: Integer;
P: PChar;
C: Char;
B, Ba: Boolean;
S: string;
begin
X0 := 0;
X1 := 0;
B := False;
Ba := False;
P := Source;
while Truedo
begin
C := P[0];
Inc(P);
case C of
'0'..'9':
begin
Ba := True;
if B then
X1 := X1 * 10 + Ord(C) - Ord('0')
else
X0 := X0 * 10 + Ord(C) - Ord('0');
end;
'~', '-':
B := True;
',', ';', #0:
begin
if Ba then
begin
if X1 = 0 then
S := Format('(%s=%d)', [Title, X0])
else
S := Format('((%s>=%d) and (%s<=%d))', [Title, X0, Title, X1]);
if Length(Res) > 0 then
Res := Res + ' or ';
Res := Res + S;
end;
if C = #0 then
Break
else
begin
do
Parse(P, Res);
Break;
end;
end;
end;
end;
end;
begin
Result := '';
do
Parse(Value, Result);
end;

两种算法的结果一样。但效率相隔甚远:
在一台奔腾III900,128M内存的机器上,运算3000次,第一种算法需要11750ms,而第二种算法只需要60ms,相差近200倍。
你相信吗?
 
beta ,我插一句,10M 不好玩,没等我看到结果我先就死机了。测试的原意是对使用怎么样的
函数结构做出取舍(当我确实需要使用这样的函数时)。当然直接使用 ASM 可能会比较直接,也
应该是一个可以选择的方案,我的测试就是基于这样的目的,给几个函数一个相同的环境,而
不是直接测试它的速度。测试的结果是看看速度和准确度(有时测试代码本身还没有被测函数精
炼,但环境一样,我仍然可以得到我要的结果)。
至于 ASM 代码,你贴出的不能归入上面函数类型,原因是上面函数只使用两个参数。这个很重
要,为什么这样说呢,因为获得字符串长度,使用 ASM 有两个方案,一个是不直接获得串长度
,利用编译器的 PChar 转换,直接在编译期就脱掉 String 的前缀。一个是用代码去获得串长
度,二者在运行期的速度如果遇到 10M 这样的大文件是不同的。而你贴出的代码已经手工将长
度做为参数代入了,就不能归入上面类型的函数。我手上没有这样的 ASM 代码,我估计 Another_eYes
手上肯定有,基本估计了一下,这个代码最少需要有 5-6 个判断跳转,设计它是个技术活,不
知道 Another_eYes 肯不肯也贴一贴。
 
to 小雨哥(真吃亏, 也许我比你大呢): 呵呵, 我没有。 不过写起来很简单。
取字符串长度的方法有两种。
一种是直接取字符串首地址的前4个byte(针对string类型的), 类似代码如下:
MOV ECX,[EAX-4]
另一种是PChar型取长度的(如果有10M数据,劝您别用这种方式, 不然会死得很难看的)
类似代码如下:
PUSH EDI
MOV EDI,EAX
XOR AL,AL
MOV ECX,0FFFFFFFFH
REPNZ SCASB
MOV EAX,0FFFFFFFEH
SUB EAX,ECX
POP EDI
 

Similar threads

S
回复
0
查看
950
SUNSTONE的Delphi笔记
S
S
回复
0
查看
774
SUNSTONE的Delphi笔记
S
S
回复
0
查看
3K
SUNSTONE的Delphi笔记
S
S
回复
0
查看
2K
SUNSTONE的Delphi笔记
S
I
回复
0
查看
763
import
I
顶部