𝔩𝔢𝔩𝕠𝔭𝔢𝔷
Connect With Me on LinkedIn

Python Study

Caesar Cipher Encryptor

Mastering the Caesar Cipher Encryptor in Python

Introduction

The Caesar Cipher is one of the simplest and most widely known encryption techniques. Named after Julius Caesar, who allegedly used it to communicate with his generals, this cipher is a great starting point for understanding basic cryptography concepts. In this article, we'll implement a Caesar Cipher encryptor in Python, exploring various optimization techniques and handling edge cases.

Problem Statement

Given:

  • A non-empty string of lowercase letters
  • A non-negative integer representing a key

Objective:

  • Return a new string obtained by shifting every letter in the input string by k positions in the alphabet, where k is the key

Assumption:

  • The letters will wrap around the alphabet, as in the letter z shifted by one returns the letter a

Example:

  • Input:
    • String: xyz
    • Key: 2
  • Output: zab

Strategy and Hypothesis

We'll use Unicode mapping to implement the Caesar Cipher. We can perform the shift operation mathematically by converting characters to their Unicode code points. We must handle the wrap-around case when the shifted index exceeds the alphabet's range.

  • For information on Unicode code points
  • For information on the character that can take us from integer to character chr(i)
  • For information on how to turn characters to their equivalent code point ord(c)

Implementation

Initial Attempt

def getNewCode(char, key):
    i_code = ord(char) + key
    wrap = i_code % 122
    if wrap > 0:
        return 96 + wrap
    else:
        return i_code

def caesarCipherEncryptor(string, key):
    solution = ""
    for char in string:
        i_code = getNewCode(char, key)
        solution += chr(i_code)

    return solution

This passes a few test cases but doesn't work for multiple wraps. It goes beyond the Unicode characters we want to use when there are multiple wraps.

Improved Approach

def shiftChar(char, key):
    i_code = ord(char) + (key % 26)
    wrap = i_code - 122
    i_code = 96 + wrap if wrap > 0 else i_code
    new_char = chr(i_code)
    return new_char

def caesarCipherEncryptor(string, key):
    solution = ""
    for char in string:
        new_char = shiftChar(char, key)
        solution += new_char

    return solution

This fixes the original issue by ensuring we consider multiple wraps by dividing the key by the alphabet count and only using the remainder.

Optimization Analysis

Optimizing for Space

The current implementation uses a string to build the solution, which creates a new string object with each concatenation. We can optimize for space by using list comprehension and joining the characters at the end:

def shiftChar(char, key):
    i_code = ord(char) + (key % 26)
    wrap = i_code - 122
    i_code = 96 + wrap if wrap > 0 else i_code
    return chr(i_code)

def caesarCipherEncryptor(string, key):
    return ''.join(shiftChar(char, key) for char in string)

This approach reduces memory usage by avoiding multiple string creations and leverages Python's efficient list comprehension feature.

Optimizing for Time

While the current implementation is already quite efficient with O(n) time complexity, we can further optimize by pre-computing the shifted alphabet:

def caesarCipherEncryptor(string, key):
    alphabet = 'abcdefghijklmnopqrstuvwxyz'
    shifted_alphabet = alphabet[key % 26:] + alphabet[:key % 26]
    return ''.join(shifted_alphabet[alphabet.index(char)] for char in string)

This approach reduces the number of mathematical operations per character by using a lookup table (the shifted alphabet) instead of calculating each character shift individually.

Optimal Space & Time

The pre-computed alphabet approach from the "Optimizing for Time" section balances space and time efficiency well. It maintains O(n) time complexity while using only O(1) additional space (as the alphabet size is constant).

This solution eliminates the need for the shiftChar function and replaces mathematical operations with faster list indexing, making it generally more efficient for larger inputs.

The space-optimized version might be preferable for smaller inputs or when memory is a greater concern than CPU cycles. The choice between the two would depend on the system's specific use case and constraints.

Key Takeaways

  1. Unicode mapping provides a convenient way to perform character shifts in Python.
  2. Consider wrap-around cases with cyclic data structures like the alphabet.
  3. The modulo operator (%) is crucial for handling keys larger than the alphabet size.
  4. List comprehensions and generator expressions can produce more concise and efficient code.
  5. Pre-computing values (like the shifted alphabet) can improve performance in certain scenarios.

Conclusion

Implementing the Caesar Cipher Encryptor in Python demonstrates the importance of understanding character encoding, modular arithmetic, and optimization techniques. While the algorithm is simple, this exercise showcases how to handle edge cases, optimize for space and time, and write clean, efficient Python code. As you tackle more complex cryptography problems, remember that the principles learned here—like handling cyclic structures and pre-computing values—will continue to be valuable tools in your programming arsenal.

Previous
Typescript Cell