contributed by < YangYeh-PD
>
The task is not simply to build APIs for queue operations, but rather to execute the fundamental operations adhering to the specified queue interface. Pay attention to how you express this.
To construct APIs for queue, we first need to know the definitions and how it should be constructed. Consider the definition of the element element_t
in a queue
where we can store a string by the character pointer value
, and list_head
is defined in header file <list.h>
Thus, the implementations of a node are
list_head
into the structure.I came up with the wrong queue picture, here is the correct one.ChenYang YehSat, Feb 24, 2024 19:41 PM
where the head of the queue is defined as
queue_contex_t
.Here is the list of implementations (queue.c
) of queue that support both FIFO and LIFO.
Instead of focusing on the LIFO and FIFO properties, you should explain why the queue_context_t
was designed with its specific members in a C structure.
Thus,
queue_contex_t
not only stores queue basic information, but also connects each independent queues into a chain, which helps us manipulate data.ChenYang YehSun, Feb 25, 2024 03:16 AM
q_new()
Address your motivations and considerations here.
Since we import <lish.h>, we can use the function in the library rather than make the new one, or operate by myself.
Inspired by <
csotaku0926
>
q_free()
Address your motivations and considerations here.
Because we need to free all the memories of the queue, we need to know the pointer of each node, not only the pointer to the structure member list_head
.
The macro container_of()
is defined in <lish.h> which calculates address of structure that contains address pointer. Using macro list_for_each_entry_safe()
to make code less complicated.
Since the member of struct list_head *p
is a pointer itself, we need to pass the indirect pointer &head
which represent the address of the pointer head
.
q_insert_head()
Address your motivations and considerations here.
We can incorporate whether head == NULL
or the failure memory allocation in the same if
statement. And again, we use the API list_add()
instead of adding by myself.
Well, actually we cannot, since it will cause memory leak when first allocating memory to
node
and whenhead == NULL
ChenYang YehSat, Mar 2, 2024 12:50 PM
We need to allocated memory and copy the string into s
and cannot directly assign the pointer to value
. Note that we store strlen(s)
into len
to reduce the function call strlen()
.
If node
is not added into the list successfully, we need to free the space allocated by node
, or it will again cause memory leak.
Inspired by <
marvin0102
>
prev
and next
next
of the head
and prev
of the next node to the inserted node.Given that the preceding is merely a brief segment of code, it's not necessary to indicate specific line numbers for reference. You may simply emphasize the code of interest by incorporating it directly into the ongoing discussion for clarity.
Always write meaningful and informative technical reports with great fluency.
To ensure this report is both informative and flows well, it's important to start by outlining the motivation, considerations, and examples for the suggested functions before compiling a list of code snippets.
Guide to Technical Report Writing
Thanks!ChenYang YehSat, Feb 24, 2024 08:10 AM
q_remove_head()
According to the definition in ISO/IEC 9899 §7.21.2.1, memcpy()
only copies n
characters, which means we still need to add \0
at the end of the string.
The memcpy function copies n characters from the object pointed to by s2 into the object pointed to s1. If copying takes place between objects that overlap, the behavior is undefined.
But why don't we use strcpy()
instead of memcpy()
?
In ISO/IEC 9899 §7.21.2.4, strcpy()
copies the string s2
into s1
without checking whether the memory allocated in s1
is enough, which may cause undefined behavior.
The strcpy function copies the string pointed to by
s2
(including the terminating null character) into the array pointed to bys1
.
In §6.5.10 footnote 92)
If prior invalid pointer operations (such as accesses outside array bounds), produced undefined behavior.
However, it still cannot pass
make test
normally.ERROR: Removed value dolphinU��� != expected value dolphin
ERROR: Removed value bearU��� != expected value bearI don't know why I should add
-5
behind of thestrlen()
…, but it works.
ChenYang YehSun, Feb 25, 2024 00:48 AM
Remove the hacky code and utilize the tools to resolve memory issues.
The problem is solved. It raised from
memcpy()
ininsert_head()
andinsert_tail()
, because I forgot to add'\0'
at the end of the string.
ChenYang YehTue, Feb 27, 2024 06:19 AM
q_delete_mid()
I figured out three different ways for implementation.
Eventually, once the fast pointer come back to the head
, the slow pointer would point to the middle point. But what is the time complexity?
Since the fast pointer traverse 2 nodes and the slow one traverse a node each time, the time complexity would be
2. We also can use q_size()
to get the total size of queue, then divided it by 2 and traverse to the destination point. Since q_size()
takes time, and traversing takes , the time complexity would also be
3. Last but not least, we can make best use of the property of circular doubly linked list. We define left and right pointers, then traverse from head in different ways until they meet, then the meeting point will definitely be the midpoint of the queue.
(Since both left and right pointers have traversed half of the queue.)
Enforce the consistent style when you are going to list the code snip.
Since both left and right pointers take only
the time complexity would be
Check this note and identify the performance concerns by measurements.
Done!ChenYang YehThu, Feb 29, 2024 07:32 AM
In programs with good temporal locality, a memory location referenced once may be referred to multiple times shortly thereafter. In programs with good spatial locality, if a memory location is referenced, the program is likely to reference nearby memory locations in subsequent operations.
From the perspective of spatial locality, the storage locations of nodes are not regular in the linked list, meaning each node is scattered in memory, resulting in poor spatial locality. Therefore, it can be inferred that the spatial locality performance of all algorithms above is the same.
From the view of temporal locality, in the "fast and slow pointer" approach, before the loop concludes and after the fast pointer has completed its traversal of the first half of the linked list, the slow pointer has accessed the same node. Thus time interval between two accesses of the same node is relatively shorter. However, in "single pointer" approach, the second access to the same node occurs after the completion of the first traversal. The time interval between two accesses of the same node is relatively longer.
Method | Temporal Locality | Spatial Locality |
---|---|---|
Single pointer | shorter | same |
Fast and slow pointer | longer | same |
Thus, "Fast and slow pointer" is more cache friendly than single pointer, since it does not frequently trigger cache eviction by changing the contents in memory and reduces the occurrence of cache misses.
TODO: Use perf
to show preliminary measurements.
perf
Consider a singly linked list defined as
TODO: Increase the number of members in the structure above to render cache misses more expensive and enable straightforward comparisons between different approaches.
After we construct the list, we can find the midpoint by both single or fast-slow pointer as we have mentioned above. The followings are c implementations respectively.
Single porinter
Fast-slow pointer
Here are the results of cache reference and cache miss obtained through perf testing with different input sizes.
In most cases, the version employing the fast-slow pointer tends to have fewer cache references and cache misses compared to the single pointer version.
q_delete_dup()
We first define two pointers to element_t
, reference
and traverse
, where reference = head->next
and traverse = reference->next
at the beginning, which means traverse
is two steps ahead of the head
.
If traverse == reference
, then traverse
will keep traversing until it reach the node where traverse != reference
.
Then, reference
will move ahead and delete the previous node until it reach the traverse
node.
But there is a error message in test trace 06.
ERROR: Attempted to free unallocated block. Address = 0x5555555555555555
This may result from deleting the head node by
q_release_element
. But I have checked my code and ensured that such the behaviors is avoided. So I still cannot figure it out.
The question has been resolved. There have two problems in the original code.
- I forget to renew
str_tra
when traverse to the next node.- Because I delete the previous node of the reference node, if the deleted node is coincidentally the first node of the queue, then we call the function
q_release_element(container_of(reference->prev, element_t, list))
is equivalent to
q_release_element(container_of(head, element_t, list))
then the segmentation fault occured. Thus, we need the pointerdelete
to store the deleted node, then free it.
ChenYang YehThu, Feb 29, 2024 07:02 AM
q_reverseK()
This took me very long time to deal with it. I devided this question into three different parts.
HEAD
to record the starting point, and TAIL
to traverse the queue to ensure that the queue is long enough for reverse. To avoid the special case of the first node, we declare TAIL
as the indirect pointer to the next
;HEAD
declare before to delete each node and add it into the stack we just declared.list_splice()
to concatenate the list together.Can you shorten your code by facilitating List APIs?
I can't. Since I don't want to iterate the whole queue, so I cannot use such a macro of
list_for_each_entry(_safe)
. I need to write the loop by myself.ChenYang YehSat, Mar 2, 2024 13:36 PM
q_sort()
I use classic merge sort as implementation.
q_delete_mid()
to find the midpoint.head
and stack
in my code.head
in ascending or decending order.q_merge()
I merge all of queue by the merging technique above. However, since I merge every queue into the first one in order, so the size of queues at the end of the algorithm may be quite unbalanced.
The Fisher-Yates Shuffle, also known as the Knuth Shuffle, is an algorithm used for randomly shuffling the elements of an array or a list. It was first proposed by Ronald Fisher and Frank Yates in 1938 and later popularized by Donald Knuth in his book "The Art of Computer Programming."
The basic idea of the Fisher-Yates Shuffle is to iterate through the array from the first element to the last, moving each element with a randomly selected element to the tail. This process continues until the entire array is shuffled.
q_shuffle()
I added the shuffle
command in qtest.c
and implemented q_shuffle()
using the Fisher-Yates method in queue.c
.
and here is the testing result in qtest
.
To test the randomness of shuffling, I designed an experiment for the Fisher-Yates Shuffle.
Using the Fisher-Yates Shuffle, I changed the order of the string "abc"
.
After shuffling, I calculated the occurrences of the 6 possible different permutations. The experiment was repeated 90,000 times.
The implementation of shuffle()
is
and here is the result
In statistics, after shuffling, we hope that the probability of each permutation appearing is equal.
That is, if we denote by a random variable representing any possible permutation in th experiment, then
where is uniform distribution. ( = "abc", = "cba")
We use hypothesis testing to verify whether if is uniformly distributed or not. Assume that
If the hypothesis is true (cannot be rejected), then the random variable
has an approximate chi-square distribution with degrees of freedom.
Reference: Introduction to Mathematical Statistics by Hogg, McKean and Craig 8/e P.286
Setting the significance value , and , then
Since , and , we cannot reject and hence pass chi-square test.
We expect that a sorted array or a list, after shuffling, will exhibit no discernible pattern in its arrangement, indicating a sufficiently high level of randomness (or maximum entropy).
We first show that if , then the Shannon Entropy reaches at maximum. Then we check whether the entropy of Fisher-Yates Shuffle is match the one of uniform distribution.
The Shannon Entropy is defined as
Here,
We first let then . Since
by Jensen's Inequality, for each convex function,
Since , it follows that
And note that in uniform distribution,
Therefore, under uniform distribution, the entropy is at its maximum value.
Reference:
Proving that Shannon Entropy is maximal for the uniform distribution using convexity.
In our experiment, since there are six possible string permutations, the probability of each string appearing under a uniform distribution is , so the ideal entropy is
The experimental entropy is
Which is very close to .
To implement ttt
command into qtest
, I have done some updates and modifications in the branch of project and qtest.c respectively, including
Add ADD_COMMAND()
in console_init()
.
ADD_COMMAND()
is a macro which defined in console.h.
Obviously, add_cmd()
helps us add commands into qtest
.
Create do_ttt()
function.
Just copy the main function into do_ttt
, then add functions definition of
record_move()
print_moves()
get_input()
do_ttt()
, or it will cause implicit function definition.Add all of relevant files
Here is the structure of my current project.
I tried to include
mcts.[ch]
,negamax.[ch]
andutil.h
into my own created directoryagents
, however, it didn't work with makefile, as now I save them in the same hierarchy.ChenYang YehFri, Mar 8, 2024 3:17 AM
Modify Makefile
Since all of files dependency and compile message has been constructed in original Makefile
, all we need to do is add .o
file in OBJS
.