Skip to content

Commit

Permalink
add course_en.md
Browse files Browse the repository at this point in the history
  • Loading branch information
DawnMagnet committed Apr 15, 2024
1 parent 27b9d7b commit 446ff1f
Showing 1 changed file with 184 additions and 0 deletions.
184 changes: 184 additions & 0 deletions course8/course_en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Modern Programming Ideology: Implementing Queues with Mutable Data Structures

## Overview

In modern programming concepts, a queue is an important data structure that follows the First-In-First-Out (FIFO) principle. This course will show you how to use mutable data structures to implement queues, focusing on two implementation methods: circular queues and singly linked lists, and exploring the concepts of tail call and tail recursion.

## Basic Operations of Queues

A queue supports the following basic operations:

- `make()`: Creates an empty queue.
- `push(t: Int)`: Adds an integer element to the queue.
- `pop()`: Removes an element from the queue.
- `peek()`: Views the front element of the queue.
- `length()`: Obtains the length of the queue.

## Implementation of Circular Queues

Circular queues implement queues using arrays, which provide a continuous storage space where each position can be modified. Once an array is allocated, its length remains fixed.

### Creating an Array

```moonbit expr
let a: Array[Int] = Array::make(5, 0)
```

### Adding Elements

```moonbit expr
let a: Array[Int] = Array::make(5, 0)
a[0] = 1
a[1] = 2
println(a) // Output: [1, 2, 0, 0, 0]
```

### Simple Implementation of Circular Queues

```moonbit no-check
struct Queue {
mut array: Array[Int]
mut start: Int
mut end: Int // end points to the empty position at the end of the queue
mut length: Int
}
fn push(self: Queue, t: Int) -> Queue {
self.array[self.end] = t
self.end = (self.end + 1) % self.array.length() // wrap around to the start of the array if beyond the end
self.length = self.length + 1
self
}
```

### Expanding the Queue

When the number of elements exceeds the length of the array, an expansion operation is required:

```moonbit no-check
fn push(self: Queue, t: Int) -> Queue {
if self.length == self.array.length() {
let new_array: Array[Int] = Array::make(self.array.length() * 2, 0)
let mut i = 0
while i < self.array.length(), i = i + 1 {
new_array[i] = self.array[(self.start + i) % self.array.length()]
}
self.start = 0
self.end = self.array.length()
self.array = new_array
}
self.push(t) // Recursive call to complete the element addition
}
```

### Removing Elements

```moonbit no-check
fn pop(self: Queue) -> Queue {
self.array[self.start] = 0
self.start = (self.start + 1) % self.array.length()
self.length = self.length - 1
self
}
```

### Maintaining the Length of the Queue

```moonbit no-check
fn length(self: Queue) -> Int {
self.length
}
```

### Generic Version of Circular Queues

```moonbit no-check
fn make[T]() -> Queue[T] {
{
array: Array::make(5, T::default()), // Initialize the array with the default value of the type
start: 0,
end: 0,
length: 0
}
}
```

## Implementation of Singly Linked Lists

A singly linked list consists of a series of nodes, each containing data and a reference to the next node.

### Definition of Nodes and Linked Lists

```moonbit
struct Node[T] {
val : T
mut next : Option[Node[T]]
}
struct LinkedList[T] {
mut head : Option[Node[T]]
mut tail : Option[Node[T]]
}
```

### Adding Elements

```moonbit
fn push[T](self: LinkedList[T], value: T) -> LinkedList[T] {
let node = { val: value, next: None }
match self.tail {
None => {
self.head = Some(node)
self.tail = Some(node)
}
Some(n) => {
n.next = Some(node)
self.tail = Some(node)
}
}
self
}
```

### Calculating the Length of the Linked List

A simple recursive function is used to calculate the length of the linked list:

```moonbit
fn length[T](self : LinkedList[T]) -> Int {
fn aux(node : Option[Node[T]]) -> Int {
match node {
None => 0
Some(node) => 1 + aux(node.next)
}
}
aux(self.head)
}
```

### Stack Overflow Problem

When the linked list is too long, the recursive calculation of the length can cause a stack overflow. To solve this problem, we can use tail call optimization.

### Tail Calls and Tail Recursion

A tail call is when the last operation of a function is a function call, and tail recursion is when the last operation is a recursive call to the function itself. Using tail calls can prevent stack overflow problems.

The optimized function for calculating the length of the linked list:

```moonbit
fn length_[T](self: LinkedList[T]) -> Int {
fn aux2(node: Option[Node[T]], cumul: Int) -> Int {
match node {
None => cumul
Some(node) => aux2(node.next, 1 + cumul)
}
}
aux2(self.head, 0)
}
```

## Summary

This course introduced how to define queues using mutable data structures, including the implementation of circular queues and singly linked lists. We also learned about tail calls and tail recursion, and how to optimize with tail calls to avoid stack overflow problems. With this knowledge, we can more effectively manage and manipulate data, improving the performance and stability of our programs.

0 comments on commit 446ff1f

Please sign in to comment.