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
You've built a robust foundation in Data Structures and Algorithms. Now, it's time to apply that knowledge to the kinds of challenges you'll face in technical interviews at companies like FAANG (Facebook, Apple, Amazon, Netflix, Google) and other leading tech firms. These interviews aren't just about finding the correct answer; they're about demonstrating your problem-solving process, ability to optimize, communication skills, and handling edge cases.
In this chapter, we'll walk through several frequently asked interview problems. For each, we'll:
One of the most fundamental and frequently asked problems, often used as a warm-up or screening question.
Given an array of integers `nums` and an integer `target`, return indices of the two numbers such that they add up to `target`.
You may assume that each input would have exactly one solution, and you may not use the same element twice. You can return the answer in any order.
Example: `nums = [2, 7, 11, 15]`, `target = 9`
Output: `[0, 1]` (because `nums[0] + nums[1] == 2 + 7 == 9`)
Iterate through each number and for each number, iterate through the rest of the array to find its complement.
Time Complexity: $O(N^2)$ (due to nested loops)
Space Complexity: $O(1)$
We can optimize this to $O(N)$ time by using a hash map (or dictionary in Python, `unordered_map` in C++).
The idea is to iterate through the array once. For each number `num` at `index`:
// C++ Solution for Two Sum (Optimized)
#include <vector>
#include <unordered_map>
#include <iostream>
std::vector<int> twoSum(std::vector<int>& nums, int target) {
std::unordered_map<int, int> num_map; // Stores {number: index}
for (int i = 0; i < nums.size(); ++i) {
int complement = target - nums[i];
// Check if the complement exists in our map
if (num_map.count(complement)) {
// Found it! Return the indices
return {num_map[complement], i};
}
// If not found, add the current number to the map for future lookups
num_map[nums[i]] = i;
}
// According to problem statement, exactly one solution exists, so this line won't be reached
return {};
}
// Example Usage:
// int main() {
// std::vector<int> nums = {2, 7, 11, 15};
// int target = 9;
// std::vector<int> result = twoSum(nums, target);
// std::cout << "[" << result[0] << ", " << result[1] << "]" << std::endl; // Output: [0, 1]
// return 0;
// }
Time Complexity: $O(N)$ (single pass through array, hash map lookups/insertions average $O(1)$)
Space Complexity: $O(N)$ (for the hash map, storing up to $N$ elements in the worst case)
A common problem testing your understanding of stacks and string parsing.
Given a string `s` containing just the characters '(', ')', '{', '}', '[', ']', determine if the input string is valid.
A valid string must satisfy these rules:
The Last-In-First-Out (LIFO) nature of a stack is perfect for this problem. When we encounter an opening bracket, we push it onto the stack. When we encounter a closing bracket, we check the top of the stack.
// C++ Solution for Valid Parentheses
#include <string>
#include <stack>
#include <unordered_map>
#include <iostream>
bool isValid(std::string s) {
std::stack<char> st;
std::unordered_map<char, char> mappings = {
{')', '('},
{'}', '{'},
{']', '['}
};
for (char c : s) {
if (c == '(' || c == '{' || c == '[') {
st.push(c); // Push opening brackets
} else { // It's a closing bracket
if (st.empty()) { // No matching opening bracket
return false;
}
char top_element = st.top();
st.pop();
// Check if the popped opening bracket matches the current closing bracket
if (top_element != mappings[c]) {
return false;
}
}
}
// If the stack is empty, all brackets matched
return st.empty();
}
// Example Usage:
// int main() {
// std::cout << "()[]{}: " << (isValid("()[]{}") ? "true" : "false") << std::endl; // true
// std::cout << "([{}]): " << (isValid("([{}])") ? "true" : "false") << std::endl; // true
// std::cout << "({[)]}: " << (isValid("({[)]") ? "true" : "false") << std::endl; // false
// std::cout << "(: " << (isValid("(") ? "true" : "false") << std::endl; // false
// std::cout << "]: " << (isValid("]") ? "true" : "false") << std::endl; // false
// return 0;
// }
Time Complexity: $O(N)$ (single pass through the string)
Space Complexity: $O(N)$ (in the worst case, e.g., "((((()))))", the stack could store all `N/2` opening brackets)
A fundamental linked list problem that tests pointer manipulation.
Merge two sorted linked lists `list1` and `list2` into a single sorted list. The new list should be made by splicing together the nodes of the first two lists. Return the head of the merged linked list.
Example: `list1 = [1,2,4]`, `list2 = [1,3,4]`
Output: `[1,1,2,3,4,4]`
This is typically preferred over recursion for linked list problems in interviews to avoid potential stack overflow with very long lists.
// C++ Solution for Merge Two Sorted Lists (Iterative)
#include <cstddef> // For NULL
#include <iostream>
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
// Create a dummy node to simplify handling the head of the merged list
ListNode dummy_head(0);
ListNode* current = &dummy_head; // Pointer to the last node of the merged list
while (list1 != nullptr && list2 != nullptr) {
if (list1->val <= list2->val) {
current->next = list1;
list1 = list1->next;
} else {
current->next = list2;
list2 = list2->next;
}
current = current->next; // Move current pointer forward
}
// Attach the remaining part of the non-null list
if (list1 != nullptr) {
current->next = list1;
} else if (list2 != nullptr) {
current->next = list2;
}
return dummy_head.next; // The head of the actual merged list
}
// Helper to print list (for testing)
// void printList(ListNode* head) {
// while (head) {
// std::cout << head->val << " -> ";
// head = head->next;
// }
// std::cout << "NULL" << std::endl;
// }
// Example Usage:
// int main() {
// ListNode* l1 = new ListNode(1, new ListNode(2, new ListNode(4))); // 1->2->4
// ListNode* l2 = new ListNode(1, new ListNode(3, new ListNode(4))); // 1->3->4
// ListNode* merged_head = mergeTwoLists(l1, l2);
// printList(merged_head); // Output: 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> NULL
// // Don't forget to free memory in real applications!
// return 0;
// }
Time Complexity: $O(M + N)$, where $M$ and $N$ are the lengths of `list1` and `list2`. We iterate through each node once.
Space Complexity: $O(1)$ (constant extra space, excluding the output list, as we are only manipulating pointers).
A challenging array problem that requires clever use of prefix/suffix products without division.
Given an integer array `nums`, return an array `answer` such that `answer[i]` is equal to the product of all the elements of `nums` except `nums[i]`.
The problem guarantees that the product of elements of any prefix or suffix of `nums` is within a 32-bit integer. You must write an algorithm that runs in $O(N)$ time and **without using the division operator**.
Example: `nums = [1, 2, 3, 4]`
Output: `[24, 12, 8, 6]` (e.g., `answer[0]` is $2*3*4=24$)
The key insight is that `answer[i]` is `(product of elements to the left of i) * (product of elements to the right of i)`. We can calculate these two products in separate passes.
// C++ Solution for Product of Array Except Self
#include <vector>
#include <iostream>
#include <numeric> // For std::accumulate (not used in final, but helpful for initial thoughts)
std::vector<int> productExceptSelf(std::vector<int>& nums) {
int n = nums.size();
std::vector<int> answer(n);
// First pass: Calculate prefix products
// answer[i] will contain product of all elements to the left of i
// answer[0] will be 1 (as there are no elements to the left of the first element)
answer[0] = 1;
for (int i = 1; i < n; ++i) {
answer[i] = answer[i-1] * nums[i-1];
}
// Second pass: Calculate suffix products and combine with prefix products
// right_product will store product of all elements to the right of current element
int right_product = 1;
for (int i = n - 1; i >= 0; --i) {
answer[i] = answer[i] * right_product; // Combine prefix and suffix products
right_product = right_product * nums[i]; // Update right_product for next iteration
}
return answer;
}
// Example Usage:
// int main() {
// std::vector<int> nums = {1, 2, 3, 4};
// std::vector<int> result = productExceptSelf(nums);
// std::cout << "[";
// for (int i = 0; i < result.size(); ++i) {
// std::cout << result[i] << (i == result.size() - 1 ? "" : ", ");
// }
// std::cout << "]" << std::endl; // Output: [24, 12, 8, 6]
// return 0;
// }
Time Complexity: $O(N)$ (two passes through the array)
Space Complexity: $O(1)$ (excluding the output array, which is required by the problem)
These seemingly abstract problems have direct analogues in real-world systems:
We've tackled interview-style problems. Our next chapter will broaden our horizons to Competitive Coding Patterns. This will involve recognizing more specialized problem types and advanced techniques that are common in coding contests, helping you solve problems faster and more efficiently under pressure.