C++ programming
vector elements
pair search
algorithm optimization
data structures

Find if vector contains pair with second element equal to X

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Checking whether a std::vector of pairs contains an element whose second value equals a target is a common task in C++ codebases. The best solution depends on how often you query and how often data changes. This article covers practical approaches, from simple one-pass checks to indexed lookup structures, with clear tradeoffs.

Start with a Clear One-Pass Check

If you only need to query occasionally, a linear scan is usually the right answer. It is simple, readable, and avoids premature indexing complexity.

cpp
1#include <algorithm>
2#include <iostream>
3#include <utility>
4#include <vector>
5
6bool contains_second_equal(
7    const std::vector<std::pair<int, int>>& items,
8    int target
9) {
10    return std::find_if(items.begin(), items.end(), [target](const auto& p) {
11        return p.second == target;
12    }) != items.end();
13}
14
15int main() {
16    std::vector<std::pair<int, int>> items{{1, 10}, {2, 20}, {3, 30}};
17    std::cout << std::boolalpha << contains_second_equal(items, 20) << "\n";
18    std::cout << std::boolalpha << contains_second_equal(items, 99) << "\n";
19}

Complexity is O(n) and memory overhead is effectively zero.

Returning the Match Instead of a Boolean

In real programs, you often need the matching pair, not only presence. Return an iterator or optional value so callers can inspect the full item without another scan.

cpp
1#include <algorithm>
2#include <optional>
3#include <utility>
4#include <vector>
5
6std::optional<std::pair<int, int>> find_by_second(
7    const std::vector<std::pair<int, int>>& items,
8    int target
9) {
10    auto it = std::find_if(items.begin(), items.end(), [target](const auto& p) {
11        return p.second == target;
12    });
13
14    if (it == items.end()) {
15        return std::nullopt;
16    }
17    return *it;
18}

This keeps API intent explicit and avoids duplicate work.

Fast Repeated Lookups with an Index

When you perform many queries against mostly static data, build an index keyed by the second element. An unordered_set is enough for membership checks.

cpp
1#include <unordered_set>
2#include <utility>
3#include <vector>
4
5std::unordered_set<int> build_second_index(
6    const std::vector<std::pair<int, int>>& items
7) {
8    std::unordered_set<int> index;
9    index.reserve(items.size());
10
11    for (const auto& p : items) {
12        index.insert(p.second);
13    }
14    return index;
15}
16
17// Usage:
18// auto index = build_second_index(items);
19// bool exists = index.contains(target);  // C++20

Build cost is O(n). Each average lookup is O(1). This is a good trade when query count is high.

Handling Duplicates Correctly

If several pairs may share the same second value and you need all matches, use a map from second value to a vector of pairs.

cpp
1#include <unordered_map>
2#include <utility>
3#include <vector>
4
5using Pair = std::pair<int, int>;
6using BucketMap = std::unordered_map<int, std::vector<Pair>>;
7
8BucketMap group_by_second(const std::vector<Pair>& items) {
9    BucketMap grouped;
10    for (const auto& p : items) {
11        grouped[p.second].push_back(p);
12    }
13    return grouped;
14}
15
16// auto grouped = group_by_second(items);
17// auto it = grouped.find(target);
18// if (it != grouped.end()) { /* it->second has all matches */ }

This preserves multiplicity, which a plain set cannot.

For read-heavy workflows where updates are rare, sorting by second value and using binary search can be memory-efficient.

cpp
1#include <algorithm>
2#include <iostream>
3#include <utility>
4#include <vector>
5
6int main() {
7    std::vector<std::pair<int, int>> items{{8, 40}, {2, 10}, {7, 30}, {9, 20}};
8
9    std::sort(items.begin(), items.end(), [](const auto& a, const auto& b) {
10        return a.second < b.second;
11    });
12
13    int target = 30;
14    auto it = std::lower_bound(
15        items.begin(), items.end(), target,
16        [](const auto& p, int value) { return p.second < value; }
17    );
18
19    bool exists = (it != items.end() && it->second == target);
20    std::cout << std::boolalpha << exists << "\n";
21}

Lookups become O(log n), and you avoid maintaining a separate hash index.

Choosing the Right Strategy

A simple decision guide:

  • one or few queries: linear scan
  • many queries, few updates: index or sorted vector
  • many queries with duplicate retrieval: grouped map
  • heavy updates and queries: consider a dedicated container design

Measure on real data before optimizing. For small vectors, linear scan often wins due to cache locality and low overhead.

Testing Recommendations

Write focused tests around behavior rather than algorithm choice:

  • empty vector should return false
  • one matching element should return true
  • duplicate second values should return all expected entries for grouped lookup
  • negative and boundary numeric values should behave correctly

This lets you change implementation later without breaking contract.

Common Pitfalls

  • Building a complex index for a one-time lookup.
  • Forgetting to rebuild or update the index after mutating the vector.
  • Using binary search without sorting by the second element.
  • Returning references to elements from containers that may reallocate.
  • Ignoring duplicate semantics and accidentally dropping data.

Summary

  • 'std::find_if is the best default for occasional checks.'
  • Return iterators or optionals when callers need matched values.
  • Use hash-based indexing for high query volume.
  • Use grouped structures when duplicates matter.
  • Keep complexity aligned with actual workload, then validate with tests.

Course illustration
Course illustration

All Rights Reserved.