βββ README.md
/README.md:
--------------------------------------------------------------------------------
1 | # 59 Essential Searching Algorithms Interview Questions in 2025
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | #### You can also find all 59 answers here π [Devinterview.io - Searching Algorithms](https://devinterview.io/questions/data-structures-and-algorithms/searching-algorithms-interview-questions)
11 |
12 |
13 |
14 | ## 1. What is _Linear Search_ (Sequential Search)?
15 |
16 | **Linear Search**, also known as **Sequential Search**, is a straightforward and easy-to-understand search algorithm that works well for **small** and **unordered** datasets. However it might be inefficient for larger datasets.
17 |
18 | ### Steps of Linear Search
19 |
20 | 1. **Initialization**: Set the start of the list as the current position.
21 | 2. **Comparison/Match**: Compare the current element to the target. If they match, you've found your element.
22 | 3. **Iteration**: Move to the next element in the list and repeat the Comparison/Match step. If no match is found and there are no more elements, the search concludes.
23 |
24 | ### Complexity Analysis
25 |
26 | - **Time Complexity**: $O(n)$ In the worst-case scenario, when the target element is either the last element or not in the array, the algorithm will make $n$ comparisons, where $n$ is the length of the array.
27 |
28 | - **Space Complexity**: $O(1)$ Uses constant extra space
29 |
30 | ### Code Example: Linear Search
31 |
32 | Here is the Python code:
33 |
34 | ```python
35 | def linear_search(arr, target):
36 | for i, val in enumerate(arr):
37 | if val == target:
38 | return i # Found, return index
39 | return -1 # Not found, return -1
40 |
41 | # Example usage
42 | my_list = [4, 2, 6, 8, 10, 1]
43 | target_value = 8
44 | result_index = linear_search(my_list, target_value)
45 | print(f"Target value found at index: {result_index}")
46 | ```
47 |
48 | ### Practical Applications
49 |
50 | 1. **One-time search**: When you're searching just once, more complex algorithms like binary search might be overkill because of their setup needs.
51 | 2. **Memory efficiency**: Without the need for extra data structures, linear search is a fit for environments with memory limitations.
52 | 3. **Small datasets**: For limited data, a linear search is often speedy enough. Even for sorted data, it might outpace more advanced search methods.
53 | 4. **Dynamic unsorted data**: For datasets that are continuously updated and unsorted, maintaining order for other search methods can be counterproductive.
54 | 5. **Database queries**: In real-world databases, an SQL query lacking the right index may resort to linear search, emphasizing the importance of proper indexing.
55 |
56 |
57 | ## 2. Explain what is _Binary Search_.
58 |
59 | **Binary Search** is a highly efficient searching algorithm often implemented for **already-sorted lists**, reducing the search space by 50% at every step. This method is especially useful when the list won't be modified frequently.
60 |
61 | ### Binary Search Algorithm
62 |
63 | 1. **Initialize**: Point to the start (`low`) and end (`high`) of the list.
64 | 2. **Compare and Divide**: Calculate the midpoint (`mid`), compare the target with the element at `mid`, and adjust the search range accordingly.
65 | 3. **Repeat**: Repeat the above step until the target is found or the search range is exhausted.
66 |
67 | ### Visual Representation
68 |
69 | 
70 |
71 | ### Complexity Analysis
72 |
73 | - **Time Complexity**: $O(\log n)$. Each iteration reduces the search space in half, resulting in a logarithmic time complexity.
74 |
75 | - **Space Complexity**: $O(1)$. Constant space is required as the algorithm operates on the original list and uses only a few extra variables.
76 |
77 | ### Code Example: Binary Search
78 |
79 | Here is the Python code:
80 |
81 | ```python
82 | def binary_search(arr, target):
83 | low, high = 0, len(arr) - 1
84 |
85 | while low <= high:
86 | mid = (low + high) // 2 # Calculate the midpoint
87 |
88 | if arr[mid] == target: # Found the target
89 | return mid
90 | elif arr[mid] < target: # Target is in the upper half
91 | low = mid + 1
92 | else: # Target is in the lower half
93 | high = mid - 1
94 |
95 | return -1 # Target not found
96 | ```
97 |
98 | ### Binary Search Variations
99 |
100 | - **Iterative**: As shown in the code example above, this method uses loops to repeatedly divide the search range.
101 | - **Recursive**: Can be useful in certain scenarios, with the added benefit of being more concise but potentially less efficient due to the overhead of function calls and stack usage.
102 |
103 | ### Practical Applications
104 |
105 | 1. **Databases**: Enhances query performance in sorted databases and improves the efficiency of sorted indexes.
106 | 2. **Search Engines**: Quickly retrieves results from vast directories of keywords or URLs.
107 | 3. **Version Control**: Tools like 'Git' pinpoint code changes or bugs using binary search.
108 | 4. **Optimization Problems**: Useful in algorithmic challenges to optimize solutions or verify conditions.
109 | 5. **Real-time Systems**: Critical for timely operations in areas like air traffic control or automated trading.
110 | 6. **Programming Libraries**: Commonly used in standard libraries for search and sort functions.
111 |
112 |
113 | ## 3. Compare _Binary Search_ vs. _Linear Search_.
114 |
115 | **Binary Search** and **Linear Search** are two fundamental algorithms for locating data in an array. Let's look at their differences.
116 |
117 | ### Key Concepts
118 |
119 | - **Linear Search**: This method sequentially scans the array from the start to the end, making it suitable for both sorted and unsorted data.
120 |
121 | - **Binary Search**: This method requires a sorted array and uses a divide-and-conquer strategy, halving the search space with each iteration.
122 |
123 | ### Visual Representation
124 |
125 | 
126 |
127 | ### Key Distinctions
128 |
129 | #### Data Requirements
130 |
131 | - **Linear Search**: Suitable for both sorted and unsorted datasets.
132 | - **Binary Search**: Requires the dataset to be sorted.
133 |
134 | #### Data Structure Suitability
135 |
136 | - **Linear Search**: Universal, can be applied to any data structure.
137 | - **Binary Search**: Most efficient with sequential access data structures like arrays or lists.
138 |
139 | #### Comparison Types
140 |
141 | - **Linear Search**: Examines each element sequentially for a match.
142 | - **Binary Search**: Utilizes ordered comparisons to continually halve the search space.
143 |
144 | #### Search Approach
145 |
146 | - **Linear Search**: Sequential, it checks every element until a match is found.
147 | - **Binary Search**: Divide-and-Conquer, it splits the search space in half repeatedly until the element is found or the space is exhausted.
148 |
149 | ### Complexity Analysis
150 |
151 | - **Time Complexity**:
152 | - Linear Search: $O(n)$
153 | - Binary Search: $O(\log n)$
154 |
155 | - **Space Complexity**:
156 | - Linear Search: $O(1)$
157 | - Binary Search: $O(1)$ for iterative and $O(\log n)$ for recursive implementations.
158 |
159 | ### Key Takeaways
160 |
161 | - **Linear Search**: It's a straightforward technique, ideal for small datasets or datasets that frequently change.
162 | - **Binary Search**: Highly efficient for sorted datasets, especially when the data doesn't change often, making it optimal for sizable, stable datasets.
163 |
164 |
165 | ## 4. What characteristics of the data determine the choice of a _searching algorithm_?
166 |
167 | The ideal searching algorithm varies based on a number of data-specific factors. Let's take a look at those factors:
168 |
169 | ### Data Characteristics
170 |
171 | 1. **Size**: For small, unsorted datasets, linear search can be efficient, while for larger datasets, binary search on sorted data brings better performance.
172 |
173 | 2. **Arrangement**: Sorted or unsorted? The type of arrangement can be critical in deciding the appropriate searching method.
174 |
175 | 3. **Repeatability of Elements**: When elements are non-repetitive or unique, binary search is a more fitting choice, as it necessitates sorted uniqueness.
176 |
177 | 4. **Physical Layout**: In some circumstances, data systems like databases are optimized for specific methods, influencing the algorithmical choice.
178 |
179 | 5. **Persistence**: When datasets are subject to frequent updates, the choice of searching algorithm can impact performance.
180 |
181 | 6. **Hierarchy and Relationships**: Certain data structures like trees or graphs possess a natural hierarchy, calling for specialized search algorithms.
182 |
183 | 7. **Data Integrity**: For some databases, where data consistency is a top priority, algorithms supporting atomic transactions are essential.
184 |
185 | 8. **Memory Structure**: For linked lists or arrays, memory layout shortcuts can steer an algorithmic choice.
186 |
187 | 9. **Metric Type**: If using multidimensional data, the chosen metric (like Hamming or Manhattan distance) can direct the search method employed.
188 |
189 | 10. **Homogeneity**: The uniformity of data types can influence the algorithm choice. For heterogeneous data, specialized methods like hybrid search are considered.
190 |
191 | ### Behavioral Considerations
192 |
193 | 1. **Access Patterns**: If the data is frequently accessed in a specific manner, caching strategies can influence the selection of the searching algorithm.
194 |
195 | 2. **Search Frequency**: If the dataset undergoes numerous consecutive searches, pre-processing steps like sorting can prove advantageous.
196 |
197 | 3. **Search Type**: Depending on whether an exact or approximate match is sought, like in fuzzy matching, different algorithms might be applicable.
198 |
199 | 4. **Performance Requirements**: If real-time performance is essential, algorithms with stable, short, and predictable time complexities are preferred.
200 |
201 | 5. **Space Efficiency**: The amount of memory the algorithm consumes can be a decisive factor, especially in resource-limited environments.
202 |
203 |
204 | ## 5. Name some _Optimization Techniques_ for _Linear Search_.
205 |
206 | **Linear Search** is a simple searching technique. However, its efficiency can decrease with larger datasets. Let's explore techniques to enhance its performance.
207 |
208 | ### Linear Search Optimization Techniques
209 |
210 | #### Early Exit
211 |
212 | - **Description**: Stop the search as soon as the target is found.
213 | - **How-To**: Use a `break` or `return` statement to exit the loop upon finding the target.
214 |
215 | ```python
216 | def linear_search_early_exit(lst, target):
217 | for item in lst:
218 | if item == target:
219 | return True
220 | return False
221 | ```
222 |
223 | #### Bidirectional Search
224 |
225 | - **Description**: Search from both ends of the list simultaneously.
226 | - **How-To**: Use two pointers, one starting at the beginning and the other at the end. Move them towards each other, until they meet or find the target.
227 |
228 | ```python
229 | def bidirectional_search(lst, target):
230 | left, right = 0, len(lst) - 1
231 | while left <= right:
232 | if lst[left] == target or lst[right] == target:
233 | return True
234 | left += 1
235 | right -= 1
236 | return False
237 | ```
238 |
239 | #### Skip Search & Search by Blocks
240 |
241 | - **Description**: Bypass certain elements to reduce search time.
242 | - **How-To**: In sorted lists, skip sections based on element values or check every $n$th element.
243 |
244 | ```python
245 | def skip_search(lst, target, n=3):
246 | length = len(lst)
247 | for i in range(0, length, n):
248 | if lst[i] == target:
249 | return True
250 | elif lst[i] > target:
251 | return target in lst[i-n:i]
252 | return False
253 | ```
254 |
255 | #### Positional Adjustments
256 |
257 | - **Description**: Reorder the list based on element access frequency.
258 | - **Techniques**:
259 | - Transposition: Move frequently accessed elements forward.
260 | - Move to Front (MTF): Place high-frequency items at the start.
261 | - Move to End (MTE): Shift rarely accessed items towards the end.
262 |
263 | ```python
264 | def mtf(lst, target):
265 | for idx, item in enumerate(lst):
266 | if item == target:
267 | lst.pop(idx)
268 | lst.insert(0, item)
269 | return True
270 | return False
271 | ```
272 |
273 | #### Indexing
274 |
275 | - **Description**: Build an index for faster lookups.
276 | - **How-To**: Pre-process the list to create an index linking elements to positions.
277 |
278 | ```python
279 | def create_index(lst):
280 | return {item: idx for idx, item in enumerate(lst)}
281 |
282 | index = create_index(my_list)
283 | def search_with_index(index, target):
284 | return target in index
285 | ```
286 |
287 | #### Parallelism
288 |
289 | - **Description**: Exploit multi-core systems to speed up the search.
290 | - **How-To**: Split the list into chunks and search each using multiple cores.
291 |
292 | ```python
293 | from concurrent.futures import ProcessPoolExecutor
294 |
295 | def search_chunk(chunk, target):
296 | return target in chunk
297 |
298 | def parallel_search(lst, target):
299 | chunks = [lst[i::4] for i in range(4)]
300 | with ProcessPoolExecutor() as executor:
301 | results = list(executor.map(search_chunk, chunks, [target]*4))
302 | return any(results)
303 | ```
304 |
305 |
306 | ## 6. What is _Sentinel Search_?
307 |
308 | **Sentinel Search**, sometimes referred to as **Move-to-Front Search** or **Self-Organizing Search**, is a variation of the linear search that optimizes search performance for frequently accessed elements.
309 |
310 | ### Core Principle
311 |
312 | Sentinel Search improves efficiency by:
313 |
314 | - Adding a "**sentinel**" to the list to guarantee a stopping point, removing the need for checking array bounds.
315 | - Rearranging elements by moving found items closer to the front over time, making future searches for the same items faster.
316 |
317 | ### Sentinel Search Algorithm
318 |
319 | 1. **Append Sentinel**:
320 | - Add the target item as a sentinel at the list's end. This ensures the search always stops.
321 |
322 | 2. **Execute Search**:
323 | - Start from the first item and progress until the target or sentinel is reached.
324 | - If the target is found before reaching the sentinel, optionally move it one position closer to the list's front to improve subsequent searches.
325 |
326 | ### Complexity Analysis
327 |
328 | - **Time Complexity**: Remains $O(n)$, reflecting the potential need to scan the entire list.
329 | - **Space Complexity**: $O(1)$, indicating constant extra space use.
330 |
331 | ### Code Example: Sentinel Search
332 |
333 | Here is the Python code:
334 |
335 | ```python
336 | def sentinel_search(arr, target):
337 | # Append the sentinel
338 | arr.append(target)
339 | i = 0
340 |
341 | # Execute the search
342 | while arr[i] != target:
343 | i += 1
344 |
345 | # If target is found (before sentinel), move it closer to the front
346 | if i < len(arr) - 1:
347 | arr[i], arr[max(i - 1, 0)] = arr[max(i - 1, 0)], arr[i]
348 | return i
349 |
350 | # If only the sentinel is reached, the target is not in the list
351 | return -1
352 |
353 | # Demonstration
354 | arr = [1, 2, 3, 4, 5]
355 | target = 3
356 | index = sentinel_search(arr, target)
357 | print(f"Target found at index {index}") # Expected Output: Target found at index 1
358 | ```
359 |
360 |
361 | ## 7. What are the _Drawbacks_ of _Sentinel Search_?
362 |
363 | The **Sentinel Linear Search** slightly improves efficiency over the standard method by reducing average comparisons from roughly $2n$ to $n + 2$ using a sentinel value.
364 |
365 | However, both approaches share an $O(n)$ worst-case time complexity. Despite its advantages, the Sentinel Search has several drawbacks.
366 |
367 | ### Drawbacks of Sentinel Search
368 |
369 | 1. **Data Safety Concerns**: Using a sentinel can introduce risks, especially when dealing with shared or read-only arrays. It might inadvertently alter data or cause access violations.
370 |
371 | 2. **List Integrity**: Sentinel search necessitates modifying the list to insert the sentinel. This alteration can be undesirable in scenarios where preserving the original list is crucial.
372 |
373 | 3. **Limited Applicability**: The sentinel approach is suitable for data structures that support expansion, such as dynamic arrays or linked lists. For static arrays, which don't allow resizing, this method isn't feasible.
374 |
375 | 4. **Compiler Variability**: Some modern compilers optimize boundary checks, which could reduce or negate the efficiency gains from using a sentinel.
376 |
377 | 5. **Sparse Data Inefficiency**: In cases where the sentinel's position gets frequently replaced by genuine data elements, the method can lead to many unnecessary checks, diminishing its effectiveness.
378 |
379 | 6. **Code Complexity vs. Efficiency**: The marginal efficiency boost from the sentinel method might not always justify the added complexity, especially when considering code readability and maintainability.
380 |
381 |
382 | ## 8. How does the presence of _duplicates_ affect the performance of _Linear Search_?
383 |
384 | When dealing with **duplicates** in the data set, a **Linear Search** algorithm will generally **keep searching** even after finding a match. In such instances, processing time might be impacted, and the overall **efficiency** can vary based on different factors, such as the specific structure of the data.
385 |
386 | ### Complexity Analysis
387 |
388 | - **Time Complexity**: $O(n)$ - This is because, in the worst-case scenario, every element in the list needs to be checked.
389 |
390 | - **Space Complexity**: $O(1)$ - Linear search Algorithm uses only a constant amount of extra space.
391 |
392 | ### Code Example: Linear Search with Duplicates
393 |
394 | Here is the Python code:
395 |
396 | ```python
397 | def linear_search_with_duplicates(arr, target):
398 | for i, val in enumerate(arr):
399 | if val == target:
400 | return i # Returns the first occurrence found
401 | return -1
402 | ```
403 |
404 |
405 | ## 9. Implement an _Order-Agnostic Linear Search_ that works on _sorted_ and _unsorted arrays_.
406 |
407 | ### Problem Statement
408 |
409 | The **Order-Agnostic Linear Search** algorithm searches arrays that can either be **in ascending or descending order**. The goal is to find a specific target value.
410 |
411 | ### Solution
412 |
413 | The **Order-Agnostic Linear Search** is quite straightforward. Here's how it works:
414 |
415 | 1. Begin with the assumption that the array could be sorted in any order.
416 | 2. Perform a linear search from the beginning to the end of the array.
417 | 3. Check each element against the target value.
418 | 4. If an item matches the target, return the index.
419 | 5. If the end of the array is reached without finding the target, return -1.
420 |
421 | #### Complexity Analysis
422 |
423 | - **Time Complexity**: $O(N)$ - This is true for both the worst and average cases.
424 | - **Space Complexity**: $O(1)$ - The algorithm uses a fixed amount of space, irrespective of the array's size.
425 |
426 | #### Implementation
427 |
428 | Here is the Python code:
429 |
430 | ```python
431 | def order_agnostic_linear_search(arr, target):
432 | n = len(arr)
433 |
434 | # Handle the empty array case
435 | if n == 0:
436 | return -1
437 |
438 | # Determine the array's direction
439 | is_ascending = arr[0] < arr[n-1]
440 |
441 | # Perform linear search based on the array's direction
442 | for i in range(n):
443 | if (is_ascending and arr[i] == target) or (not is_ascending and arr[n-1-i] == target):
444 | return i if is_ascending else n-1-i
445 |
446 | # The target is not in the array
447 | return -1
448 | ```
449 |
450 |
451 | ## 10. Modify _Linear Search_ to perform on a _multi-dimensional array_.
452 |
453 | ### Problem Statement
454 |
455 | The task is to **adapt** the **Linear Search** algorithm so it can perform on a **multi-dimensional** array.
456 |
457 | ### Solution
458 |
459 | Performing a Linear Search on a multi-dimensional array involves systematically walking through its elements in a methodical manner, usually by using nested loops.
460 |
461 | Let's first consider an illustration:
462 |
463 | Suppose you have the following 3x3 grid of numbers:
464 |
465 | $$
466 | \begin{array}{ccc}
467 | 2 & 5 & 8 \\
468 | 3 & 6 & 9 \\
469 | 4 & 7 & 10
470 | \end{array}
471 | $$
472 |
473 | To search for the number 6:
474 |
475 | 1. Begin with the first row from left to right $(2, 5, 8)$.
476 | 2. Move to the second row $(3, 6, 9)$.
477 | 3. Here, you find the number 6 in the second position.
478 |
479 | The process can be codified to work with `n`-dimensional arrays, allowing you to perform a linear $O(n)$ search.
480 |
481 | #### Complexity Analysis
482 |
483 | - Time Complexity: $O(N)$ where $N$ is the total number of elements in the array.
484 | - Space Complexity: $O(1)$. No additional space is used beyond a few variables for bookkeeping.
485 |
486 | #### Implementation
487 |
488 | Here is the Python code for searching through a 2D array:
489 |
490 | ```python
491 | def linear_search_2d(arr, target):
492 | rows = len(arr)
493 | cols = len(arr[0])
494 |
495 | for r in range(rows):
496 | for c in range(cols):
497 | if arr[r][c] == target:
498 | return (r, c)
499 | return (-1, -1) # If the element is not found
500 |
501 | # Example usage
502 | arr = [
503 | [2, 5, 8],
504 | [3, 6, 9],
505 | [4, 7, 10]
506 | ]
507 | target = 6
508 | print(linear_search_2d(arr, target)) # Output: (1, 1)
509 | ```
510 |
511 | #### Algorithm Optimizations
512 |
513 | While the standard approach involves visiting every element, sorting the data beforehand can enable binary search in each row, resulting in a strategy resembling the **Binary Search algorithm**.
514 |
515 |
516 | ## 11. Explain why complexity of _Binary Search_ is _O(log n)_.
517 |
518 | The **Binary Search** algorithm is known for its efficiency, often completing in $O(\log n)$βalso known as **logarithmic**βtime.
519 |
520 | ### Mathematical Background
521 |
522 | To understand why $x = \log_2 N$ yields $O(\log n)$, consider the following:
523 |
524 | - $N = 2^x$: Each halving step $x$ corresponds to $N$ reductions by a factor of 2.
525 | - Taking the logarithm of both sides with base 2, we find $x = \log_2 N$, which is equivalent to $\log N$ in base-2 notation.
526 |
527 | Therefore, with each step, the algorithm roughly reduces the search space in **half**, leading to a **logarithmic time** complexity.
528 |
529 | ### Visual Representation
530 |
531 | 
532 |
533 |
534 | ## 12. Compare _Recursive_ vs. _Iterative Binary Search_.
535 |
536 | Both **Recursive** and **Iterative** Binary Search leverage the **divide-and-conquer** strategy to search through sorted data. Let's look at their differences in implementation.
537 |
538 | ### Complexity Comparison
539 |
540 | - **Time Complexity**: $O(\log n)$ for both iterative and recursive approaches, attributed to halving the search space each iteration.
541 | - **Space Complexity**:
542 | - Iterative: Uses constant $O(1)$ space, free from function call overhead.
543 | - Recursive: Typically $O(\log n)$ because of stack memory from function calls. This can be reduced to $O(1)$ with tail recursion, but support varies across compilers.
544 |
545 | ### Considerations
546 |
547 | - **Simplicity**: Iterative approaches are often more intuitive to implement.
548 | - **Memory**: Recursive methods might consume more memory due to their reliance on the function call stack.
549 | - **Compiler Dependency**: Tail recursion optimization isn't universally supported.
550 |
551 | ### Code Example: Iterative Binary Search
552 |
553 | Here is the Python code:
554 |
555 | ```python
556 | def binary_search_iterative(arr, target):
557 | low, high = 0, len(arr) - 1
558 | while low <= high:
559 | mid = (low + high) // 2
560 | if arr[mid] == target:
561 | return mid
562 | elif arr[mid] < target:
563 | low = mid + 1
564 | else:
565 | high = mid - 1
566 | return -1
567 |
568 | # Test
569 | array = [1, 3, 5, 7, 9, 11]
570 | print(binary_search_iterative(array, 7)) # Output: 3
571 | ```
572 |
573 | ### Code Example: Recursive Binary Search
574 |
575 | Here is the Python code:
576 |
577 | ```python
578 | def binary_search_recursive(arr, target, low=0, high=None):
579 | if high is None:
580 | high = len(arr) - 1
581 |
582 | if low > high:
583 | return -1
584 |
585 | mid = (low + high) // 2
586 |
587 | if arr[mid] == target:
588 | return mid
589 | elif arr[mid] < target:
590 | return binary_search_recursive(arr, target, mid + 1, high)
591 | else:
592 | return binary_search_recursive(arr, target, low, mid - 1)
593 |
594 | # Test
595 | array = [1, 3, 5, 7, 9, 11]
596 | print(binary_search_recursive(array, 7)) # Output: 3
597 | ```
598 |
599 |
600 | ## 13. In _Binary Search_, why _Round Down_ the midpoint instead of _Rounding Up_?
601 |
602 | Both **rounding up** and **rounding down** are acceptable in binary search. The essence of the method lies in the distribution of elements in relation to our guess:
603 |
604 | - For an odd number of remaining elements, there are $(n-1)/2$ elements on each side of our guess.
605 | - For an even number, there are $n/2$ elements on one side and $n/2-1$ on the other. The method of rounding determines which side has the smaller portion.
606 |
607 | Rounding consistently, especially rounding down, helps in avoiding **overlapping search ranges** and possible **infinite loops**. This ensures an even or near-even distribution of elements between the two halves, streamlining the search. This balance becomes particularly noteworthy when the total number of elements is **even**.
608 |
609 | ### Code Example: Rounding Down in Binary Search
610 |
611 | Here is the Python code:
612 |
613 | ```python
614 | def binary_search(arr, target):
615 | low, high = 0, len(arr) - 1
616 | while low <= high:
617 | mid = (low + high) // 2 # Rounding down
618 | if arr[mid] == target:
619 | return mid
620 | elif arr[mid] < target:
621 | low = mid + 1
622 | else:
623 | high = mid - 1
624 | return -1
625 | ```
626 |
627 |
628 | ## 14. Write a _Binary Search_ algorithm that finds the first occurrence of a given value.
629 |
630 | ### Problem Statement
631 |
632 | The goal is to use the **Binary Search** algorithm to find the **first occurrence** of a given value.
633 |
634 | ### Solution
635 |
636 | We can modify the standard binary search algorithm to find the `first` occurrence of the target value by continuing the search in the `left` partition, **even** when the midpoint element matches the target. By doing this, we ensure that we find the leftmost occurrence.
637 |
638 | Consider the array:
639 |
640 | $$
641 | $$
642 | &\text{Array:} & 2 & 4 & 10 & 10 & 10 & 18 & 20 \\
643 | &\text{Index:} & 0 & 1 & 2 & 3 & 4 & 5 & 6 \\
644 | $$
645 | $$
646 |
647 | #### Algorithm Steps
648 |
649 | 1. Initialize `start` and `end` pointers. Perform the usual binary search, calculating the `mid` point.
650 | 2. Evaluate both left and right subarrays:
651 | - If `mid`'s value is less than the target, explore the **right subarray**.
652 | - If `mid`'s value is greater than or equal to the target, explore the **left subarray**.
653 | 3. Keep track of the **last successful** iteration (`result`). This denotes the last position where the target was found, hence updating the possible earliest occurrence.
654 | 4. Repeat steps 1-3 until `start` crosses or equals `end`.
655 |
656 | #### Complexity Analysis
657 |
658 | - **Time Complexity**: $O(\log n)$ - The search space halves with each iteration.
659 | - **Space Complexity**: $O(1)$ - It is a constant as we are only using a few variables.
660 |
661 | #### Implementation
662 |
663 | Here is the Python code:
664 |
665 | ```python
666 | def first_occurrence(arr, target):
667 | start, end = 0, len(arr) - 1
668 | result = -1
669 |
670 | while start <= end:
671 | mid = start + (end - start) // 2
672 |
673 | if arr[mid] == target:
674 | result = mid
675 | end = mid - 1
676 | elif arr[mid] < target:
677 | start = mid + 1
678 | else:
679 | end = mid - 1
680 |
681 | return result
682 |
683 | # Example
684 | arr = [2, 4, 10, 10, 10, 18, 20]
685 | target = 10
686 | print("First occurrence of", target, "is at index", first_occurrence(arr, target))
687 | ```
688 |
689 |
690 | ## 15. How would you apply _Binary Search_ to an array of objects sorted by a specific key?
691 |
692 | Let's explore how **Binary Search** can be optimized for sorted arrays of objects.
693 |
694 | ### Core Concepts
695 |
696 | **Binary Search** works by repeatedly dividing the search range in half, based on the comparison of a target value with the middle element of the array.
697 |
698 | For using Binary Search on sorted arrays of objects, the **specific key** according to which objects are sorted must also be considered when comparing target and middle values.
699 |
700 | For example, if $\text{Key}(\text{obj}_1) < \text{Key}(\text{obj}_2)$ is true, then $\text{obj}_1$ comes before $\text{obj}_2$ in the sorted array according to the key.
701 |
702 | ### Algorithm Steps
703 |
704 | 1. **Initialize** two pointers, `start` and `end`, for the search range. Set them to the start and end of the array initially.
705 |
706 | 2. **Middle Value**: Calculate the index of the middle element. Then, retrieve the key of the middle object.
707 |
708 | 3. **Compare with Target**: Compare the key of the middle object with the target key. Based on the comparison, adjust the range pointers:
709 | - If the key of the middle object is equal to the target key, you've found the object. End the search.
710 | - If the key of the middle object is smaller than the target key, move the `start` pointer to the next position after the middle.
711 | - If the key of the middle object is larger than the target key, move the `end` pointer to the position just before the middle.
712 |
713 | 4. **Re-Evaluate Range**: After adjusting the range pointers, check if the range is valid. If so, repeat the process with the updated range. Otherwise, the search ends.
714 |
715 | 5. **Output**: Return the index of the found object if it exists in the array. If not found, return a flag indicating absence.
716 |
717 | ### Code Example: Binary Search on Objects
718 |
719 | Here is the Python code:
720 |
721 | ```python
722 | def binary_search_on_objects(arr, target_key):
723 | start, end = 0, len(arr) - 1
724 |
725 | while start <= end:
726 | mid = (start + end) // 2
727 | mid_obj = arr[mid]
728 | mid_key = getKey(mid_obj)
729 |
730 | if mid_key == target_key:
731 | return mid # Found the target at index mid
732 | elif mid_key < target_key:
733 | start = mid + 1 # Move start past mid
734 | else:
735 | end = mid - 1 # Move end before mid
736 |
737 | return -1 # Target not found
738 | ```
739 |
740 | ### Complexities
741 |
742 | - **Time Complexity**: $O(\log n)$ where $n$ is the number of objects in the array.
743 |
744 | - **Space Complexity**: $O(1)$, as the algorithm is using a constant amount of extra space.
745 |
746 |
747 |
748 |
749 | #### Explore all 59 answers here π [Devinterview.io - Searching Algorithms](https://devinterview.io/questions/data-structures-and-algorithms/searching-algorithms-interview-questions)
750 |
751 |
752 |
753 |
754 |
755 |
756 |
757 |
758 |
--------------------------------------------------------------------------------