└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # 59 Essential Searching Algorithms Interview Questions in 2025 2 | 3 |
4 |

5 | 6 | data-structures-and-algorithms 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 | ![Binary Search](https://firebasestorage.googleapis.com/v0/b/dev-stack-app.appspot.com/o/searching%2Fbinary-search-main.gif?alt=media&token=7ec5991e-37dd-4bed-b5fa-cc24b3f637ae&_gl=1*1n62otv*_ga*OTYzMjY5NTkwLjE2ODg4NDM4Njg.*_ga_CW55HF8NVT*MTY5NjI0NDc2Ny4xMzYuMS4xNjk2MjQ1MDYwLjU0LjAuMA..) 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 | ![Binary vs Linear Search](https://firebasestorage.googleapis.com/v0/b/dev-stack-app.appspot.com/o/searching%2Fbinary-search-gif.gif?alt=media&token=311cc083-b244-4614-bb0e-6b36eb37c3aa&_gl=1*17rzhwb*_ga*OTYzMjY5NTkwLjE2ODg4NDM4Njg.*_ga_CW55HF8NVT*MTY5NjI1MDc3My4xMzcuMS4xNjk2MjUwODU1LjQ2LjAuMA..) 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 | ![Binary Search Graphical Representation](https://i.stack.imgur.com/spHFh.png) 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 | data-structures-and-algorithms 755 | 756 |

757 | 758 | --------------------------------------------------------------------------------