Академическая задача: Удалить начальные и конечные пробелы из строки, используя петлю while
.
Как мы подходим к этой проблеме?
Что ж, мы, конечно, хотели бы создать функцию , которая обрезает строку. Таким образом, мы можем просто вызывать эту функцию каждый раз, когда нам нужно выполнить такую операцию. Это сделает код более читабельным и простым в обслуживании.
Очевидно, что эта функция принимает строку и возвращает строку. Следовательно, его декларация должна быть
function Trim(const AText: string): string;
Здесь я следую условию добавления префикса к аргументу "A". Я также использую префикс const
, чтобы сообщить компилятору, что мне не нужно изменять аргумент внутри функции; это может улучшить производительность (хотя и незначительно).
Определение будет выглядеть так:
function Trim(const AText: string): string;
begin
// Compute the trimmed string and save it in the result variable.
end;
Первая попытка
Теперь давайте попробуем реализовать этот алгоритм, используя цикл while
. Наша первая попытка будет очень медленной, но за ней довольно легко последовать.
Сначала давайте скопируем строку аргумента AText
в переменную result
; когда функция вернется, значением result
будет его возвращаемое значение:
result := AText;
Теперь давайте попробуем удалить ведущих пробелов.
while result[1] = ' ' do
Delete(result, 1, 1);
Мы проверяем, является ли первый символ result[1]
пробелом, и если это так, мы используем процедуру Delete
, чтобы удалить его из строки (в частности, Delete(result, 1, 1)
удаляет символ 1
из строки начиная с символа с индексом 1
). Затем мы делаем это снова и снова, пока первый символ не станет чем-то отличным от пробела.
Например, если result
изначально равно ' Hello, World!'
, это сделает его равным 'Hello, World!'
.
Полный код, пока:
function Trim(const AText: string): string;
begin
result := AText;
while result[1] = ' ' do
Delete(result, 1, 1);
end;
Теперь попробуйте сделать это со строкой, состоящей только из пробелов, таких как ' '
, или пустой строки, ''
. Что просходит? Почему?
Подумайте об этом.
Очевидно, что в таком случае result
рано или поздно будет пустой строкой, а затем символ result[1]
не будет существовать. (Действительно, если бы существовал первый символ result
, result
имел бы длину не менее 1, и поэтому это не была бы пустая строка, состоящая ровно из нулевых символов.)
Доступ к несуществующему символу приведет к сбою программы.
Чтобы исправить эту ошибку, мы изменим цикл следующим образом:
while (Length(result) >= 1) and (result[1] = ' ') do
Delete(result, 1, 1);
Из-за метода, известного как «ленивая логическая оценка» (или «оценка короткого замыкания» ), второй операнд оператора and
, то есть result[1] = ' '
, даже не будет запускается, если первый операнд, в данном случае Length(result) >= 1
, имеет значение false
. Действительно, false and <anything>
равно false
, поэтому мы уже знаем значение соединения в этом случае.
Другими словами, result[1] = ' '
будет оцениваться только если Length(result) >= 1
, и в этом случае ошибки не будет. Кроме того, алгоритм выдает правильный ответ, потому что если мы в конечном итоге обнаружим, что Length(result) = 0
, ясно, что мы закончили и должны вернуть пустую строку.
Удаление завершающих пробелов аналогичным образом, в итоге мы получим
function Trim(const AText: string): string;
begin
result := AText;
while (Length(result) >= 1) and (result[1] = ' ') do
Delete(result, 1, 1);
while (Length(result) >= 1) and (result[Length(result)] = ' ') do
Delete(result, Length(result), 1);
end;
крошечное улучшение
Мне не совсем нравятся литералы пробела ' '
, потому что визуально сложно определить, сколько пробелов есть. Действительно, у нас может быть даже другой символ пробела, чем простой пробел. Следовательно, я бы написал #32
или #$20
вместо этого. 32
(десятичный) или $20
(шестнадцатеричный) - код символа обычного пробела.
(намного) лучшее решение
Если вы попытаетесь обрезать строку, содержащую много миллионов символов (включая несколько миллионов начальных и конечных пробелов), используя вышеупомянутый алгоритм, вы заметите, что он удивительно медленный. Это потому, что в каждой итерации нам нужно перераспределять память для строки.
Гораздо лучший алгоритм будет просто определять количество начальных и конечных пробелов путем чтения символов в строке, а затем за один шаг выполнить выделение памяти для новой строки.
В следующем коде я определяю индекс FirstPos
первого непробельного символа в строке и индекс LastPos
последнего непробельного символа в строке:
function Trim2(const AText: string): string;
var
FirstPos, LastPos: integer;
begin
FirstPos := 1;
while (FirstPos <= Length(AText)) and (AText[FirstPos] = #32) do
Inc(FirstPos);
LastPos := Length(AText);
while (LastPos >= 1) and (AText[LastPos] = #32) do
Dec(LastPos);
result := Copy(AText, FirstPos, LastPos - FirstPos + 1);
end;
Я оставлю это в качестве упражнения для читателя, чтобы выяснить точную работу алгоритма. В качестве бонусного упражнения попробуйте сравнить два алгоритма: насколько быстрее последний? (Подсказка: речь идет о порядках!)
Простой тест
Ради полноты я написал следующий очень простой тест:
const
N = 10000;
var
t: cardinal;
dur1, dur2: cardinal;
S: array[1..N] of string;
S1: array[1..N] of string;
S2: array[1..N] of string;
i: Integer;
begin
Randomize;
for i := 1 to N do
S[i] := StringOfChar(#32, Random(10000)) + StringOfChar('a', Random(10000)) + StringOfChar(#32, Random(10000));
t := GetTickCount;
for i := 1 to N do
S1[i] := Trim(S[i]);
dur1 := GetTickCount - t;
t := GetTickCount;
for i := 1 to N do
S2[i] := Trim2(S[i]);
dur2 := GetTickCount - t;
Writeln('trim1: ', dur1, ' ms');
Writeln('trim2: ', dur2, ' ms');
end.
Я получил следующий вывод:
trim1: 159573 ms
trim2: 484 ms