Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 212 additions & 43 deletions sorts/patience_sort.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,235 @@
"""
Patience Sort Algorithm

Patience Sort is a sorting algorithm inspired by the card game "Patience"
(also known as Solitaire). It works by:
1. Distributing elements into sorted "piles" (like stacking cards)
2. Merging the piles using a min-heap

The algorithm also naturally finds the Longest Increasing Subsequence (LIS)
— the number of piles equals the length of the LIS.

Time Complexity:
- Best: O(n log n)
- Average: O(n log n)
- Worst: O(n log n)

Space Complexity: O(n)

Stability: Stable — equal elements maintain their relative order.

>>> patience_sort([6, 3, 5, 1, 8, 2, 4, 7])
[1, 2, 3, 4, 5, 6, 7, 8]

>>> patience_sort([])
[]

>>> patience_sort([1])
[1]

>>> patience_sort([5, 4, 3, 2, 1])
[1, 2, 3, 4, 5]

>>> patience_sort([1, 2, 3, 4, 5])
[1, 2, 3, 4, 5]

>>> patience_sort([3, 3, 1, 1, 2, 2])
[1, 1, 2, 2, 3, 3]

>>> patience_sort([-5, 3, -2, 8, -1, 0])
[-5, -2, -1, 0, 3, 8]

>>> patience_sort([1.5, 0.5, 2.5, 1.0])
[0.5, 1.0, 1.5, 2.5]
"""

from __future__ import annotations

import heapq
from bisect import bisect_left
from functools import total_ordering
from heapq import merge

"""
A pure Python implementation of the patience sort algorithm

For more information: https://en.wikipedia.org/wiki/Patience_sorting
def patience_sort(array: list) -> list:
"""
Sort a list using the Patience Sort algorithm.

This algorithm is based on the card game patience
The algorithm works in two phases:
Phase 1 — Pile Creation:
Iterate through the input. For each element, find the leftmost pile
whose top card is >= the current element (using binary search).
If found, place it on that pile. Otherwise, create a new pile.

For doctests run following command:
python3 -m doctest -v patience_sort.py
Phase 2 — Merging:
Use a min-heap to merge all piles efficiently, always extracting
the smallest element across all pile tops.

For manual testing run:
python3 patience_sort.py
"""
Args:
array: A list of comparable elements to sort.

Returns:
A new sorted list.

>>> patience_sort([10, 7, 8, 9, 1, 5])
[1, 5, 7, 8, 9, 10]
"""
if len(array) <= 1:
return list(array)

# Phase 1: Create piles
piles = _create_piles(array)

# Phase 2: Merge piles using a min-heap
return _merge_piles(piles)


def _create_piles(array: list) -> list[list]:
"""
Distribute elements into piles.

Each pile is a stack where the top element (last in the list) is the
smallest. We place each new element on the leftmost pile whose top
is >= the element. The pile_tops list tracks the top of each pile
for efficient binary search.

Within each pile, elements are in decreasing order from bottom to top.

@total_ordering
class Stack(list):
def __lt__(self, other):
return self[-1] < other[-1]
Args:
array: The input list.

def __eq__(self, other):
return self[-1] == other[-1]
Returns:
A list of piles (each pile is a list, with the top at the end).

>>> piles = _create_piles([6, 3, 5, 1])
>>> len(piles) >= 1
True
"""
piles: list[list] = []
pile_tops: list = [] # Track top of each pile for binary search

for element in array:
# Find the leftmost pile whose top is >= element
pos = bisect_left(pile_tops, element)

if pos < len(piles):
# Place on existing pile
piles[pos].append(element)
pile_tops[pos] = element
else:
# Create a new pile
piles.append([element])
pile_tops.append(element)

return piles


def _merge_piles(piles: list[list]) -> list:
"""
Merge piles using a min-heap.

Since elements within each pile are in decreasing order (bottom to top),
we pop from the top of each pile (the end of each list) to get the
smallest available element from that pile.

We use a heap of (top_element, pile_index) to efficiently find which
pile has the smallest top.

Args:
piles: List of piles to merge.

Returns:
A single sorted list.

>>> _merge_piles([[3, 1], [4, 2], [5]])
[1, 2, 3, 4, 5]
"""
result = []

def patience_sort(collection: list) -> list:
"""A pure implementation of patience sort algorithm in Python
# Initialize heap with the top (last element) of each pile
# Heap entries: (value, pile_index)
heap = []
for i, pile in enumerate(piles):
if pile:
# The top of the pile is the last element (smallest in that pile)
heapq.heappush(heap, (pile[-1], i))

:param collection: some mutable ordered collection with heterogeneous
comparable items inside
:return: the same collection ordered by ascending
while heap:
value, pile_idx = heapq.heappop(heap)
result.append(value)

Examples:
>>> patience_sort([1, 9, 5, 21, 17, 6])
[1, 5, 6, 9, 17, 21]
# Remove the top element from this pile
piles[pile_idx].pop()

>>> patience_sort([])
[]
# If pile still has elements, push the new top
if piles[pile_idx]:
heapq.heappush(heap, (piles[pile_idx][-1], pile_idx))

>>> patience_sort([-3, -17, -48])
[-48, -17, -3]
return result


def longest_increasing_subsequence_length(array: list) -> int:
"""
Find the length of the Longest Increasing Subsequence (LIS).

A beautiful property of Patience Sort: the number of piles created
equals the length of the LIS.

Args:
array: A list of comparable elements.

Returns:
The length of the longest increasing subsequence.

>>> longest_increasing_subsequence_length([6, 3, 5, 1, 8, 2, 4, 7])
4

>>> longest_increasing_subsequence_length([1, 2, 3, 4, 5])
5

>>> longest_increasing_subsequence_length([5, 4, 3, 2, 1])
1

>>> longest_increasing_subsequence_length([])
0

>>> longest_increasing_subsequence_length([3, 1, 4, 1, 5, 9])
4
"""
stacks: list[Stack] = []
# sort into stacks
for element in collection:
new_stacks = Stack([element])
i = bisect_left(stacks, new_stacks)
if i != len(stacks):
stacks[i].append(element)
if not array:
return 0

# The number of piles = LIS length
pile_tops: list = []

for element in array:
pos = bisect_left(pile_tops, element)

if pos < len(pile_tops):
pile_tops[pos] = element
else:
stacks.append(new_stacks)
pile_tops.append(element)

# use a heap-based merge to merge stack efficiently
collection[:] = merge(*(reversed(stack) for stack in stacks))
return collection
return len(pile_tops)


if __name__ == "__main__":
user_input = input("Enter numbers separated by a comma:\n").strip()
unsorted = [int(item) for item in user_input.split(",")]
print(patience_sort(unsorted))
import doctest

doctest.testmod()

# Demo
print("=== Patience Sort ===\n")

test_cases = [
[6, 3, 5, 1, 8, 2, 4, 7],
[5, 4, 3, 2, 1],
[1, 2, 3, 4, 5],
[-5, 3, -2, 8, -1, 0],
[3, 3, 1, 1, 2, 2],
]

for arr in test_cases:
sorted_arr = patience_sort(arr)
lis_len = longest_increasing_subsequence_length(arr)
print(f" Input: {arr}")
print(f" Sorted: {sorted_arr}")
print(f" LIS length: {lis_len}\n")