๐ซ [CS50x 2025] 5 Data Structures
๐ Linked Lists
To implement a sorted linked list of numbers, we have:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
int main(void)
{
// Memory for numbers
node *list = NULL;
// Build list
for (int i = 0; i < 3; i++)
{
// Allocate node for number
node *n = malloc(sizeof(node));
if (n == NULL)
{
return 1;
}
n->number = get_int("Number: ");
n->next = NULL;
// If list is empty
if (list == NULL)
{
list = n;
}
// If number belongs at beginning of list
else if (n->number < list->number)
{
n->next = list;
list = n;
}
// If number belongs later in list
else
{
// Iterate over nodes in list
for (node *ptr = list; ptr != NULL; ptr = ptr->next)
{
// If at end of list
if (ptr->next == NULL)
{
// Append node
ptr->next = n;
break;
}
// If in middle of list
if (n->number < ptr->next->number)
{
n->next = ptr->next;
ptr->next = n;
break;
}
}
}
}
// Print numbers
...
// Free memory
...
}
This chunk is probably the trickiest.
1
2
3
4
5
6
if (n->number < ptr->next->number)
{
n->next = ptr->next;
ptr->next = n;
break;
}
๐ฏ Letโs visualize it
Letโs say your list currently looks like this:
1
[2] โ [5] โ [8] โ [10]
And the new number is 6
.
We want to insert 6
in the right place so the list stays sorted.
Now we walk with a pointer (ptr
) starting at the head:
- ptr points at [2],
ptr->next
is [5] -6 < 5
? โ - ptr moves to [5],
ptr->next
is [8] -6 < 8
? โ
BOOM. This is where we need to insert 6
!
๐ง So what does the code do?
1
n->next = ptr->next;
- New node (
n
) is pointing to whatptr
was previously pointing to (e.g. [8]).
1
ptr->next = n;
- Now the current nodeโs next is the new node!
So it becomes:
1
[2] โ [5] โ [6] โ [8] โ [10]
A perfect little insertion.
๐ฒ Trees
To make this 3 generations tree into code,
we have:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// Implements a list of numbers as a binary search tree
#include <stdio.h>
#include <stdlib.h>
// Represents a node
typedef struct node
{
int number;
struct node *left;
struct node *right;
}
node;
void free_tree(node *root);
void print_tree(node *root);
int main(void)
{
// Tree of size 0
node *tree = NULL;
// Add number to list
node *n = malloc(sizeof(node));
if (n == NULL)
{
return 1;
}
n->number = 2;
n->left = NULL;
n->right = NULL;
tree = n;
// Add number to list
n = malloc(sizeof(node));
if (n == NULL)
{
free_tree(tree);
return 1;
}
n->number = 1;
n->left = NULL;
n->right = NULL;
tree->left = n;
// Add number to list
n = malloc(sizeof(node));
if (n == NULL)
{
free_tree(tree);
return 1;
}
n->number = 3;
n->left = NULL;
n->right = NULL;
tree->right = n;
// Print tree
print_tree(tree);
// Free tree
free_tree(tree);
return 0;
}
void free_tree(node *root)
{
if (root == NULL)
{
return;
}
free_tree(root->left);
free_tree(root->right);
free(root);
}
void print_tree(node *root)
{
if (root == NULL)
{
return;
}
print_tree(root->left);
printf("%i\n", root->number);
print_tree(root->right);
}
๐ณ Understand tree = n;
1
tree = n;
We earlier had this:
1
node *tree = NULL;
Which means:
โIโm declaring a pointer called
tree
that will eventually point to the root of the binary tree.โ
Then created the first node (with number 2
), like this:
1
2
3
4
node *n = malloc(sizeof(node));
n->number = 2;
n->left = NULL;
n->right = NULL;
Now this new node n
exists in memory - itโs the root of your future tree.
So when we do:
1
tree = n;
Weโre saying:
โLet
tree
point to the root node (which we just created and stored inn
).โ
๐ซ Print Recursion
1
2
3
4
5
6
7
8
9
10
11
12
void print_tree(node *root)
{
if (root == NULL)
{
return;
}
print_tree(root->left);
printf("%i\n", root->number);
print_tree(root->right);
// no need this line ๐๐ป
// print_tree(root->right);
}
This is called: in-order traversal.
Left โ Root โ Right
So if your tree looks like:
2
/ \
1 3
The steps are:
- Go to
left
subtree โ print1
- Print
2
- Go to
right
subtree โ print3
This gives:
1
2
3
When we call:
1
print_tree(tree); // tree points to root = 2
๐ Recursion Steps (visually)
1
2
3
4
5
6
7
8
9
10
print_tree(2)
โโโ print_tree(1)
โ โโโ print_tree(NULL) โ returns
โ โโโ print 1
โ โโโ print_tree(NULL) โ returns
โโโ print 2
โโโ print_tree(3)
โโโ print_tree(NULL) โ returns
โโโ print 3
โโโ print_tree(NULL) โ returns
๐งพ Printed Output:
1
2
3
Thatโs recursion: the function calls itself, each time focusing on a smaller part of the tree. Once it hits NULL
, it climbs back up the call stack and finishes printing the current node, then goes to the right.
๐งช random() % 2
It says: โGive me a random number, but I only care whether itโs 0 or 1.โ
Because:
1
2
random() โ gives you a **big random integer** (like 7834724 or 902394)
% 2 โ turns it into either 0 or 1
๐ฒ This is like flipping a coin
random() value | random() % 2 result |
---|---|
4582738 | 0 |
9204737 | 1 |
8273418 | 0 |
โฆ | 1 or 0 randomly |
๐ค Why does it feel weird & What does random() % 2
actually do?
โGive a big random numberโฆ and then just
mod 2
it into 0 or 1? Why not directly get 0 or 1?โ
Thatโs the illusion of complexity:
- Weโre using a full-sized random number generator to simulate something super small (a coin flip).
- It feels wasteful, and it kinda is - but it works fine.
random() % 2
:
- Calls a system-level random number generator (which gives a big number, maybe 32-bit or 64-bit).
- Then throws away most of that randomness by doing
% 2
, just keeping the least significant bit (either 0 or 1).
๐ A better method - random() & 1
1
2
3
4
int random_bit()
{
return random() & 1; // faster, just grabs the lowest bit
}
This is bitwise AND, not regular math.
1
random() & 1
means:
Take the random number, and keep only its least significant bit (the very last bit, the โ1s placeโ).
โ๏ธ How it works
First, what is &
?
Itโs called the bitwise AND operator. It compares each bit in two numbers:
Rule:
1 & 1 = 1
everything else is 0:
- 1 & 0 = 0
- 0 & 1 = 0
- 0 & 0 = 0
Now letโs say random()
gives you this 8-bit number (pretend for simplicity):
1
random() = 01101110 (binary) = 110 in decimal
Now do:
01101110 (random number)
& 00000001 (binary for 1)
----------
00000000 โ means even โ result is 0
Another example:
1 0 1 1 0 1 1 1 (random number in binary)
& 0 0 0 0 0 0 0 1 (only the last bit is 1)
-------------------
0 0 0 0 0 0 0 1 โ Result: 00000001 (which is just `1`)
So essentially:
- If the last bit is 0 โ result is 0 (even number)
- If the last bit is 1 โ result is 1 (odd number)
Everything else gets wiped out.
Thatโs a fast way to flip a coin:
- 0 = heads
- 1 = tails
๐ srandom(time(0));
Itโs doing two things:
time(0)
This returns the current time in seconds since January 1, 1970 (Unix Epoch). So every time you run your program, this number changes - unless you run it again in the same second.
Example:
1
time(0) โ 1718501826 // A big changing number, like a timestamp
srandom(...)
This is like โplanting a seedโ into your random number generator. It tells the system:
โHey, start the randomness with THIS specific number.โ
So:
1
srandom(time(0));
is basically:
โUse the current time as the seed to start randomness!โ
Because if you donโt do this, then your program will give the same โrandomโ numbers every time you run it.
๐ก Since time(0)
changes every second, why not just use it directly?
time(0)
is just a number
If you write:
1
int r = time(0) % 2;
Youโre not getting randomness - youโre just splitting the current second into either 0
or 1
. So the result only changes once per second, and itโs 100% predictable.
Imagine:
1
2
time(0) = 1718502033 โ 1718502033 % 2 = 1
time(0) = 1718502034 โ 1718502034 % 2 = 0
Thatโs not โrandomโ - thatโs a clock. Not chaos.
But with srandom(time(0))
, weโre doing this:
1
2
3
4
5
// Set the seed
srandom(time(0)); // ๐ฑ
int a = random(); // ๐ฒ fully chaotic
int b = random(); // ๐ฒ new chaos from same seed
This way, you initialize the random number generator using the current time - so:
- Each time you run the program in a new second, it uses a different seed, and therefore gives totally different random numbers.
- But within the same run, it produces a whole sequence of chaotic values.
๐ Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
srandom(time(0)); // ๐งช seed it with current time
// Print 5 random numbers
for (int i = 0; i < 5; i++)
{
printf("Random %d: %ld\n", i, random());
}
return 0;
}
Run this once:
1
2
3
4
5
Random 0: 123456789
Random 1: 219838201
Random 2: 882182838
Random 3: 498192388
Random 4: 388128127
Wait 2 secondsโฆ
Run again:
1
2
3
4
5
Random 0: 129192139
Random 1: 473882019
Random 2: 892831002
Random 3: 192139103
Random 4: 182391281
Different every time.