Skip to content

Commit 29025df

Browse files
DawnMagnetDawnMagnetDawnMagnet
authored
update lecture&course of course8 (#14)
* update lecture&course of course7 * update course_en 7 * fix errors * repair course7 * repair code format * update some details in course7 * fix course and lec7 * fix text * fix course_en7 * fix lec7_en * fix mdlint * fix mdlint * fix lowercase and author * fix error * fix error fix error * add lec8_en.md * add course_en.md * fix lec8_en * fix course7_en * fix * fix some no-check * add content to course8_en.md * fix content to course8_en.md --------- Co-authored-by: DawnMagnet <[email protected]> Co-authored-by: DawnMagnet <[email protected]>
1 parent f14e031 commit 29025df

File tree

5 files changed

+484
-0
lines changed

5 files changed

+484
-0
lines changed

course8/course_en.md

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# Modern Programming Ideology: Implementing Queues with Mutable Data Structures
2+
3+
## Overview
4+
5+
Queues are a fundamental concept in computer science, following the First-In-First-Out (FIFO) principle, meaning the first element added to the queue will be the first one to be removed. The course will explore two primary methods of implementing queues: circular queues and singly linked lists. Additionally, it will delve into tail call and tail recursion concepts, which are essential for optimizing recursive functions.
6+
7+
## Basic Operations of Queues
8+
9+
A queue supports the following basic operations:
10+
11+
- `make()`: Creates an empty queue.
12+
- `push(t: Int)`: Adds an integer element to the queue.
13+
- `pop()`: Removes an element from the queue.
14+
- `peek()`: Views the front element of the queue.
15+
- `length()`: Obtains the length of the queue.
16+
17+
## Implementation of Circular Queues
18+
19+
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.
20+
21+
### Creating an Array
22+
23+
```moonbit expr
24+
let a: Array[Int] = Array::make(5, 0)
25+
```
26+
27+
### Adding Elements
28+
29+
```moonbit expr
30+
let a: Array[Int] = Array::make(5, 0)
31+
a[0] = 1
32+
a[1] = 2
33+
println(a) // Output: [1, 2, 0, 0, 0]
34+
```
35+
36+
### Simple Implementation of Circular Queues
37+
38+
```moonbit
39+
struct Queue {
40+
mut array: Array[Int]
41+
mut start: Int
42+
mut end: Int // end points to the empty position at the end of the queue
43+
mut length: Int
44+
}
45+
46+
fn push(self: Queue, t: Int) -> Queue {
47+
self.array[self.end] = t
48+
self.end = (self.end + 1) % self.array.length() // wrap around to the start of the array if beyond the end
49+
self.length = self.length + 1
50+
self
51+
}
52+
```
53+
54+
### Expanding the Queue
55+
56+
When the number of elements exceeds the length of the array, an expansion operation is required:
57+
58+
```moonbit
59+
fn _push(self: Queue, t: Int) -> Queue {
60+
if self.length == self.array.length() {
61+
let new_array: Array[Int] = Array::make(self.array.length() * 2, 0)
62+
let mut i = 0
63+
while i < self.array.length() {
64+
new_array[i] = self.array[(self.start + i) % self.array.length()]
65+
i = i + 1
66+
}
67+
self.start = 0
68+
self.end = self.array.length()
69+
self.array = new_array
70+
}
71+
self.push(t) // Recursive call to complete the element addition
72+
}
73+
```
74+
75+
### Removing Elements
76+
77+
```moonbit
78+
fn pop(self: Queue) -> Queue {
79+
self.array[self.start] = 0
80+
self.start = (self.start + 1) % self.array.length()
81+
self.length = self.length - 1
82+
self
83+
}
84+
```
85+
86+
### Maintaining the Length of the Queue
87+
88+
The `length` function simply returns the current `length` of the queue.
89+
90+
```moonbit
91+
fn length(self: Queue) -> Int {
92+
self.length
93+
}
94+
```
95+
96+
### Generic Version of Circular Queues
97+
98+
A generic version of the `Queue` is also provided, allowing it to store any type of data, not just integers.
99+
100+
```moonbit no-check
101+
struct Queue[T] {
102+
mut array: Array[T]
103+
mut start: Int
104+
mut end: Int // end points to the empty position at the end of the queue
105+
mut length: Int
106+
}
107+
fn make[T]() -> Queue[T] {
108+
{
109+
array: Array::make(5, T::default()), // Initialize the array with the default value of the type
110+
start: 0,
111+
end: 0,
112+
length: 0
113+
}
114+
}
115+
```
116+
117+
## Implementation of Singly Linked Lists
118+
119+
Singly linked lists are composed of nodes, where each node contains data and a reference (or pointer) to the next node in the sequence.
120+
121+
### Definition of Nodes and Linked Lists
122+
123+
The `Node` struct contains a value and a mutable reference to the next node:
124+
125+
```moonbit
126+
struct Node[T] {
127+
val : T
128+
mut next : Option[Node[T]]
129+
}
130+
131+
struct LinkedList[T] {
132+
mut head : Option[Node[T]]
133+
mut tail : Option[Node[T]]
134+
}
135+
```
136+
137+
`Node[T]` is a generic struct that represents a node in a linked list. It has two fields: `val` and `next`. The `val` field is used to store the value of the node, and its type is `T`, which can be any valid data type. The `next` field represents the reference to the next node in the linked list. It is an optional field that can either hold a reference to the next node or be empty (`None`), indicating the end of the linked list.
138+
139+
`LinkedList[T]` is a generic struct that represents a linked list. It has two mutable fields: `head` and `tail`. The `head` field represents the reference to the first node (head) of the linked list and is initially set to `None` when the linked list is empty. The `tail` field represents the reference to the last node (tail) of the linked list and is also initially set to `None`. The presence of the `tail` field allows for efficient appending of new nodes to the end of the linked list.
140+
141+
### Adding Elements
142+
143+
![](../pics/linked_list.drawio.svg)
144+
145+
![](../pics/linked_list_2.drawio.svg)
146+
147+
- When we add elements, we determine whether the linked list is not empty
148+
- if not, add it to the end of the queue and maintain the linked list relationship
149+
150+
```moonbit
151+
fn push[T](self: LinkedList[T], value: T) -> LinkedList[T] {
152+
let node = { val: value, next: None }
153+
match self.tail {
154+
None => {
155+
self.head = Some(node)
156+
self.tail = Some(node)
157+
}
158+
Some(n) => {
159+
n.next = Some(node)
160+
self.tail = Some(node)
161+
}
162+
}
163+
self
164+
}
165+
```
166+
167+
### Calculating the Length of the Linked List
168+
169+
Initially, a simple recursive function is used to calculate the length of the linked list. However, this can lead to a stack overflow if the list is too long.
170+
171+
```moonbit
172+
fn length[T](self : LinkedList[T]) -> Int {
173+
fn aux(node : Option[Node[T]]) -> Int {
174+
match node {
175+
None => 0
176+
Some(node) => 1 + aux(node.next)
177+
}
178+
}
179+
180+
aux(self.head)
181+
}
182+
```
183+
184+
### Stack Overflow Problem
185+
186+
![alt text](stackoverflow.jpg)
187+
188+
To address the stack overflow issue, the concept of tail calls and tail recursion is introduced. A tail call is a function call that is the last operation in a function, and tail recursion is a specific case where the function calls itself as the last operation.
189+
190+
The following code is a recursion but not a tail recursion function, so it will get into infinity loops and its memory occupation will getting larger during the time. Finally our program will crash because of memory limit excedeed.
191+
192+
![overflow](overflow.png)
193+
194+
### Tail Calls and Tail Recursion
195+
196+
![alt text](tailrecur.jpg)
197+
198+
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. Cause when a function reaches tail call, then the resources hold by the function had already been released, so the resource will not be keep until the next function calling, so even there is a infinity function calling chain, the program will not be crashed.
199+
200+
The optimized `length_` function uses tail recursion to calculate the length of the linked list without risking a stack overflow:
201+
202+
```moonbit
203+
fn length_[T](self: LinkedList[T]) -> Int {
204+
fn aux2(node: Option[Node[T]], cumul: Int) -> Int {
205+
match node {
206+
None => cumul
207+
Some(node) => aux2(node.next, 1 + cumul)
208+
}
209+
}
210+
// tail recursive
211+
aux2(self.head, 0)
212+
}
213+
```
214+
215+
This function uses an accumulator (`cumul`) to keep track of the length as it traverses the list.
216+
217+
## Summary
218+
219+
The course concludes by summarizing the methods taught for implementing queues using mutable data structures, such as circular queues and singly linked lists. It also highlights the importance of understanding and utilizing tail calls and tail recursion to optimize recursive functions and prevent stack overflow, ultimately leading to more efficient and stable program performance.

0 commit comments

Comments
 (0)