Vous êtes sur la page 1sur 18

ECNG 2007: Computer Systems and 

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  
 

Vous aimerez peut-être aussi