Académique Documents
Professionnel Documents
Culture Documents
Software Design (2018/2019)
Lab 3 – Data Structures and Sorting
1. Data Structures
Data structure play an important role in programming, since they form the basis of how data can be
searched, sorted and organized in an efficient manner. This section deals with stacks, queues, vectors
and trees. The information for this section is taken from https://www.hackerearth.com and
http://www.bogotobogo.com.
1.1 Stacks
Stacks are dynamic data structures that follow the Last In First Out (LIFO) principle. The last item to be
inserted into a stack is the first one to be deleted from it. For example, if you have a stack of books
inside a box. The book at the top of the stack is the first item to be moved if you require a book from the
stack.
Stacks have restrictions on the insertion and deletion of elements. Elements can be inserted or deleted
only from one end of the stack, typically the top. The element at the top is referred to as the top
element. The operations of inserting and deleting elements are referred to as push() and pop()
respectively.
Returning to our example, if the first book on top of the stack is taken and not replaced, then the second
book automatically becomes the top element of that stack.
Note that the size of stack changes with each push() and pop() operation. Each operation increases and
decreases the size of the stack by 1, respectively.
The figure on the next page gives a visual representation of a stack:
1 | P a g e
Now, let’s observe some code for the functionality of the stack. We first define our class “myStack” with
all the associated methods that we will be using:
class myStack {
private:
float *bottom;
float *top;
int size;
public:
myStack(int size = 20);
void push(float val);
int num_items() const;
float pop();
int full() const;
int empty() const;
void print() const;
float topElement() const;
~myStack();
};
The use of the keyword const is to ensure a function or method becomes constant. This means that the
method will not modify the object on which it is called. This prevents accidental changes to objects.
2 | P a g e
A brief purpose of each method is listed below:
myStack() is the constructor of the class.
push() inserts an element at the top of the stack.
num_items() returns how many elements are within the stack.
pop() removes an element from the top of the stack.
full() checks to see if the stack is full.
empty() checks to see if the stack is empty.
print() outputs all the contents of the stack to the screen.
topElement() returns the element currently at the top of the stack.
~myStack() is the destructor of the class.
The code for each of these methods can be seen below:
myStack::myStack(int N) {
bottom = new float[N];
top = bottom;
size = N;
}
myStack::~myStack() {
delete[] bottom;
}
int myStack::num_items() const {
return (top ‐ bottom);
}
void myStack::push(float val) {
*top = val;
top++;
}
float myStack::pop() {
top‐‐;
return *top;
}
int myStack::full() const {
return (num_items() >= size);
}
int myStack::empty() const{
return (num_items() <= 0);
}
void myStack::print() const {
std::cout << "Stack currently holds " << num_items() << " items: ";
for (float *element = bottom; element < top; element++) {
std::cout << " " << *element;
3 | P a g e
}
std::cout << "\n";
}
float myStack::topElement() const {
return *(top‐1);
}
Note the use of private variables, pointers and the new and delete constructs. Each of these methods
are tested in the main function. The code for the main function is seen below:
int main(){
myStack S(4);
S.print();
std::cout << "\n";
S.push(2.31);
S.push(1.19);
S.push(6.78);
S.push(0.54);
S.print();
std::cout << "\n";
if (!S.full()) S.push(6.7); // this should do nothing, as
// stack is already full.
std::cout << "The top element in the stack is " << S.topElement() << std::endl;
S.print();
std::cout << "\n";
std::cout << "Popped value is: " << S.pop() << "\n";
S.print();
std::cout << "\n";
S.push(S.pop() + S.pop());
std::cout << "Replace top two items with their sum: \n";
S.print();
std::cout << "\n";
S.pop();
S.pop();
S.print();
std::cout << "\n";
if (!S.empty()) S.pop(); // this should also do nothing,
// as stack is already empty.
if (S.num_items() != 0)
{
std::cout << "Error: Stack is corrupt!\n";
}
4 | P a g e
// destructor for S automatically called
getchar();
return EXIT_SUCCESS;
}
Note how each of the methods operate with respect to the stack, as well as how the contents of the
stack are modified.
1.2 Queues
Queues are data structures that follow the First In First Out (FIFO) principle. The first element that is
added to the queue is the first one to be removed. The queue demonstrated here is referred to as a
circular queue.
Elements are always added to the back and removed from the front. Think of it as a line of people
waiting for a bus. The person who is at the beginning of the line is the first one to enter the bus.
The figure below gives a visual representation of a queue:
5 | P a g e
Now, let’s observe the code for the functionality of the queue.
#include<iostream>
#define MAX_SIZE 101 //maximum size of the array that will store Queue.
// Creating a class named Queue.
class Queue
{
private:
int A[MAX_SIZE];
int front, rear;
public:
// Constructor ‐ set front and rear as ‐1.
// We are assuming that for an empty Queue, both front and rear will be ‐1.
Queue()
{
front = ‐1;
rear = ‐1;
}
// To check wheter Queue is empty or not
bool IsEmpty()
{
return (front == ‐1 && rear == ‐1);
}
bool IsFull() {
if ((rear + 1) % MAX_SIZE == front) {
return true;
}
else {
return false;
}
}
// Inserts an element in queue at rear end
void Enqueue(int x)
{
std::cout << "Enqueuing " << x << " \n";
if (IsFull())
{
std::cout << "Error: Queue is Full\n";
return;
}
if (IsEmpty())
{
front = rear = 0;
}
else
{
rear = (rear + 1) % MAX_SIZE;
}
A[rear] = x;
}
// Removes an element in Queue from front end.
void Dequeue()
6 | P a g e
{
std::cout << "Dequeuing \n";
if (IsEmpty())
{
std::cout << "Error: Queue is Empty\n";
return;
}
else if (front == rear)
{
rear = front = ‐1;
}
else
{
front = (front + 1) % MAX_SIZE;
}
}
// Returns element at front of queue.
int Front()
{
if (front == ‐1)
{
std::cout << "Error: cannot return front from empty queue\n";
return ‐1;
}
return A[front];
}
/*
Printing the elements in queue from front to rear.
This function is only to test the code.
This is not a standard function for Queue implementation.
*/
void Print()
{
// Finding number of elements in queue
int count = (rear + MAX_SIZE ‐ front) % MAX_SIZE + 1;
std::cout << "Queue : ";
for (int i = 0; i <count; i++)
{
int index = (front + i) % MAX_SIZE; // Index of element while
travesing circularly from front
std::cout << A[index] << " ";
}
std::cout << "\n\n";
}
};
Note the difference in how the methods are coded, when compared to the stack implementation. You
may use either way, whichever one is more comfortable.
7 | P a g e
A brief purpose of each method is listed below:
Queue() is the constructor for the class.
IsEmpty() checks if the queue is empty.
IsFull() checks if the queue is full.
Enqueue() adds an element to the rear end of the queue.
Dequeue() removes an element at the front end of the queue.
Front() returns the element at the front end of the queue.
Print() outputs all the contents of the queue to the screen.
The code for the main function, which tests the functionality of each method, can be seen below:
int main()
{
Queue Q; // creating an instance of Queue.
Q.Enqueue(2); Q.Print();
Q.Enqueue(4); Q.Print();
Q.Enqueue(6); Q.Print();
Q.Dequeue(); Q.Print();
Q.Enqueue(8); Q.Print();
getchar();
}
Try reducing the queue size to 2 and observe what happens. Try also to implement a size method for the
queue.
1.3 Binary Tree
A binary tree is made of nodes, where each node contains a left pointer, a right pointer and a data
element.
The root pointer points to the topmost node in the tree. The left and right pointers recursively point to
smaller subtrees on either side. A null pointer represents a binary tree with no elements i.e. an empty
tree.
A binary search tree (BST) or ordered binary tree is a type of binary tree where the nodes are arranged
in order. What this means is that for each node, all elements in its left subtree are less than or equal to
the node, and all the elements in its right subtree are greater than the node.
Binary search trees are fast at insert and lookup operations. On average, a binary search tree algorithm
can locate a node in an n node tree in order 𝐥𝐨𝐠 𝟐 𝒏 time. Binary search trees are good for dictionary
8 | P a g e
problems where the code inserts and looks up information indexed by some key. The defined time
stated in the previous sentence is in the average case; it is quite possible for a particular tree to be
slower depending on its shape.
The figure below gives a visual representation of what a binary search tree looks like:
If we consider the root node with data “10”, the following statements can be made:
Data in the left subtree is given as [5, 1, 6]
All the left subtree data elements are less than 10
Data in the right subtree is given as [19, 17]
All the right subtree data elements are more than 10
[10] is considered to be the root node.
[5, 19] are considered to be children nodes of [10]. In this case, [10] is the parent node of [5, 19].
[1,6] are children nodes of [5], as well as [17] is a child node of [19].
A leaf (external) node is a node that has no children, while a nonleaf node is referred to as an internal
node. A level of a tree consists of all nodes at the same depth. The height of a node in a tree is the
number of edges (hops) on the longest downward path from the node to a leaf, and the height of a tree
is the height of its root.
In our example figure above, leaf nodes are considered to be [1, 6, 17] which are all on the same level.
The height of the tree is considered to be 3, since there are a maximum of 3 levels.
9 | P a g e
The following are some functions that are important for the operation of a binary search tree:
1. Look Up: A fast and simple operation. Particularly useful for data storage. Eliminates half the
nodes from a search on each iteration until the result is found.
2. New Node: Makes the root node
3. Insert Node: This begins as how a search would begin. If the root is not equal to the insertion
value, the left and right subtrees are searched. Eventually, an external node is reached and the
insertion value is added as its left or right child, depending on the value of the external node.
4. Is the given tree BST: This checks if the node traversal outputs the elements in increasing order.
5. Size: Total number of nodes in the tree.
6. Maximum / Minimum Depth: The number of nodes along the longest / shortest path from the
root node down to the farthest leaf node.
7. Is balanced: Defined as a binary tree in which the height of two subtrees of every node never
differ by more than one.
8. Minimum / Maximum Value
9. Clear (Delete Node)
10. In Order Print: Prints the values in a binary search tree in sorted order. Performs the traversal
operation first on the left subtree, then the node itself, then finally the right subtree.
11. Pre Order Print: Prints the values in a binary search tree in a counterclockwise manner, starting
at the root. Performs the traversal operation first on the node itself, then the left subtree, then
finally the right subtree.
12. Post Order Print: Performs the traversal operation first on the left subtree, then the right
subtree, then finally on the node itself.
The next few pages show the associated code for all of these functions:
10 | P a g e
/* Binary Tree */
#include <iostream>
struct Tree
{
char data;
Tree *left;
Tree *right;
Tree *parent;
};
Tree * lookUp(struct Tree *node, char key)
{
if (node == NULL) return node;
if (node‐>data == key) return node;
else {
if (node‐>data < key)
return lookUp(node‐>right, key);
else
return lookUp(node‐>left, key);
}
}
Tree *newTreeNode(int data)
{
Tree *node = new Tree;
node‐>data = data;
node‐>left = NULL;
node‐>right = NULL;
node‐>parent = NULL;
return node;
}
Tree* insertTreeNode(struct Tree *node, int data)
{
static Tree *p;
Tree *retNode;
//if(node != NULL) p = node;
if (node == NULL) {
retNode = newTreeNode(data);
retNode‐>parent = p;
return retNode;
}
if (data <= node‐>data) {
p = node;
node‐>left = insertTreeNode(node‐>left, data);
}
else {
p = node;
node‐>right = insertTreeNode(node‐>right, data);
}
return node;
}
11 | P a g e
void isBST(struct Tree *node)
{
static int lastData = INT_MIN;
if (node == NULL) return;
isBST(node‐>left);
/* check if the given tree is BST */
if (lastData < node‐>data)
lastData = node‐>data;
else {
std::cout << "Not a BST" << std::endl;
return;
}
isBST(node‐>right);
return;
}
int treeSize(struct Tree *node)
{
if (node == NULL) return 0;
else
return treeSize(node‐>left) + 1 + treeSize(node‐>right);
}
int maxDepth(struct Tree *node)
{
if (node == NULL || (node‐>left == NULL && node‐>right == NULL))
return 0;
int leftDepth = maxDepth(node‐>left);
int rightDepth = maxDepth(node‐>right);
return leftDepth > rightDepth ?
leftDepth + 1 : rightDepth + 1;
}
int minDepth(struct Tree *node)
{
if (node == NULL || (node‐>left == NULL && node‐>right == NULL))
return 0;
int leftDepth = minDepth(node‐>left);
int rightDepth = minDepth(node‐>right);
return leftDepth < rightDepth ?
leftDepth + 1 : rightDepth + 1;
}
bool isBalanced(struct Tree *node)
{
if (maxDepth(node) ‐ minDepth(node) <= 1)
return true;
else
return false;
}
12 | P a g e
/* Tree Minimum */
Tree* minTree(struct Tree *node)
{
if (node == NULL) return NULL;
while (node‐>left)
node = node‐>left;
return node;
}
/* Tree Maximum */
Tree* maxTree(struct Tree *node)
{
while (node‐>right)
node = node‐>right;
return node;
}
void clear(struct Tree *node)
{
if (node != NULL) {
clear(node‐>left);
clear(node‐>right);
delete node;
}
}
/* print tree in order */
/* 1. Traverse the left subtree.
2. Visit the root.
3. Traverse the right subtree.
*/
void printTreeInOrder(struct Tree *node)
{
if (node == NULL) return;
printTreeInOrder(node‐>left);
std::cout << node‐>data << " ";
printTreeInOrder(node‐>right);
}
/* print tree in postorder*/
/* 1. Traverse the left subtree.
2. Traverse the right subtree.
3. Visit the root.
*/
void printTreePostOrder(struct Tree *node)
{
if (node == NULL) return;
printTreePostOrder(node‐>left);
printTreePostOrder(node‐>right);
std::cout << node‐>data << " ";
}
/* print in preorder */
/* 1. Visit the root.
2. Traverse the left subtree.
13 | P a g e
3. Traverse the right subtree.
*/
void printTreePreOrder(struct Tree *node)
{
if (node == NULL) return;
std::cout << node‐>data << " ";
printTreePreOrder(node‐>left);
printTreePreOrder(node‐>right);
}
/* get the level of a node element: root level = 0 */
int getLevel(struct Tree *node, int elm, int level)
{
if (node == NULL) return 0;
if (elm == node‐>data)
return level;
else if (elm < node‐>data)
return getLevel(node‐>left, elm, level + 1);
else
return getLevel(node‐>right, elm, level + 1);
}
int main(int argc, char **argv)
{
char ch, ch1, ch2;
Tree *found;
Tree *succ;
Tree *pred;
Tree *ancestor;
char charArr[9]
= { 'A','B','C','D','E','F','G','H','I' };
Tree *root = newTreeNode('F');
insertTreeNode(root, 'B');
insertTreeNode(root, 'A');
insertTreeNode(root, 'D');
insertTreeNode(root, 'C');
insertTreeNode(root, 'E');
insertTreeNode(root, 'G');
insertTreeNode(root, 'I');
insertTreeNode(root, 'H');
/* is the tree BST? */
isBST(root);
/* size of tree */
std::cout << "size = " << treeSize(root) << std::endl;
/* max depth */
std::cout << "max depth = " << maxDepth(root) << std::endl;
/* min depth */
std::cout << "min depth = " << minDepth(root) << std::endl;
/* balanced tree? */
14 | P a g e
if (isBalanced(root))
std::cout << "This tree is balanced!\n";
else
std::cout << "This tree is not balanced!\n";
/* min value of the tree*/
if (root)
std::cout << "Min value = " << minTree(root)‐>data << std::endl;
/* max value of the tree*/
if (root)
std::cout << "Max value = " << maxTree(root)‐>data << std::endl;
/* get the level of a data: root level = 0 */
std::cout << "Node B is at level: " << getLevel(root, 'B', 0) << std::endl;
std::cout << "Node H is at level: " << getLevel(root, 'H', 0) << std::endl;
std::cout << "Node F is at level: " << getLevel(root, 'F', 0) << std::endl;
ch = 'B';
found = lookUp(root, ch);
if (found) {
std::cout << "Min value of subtree " << ch << " as a root is "
<< minTree(found)‐>data << std::endl;
std::cout << "Max value of subtree " << ch << " as a root is "
<< maxTree(found)‐>data << std::endl;
}
/* print tree in order */
std::cout << "increasing sort order\n";
printTreeInOrder(root);
std::cout << std::endl;
/* print tree in postorder*/
std::cout << "post order \n";
printTreePostOrder(root);
std::cout << std::endl;
/* print tree in preorder*/
std::cout << "pre order \n";
printTreePreOrder(root);
std::cout << std::endl;
/* lookUp */
ch = 'D';
found = lookUp(root, ch);
if (found)
std::cout << found‐>data << " is in the tree\n";
else
std::cout << ch << " is not in the tree\n";
/* lookUp */
ch = 'M';
found = lookUp(root, ch);
if (found)
std::cout << found‐>data << " is in the tree\n";
else
std::cout << ch << " is not in the tree\n";
15 | P a g e
/* deleting a tree */
clear(root);
getchar();
return 0;
}
2. Sorting
There are three sorting algorithms that are presented for this lab: insertion, bubble and selection. The
corresponding C++ code for these algorithms is also presented in this section.
2.1 Insertion
The insertion sort algorithm traverses the data it needs to until the segment that is being sorted, is
sorted. This does mean that it will iterate through all of the data after every pass.
Two loops are required for insertion sort, with two loop variables, ‘i’ and ‘j’. Both loop variables begin on
the same index after every pass of the first loop. The second loop only executes if variable ‘j’ is greater
than index 0 AND arr[j] is less than arr[j‐1].
This means that if ‘j’ has not reached the end of the data AND the value of the index where ‘j’ is smaller
than the value of the index to the left of ‘j’, finally ‘j’ is decremented. As long as these two conditions are
met in the second loop, it will keep executing.
The code for the insertion sort is seen below. Try to figure out how you would output each stage of the
sorting process, in order to understand the algorithm better.
void insertion_sort(int arr[], int length) {
int j, temp;
for (int i = 0; i < length; i++) {
j = i;
while (j > 0 && arr[j] < arr[j ‐ 1]) {
temp = arr[j];
arr[j] = arr[j ‐ 1];
arr[j ‐ 1] = temp;
j‐‐;
}
}
}
16 | P a g e
2.2 Selection
Selection sort is a basic algorithm for sorting data. Its simplicity proves useful for sorting small amounts
of data.
Selection sort works by starting at the beginning array (index 0) and traverses the entire array,
comparing each value with the current index. If the value is smaller than the current index, then that
index is saved.
Once the loop has traversed all the data and if a smaller value than the current index was found, a swap
is made between the current index, in the index where the smaller value was found. The current index is
then incremented, now to index 1, and the algorithm repeats.
The code for the selection sort is seen below. Try to figure out how you would output each stage of the
process, in order to understand the algorithm better.
void selectSort(int arr[], int n)
{
//pos_min is short for position of min
int pos_min, temp;
for (int i = 0; i < n ‐ 1; i++)
{
pos_min = i;//set pos_min to the current index of array
for (int j = i + 1; j < n; j++)
{
if (arr[j] < arr[pos_min])
pos_min = j;
//pos_min will keep track of the index that min is in, this is
needed when a swap happens
}
//if pos_min no longer equals i than a smaller value must have been found,
so a swap must occur
if (pos_min != i)
{
temp = arr[i];
arr[i] = arr[pos_min];
arr[pos_min] = temp;
}
}
}
17 | P a g e
2.3 Bubble
For a bubble sort, as elements are sorted, they gradually “bubble” (or rise) to their proper location in the
array.
The bubble sort repeatedly compares adjacent elements of an array. The first and second elements are
compared and swapped if they are out of order. Then the second and third elements and compared and
swapped if out or order. The sorting process continues until the last two elements of the array are
compared and swapped if out of order.
When this first pass through the array is complete, the bubble sort returns to elements one and two and
starts the process all over again. The bubble sort knows that it is finished when it examines the entire
array and no swaps are needed. The bubble sort keeps track of occurring swaps by the use of a flag.
The bubble sort algorithm is easy to program, but it is slower than many other sorts, since it performs
more instructions.
The code for the bubble sort algorithm can be seen below:
void swap(int *xp, int *yp)
{
int temp = *xp;
*xp = *yp;
*yp = temp;
}
// An optimized version of Bubble Sort
void bubbleSort(int arr[], int n)
{
int i, j;
bool swapped;
for (i = 0; i < n ‐ 1; i++)
{
swapped = false;
for (j = 0; j < n ‐ i ‐ 1; j++)
{
if (arr[j] > arr[j + 1])
{
swap(&arr[j], &arr[j + 1]);
swapped = true;
}
}
// IF no two elements were swapped by inner loop, then break
if (swapped == false)
break;
}
}
18 | P a g e