This course is designed to help learners master the core concepts of Data Structures and Algorithms (DSA) using the C programming language. Starting from the basics and advancing to complex topics like graphs, dynamic programming, and memory optimization, this course is ideal for students, job seekers, and aspiring developers. You’ll learn how to structure and manipulate data efficiently, solve real-world coding problems, and prepare for technical interviews at top companies. The content is structured step-by-step, combining theory with hands-on coding examples and practice problems to reinforce understanding. Whether you're preparing for university exams, campus placements, or competitive programming, this course provides a strong foundation in logic building, code efficiency, and problem-solving using C. Key Highlights: Covers all major DSA topics from beginner to advanced level 100+ coding examples with explanations Focus on time and space complexity optimization Designed for coding interviews, competitive exams, and CS fundamentals
Competitive programming is a sport where you write code to solve algorithmic problems as efficiently as possible, often within strict time and memory limits. Unlike typical interview questions, competitive problems can sometimes require more specialized algorithms, clever observations, or advanced optimizations. However, a significant portion of competitive programming success comes from recognizing recurring **patterns** and applying established algorithmic templates.
This chapter will introduce you to several powerful and widely applicable competitive coding patterns. Learning these patterns will help you:
A technique commonly used for problems involving sorted arrays or sequences. Two pointers, usually `left` and `right`, move towards each other or in the same direction to solve the problem efficiently.
// Conceptual C++: Two Pointers for Sorted Array Sum
bool findPairSum(const std::vector<int>& arr, int K) {
int left = 0;
int right = arr.size() - 1;
while (left < right) {
int current_sum = arr[left] + arr[right];
if (current_sum == K) {
return true;
} else if (current_sum < K) {
left++; // Need a larger sum, move left pointer right
} else {
right--; // Need a smaller sum, move right pointer left
}
}
return false;
}
Time Complexity: $O(N)$ This pattern is used to solve problems on arrays or strings that require finding a subsegment (or "window") that satisfies a certain condition. The window expands and contracts.
// Conceptual C++: Sliding Window
int longestSubstringWithoutRepeatingChars(const std::string& s) {
std::unordered_map<char, int> char_map; // Stores char:last_seen_index
int max_len = 0;
int window_start = 0;
for (int window_end = 0; window_end < s.length(); ++window_end) {
char right_char = s[window_end];
if (char_map.count(right_char)) {
// If char seen before, move window_start past its last occurrence
window_start = std::max(window_start, char_map[right_char] + 1);
}
char_map[right_char] = window_end; // Update char's last seen index
max_len = std::max(max_len, window_end - window_start + 1);
}
return max_len;
}
Time Complexity: $O(N)$ This powerful pattern is applied when the problem asks for the minimum possible maximum value or the maximum possible minimum value. The key is that the "answer" space exhibits a monotonic property.
// Conceptual C++: Binary Search on Answer
// `check(time)` function would determine if all tasks can be completed within `time`
// (e.g., by summing up (task_duration / time) for each task)
int binarySearchOnAnswer(int low_bound, int high_bound, ...) {
int ans = high_bound; // Or -1 depending on problem
int left = low_bound;
int right = high_bound;
while (left <= right) {
int mid = left + (right - left) / 2;
if (check(mid)) { // If 'mid' is a possible answer (or better than current best)
ans = mid;
right = mid - 1; // Try for an even smaller answer
} else {
left = mid + 1; // 'mid' is too small, need a larger answer
}
}
return ans;
}
Time Complexity: $O(\log(\text{Range}) \cdot \text{CostOfCheckFunction})$ Beyond basic graph traversal, BFS and DFS are fundamental for exploring connected components, finding paths, detecting cycles, and navigating state spaces.
// Conceptual C++: DFS for Number of Islands
void dfs(std::vector<std::vector<char>>& grid, int r, int c) {
int R = grid.size();
int C = grid[0].size();
if (r < 0 || r >= R || c < 0 || c >= C || grid[r][c] == '0') {
return; // Out of bounds or not land
}
grid[r][c] = '0'; // Mark as visited
dfs(grid, r + 1, c);
dfs(grid, r - 1, c);
dfs(grid, r, c + 1);
dfs(grid, r, c - 1);
}
int numIslands(std::vector<std::vector<char>>& grid) {
int num_islands = 0;
for (int r = 0; r < grid.size(); ++r) {
for (int c = 0; c < grid[0].size(); ++c) {
if (grid[r][c] == '1') {
num_islands++;
dfs(grid, r, c); // Explore this island
}
}
}
return num_islands;
}
Time Complexity: $O(V+E)$ or $O(Rows \cdot Cols)$ for grids (each cell visited at most once) Making the locally optimal choice at each step, with the hope that this choice will lead to a globally optimal solution. Greedy strategies often simplify problems but require careful proof of correctness.
// Conceptual C++: Greedy for Activity Selection
// Activities are sorted by finish time.
int maxActivities(std::vector<std::pair<int, int>>& activities) { // {start_time, finish_time}
if (activities.empty()) return 0;
std::sort(activities.begin(), activities.end(),
[](const auto& a, const auto& b) { return a.second < b.second; });
int count = 1;
int last_finish_time = activities[0].second;
for (size_t i = 1; i < activities.size(); ++i) {
if (activities[i].first >= last_finish_time) { // If current activity can be selected
count++;
last_finish_time = activities[i].second;
}
}
return count;
}
Time Complexity: $O(N \log N)$ (due to sorting, otherwise $O(N)$) Used to find all possible solutions or configurations by systematically trying all paths. It involves making a choice, exploring its consequences (recursively), and then undoing the choice (backtracking) to try other possibilities. Pruning involves cutting off branches that cannot lead to a valid solution.
// Conceptual C++: Backtracking for Subsets
void generateSubsets(const std::vector<int>& nums, int index,
std::vector<int>& current_subset,
std::vector<std::vector<int>>& all_subsets) {
// Base case: Add the current_subset to results
all_subsets.push_back(current_subset);
// Recursive step: Explore choices
for (int i = index; i < nums.size(); ++i) {
current_subset.push_back(nums[i]); // Choose
generateSubsets(nums, i + 1, current_subset, all_subsets); // Explore
current_subset.pop_back(); // Un-choose (backtrack)
}
}
std::vector<std::vector<int>> subsets(const std::vector<int>& nums) {
std::vector<std::vector<int>> all_subsets;
std::vector<int> current_subset;
generateSubsets(nums, 0, current_subset, all_subsets);
return all_subsets;
}
Time Complexity: $O(2^N \cdot N)$ (or $O(2^N)$ if copying subset is $O(1)$ amortized) We've dedicated a lot of time to DP, but it's such a fundamental pattern that it deserves mention here again in the context of competitive programming. Often, DP problems in contests might combine with other patterns (e.g., Bitmask DP, Tree DP).
While competitive coding focuses on abstract efficiency, these patterns underlie many real-world system designs:
Theoretical knowledge is powerful, but practical application solidifies understanding. Our next chapter will bridge the gap by guiding you through Building Mini Projects Using DSA. You'll learn how to integrate the data structures and algorithms you've learned into functional, menu-driven applications, providing a hands-on experience of their real-world utility.