C++ remove·remove_if가 원소를 지우지 않는 이유: erase-remove와 C++20 erase_if
이 글의 핵심
remove만 호출하고 erase를 빠뜨리면 벡터 크기는 그대로인 채 쓰레기 값이 남는 버그가 생깁니다. unique가 인접한 중복만 지운다는 점, list에서는 멤버 함수 remove가 더 효율적인 이유, 문자열 정리와 데이터 필터링 예제를 통해 삭제 패턴을 안전하게 쓰는 방법을 설명합니다.
들어가며
STL 제거 알고리즘은 요소를 제거하는 것이 아니라 끝으로 이동합니다. 실제 제거는 erase와 함께 사용하는 erase-remove idiom이 표준 패턴입니다.
왜 remove는 지우지 못하는가
처음 std::remove를 쓰면 거의 누구나 “호출했는데 크기가 그대로”라는 상황을 겪습니다. 저도 벡터에서 특정 ID를 지운 뒤 size()로 개수를 검사하는 테스트가 계속 실패해서 한참 들여다본 적이 있습니다. 이유는 STL의 설계에 있습니다. <algorithm>의 함수들은 컨테이너가 아니라 반복자 쌍만 받습니다. 반복자는 원소를 읽고 쓸 수는 있지만 “컨테이너의 크기를 줄이는” 능력은 없습니다. 그래서 remove가 할 수 있는 최선은 남길 원소를 앞쪽으로 모아 놓고 “여기까지가 유효한 범위”라는 새 끝 반복자를 돌려주는 것이고, 실제로 메모리를 줄이는 일은 컨테이너의 멤버 함수 erase에게 맡깁니다. 이 분리 덕분에 같은 remove가 vector, deque, string, 심지어 일반 배열에도 동작합니다. 배열은 크기를 줄일 수 없으니 반환된 새 끝 위치를 기억해 두고 거기까지만 쓰면 됩니다.
remove가 남는 원소의 상대 순서를 유지한다는 점도 알아 둘 만합니다. 순서가 상관없다면 지울 원소를 마지막 원소와 바꾸고 pop_back하는 방식이 이동 횟수를 더 줄일 수 있지만, 표준 remove는 안정성을 보장하는 쪽을 택했습니다.
remove와 remove_if: 남길 원소를 앞으로 모으기
remove 후 벡터 상태 살펴보기
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 2, 4, 2, 5};
std::cout << "원본: ";
for (int x : v) std::cout << x << " ";
std::cout << std::endl;
// remove: 2를 끝으로 이동
auto newEnd = std::remove(v.begin(), v.end(), 2);
std::cout << "remove 후: ";
for (int x : v) std::cout << x << " ";
std::cout << std::endl;
std::cout << "크기: " << v.size() << std::endl; // 7 (변경 안 됨!)
// erase: 실제 제거
v.erase(newEnd, v.end());
std::cout << "erase 후: ";
for (int x : v) std::cout << x << " ";
std::cout << std::endl;
std::cout << "크기: " << v.size() << std::endl; // 4
return 0;
}
출력:
원본: 1 2 3 2 4 2 5
remove 후: 1 3 4 5 4 2 5 (끝 부분은 쓰레기 값)
크기: 7
erase 후: 1 3 4 5
크기: 4
“remove 후” 줄을 보면 동작 방식이 그대로 드러납니다. remove는 앞에서부터 원소를 하나씩 읽으며 2가 아닌 원소를 “쓰기 위치”에 차례로 덮어씁니다(정확히는 이동 대입). 그래서 앞의 네 칸은 1 3 4 5가 되고, 뒤의 세 칸 4 2 5는 덮어쓰이지 않은 원래 값이 남아 있을 뿐입니다. 지운 값 2가 뒤로 “이동”하는 것이 아니라는 점에 주의하세요. 뒤쪽에 2가 하나 보이는 것은 우연이고, 표준은 이 구간의 값을 “유효하지만 지정되지 않은 상태”로만 보장합니다. int라면 원래 값이 남지만, std::string이나 unique_ptr처럼 이동 시 원본이 비는 타입이라면 빈 문자열이나 nullptr이 남습니다. 이 구간에 접근하는 것 자체가 버그의 신호이므로 반드시 erase로 잘라 내야 합니다.
비용 면에서는 remove가 원소를 한 번씩만 훑으므로 O(n)이고, 남는 원소마다 최대 한 번 이동합니다. 반면 루프를 돌며 찾을 때마다 v.erase(it)를 호출하면 지울 때마다 뒤의 원소 전체를 한 칸씩 당기므로 최악 O(n²)이 됩니다. 삭제할 원소가 많을수록 erase-remove 관용구가 압도적으로 유리한 이유입니다.
erase-remove 관용구로 실제로 지우기
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 2, 4, 2, 5};
// ✅ erase-remove idiom (한 줄)
v.erase(std::remove(v.begin(), v.end(), 2), v.end());
for (int x : v) std::cout << x << " "; // 1 3 4 5
std::cout << std::endl;
return 0;
}
한 줄 관용구에서 흔히 하는 실수는 erase의 두 번째 인자를 빠뜨리는 것입니다. v.erase(std::remove(v.begin(), v.end(), 2));라고 쓰면 반복자 하나만 받는 오버로드가 호출되어, 새 끝 위치의 원소 하나만 지우고 나머지 쓰레기 구간은 남습니다. 컴파일 에러도 경고도 없고, 지울 원소가 정확히 하나일 때는 결과가 맞아 보이기 때문에 테스트를 통과하기도 합니다. 더 나쁜 경우는 지울 원소가 하나도 없을 때로, 이때 remove가 v.end()를 반환하고 erase(v.end())는 미정의 동작입니다.
또 하나 알려진 함정은 지울 값을 컨테이너 안의 원소로 넘기는 것입니다. std::remove(v.begin(), v.end(), v[0])에서 세 번째 인자는 const T&, 즉 v[0] 자체를 가리키는 참조입니다. 알고리즘이 진행되며 v[0] 자리에 다른 값이 덮어쓰이면 비교 기준이 중간에 바뀌어 엉뚱한 원소가 지워집니다. 값을 먼저 지역 변수에 복사해 두고 넘기면 안전합니다(C++20의 std::erase도 같은 이유로 주의가 필요합니다).
remove_if로 조건부 제거
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 짝수 제거
v.erase(
std::remove_if(v.begin(), v.end(), [](int x) {
return x % 2 == 0;
}),
v.end()
);
for (int x : v) std::cout << x << " "; // 1 3 5 7 9
std::cout << std::endl;
return 0;
}
여러 조건을 한 predicate로 묶기
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"Alice", 25},
{"Bob", 17},
{"Charlie", 30},
{"David", 15}
};
// 미성년자 제거
people.erase(
std::remove_if(people.begin(), people.end(), [](const Person& p) {
return p.age < 18;
}),
people.end()
);
for (const auto& p : people) {
std::cout << p.name << " (" << p.age << ")" << std::endl;
}
return 0;
}
출력:
Alice (25)
Charlie (30)
remove_if의 조건자에는 규칙이 하나 있습니다. 같은 원소에 대해 항상 같은 답을 내야 하고, 호출 횟수나 순서에 의존하면 안 됩니다. 표준은 조건자를 정확히 n번 호출한다고 보장하지만, 조건자 객체가 내부에서 복사될 수 있어 “세 번째로 만나는 원소만 지우기” 같은 상태 기반 람다([count = 0](int) mutable { return ++count == 3; })는 구현에 따라 예상과 다르게 동작할 수 있습니다. 위치 기반으로 지우고 싶다면 인덱스 루프가 더 명확합니다.
지워지는 원소에 대해 뭔가 처리(로그 남기기, 리소스 해제)를 하고 싶어서 조건자 안에서 부수 효과를 넣는 경우도 있는데, 그 시점에 원소는 아직 이동되지 않은 상태이므로 읽기만 한다면 괜찮습니다. 다만 지울 원소를 다른 곳으로 옮겨야 한다면 remove_if는 이동된 뒤의 값을 보장하지 않으므로, std::stable_partition으로 남길 것과 지울 것을 나눈 다음 뒤쪽 구간을 처리하고 erase하는 편이 맞습니다.
C++20 std::erase와 erase_if
값으로 한 줄 제거
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 2, 4, 2, 5};
// C++20: std::erase (간결)
std::erase(v, 2);
for (int x : v) std::cout << x << " "; // 1 3 4 5
std::cout << std::endl;
return 0;
}
조건으로 한 줄 제거
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// C++20: std::erase_if (간결)
std::erase_if(v, [](int x) { return x % 2 == 0; });
for (int x : v) std::cout << x << " "; // 1 3 5 7 9
std::cout << std::endl;
return 0;
}
C++20의 std::erase/std::erase_if는 알고리즘이 아니라 컨테이너별 비멤버 함수라서 <algorithm>이 아니라 각 컨테이너 헤더(<vector>, <string>, <list>, <map> 등)에 선언되어 있습니다. 내부적으로 vector는 erase-remove를, list는 멤버 remove를, map은 반복하며 erase를 수행해 컨테이너마다 가장 효율적인 방식으로 처리해 줍니다. 반환값은 지워진 원소의 개수라서, 몇 개가 삭제됐는지 확인하는 코드도 간단해집니다. 앞에서 설명한 두 가지 실수(erase 누락, 두 번째 인자 누락)가 원천적으로 불가능해지므로, C++20을 쓸 수 있다면 이쪽을 기본으로 삼는 것이 좋습니다. 다만 std::erase_if는 컨테이너 전체에만 적용되므로, 범위의 일부에서만 지워야 할 때는 여전히 erase-remove가 필요합니다.
unique로 인접 중복 제거
sort + unique + erase 조합
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 1, 2, 2, 2, 3, 3, 4, 5, 5};
std::cout << "원본: ";
for (int x : v) std::cout << x << " ";
std::cout << std::endl;
// ❌ 정렬 안 하면 인접 중복만 제거
auto v1 = v;
v1.erase(std::unique(v1.begin(), v1.end()), v1.end());
std::cout << "정렬 안 함: ";
for (int x : v1) std::cout << x << " "; // 1 2 3 4 5
std::cout << std::endl;
// ✅ 정렬 후 중복 제거
std::vector<int> v2 = {1, 2, 1, 3, 2, 4, 3, 5};
std::sort(v2.begin(), v2.end());
v2.erase(std::unique(v2.begin(), v2.end()), v2.end());
std::cout << "정렬 후: ";
for (int x : v2) std::cout << x << " "; // 1 2 3 4 5
std::cout << std::endl;
return 0;
}
첫 번째 경우(v1)가 올바른 결과를 낸 것은 원본 v가 이미 정렬되어 있었기 때문입니다. unique는 “직전 원소와 같으면 버린다”는 단순한 규칙만 적용하므로, 같은 값이 떨어져 있으면 알아채지 못합니다. 정렬되지 않은 데이터에서의 실패는 아래 “정렬하지 않고 unique를 호출함” 항목에서 볼 수 있습니다. 이 성질은 단점이기만 한 것은 아니어서, 로그의 연속 중복 줄을 하나로 합치거나(uniq 명령과 같은 동작) 연속 공백을 줄이는 것처럼 인접 중복만 지우고 싶을 때는 정렬 없이 unique를 쓰는 것이 정답입니다.
정렬 후 중복 제거는 O(n log n)이고 원래 순서가 사라집니다. 순서를 유지하면서 중복을 지워야 한다면 std::unordered_set에 이미 본 값을 기록하며 remove_if하는 방식(평균 O(n), 추가 메모리 사용)이 있습니다. 값이 적고 순서가 필요 없다면 처음부터 std::set에 넣는 것도 방법입니다.
비교 함수를 넘기는 unique
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
#include <cctype>
int main() {
std::vector<std::string> words = {"apple", "APPLE", "banana", "Banana"};
// 대소문자 무시 중복 제거
std::sort(words.begin(), words.end(), [](const std::string& a, const std::string& b) {
return std::lexicographical_compare(
a.begin(), a.end(),
b.begin(), b.end(),
[](char c1, char c2) { return std::tolower(c1) < std::tolower(c2); }
);
});
words.erase(
std::unique(words.begin(), words.end(), [](const std::string& a, const std::string& b) {
return std::equal(
a.begin(), a.end(),
b.begin(), b.end(),
[](char c1, char c2) { return std::tolower(c1) == std::tolower(c2); }
);
}),
words.end()
);
for (const auto& word : words) {
std::cout << word << std::endl;
}
return 0;
}
출력:
apple
banana
이 예제는 정렬 기준과 중복 판정 기준을 같은 규칙(대소문자 무시)으로 맞췄다는 점이 핵심입니다. 두 기준이 다르면, 예를 들어 대소문자를 구분해 정렬한 뒤 대소문자를 무시하고 unique를 하면 “APPLE”, “Banana”, “apple”, “banana” 순으로 정렬되어 같은 단어가 인접하지 않으므로 아무것도 지워지지 않습니다.
출력에 대해서도 한 가지 짚어야 합니다. 대소문자 무시 비교에서 “apple”과 “APPLE”은 동등하므로 std::sort 후 둘의 순서는 정해지지 않고, unique는 그중 앞에 온 것을 남깁니다. 따라서 구현에 따라 “APPLE”이나 “Banana”가 남을 수도 있습니다. 어떤 표기를 남길지가 중요하다면 std::stable_sort로 원래 순서를 보존하세요. 또 std::tolower, std::isspace, std::isalpha에 char를 그대로 넘기면 UTF-8 한글처럼 음수 값을 가진 바이트에서 미정의 동작이 됩니다. MSVC 디버그 빌드에서는 “Debug Assertion Failed … c >= -1 && c <= 255”로 바로 멈춥니다. 한글이 섞일 수 있는 문자열이라면 static_cast<unsigned char>(c)로 변환해서 넘겨야 합니다. 이 글의 문자열 예제들도 ASCII 입력을 가정합니다.
erase 누락·정렬 누락·list에서의 제거
remove만 호출하고 erase를 빠뜨림
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 2, 4};
// ❌ erase 없음 (크기 변경 안 됨)
auto newEnd = std::remove(v.begin(), v.end(), 2);
std::cout << "크기: " << v.size() << std::endl; // 5 (변경 안 됨!)
std::cout << "내용: ";
for (int x : v) std::cout << x << " "; // 1 3 4 2 4 (쓰레기 값)
std::cout << std::endl;
// ✅ erase 호출
v = {1, 2, 3, 2, 4};
v.erase(std::remove(v.begin(), v.end(), 2), v.end());
std::cout << "크기: " << v.size() << std::endl; // 3
std::cout << "내용: ";
for (int x : v) std::cout << x << " "; // 1 3 4
std::cout << std::endl;
return 0;
}
정렬하지 않고 unique를 호출함
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 1, 3, 2};
// ❌ 정렬 안 됨 (인접 중복만 제거)
auto v1 = v;
v1.erase(std::unique(v1.begin(), v1.end()), v1.end());
std::cout << "정렬 안 함: ";
for (int x : v1) std::cout << x << " "; // 1 2 1 3 2 (중복 남음)
std::cout << std::endl;
// ✅ 정렬 후
auto v2 = v;
std::sort(v2.begin(), v2.end());
v2.erase(std::unique(v2.begin(), v2.end()), v2.end());
std::cout << "정렬 후: ";
for (int x : v2) std::cout << x << " "; // 1 2 3
std::cout << std::endl;
return 0;
}
조건마다 remove_if를 여러 번 호출함
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// ❌ 여러 번 호출 (비효율)
v.erase(std::remove(v.begin(), v.end(), 2), v.end());
v.erase(std::remove(v.begin(), v.end(), 4), v.end());
v.erase(std::remove(v.begin(), v.end(), 6), v.end());
// ✅ 한 번에 (효율적)
v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
v.erase(
std::remove_if(v.begin(), v.end(), [](int x) {
return x == 2 || x == 4 || x == 6;
}),
v.end()
);
for (int x : v) std::cout << x << " "; // 1 3 5 7 8 9 10
std::cout << std::endl;
return 0;
}
remove를 세 번 호출하면 벡터를 세 번 훑고 남는 원소도 최대 세 번 이동합니다. remove_if로 합치면 한 번에 끝납니다. 지울 값이 많아지면 x == 2 || x == 4 || ... 대신 지울 값을 std::unordered_set이나 정렬된 벡터에 담고 조건자에서 조회하는 방식이 코드와 성능 모두 낫습니다.
list에서는 멤버 remove가 낫다
#include <algorithm>
#include <vector>
#include <list>
#include <iostream>
int main() {
// vector: erase-remove idiom
std::vector<int> vec = {1, 2, 3, 2, 4};
vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end());
std::cout << "vector: ";
for (int x : vec) std::cout << x << " ";
std::cout << std::endl;
// list: remove 멤버 함수 (더 효율적)
std::list<int> lst = {1, 2, 3, 2, 4};
lst.remove(2); // O(n), 원소 이동 없이 노드만 해제
std::cout << "list: ";
for (int x : lst) std::cout << x << " ";
std::cout << std::endl;
return 0;
}
std::list에 std::remove를 써도 결과는 맞지만, 노드 연결만 바꾸면 끝날 일을 값을 복사·이동해서 처리하므로 손해입니다. 멤버 remove는 해당 노드를 연결 리스트에서 떼어 내 해제할 뿐이라, 남는 원소의 주소와 그 원소를 가리키던 반복자·참조가 그대로 유효합니다. 이것이 list를 쓰는 주된 이유이기도 합니다. std::set이나 std::map 같은 연관 컨테이너에는 std::remove를 아예 쓸 수 없습니다. 키가 const라 대입이 불가능해서 “assignment of read-only location” 계열의 컴파일 에러가 납니다. 연관 컨테이너는 erase(key)나, 조건부라면 C++20 std::erase_if(m, pred)를 씁니다.
문자열 정리·데이터 필터링·중복 제거 예제
공백과 알파벳 외 문자 제거
#include <algorithm>
#include <string>
#include <cctype>
#include <iostream>
std::string cleanString(std::string str) {
// 공백 제거
str.erase(
std::remove_if(str.begin(), str.end(), [](char c) {
return std::isspace(c);
}),
str.end()
);
return str;
}
std::string removeNonAlpha(std::string str) {
// 알파벳 아닌 문자 제거
str.erase(
std::remove_if(str.begin(), str.end(), [](char c) {
return !std::isalpha(c);
}),
str.end()
);
return str;
}
int main() {
std::string text1 = " Hello World ";
std::cout << "[" << cleanString(text1) << "]" << std::endl;
// [HelloWorld]
std::string text2 = "Hello123World456";
std::cout << "[" << removeNonAlpha(text2) << "]" << std::endl;
// [HelloWorld]
return 0;
}
조건에 맞는 레코드 걸러내기
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
struct Product {
std::string name;
double price;
bool inStock;
};
int main() {
std::vector<Product> products = {
{"Laptop", 1200.0, true},
{"Mouse", 25.0, false},
{"Keyboard", 75.0, true},
{"Monitor", 300.0, false},
{"Headset", 80.0, true}
};
// 재고 없는 제품 제거
products.erase(
std::remove_if(products.begin(), products.end(), [](const Product& p) {
return !p.inStock;
}),
products.end()
);
std::cout << "재고 있는 제품:" << std::endl;
for (const auto& p : products) {
std::cout << "- " << p.name << ": $" << p.price << std::endl;
}
return 0;
}
출력:
재고 있는 제품:
- Laptop: $1200
- Keyboard: $75
- Headset: $80
정렬 후 중복 제거한 결과 확인
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};
std::cout << "원본: ";
for (int x : v) std::cout << x << " ";
std::cout << std::endl;
// 정렬 후 중복 제거
std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end()), v.end());
std::cout << "중복 제거: ";
for (int x : v) std::cout << x << " ";
std::cout << std::endl;
return 0;
}
출력:
원본: 3 1 4 1 5 9 2 6 5 3
중복 제거: 1 2 3 4 5 6 9
데이터 정리 유틸리티 만들기
#include <algorithm>
#include <vector>
#include <string>
#include <cctype>
#include <iostream>
class DataCleaner {
public:
// 빈 문자열 제거
static void removeEmpty(std::vector<std::string>& vec) {
vec.erase(
std::remove_if(vec.begin(), vec.end(), [](const std::string& s) {
return s.empty();
}),
vec.end()
);
}
// 중복 제거
static void removeDuplicates(std::vector<int>& vec) {
std::sort(vec.begin(), vec.end());
vec.erase(std::unique(vec.begin(), vec.end()), vec.end());
}
// 범위 밖 값 제거
static void removeOutOfRange(std::vector<int>& vec, int min, int max) {
vec.erase(
std::remove_if(vec.begin(), vec.end(), [min, max](int x) {
return x < min || x > max;
}),
vec.end()
);
}
// 공백 문자 제거
static std::string removeWhitespace(std::string str) {
str.erase(
std::remove_if(str.begin(), str.end(), [](char c) {
return std::isspace(c);
}),
str.end()
);
return str;
}
// 연속 공백을 하나로
static std::string normalizeSpaces(std::string str) {
// 앞뒤 공백 제거
auto start = std::find_if_not(str.begin(), str.end(), [](char c) {
return std::isspace(c);
});
auto end = std::find_if_not(str.rbegin(), str.rend(), [](char c) {
return std::isspace(c);
}).base();
str = std::string(start, end);
// 연속 공백을 하나로
str.erase(
std::unique(str.begin(), str.end(), [](char a, char b) {
return std::isspace(a) && std::isspace(b);
}),
str.end()
);
return str;
}
};
int main() {
// 빈 문자열 제거
std::vector<std::string> strings = {"hello", "", "world", "", "test"};
DataCleaner::removeEmpty(strings);
std::cout << "빈 문자열 제거: ";
for (const auto& s : strings) std::cout << s << " ";
std::cout << std::endl;
// 중복 제거
std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};
DataCleaner::removeDuplicates(numbers);
std::cout << "중복 제거: ";
for (int x : numbers) std::cout << x << " ";
std::cout << std::endl;
// 범위 밖 제거
std::vector<int> values = {1, 5, 10, 15, 20, 25, 30};
DataCleaner::removeOutOfRange(values, 10, 20);
std::cout << "범위 밖 제거: ";
for (int x : values) std::cout << x << " ";
std::cout << std::endl;
// 공백 제거
std::string text = " Hello World ";
std::cout << "공백 제거: [" << DataCleaner::removeWhitespace(text) << "]" << std::endl;
// 공백 정규화
std::cout << "공백 정규화: [" << DataCleaner::normalizeSpaces(text) << "]" << std::endl;
return 0;
}
출력:
빈 문자열 제거: hello world test
중복 제거: 1 2 3 4 5 6 9
범위 밖 제거: 10 15 20
공백 제거: [HelloWorld]
공백 정규화: [Hello World]
normalizeSpaces는 두 알고리즘을 조합합니다. 먼저 find_if_not을 앞쪽과 역방향(rbegin)으로 한 번씩 호출해 앞뒤 공백을 잘라 냅니다. 역방향 반복자의 .base()는 가리키는 원소의 다음 위치를 돌려주므로, 마지막 비공백 문자 바로 뒤가 되어 std::string(start, end)의 끝으로 딱 맞습니다. 그다음 unique에 “둘 다 공백이면 같다”는 조건자를 줘서 연속된 공백을 첫 번째 하나만 남깁니다. 탭과 공백이 섞여 있으면 첫 번째 문자(탭일 수도 있음)가 남는다는 점, 문자열 전체가 공백이면 빈 문자열이 된다는 점도 의도한 동작인지 확인해 두세요.
제거 알고리즘 요약
- remove: 끝으로 이동 (크기 변경 안 됨)
- erase: 실제 제거 (크기 변경)
- erase-remove:
v.erase(remove(...), v.end()) - unique: 인접 중복 제거 (정렬 필요)
- C++20:
std::erase,std::erase_if
알고리즘별 동작과 복잡도
| 알고리즘 | 동작 | 시간복잡도 | 사용 시기 |
|---|---|---|---|
| remove | 값 제거 | O(n) | 특정 값 |
| remove_if | 조건 제거 | O(n) | 조건부 |
| unique | 중복 제거 | O(n) | 정렬 후 |
| erase | 실제 제거 | O(n) | remove 후 |
| list::remove | 직접 제거 | O(n) | list 전용 |
이어서 볼 STL 알고리즘
- C++ Algorithm Replace
- C++ Algorithm Copy
- C++ 정렬 알고리즘 구현과 비교: std::sort의 pdqsort, stable_sort, 병렬 정렬, 기수 정렬
같이 보면 좋은 글
- C++ Algorithm Copy
- C++ Algorithm Count
- C++ Algorithm Generate
- C++ STL 알고리즘 기초: sort·find·transform·accumulate 실전 활용
- C++ Algorithm Heap
- C++ replace·replace_if·replace_copy: 값 치환과 transform 중 무엇을 쓸까
- C++ reverse·rotate·reverse_copy
자주 묻는 질문 (FAQ)
Q. std::list에서도 erase-remove 관용구를 써야 하나요?
A. std::list는 멤버 함수 remove와 remove_if를 제공하므로 이쪽을 쓰는 것이 더 낫습니다. 멤버 함수는 노드 연결만 바꿔 원소를 실제로 제거하기 때문에 한 번에 끝나고, 값을 옮기는 비용도 없습니다. std::remove 알고리즘은 원소를 앞으로 당겨 덮어쓰는 방식이라 연속 메모리를 쓰는 vector·deque에 맞는 방식이며, C++20부터는 std::erase와 std::erase_if로 두 경우를 같은 문법으로 쓸 수 있습니다.