diff --git a/DIRECTORY.md b/DIRECTORY.md index 79fa0c50..93537bcb 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -110,6 +110,9 @@ * [Test Car Pooling](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/car_pooling/test_car_pooling.py) * Count Days * [Test Count Days Without Meetings](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/count_days/test_count_days_without_meetings.py) + * Employee Free Time + * [Interval](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/employee_free_time/interval.py) + * [Test Employee Free Time](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/employee_free_time/test_employee_free_time.py) * Full Bloom Flowers * [Test Full Bloom Flowers](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/full_bloom_flowers/test_full_bloom_flowers.py) * Insert Interval @@ -129,6 +132,9 @@ * Memoization * [Fibonacci](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/memoization/fibonacci.py) * [Petethebaker](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/petethebaker.py) + * Prefix Sum + * Continous Sub Array Sum + * [Test Check Subarray Sum](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/prefix_sum/continous_sub_array_sum/test_check_subarray_sum.py) * Search * Binary Search * Divide Chocolate @@ -250,6 +256,8 @@ * [Min Increment For Unique](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/minincrementsforunique/min_increment_for_unique.py) * Non Overlapping Intervals * [Test Non Overlapping Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/non_overlapping_intervals/test_non_overlapping_intervals.py) + * Sub Array With Sum + * [Test Sub Array With Sum](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/sub_array_with_sum/test_sub_array_with_sum.py) * Subarrays With Fixed Bounds * [Test Subarrays With Fixed Bounds](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/subarrays_with_fixed_bounds/test_subarrays_with_fixed_bounds.py) * Circular Buffer @@ -937,7 +945,6 @@ * [Test Lonely Integer](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_lonely_integer.py) * [Test Longest Consecutive Sequence](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_longest_consecutive_sequence.py) * [Test Max Subarray](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_max_subarray.py) - * [Test Sub Array With Sum](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_sub_array_with_sum.py) * [Test Zig Zag Sequence](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_zig_zag_sequence.py) * Linked List * [Test Reorder List](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/linked_list/test_reorder_list.py) diff --git a/algorithms/intervals/employee_free_time/README.md b/algorithms/intervals/employee_free_time/README.md new file mode 100644 index 00000000..31e73d5f --- /dev/null +++ b/algorithms/intervals/employee_free_time/README.md @@ -0,0 +1,68 @@ +# Employee Free Time + +You’re given a list containing the schedules of multiple employees. Each person’s schedule is a list of non-overlapping +intervals in sorted order. An interval is specified with the start and end time, both being positive integers. Your task +is to find the list of finite intervals representing the free time for all the employees. + +> Note: The common free intervals are calculated between the earliest start time and the latest end time of all meetings +across all employees. + +## Constraints + +- 1 <= `schedule.length`, `schedule[i].length` <= 50 +- 0 <= `interval.start`, `interval.end` <= 10^8, where `interval` is any interval in the list of schedules + +## Examples + +![Example 1](./images/examples/employee_free_time_example_1.png) +![Example 2](./images/examples/employee_free_time_example_2.png) +![Example 3](./images/examples/employee_free_time_example_3.png) + +## Solution + +The solution’s core revolves around merging overlapping intervals of employees and identifying the free time gaps +between these merged intervals. Using a min-heap, we arrange the intervals based on when they start, sorting them +according to their start times. When we pop an element from the min-heap, it guarantees that the earliest available +interval is returned for processing. As intervals are popped from the min-heap, the algorithm attempts to merge them. +If the currently popped interval’s start time exceeds the merged interval’s end time, a gap is identified, indicating a +free period. After identifying each gap, the algorithm restarts the merging process to continue identifying additional +periods of free time. + +We use the following variables in our solution: + +- `latest_end_time`: Stores the end time of the previously processed interval. +- `employee_index`: Stores the employee’s index value. +- `interval_index`: Stores the interval’s index of the employee, i. +- `free_time_slots`: Stores the free time intervals. + +The steps of the algorithm are given below : +- We store the start time of each employee’s first interval, along with its index value and a value of 0, in a min-heap. +- We set previous to the start time of the first interval present in a heap. +- Then, we iterate a loop until the heap is empty, and in each iteration, we do the following: + - Pop an element from the min-heap and set `employee_index` and `interval_index` to the second and third values, + respectively, from the popped value. + - Select the interval from the input located at `employee_index`,`interval_index`. + - If the selected interval’s start time is greater than `latest_end_time`, it means that the time from `latest_end_time` + to the selected interval’s start time is free. So, add this interval to the `free_time_slots` array. + - Now, update the `latest_end_time` as `max(latest_end_time, end time of selected interval)`. + - If the current employee has any other interval, push it into the heap. +- After all the iterations, when the heap becomes empty, return the `free_time_slots` array. + +![Solution 1](./images/solutions/employee_free_time_heap_solution_1.png) +![Solution 2](./images/solutions/employee_free_time_heap_solution_2.png) +![Solution 3](./images/solutions/employee_free_time_heap_solution_3.png) +![Solution 4](./images/solutions/employee_free_time_heap_solution_4.png) +![Solution 5](./images/solutions/employee_free_time_heap_solution_5.png) +![Solution 6](./images/solutions/employee_free_time_heap_solution_6.png) +![Solution 7](./images/solutions/employee_free_time_heap_solution_7.png) + +### Time Complexity + +The time complexity of this algorithm is O(mlog(n)), where n is the number of employees and m is the total number of +intervals across all employees. This is because the time complexity of filling the heap is O(nlog(n)) and the time +complexity of processing the heap is O(mlog(n)). + +### Space Complexity + +We use a heap in the solution, which can have a maximum of n elements. Hence, the space complexity of this solution is +O(n), where n is the number of employees. diff --git a/algorithms/intervals/employee_free_time/__init__.py b/algorithms/intervals/employee_free_time/__init__.py new file mode 100644 index 00000000..01cbdece --- /dev/null +++ b/algorithms/intervals/employee_free_time/__init__.py @@ -0,0 +1,122 @@ +from typing import List, Tuple +import heapq +from algorithms.intervals.employee_free_time.interval import Interval + + +def employee_free_time(schedules: List[List[Interval]]) -> List[Interval]: + """ + Finds intervals where employees have free time + + Complexity: + Time Complexity: O(NlogN), where N is the total number of intervals across all employees. This is dominated by the sorting step. + Space Complexity: O(N) to store the flattened all_schedules list. + + Args: + schedules(list): a list of lists, where each inner list contains `Interval` objects representing an employee's schedule. + Returns: + list: Intervals of employee free time + """ + # We need to combine and merge all employee intervals(schedules) into one unified schedule to be able to check + # for free time. Since using a new list incurs a space cost of O(n), we can modify the input list, however, this + # has side effects if the schedule list is used in other parts of the program. To avoid side effects, we copy over + # the input list into a new list to handle the merging + all_schedules: List[Interval] = [] + for schedule in schedules: + for schedule_interval in schedule: + all_schedules.append(schedule_interval) + + if not all_schedules: + return [] + + # sort by start time + all_schedules.sort(key=lambda x: x.start) + + # Keep track of the latest meeting's end time. This is initialized the first schedule's end time + latest_end = all_schedules[0].end + + # This will keep track of the free time slots + free_time_slots: List[Interval] = [] + + # Now we need to find the gaps in the combined schedule. Since we have already used the first schedule's end time + # as the latest end we have seen so far, we start at the next interval. + for idx in range(1, len(all_schedules)): + current_schedule = all_schedules[idx] + current_interval_start, current_interval_end = ( + current_schedule.start, + current_schedule.end, + ) + + # If the current interval's start time is greater than the latest end time we have seen so far, it means we have + # found free time + if current_interval_start > latest_end: + # we have a free time slot + free_interval = Interval(start=latest_end, end=current_interval_start) + free_time_slots.append(free_interval) + + # update the latest_end to the maximum of the latest end time seen so far + latest_end = max(latest_end, current_interval_end) + + return free_time_slots + + +def employee_free_time_heap(schedules: List[List[Interval]]) -> List[Interval]: + """ + Finds intervals where employees have free time using a min heap + + Args: + schedules(list): a list of lists, where each inner list contains `Interval` objects representing an employee's schedule. + Returns: + list: Intervals of employee free time + """ + heap: List[Tuple[int, int, int]] = [] + # Iterate for all employees' schedules + # and add start of each schedule's first interval along with + # its index value and a value 0. + for i in range(len(schedules)): + if schedules[i]: # Only add if employee has at least one interval + heap.append((schedules[i][0].start, i, 0)) + + # Create heap from array elements. + heapq.heapify(heap) + + # Handle empty heap + if not heap: + return [] + + # Take an empty array to store results. + free_time_slots = [] + + # Set 'latest_end_time' to the start time of first interval in heap. + latest_end_time = schedules[heap[0][1]][heap[0][2]].end + + # Iterate till heap is empty + while heap: + # Pop an element from heap and set value of employee_index and interval_index + _, employee_index, interval_index = heapq.heappop(heap) + + # Select an interval + schedule_interval = schedules[employee_index][interval_index] + + # If selected interval's start value is greater than the + # latest_end_time value, it means that this interval is free. + # So, add this interval (latest_end_time, interval's end value) into result. + if schedule_interval.start > latest_end_time: + free_time_slots.append(Interval(latest_end_time, schedule_interval.start)) + + # Update the latest_end_time as maximum of latest_end_time and interval's end value. + latest_end_time = max(latest_end_time, schedule_interval.end) + + # If there is another interval in current employees' schedule, + # push that into heap. + if interval_index + 1 < len(schedules[employee_index]): + heapq.heappush( + heap, + ( + schedules[employee_index][interval_index + 1].start, + employee_index, + interval_index + 1, + ), + ) + + # When the heap is empty, return result. + return free_time_slots diff --git a/algorithms/intervals/employee_free_time/images/examples/employee_free_time_example_1.png b/algorithms/intervals/employee_free_time/images/examples/employee_free_time_example_1.png new file mode 100644 index 00000000..e8bba865 Binary files /dev/null and b/algorithms/intervals/employee_free_time/images/examples/employee_free_time_example_1.png differ diff --git a/algorithms/intervals/employee_free_time/images/examples/employee_free_time_example_2.png b/algorithms/intervals/employee_free_time/images/examples/employee_free_time_example_2.png new file mode 100644 index 00000000..05223b46 Binary files /dev/null and b/algorithms/intervals/employee_free_time/images/examples/employee_free_time_example_2.png differ diff --git a/algorithms/intervals/employee_free_time/images/examples/employee_free_time_example_3.png b/algorithms/intervals/employee_free_time/images/examples/employee_free_time_example_3.png new file mode 100644 index 00000000..604c8d25 Binary files /dev/null and b/algorithms/intervals/employee_free_time/images/examples/employee_free_time_example_3.png differ diff --git a/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_1.png b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_1.png new file mode 100644 index 00000000..ee12a0df Binary files /dev/null and b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_1.png differ diff --git a/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_2.png b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_2.png new file mode 100644 index 00000000..06586755 Binary files /dev/null and b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_2.png differ diff --git a/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_3.png b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_3.png new file mode 100644 index 00000000..2788d986 Binary files /dev/null and b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_3.png differ diff --git a/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_4.png b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_4.png new file mode 100644 index 00000000..7c785c53 Binary files /dev/null and b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_4.png differ diff --git a/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_5.png b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_5.png new file mode 100644 index 00000000..a9ec71c9 Binary files /dev/null and b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_5.png differ diff --git a/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_6.png b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_6.png new file mode 100644 index 00000000..cf0ab34b Binary files /dev/null and b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_6.png differ diff --git a/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_7.png b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_7.png new file mode 100644 index 00000000..aec78ae8 Binary files /dev/null and b/algorithms/intervals/employee_free_time/images/solutions/employee_free_time_heap_solution_7.png differ diff --git a/algorithms/intervals/employee_free_time/interval.py b/algorithms/intervals/employee_free_time/interval.py new file mode 100644 index 00000000..1e2bec05 --- /dev/null +++ b/algorithms/intervals/employee_free_time/interval.py @@ -0,0 +1,19 @@ +class Interval: + def __init__(self, start: int, end: int): + self.start = start + self.end = end + self.closed = True # by default, the interval is closed + + def set_closed(self, closed: bool) -> None: + # set the flag for closed/open + self.closed = closed + + def __str__(self): + return ( + "[" + str(self.start) + ", " + str(self.end) + "]" + if self.closed + else "(" + str(self.start) + ", " + str(self.end) + ")" + ) + + def __eq__(self, other: "Interval") -> bool: + return self.start == other.start and self.end == other.end diff --git a/algorithms/intervals/employee_free_time/test_employee_free_time.py b/algorithms/intervals/employee_free_time/test_employee_free_time.py new file mode 100644 index 00000000..a6fca078 --- /dev/null +++ b/algorithms/intervals/employee_free_time/test_employee_free_time.py @@ -0,0 +1,105 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.intervals.employee_free_time.interval import Interval +from algorithms.intervals.employee_free_time import ( + employee_free_time, + employee_free_time_heap, +) + +EMPLOYEE_FREE_TIME_TEST_CASES = [ + ( + [ + [Interval(start=1, end=2), Interval(start=5, end=6)], + [Interval(start=1, end=3)], + [Interval(start=4, end=10)], + ], + [Interval(start=3, end=4)], + ), + ( + [ + [Interval(start=1, end=3), Interval(start=6, end=7)], + [Interval(start=2, end=4)], + [Interval(start=2, end=5), Interval(start=9, end=12)], + ], + [Interval(start=5, end=6), Interval(start=7, end=9)], + ), + ( + [ + [Interval(start=2, end=3), Interval(start=7, end=9)], + [Interval(start=1, end=4), Interval(start=6, end=7)], + ], + [Interval(start=4, end=6)], + ), + ( + [ + [Interval(start=3, end=5), Interval(start=8, end=10)], + [Interval(start=4, end=6), Interval(start=9, end=12)], + [Interval(start=5, end=6), Interval(start=8, end=10)], + ], + [Interval(start=6, end=8)], + ), + ( + [ + [ + Interval(start=1, end=2), + Interval(start=3, end=4), + Interval(start=5, end=6), + Interval(start=7, end=8), + Interval(start=9, end=10), + Interval(start=11, end=12), + ], + [ + Interval(start=1, end=2), + Interval(start=3, end=4), + Interval(start=5, end=6), + Interval(start=7, end=8), + Interval(start=9, end=10), + Interval(start=11, end=12), + ], + [ + Interval(start=1, end=2), + Interval(start=3, end=4), + Interval(start=5, end=6), + Interval(start=7, end=8), + Interval(start=9, end=10), + Interval(start=11, end=12), + ], + [ + Interval(start=1, end=2), + Interval(start=3, end=4), + Interval(start=5, end=6), + Interval(start=7, end=8), + Interval(start=9, end=10), + Interval(start=11, end=12), + ], + ], + [ + Interval(start=2, end=3), + Interval(start=4, end=5), + Interval(start=6, end=7), + Interval(start=8, end=9), + Interval(start=10, end=11), + ], + ), +] + + +class EmployeeFreeTimeTestCase(unittest.TestCase): + @parameterized.expand(EMPLOYEE_FREE_TIME_TEST_CASES) + def test_employee_free_time( + self, schedule: List[List[Interval]], expected: List[Interval] + ): + actual = employee_free_time(schedule) + self.assertListEqual(expected, actual) + + @parameterized.expand(EMPLOYEE_FREE_TIME_TEST_CASES) + def test_employee_free_time_heap( + self, schedule: List[List[Interval]], expected: List[Interval] + ): + actual = employee_free_time_heap(schedule) + self.assertListEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/prefix_sum/__init__.py b/algorithms/prefix_sum/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/algorithms/prefix_sum/continous_sub_array_sum/README.md b/algorithms/prefix_sum/continous_sub_array_sum/README.md new file mode 100644 index 00000000..eb6ef225 --- /dev/null +++ b/algorithms/prefix_sum/continous_sub_array_sum/README.md @@ -0,0 +1,73 @@ +# Continuous Subarray Sum + +Given an integer array nums and an integer k, determine if nums contains a good subarray. Return true if such a subarray +exists; otherwise, return false. + +A subarray of nums is considered good if: + +- Its length is at least 2. +- The sum of its elements is a multiple of k. + +> Notes: +> - A subarray is defined as a contiguous sequence of elements within an array. +> - An integer x is a multiple of k if there exists an integer n such that x = n * k. Note that 0 is always considered a +> multiple of k. + +## Constraints + +- 1 <= `nums.length` <= 10^4 +- 0 <= `nums[i]` <= 10^5 +- 0 <= `sum(nums[i])` <= 2^31-1 +- 1 <= k <= 2^31-1 + +## Examples + +![Example 1](images/examples/continuous_sub_array_sum_example_1.png) + +## Solution + +This algorithm’s essence is identifying a subarray in nums whose sum is a multiple of k. We accomplish this by using +cumulative sums and their remainders when divided by k. A hash map helps us quickly check if a particular remainder has +been encountered before, allowing us to detect subarrays with the desired property. + +Here’s why remainders are essential: when we divide a cumulative sum by k, the remainder reveals the part that isn’t +divisible by k. If two cumulative sums yield the same remainder when divided by k, then the difference between these +cumulative sums is divisible by k. This indicates that the subarray between these two indices has a sum that’s a multiple +of k. + +For instance, if we have cumulative sums at indices i and j where i bool: + # This keeps track of the cumulative sum, but we only care about its remainder when divided by k. + cumulative_sum = 0 + + # This is a remainder to index mapping to store where a remainder was calculated and at what index. This helps with + # constant time lookups + # Setting {0: -1} handles the case where a "good" subarray starts right from the beginning of the array. If the + # running_sum % k becomes 0 at index 1, the length calculation 1−(−1)=2 correctly identifies a valid subarray. 📏 + remainder_map: Dict[int, int] = {0: -1} + + for idx, num in enumerate(nums): + cumulative_sum += num + # Compute the remainder of the cumulative sum with k + remainder = cumulative_sum % k + + # Check if the remainder has been seen before + if remainder in remainder_map: + # Ensure the subarray length is at least 2 + if idx - remainder_map[remainder] >= 2: + return True + else: + # Store the first occurrence of the remainder + remainder_map[remainder] = idx + + return False diff --git a/algorithms/prefix_sum/continous_sub_array_sum/images/examples/continuous_sub_array_sum_example_1.png b/algorithms/prefix_sum/continous_sub_array_sum/images/examples/continuous_sub_array_sum_example_1.png new file mode 100644 index 00000000..c301f3a8 Binary files /dev/null and b/algorithms/prefix_sum/continous_sub_array_sum/images/examples/continuous_sub_array_sum_example_1.png differ diff --git a/algorithms/prefix_sum/continous_sub_array_sum/test_check_subarray_sum.py b/algorithms/prefix_sum/continous_sub_array_sum/test_check_subarray_sum.py new file mode 100644 index 00000000..dfa5127e --- /dev/null +++ b/algorithms/prefix_sum/continous_sub_array_sum/test_check_subarray_sum.py @@ -0,0 +1,24 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.prefix_sum.continous_sub_array_sum import check_subarray_sum + +CHECK_SUBARRAY_SUM_TEST_CASES = [ + ([23, 2, 4, 6, 7], 6, True), + ([1, 2, 3], 5, True), + ([5, 0, 0, 3], 3, True), + ([0, 3], 2, False), + ([5, 3, 4, 2], 19, False), + ([5, 3, 4, 2, 7, 3], 8, True), +] + + +class ContinuousSubarraySumTestCase(unittest.TestCase): + @parameterized.expand(CHECK_SUBARRAY_SUM_TEST_CASES) + def test_check_subarray_sum(self, nums: List[int], k: int, expected: bool): + actual = check_subarray_sum(nums, k) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/datastructures/arrays/test_sub_array_with_sum.py b/datastructures/arrays/sub_array_with_sum/test_sub_array_with_sum.py similarity index 100% rename from tests/datastructures/arrays/test_sub_array_with_sum.py rename to datastructures/arrays/sub_array_with_sum/test_sub_array_with_sum.py diff --git a/pystrings/palindrome/README.md b/pystrings/palindrome/README.md index dbacedd2..f0c84862 100755 --- a/pystrings/palindrome/README.md +++ b/pystrings/palindrome/README.md @@ -126,7 +126,73 @@ Example Output Example Explanation We can see that longest palindromic substring is of length 7 and the string is "aaabaaa". -### Related Topics + +## Palindrome Number + +Given an integer, x, return TRUE if it is a palindrome; otherwise, FALSE. + +> A palindrome integer is one whose digits read the same from left to right and right to left. + +### Constraints + +- -2^-31 <= x <= 2^31-1 + +### Solution + +One of the first solutions that comes to mind is converting the number to a string and checking if it reads the same +backward and forward. While that works, it’s not the most efficient way, as it uses extra memory. Instead, we can solve +this problem mathematically, using only the digits of the number itself. + +A palindrome number remains the same when its digits are reversed. For example, 121 or 1221. Before checking deeper logic, +we can immediately rule out two cases where an integer can never be a palindrome: + +1. **Negative numbers**: They can’t be palindromes because of the leading minus sign, which doesn’t appear at the end + when reversed. +2. **Numbers ending with 0 (except 0 itself)**: A number like 10 or 100 can’t be a palindrome because palindrome integers + can’t start with a zero. + +For the remaining cases, the natural idea is to reverse the entire number and check whether it matches the original. +However, reversing the whole integer can lead to integer overflow. To avoid this problem, we reverse only half of the +number instead. The process works by repeatedly taking the last digit of x (using x % 10) and attaching it to a new +number that we are building in reverse order. Each time we take a digit from the end, we also shorten x by removing that +digit (using integer division by 10). We continue doing this until the reversed portion becomes at least as long as what’s +left of the original number. + +At that stage, the number is naturally divided into two halves: + +- The first half is what remains of the original number. +- The second half is represented by the reversed digits we collected. + +If the number has an odd count of digits, the middle digit doesn’t affect whether it’s a palindrome, so we simply ignore +it (using integer division by 10). + +Eventually, if the two halves match, then the number reads the same forward and backward, representing a palindrome. If +they don’t match, then it is not. + +Let’s look at the algorithm steps: + +1. Handle special cases. If x is negative, or ends with 0 but is not 0 itself, it’s not a palindrome. Return FALSE. +2. Initialize a variable, reversedHalf, to 0. This will store the reversed digits of the last half of the number. +3. While x is greater than reversedHalf: + - Take the last digit of x using x % 10. + - Add this digit to reversedHalf as reversedHalf = reversedHalf * 10 + x % 10. + - Remove the last digit from x using integer division x //= 10. +4. Once we have the last half of the number in reversedHalf, compare the two halves: + - If the length of the reversedHalf is even, then compare x == reversedHalf. If it’s a valid match, return TRUE. + - If the length of the reversedHalf is odd, then compare x == reversedHalf // 10 to ignoring the middle digit. If + it’s a valid match, return TRUE. +5. If neither condition is TRUE, return FALSE. + +### Time Complexity + +The time complexity of this solution O(log₁₀(n)) because the input is divided by 10 for every iteration. + +### Space Complexity + +As only a few variables are used, the space complexity of this solution is O(1). + +## Related Topics - String - Dynamic Programming +- Two Pointers diff --git a/pystrings/palindrome/__init__.py b/pystrings/palindrome/__init__.py index f7026edd..97d0cbb8 100755 --- a/pystrings/palindrome/__init__.py +++ b/pystrings/palindrome/__init__.py @@ -108,3 +108,57 @@ def largest_palindrome(max_factor, min_factor=0): :rtype:int """ return max(generate_palindromes(max_factor, min_factor), key=lambda tup: tup[0]) + + +def is_palindrome_number(x: int) -> bool: + """ + Checks if a number is a palindrome + Args: + x(int): number + Returns: + bool: True if it is a palindrome, False otherwise + """ + # Negative numbers are never palindromes. + # Also, any number ending in 0 (except 0 itself) cannot be a palindrome. + if x < 0 or (x % 10 == 0 and x != 0): + return False + + reversed_half = 0 # This will store the reversed last half of the digits. + + # Reverse only half of the number. + # Stop when the reversed half becomes >= the remaining half. + while x > reversed_half: + last_digit = x % 10 # Extract last digit + reversed_half = reversed_half * 10 + last_digit # Build reversed number + x //= 10 # Remove last digit from x + + # If the number has even digits, check x == reversedHalf. + # If odd digits, the middle digit doesn't matter, so remove it using //10. + # If either of the above are True, return True. Otherwise, False + if x == reversed_half or x == reversed_half // 10: + return True + else: + return False + + +def is_palindrome_number_2(x: int) -> bool: + """ + Checks if a number is a palindrome + Args: + x(int): number + Returns: + bool: True if it is a palindrome, False otherwise + """ + if x < 0 or (x % 10 == 0 and x != 0): + return False + + num_str = str(x) + left = 0 + right = len(num_str) - 1 + + while left < right: + if num_str[left] != num_str[right]: + return False + left += 1 + right -= 1 + return True diff --git a/pystrings/palindrome/test_palindrome.py b/pystrings/palindrome/test_palindrome.py index eee23398..c14a601a 100755 --- a/pystrings/palindrome/test_palindrome.py +++ b/pystrings/palindrome/test_palindrome.py @@ -3,9 +3,15 @@ from random import choice, randint from string import ascii_letters from typing import Union - -from . import is_palindrome, smallest_palindrome, largest_palindrome -from .longest_palindrome import longest_palindrome +from parameterized import parameterized +from pystrings.palindrome import ( + is_palindrome, + smallest_palindrome, + largest_palindrome, + is_palindrome_number, + is_palindrome_number_2, +) +from pystrings.palindrome.longest_palindrome import longest_palindrome class LongestPalindromeTests(unittest.TestCase): @@ -82,6 +88,30 @@ def test_15(self): self.assertEqual(is_palindrome(str(test_case)), self.reference(test_case)) +IS_PALINDROME_TEST_CASES = [ + (353, True), + (-353, False), + (90, False), + (12321, True), + (10101, True), + (1000021, False), + (0, True), + (2147447412, True), +] + + +class IsPalindromeNumber(unittest.TestCase): + @parameterized.expand(IS_PALINDROME_TEST_CASES) + def test_is_palindrome_number(self, x: int, expected: bool): + actual = is_palindrome_number(x) + self.assertEqual(actual, expected) + + @parameterized.expand(IS_PALINDROME_TEST_CASES) + def test_is_palindrome_number_2(self, x: int, expected: bool): + actual = is_palindrome_number_2(x) + self.assertEqual(actual, expected) + + class SmallestPalindromeTests(unittest.TestCase): def test_smallest_palindrome_from_double_digit_factors(self): value, factors = smallest_palindrome(max_factor=99, min_factor=10)