Previous | Tutorial index | Next

Tutorial 4: Basics of Computability and Computational Complexity

Learning Objective

To be able to explain the basics of computability and computational complexity.

4.1 Introduction: The Theoretical Limits of Programming

As a Python programmer, you will eventually write code that runs too slowly or crashes because you didn't account for edge cases. But some problems are not just difficult to solve—they are impossible to solve algorithmically. Others can be solved, but doing so would take longer than the lifespan of the universe. This tutorial bridges the gap between your Python code and the theoretical foundations of computer science.

You will learn two crucial ideas:

Understanding these concepts will save you from trying to solve unsolvable problems and help you recognize when a problem is fundamentally intractable, allowing you to look for approximations instead of exact solutions.

4.2 Computability Theory: What Can Be Computed?

Computability theory asks a fundamental question: Are there problems that no computer program can ever solve, regardless of how much memory or time it has?

4.2.1 Formalizing the Problem

In theoretical computer science, a problem is usually defined as a decision problem—a question that has a yes/no answer. For example:

The specific details of the programming language (Python, C, Java) don't matter for this theory. We need a universal, mathematically precise model of computation.

4.2.2 The Turing Machine (1936)

In 1936, Alan Turing introduced a theoretical device that formalized the concept of "algorithm" and "computation." A Turing Machine is not a physical machine but a mathematical thought experiment that defines what is computable.

Components of a Turing Machine:

  1. An Infinite Tape: Divided into cells. Each cell holds a symbol (usually 0, 1, or a blank _). This tape serves as the computer's unlimited memory.
  2. A Read/Write Head: Points to one cell on the tape. It can read the symbol, write a new symbol, and move one step left or right.
  3. A Finite State Register: Stores the current "state" of the machine. It acts like the Program Counter and internal flags.
  4. A Transition Function (δ): A set of rules that tells the machine what to do based on the current state and the symbol being read. The rule specifies:
  5. Accept/Reject States: Special states where the machine halts and outputs a definitive Yes (Accept) or No (Reject).

How it Works: The machine starts in a defined start state with the input written on the tape. It repeatedly reads the current cell, applies the transition function, writes a new symbol, moves, and changes state. If it reaches an Accept or Reject state, it halts. If it never reaches one, it runs forever.

Why is this relevant to Python? Python is Turing-complete, meaning it can simulate any Turing Machine. Conversely, a Turing Machine can simulate any Python program (given infinite time and tape). Thus, the limits of the Turing Machine are the limits of Python.

4.2.3 The Universal Turing Machine

Turing went a step further. He showed that a single Turing Machine could be designed to simulate any other Turing Machine by reading the description of that machine from the tape. This is the theoretical equivalent of a stored-program computer—the Universal Turing Machine is the blueprint for every general-purpose CPU and every interpreter (like the Python interpreter itself).

4.2.4 The Church-Turing Thesis

In the 1930s, Alonzo Church (inventor of lambda calculus) and Alan Turing independently proposed what is now known as the Church-Turing Thesis:

"Every effectively calculable function is a computable function by a Turing Machine."

In plain English: Any computation that a human can perform using a well-defined algorithm can be performed by a Turing Machine (and thus by a Python program). It is called a "thesis" rather than a theorem because it relies on the intuitive definition of "effectively calculable," which cannot be formally proven. However, it is universally accepted by computer scientists and serves as the foundation of computing.

4.2.5 Decidable vs. Undecidable Problems

Based on the Turing Machine model, we can categorize problems:

4.2.6 The Halting Problem (The Quintessential Undecidable Problem)

The Problem: Given a description of a program P and an input I, determine whether P(I) will eventually halt (finish) or run forever.

Why it matters to you: This directly applies to Python. We would love to have a perfect debugger that could tell us if our while loop will eventually stop, or if our recursive function will hit a base case. Turing proved this is impossible.

The Proof (By Contradiction / Diagonalization): Let's assume, for the sake of contradiction, that such a program does exist. Let's call it HALT(P, I). It returns True if P(I) halts, and False if it runs forever. Now, let's write a new program called TROUBLE(P) (a "troublemaker") that does the following:

  1. It takes a program P as its only input.
  2. It calls HALT(P, P)—checking whether P halts when given itself as input.
  3. If HALT(P, P) returns True (meaning P(P) halts), then TROUBLE goes into an infinite loop (it deliberately runs forever).
  4. If HALT(P, P) returns False (meaning P(P) runs forever), then TROUBLE halts immediately (it stops).

Now, what happens if we run TROUBLE(TROUBLE)—we give TROUBLE itself as input?

Because we reached a logical contradiction, our initial assumption—that HALT exists—must be false. Therefore, a perfect Halting Detector is impossible to write in any Turing-complete language, including Python.

4.2.7 Other Undecidable Problems

4.3 Computational Complexity: How Fast Can We Solve It?

Assuming a problem is decidable, the next question is: How much time and memory does it take to solve it? This is the domain of computational complexity.

4.3.1 Measuring Resources: Time and Space

We usually focus on worst-case complexity (the scenario that takes the longest), because we need guarantees for our users.

4.3.2 Big-O Notation (Asymptotic Analysis)

To compare algorithms, we ignore the constant factors and the speed of the hardware. We look at how the runtime scales as n (input size) grows to infinity. This is expressed using Big-O notation.

Notation Name Description Example in Python
O(1) Constant Runtime is independent of input size. Accessing a list element by index: my_list[i]
O(log n) Logarithmic Runtime grows slowly as input grows. Binary search on a sorted list.
O(n) Linear Runtime scales directly with input size. Searching for an item in an unsorted list (loop once).
O(n log n) Linearithmic Slightly worse than linear. Efficient sorting algorithms (Merge Sort, QuickSort).
O(n²) Quadratic Runtime squares as input grows. Nested loops (e.g., comparing every item in a list to every other).
O(2^n) Exponential Runtime doubles with each new input element. Recursive calculation of Fibonacci (naive), solving the subset-sum problem.
O(n!) Factorial Runtime explodes instantly. Solving the Traveling Salesman Problem via brute force (checking all permutations).

Scaling Visualization (n = 100):

4.3.3 Tractability: Easy vs. Hard

4.3.4 Complexity Classes: P, NP, NP-Complete, and NP-Hard

Formal complexity theory classifies decision problems into classes:

4.3.5 The $1,000,000 Question: P vs. NP

The question "Does P equal NP?" is one of the Millennium Prize Problems (offering $1,000,000). Most computer scientists believe P ≠ NP.

4.3.6 The Hidden Complexity in Python

Python abstracts complexity, but the underlying algorithms still obey these rules:

4.4 Summary Table

Aspect Computability Theory Computational Complexity Theory
Core Question Can we write an algorithm to solve this? How fast can we solve it?
Focus Halting, Decidability. Runtime, Memory usage.
Key Concept Turing Machine, Church-Turing Thesis. Big-O Notation, P vs NP.
Classic Example Halting Problem (Undecidable). Traveling Salesman Problem (NP-Hard).
Practical Implication Some bugs (infinite loops) are impossible to detect automatically. Some programs cannot run in a reasonable time for large inputs.

4.5 Quizzes

Quiz 1: Computability Theory

1. Which of the following is the primary purpose of a Turing Machine?

Answer(B) To serve as a mathematical model for defining what is computable.

2. The Church-Turing Thesis states that:

Answer(B) Every effectively calculable function can be computed by a Turing Machine.

3. An "Undecidable" problem is one that:

Answer(B) Has no algorithm that can solve it correctly for all possible inputs.

4. What conclusion do we draw from the Halting Problem?

Answer(A) It is impossible to write a perfect debugger that detects all infinite loops.

5. In the proof of the Halting Problem's undecidability, what does the TROUBLE program do if the hypothetical HALT function says that TROUBLE(TROUBLE) would halt?

Answer(C) It deliberately goes into an infinite loop.

Quiz 2: Complexity Theory and Big-O

6. What is the time complexity of accessing the first element my_list[0] in a Python list?

Answer(A) O(1)

7. You have an algorithm with O(n²) complexity. If it takes 1 second to process 100 items, roughly how long will it take to process 1000 items?

Answer(B) 100 seconds (Since 1000 is 10 times larger, 10² = 100 times slower. 1s * 100 = 100s).

8. Which of the following functions grows the fastest as n approaches infinity?

Answer(B) O(n!)

9. An algorithm that halves the input size at each step (like binary search) is typically:

Answer(C) O(log n)

Quiz 3: Complexity Classes (P, NP)

10. Which of the following is a characteristic of problems in Class P?

Answer(B) They are solvable in polynomial time on a deterministic computer.

11. Which of the following is the classic definition of a problem in Class NP?

Answer(B) A proposed solution can be verified in polynomial time.

12. If a problem is NP-Complete, and we find a polynomial-time algorithm for it, which is true?

Answer(A) We will have proven that P = NP.

13. Which of the following is considered an NP-Hard problem?

Answer(C) Finding the shortest route that visits every city exactly once (Traveling Salesman - Optimization).

14. Sorting a list of integers using Python's Timsort has what time complexity in the average case?

Answer(B) O(n log n)

15. The "Subset-Sum" problem (given a set of integers, is there a subset summing to zero?) is in which class?

Answer(B) NP-Complete

4.6 Exercises

Exercise 1: Simulating a Simple Turing Machine

Instructions: You are given a Turing Machine that adds 1 to a binary number. The tape starts with a binary number written on it (e.g., 101). The head starts at the rightmost (least significant) digit. The machine has the following rules (states: q0 = start, q1 = carry, q_accept = halt):

Task: Trace the execution for the input 101 (binary for 5). Show the state of the tape and the head position after each step. What is the final binary number on the tape?

Answer Start: Tape `_ 1 0 1 _`. Head over the rightmost '1'. State q0. 1. Read '1'. Rule 2: Write '0', Move Left, Go to q1. Tape: `_ 1 0 0 _`. Head over '0' (middle). State q1. 2. Read '0'. Rule 4: Write '1', Move Left, Go to q_accept. Tape: `_ 1 1 0 _`. Head over '1' (leftmost). State q_accept. Halt. Final tape reads `110` which is binary for 6. (5+1=6). Correct!

Exercise 2: Big-O Ranking

Instructions: Rank the following functions from slowest growing (most efficient) to fastest growing (least efficient) for large input sizes n:

, n!, 1, n log n, , 2^n, n, log n

Answer Slowest (Efficient) to Fastest (Inefficient): 1. 1 (O(1)) 2. log n (O(log n)) 3. n (O(n)) 4. n log n (O(n log n)) 5. n² (O(n²)) 6. n³ (O(n³)) 7. 2^n (O(2^n)) 8. n! (O(n!))

Exercise 3: Categorizing Python Code by Complexity

Instructions: For each of the following Python code snippets, identify the time complexity using Big-O notation (assume n = len(arr)). Briefly explain your reasoning.

Snippet A:

def find_max(arr): max_val = arr[0] for num in arr: if num > max_val: max_val = num return max_val

Snippet B:

def contains_duplicate(arr): for i in range(len(arr)): for j in range(i + 1, len(arr)): if arr[i] == arr[j]: return True return False

Snippet C:

def binary_search(arr, target): low = 0 high = len(arr) - 1 while low <= high: mid = (low + high) // 2 if arr[mid] == target: return True elif arr[mid] < target: low = mid + 1 else: high = mid - 1 return False
Answers - **Snippet A**: O(n) (Linear). It loops through the list exactly once. - **Snippet B**: O(n²) (Quadratic). It is a nested loop. The inner loop runs roughly `n²/2` times. We drop the constant `1/2`, so O(n²). - **Snippet C**: O(log n) (Logarithmic). The search space is halved in each iteration of the while loop.

Exercise 4: Decidable vs. Undecidable Classification

Instructions: Classify each of the following problems as Decidable (solvable by an algorithm) or Undecidable (no algorithm exists). Justify your answer in one sentence.

  1. Determining if a given Python program contains the word print.
  2. Determining if a given Python program will print "Hello" to the console.
  3. Determining if a given Python program will eventually run out of memory.
  4. Determining if a given number is a perfect square.
  5. Determining if a given Python program is a virus.
Answers 1. **Decidable**. This is a simple string search, solved by regex or `in` operator. 2. **Undecidable**. Rice's Theorem states that any non-trivial property of a program's output is undecidable. 3. **Undecidable**. Running out of memory is a runtime property related to termination, reducible to the Halting Problem. 4. **Decidable**. We can use Newton's method or integer square root algorithms—polynomial time. 5. **Undecidable**. Determining malicious intent (semantic property) is a non-trivial property of the program's behavior, hence undecidable by Rice's Theorem.

4.7 Homework Questions

Answer the following questions in complete sentences. Each response should be 3–5 sentences unless otherwise specified.

Short Answer Questions

1. Explain in your own words why the Halting Problem is considered undecidable. Use the concept of the "troublemaker" program in your explanation.

Sample AnswerThe Halting Problem is undecidable because if we assume a perfect Halting detector exists, we can construct a 'troublemaker' program that intentionally does the opposite of what the detector predicts. If the detector says it halts, the troublemaker loops forever; if the detector says it loops, the troublemaker halts. Feeding this troublemaker to itself creates a logical contradiction, proving that such a detector cannot exist.

2. What is the practical implication of the Church-Turing Thesis for a Python programmer?

Sample AnswerThe Church-Turing Thesis implies that Python, being Turing-complete, is theoretically as powerful as any other programming language or computing device. Any algorithm that can be expressed logically can be implemented in Python, provided it is actually computable. However, it also means Python is subject to the same fundamental limitations—it cannot solve undecidable problems any more than any other language can.

3. Distinguish between a problem being "Undecidable" and a problem being "NP-Complete."

Sample AnswerAn undecidable problem has no algorithmic solution at all, meaning we cannot write a program that always gives a correct Yes/No answer. An NP-Complete problem is decidable, meaning an algorithm exists that will eventually produce the correct answer, but the best-known algorithms require exponential time in the worst case, making them infeasible for large inputs.

4. Why is verifying a solution often easier than finding it? Provide an example from the NP class.

Sample AnswerVerification is easier because you already have a candidate solution to test, which usually requires checking the candidate against the constraints of the problem. For example, in the Subset-Sum problem, finding a subset that sums to zero is hard, but if someone gives you the subset, you can simply add up the numbers and confirm the sum in linear time.

5. A student writes a Python program with two nested loops. The outer loop runs n times, and the inner loop runs n times. The student argues it is O(n) because "they are just loops." What is wrong with this reasoning, and what is the correct complexity?

Sample AnswerThe student has overlooked the multiplicative effect of nested loops. For each iteration of the outer loop, the inner loop executes completely, resulting in `n * n = n²` total executions. The correct complexity is O(n²), not O(n).

Essay Questions

Answer the following questions in 300–500 words each.

6. Imagine you are working for a company that needs to solve the Traveling Salesperson Problem (TSP) for a logistics route involving 200 cities. Given what you know about computational complexity, discuss why a brute-force algorithm (checking every permutation) is impossible. Discuss alternative approaches (heuristics, approximations) and how complexity theory guides your software architecture decisions.

Suggested outline:

7. Explain the significance of the P vs. NP problem in the context of modern cybersecurity. Why would most cybersecurity experts prefer that P ≠ NP?

Suggested outline:

Research Questions

These questions require additional research beyond the tutorial content.

8. Research the concept of "Gödel's Incompleteness Theorems." How are these theorems philosophically related to the Halting Problem and Turing's proof of undecidability? (Hint: Consider the concept of self-reference and diagonalization).

9. Research the exact nature of the "Cook-Levin Theorem." Why was the Boolean Satisfiability Problem (SAT) chosen as the first NP-Complete problem, and how does the reduction process (polynomial-time reduction) work in theory?

10. Research the current state of quantum computing. How do quantum algorithms (like Shor's algorithm and Grover's algorithm) affect complexity classes? Specifically, does solving a problem in polynomial time on a quantum computer prove that P = NP? (Hint: Look at the definition of BQP - Bounded Quantum Polynomial Time).

Previous | Tutorial index | Next