Parents : Problem Solving In Computer Science

Siblings :

Date and time note was created$= dv.current().file.ctime
Date and time note was modified$= dv.current().file.mtime

Data structures are sort of containers to hold data strategically to improve upon the Time Complexity and space complexity of a certain program. It is also clear from name that “data” is raw information and “structures” are containers.

Topics to cover

  • Arrays (both multi-dimensional and single dimensional)
  • Strings
  • Sliding Window
  • Stack
  • Queue
  • Linked List
  • Binary Search
  • Recursion
  • Trees

Two types of data structures exist

Linear Data Structures

Linear data structures are data structures that store data in a linear fashion.

Following are the linear data structures I have read/know about a little right now.

  • Arrays
  • Linked List
  • Stack
  • Queues

Arrays

Techniques used repeatedly
Normalise values in range
  • Note: Thus to normalise the elements’ index. The number of iterations in the range and to reiterate the array again after going out of array range, we divide it by number of elements in the range.
  • `index % n | newIndex % n

Array operations

Rotate an array
  • To reiterate the values in an array from the start we mod the index value by n (size of the array)
  • To rotate an array we must find the new index where the element from the array would be located or shifted to after k rotations.
Example

For array : [1,2,3,4,5,6,7] and no. of rotations(k) = 3

To find the new index for the value at index ‘i’ we move k positions forward using i + k

But here there are two problems to consider here

  1. The array would go out of bound when we add the k>n or for the last element when k>0.
  2. There would be wasteful iterations make when k is a large number.

To go deep into this solution of this problem review the following:

Lets do the number of rotations upto number of elements in the range of the array i.e 0<=k<=n For k=0 , [1,2,3,4,5,6,7] , No change For k=1 , [7,1,2,3,4,5,6] , 1 element shifts in front For k=2 , [6,7,1,2,3,4,5] , 2 elements shift in front For k=3 , [5,6,7,1,2,3,4] , 3 elements shift in front For k=4 , [4,5,6,7,1,2,3] , 4 elements shift in front For k=5 , [3,4,5,6,7,1,2] , 5 elements shift in front For k=6 , [2,3,4,5,6,7,1] , 6 elements shift in front For k=7 , [1,2,3,4,5,6,7] , No change as all the elements reset back to original position … … … For k=108 [5,6,7,1,2,3,4] (found by the following approach not that I am sitting and doing 108 rotations by hand duh!!💁‍♂️)

As we can see the range after the k>=n starts repeating itself thus we can normalise values in range of arrays,if k>=n by finding the remainder left after dividing the range by n (as it will give the net rotations to be made)

For instance, k=108 becomes k=108 % n = 108 % 7 =3 (net rotations)

Also when in case of i=7 the number goes out of range of array and new index is to be calculated,

We find (i+k) modulus it by n i.e (i+k)%n new index =(7+3) % k=10 % 7= 3.

Merge two sorted arrays

Approach One

Time Complexity: O(n) Space Complexity: O(n)

Requirements: Two sorted arrays, a result (merged) array.

Algorithm
  • Two sorted arrays are traversed using two pointers.
  • While traversing the pointer to one array is checked with the other array pointer and whichever is smaller is pushed to merged array and the pointer also moves forward.
  • After one traversal,any remaining array’s elements are pushed to the merged array.
Pseudo code for merge function
void merge(vector<int>arr,int low,int mid,int high){
	 
}

Concatenation in arrays

If elements are to be added/appended to the end of the same array use following methods

  • use loop and insert elements using traversal by i+n operation onto new array of desired size (most optimised )
  • using Insert function in vectors
  • using push_back on the input array

Counting Elements in Array

The approaches to count elements in an array are as follows

  1. Brute force counting using 2 loops
  2. Brute forces counting by sorting the array first and then calculating the count
  3. Counting the elements simulating hash map mechanism using array when range is defined todo
  4. Counting the elements using Hash map data structure
Brute force counting using 2 loops
vector<pair<int, int>> countArrayUsingLoops(vector<int> &nums)
{
    vector<int> seen;
    int n = nums.size();
    int count = 1;
    vector<pair<int, int>> res;
    for (int i = 0; i < n; i++)
    {
        count = 1;
        if (find(seen.begin(), seen.end(), nums[i]) == seen.end())
        {
            for (int j = i + 1; j < n; j++)
            {
                if (nums[i] == nums[j])
                {
                    count++;
                    seen.push_back(nums[i]); // The element is now marked as seen or visited.
                }
            }
            res.push_back({nums[i], count});
        }
 
        //! This should be avoided as range based for loop iterates over the vector and making operations
        //! over it can cause iterators to become undefined returning ambiguous results. Instead use another vector for checking seen values.
        // for (auto &it : res)
        // {
        //     if (it.first != nums[i])
        //     {
        //         res.push_back({nums[i], count});
        //     }
        // }
    }
    return res;
}
 
Brute forces counting by sorting the array first and then calculating the count
vector<pair<int, int>> countArrayUsingSorting(vector<int> &nums)
{
    int n = nums.size();            // Size of the array
    sort(nums.begin(), nums.end()); // REQUIRED
    vector<pair<int, int>> countArray;
    int count = 1;
    for (int i = 0; i < n; i++)
    {
        if (i < n - 1)
        {
            if (nums[i] == nums[i + 1])
            {
                count++;
            }
            else
            {
                countArray.push_back({nums[i], count});
                count = 1;
            }
        }
        else
        {
            countArray.push_back({nums[i], count});
        }
    }
 
    for (auto &it : countArray)
    {
        cout << it.first << " " << it.second << endl;
    }
    return countArray;
}
Counting the elements using Hash map data structure
vector<pair<int, int>> countArrayUsingHashMap(vector<int> &nums)
{
    unordered_map<int, int> countMap;
    for (auto &it : nums)
        countMap[it]++; // Store the counts and increase if the value is found again i.e according to its frequency
    vector<pair<int, int>> res;
    for (auto &it : countMap)
        res.push_back(it);
    return res;
}
 

Linked List

Reverse a Linked List

To reverse a linked list we need to use three nodes

  1. Dummy node for taking in account new head node
  2. Next node of the current head node to remember the next node after link to it is broken from the actual head node
  3. Current head for traversing the linked list.

Reverse A Linked List.excalidraw

⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠ You can decompress Drawing data with the command palette: ‘Decompress current Excalidraw file’. For more info check in plugin settings under ‘Saving’

Excalidraw Data

Text Elements

Link to original

Non-linear Data Structures

Non-Linear data structures are data structures that store data in a Non-linear fashion.

Following are the non-linear data structures I have read/know about a little right now.

  • Trees
  • Graphs

Trees

Trees fall under the of Non-linear Data Structures as tree store Natural Hierarchical data.

Hierarchical data is a type of data :

  • contains levels
  • levels branch from another levels
  • data is arranged accordingly from high to low.

In real world data which grows eventually and enormously for which insertion and searching must be done more efficiently.Trees is a very useful data structure in such cases.

Example:

In a company the employees are stored in a natural hierarchical data format as depicted by following diagram.

Advantages of trees over other data structures:

1. Searching

Searching is faster than Linear Data Structures as the search space is reduced to a certain area.

2. Insertion

No upper limit to store data i.e a new node can be inserted at any point of time in trees while in arrays the size of data is fixed.

Tree Terminology

  • Node
  • root
  • parent
  • leaf node
  • internal nodes
  • ancestors
  • descendants
  • common ancestor
  • cousins
  • grandparent

Depth of a node “x”

  • Length of path from root to that node (x)
  • No. of edges from root to that node.

Height of a node “x”

  • No of edges in longest path from x to leaf

Height of tree

Height of tree= height of root node = max depth of the tree

Tree is a recursive data structure meaning: In Trees most operations use recursive functions as each portion of a tree is a tree in itself. Trees has a root and sub-trees

to avoid confusion between terminologies: 1. Height: Imagine measuring a person’s height, we do it from toe to head (leaf to root). 2. Depth: Imagine measuring depth of a sea, we do it from earth’s surface to ocean bed (root to leaf).

Other facts

If there are n nodes then there must be n-1 edges present this is so because the root node doesn’t consist of the edge

Binary Tree Construction

To construct a binary tree when two traversal orders are given

  • For construction of any binary tree we need two things
    • root node
    • left and right sub-tree
  • Hence in each question two orders would be given
    • one for retrieving the root node
    • another for retrieving the left and right sub-tree
Construct binary tree from in-order and pre-order

Question Link(Problem 106)

When in-order and post-order traversal is given - Take the root node from the post-order from ending. - Take the left and right sub-tree from starting.

Code
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
  public:
 
    TreeNode * makeTree(vector < int > & inorder, vector < int > & postorder, int start, int end, int & rootIndex, unordered_map < int, int > & umap) {
      if (rootIndex < 0) {
        return NULL;
      }
      if (start > end) {
        return NULL;
      }
      int rootVal;
      rootVal = postorder[rootIndex];
      rootIndex--;
 
      int inorderIndex = umap[rootVal];
      TreeNode * newNode = new TreeNode(rootVal);
      // here the order of the statements matter too they should be according to the 
      // traversal i.e here post order is taken in reverse which is DRL so the right 
      // should be considered first before left.
      newNode -> right = makeTree(inorder, postorder, inorderIndex + 1, end, rootIndex, umap);
      newNode -> left = makeTree(inorder, postorder, start, inorderIndex - 1, rootIndex, umap);
      return newNode;
    }
  TreeNode * buildTree(vector < int > & inorder, vector < int > & postorder) {
    // number of nodes in the tree 
    int n = inorder.size();
    if (n == 0) {
      return NULL;
    }
    int rootIndex = n - 1;
    // for storing the positions in the umap
    unordered_map < int, int > umap;
    for (int i = 0; i < n; i++)
      umap[inorder[i]] = i;
    // for(auto&it:umap)
    //     cout<<it.first<<" "<<it.second<<endl;
 
    return makeTree(inorder, postorder, 0, n - 1, rootIndex, umap);
  }
};
 

Time Complexity: O(N) as the recursion takes O(n) Space Complexity: O(2xN) as the stack uses N nodes at max and hashmap is used for storing indexes

  • Alternative approach includes without use of hashmap for searching then the time complexity turns to O(N^2)

Good solution

Construct binary tree from pre-order and in-order traversal

Question Link (Problem 105)

When in-order and pre-order traversal is given - Take the root node from the pre-order from starting. - Take the left and right sub-tree from starting.

Code
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    // calculate the root element in the inorder array for calculating the left and right subtrees' ranges
    int findIndexInorder(vector<int>inorder,int rootVal){
        // use the find function to find the value in vector and subtract the begin index to get the value in integer
        return (find(inorder.begin(),inorder.end(),rootVal)-inorder.begin());
    }
    // as in this ques we need to create different left and right subtree 
    // its easier to create a separate function "makeTree"
    TreeNode* makeTree(vector<int>& preorder, vector<int>& inorder,int start,int end,int & rootIndex){
        // function contains 4 parameters
        // preorder: array from which we will take the root node 
        // inorder : array from which we will calculate the left and right subtrees 
        // start : variable to store the start of the subtree 
        // end   : variable to store the end of the subtree
        // rootIndex : stores the root index
        
        if(start>end){
            return NULL;// Base condition
        }
        int rootVal=preorder[rootIndex];
        rootIndex++;
        TreeNode* newNode=new TreeNode(rootVal);
        // calculate the element in the inorder array for calculating the left and right subtrees' ranges
        int inorderIndex=findIndexInorder(inorder,rootVal);
        newNode->left=makeTree(preorder,inorder,start,inorderIndex-1,rootIndex);
        newNode->right=makeTree(preorder,inorder,inorderIndex+1,end,rootIndex);
        return newNode;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        // size for traversing and creation of the tree
        int n=preorder.size(); 
        if(n==0){
            return NULL;
        }
        int rootIndex=0;
        // in this method we take the pointers to left and right for constructing the left and right subtrees ranges.
        return makeTree(preorder,inorder,0,n-1,rootIndex);
        
        
    }
};
 

Time Complexity: O(N^2) as the recursion takes O(n) and findIndex() too takes O(n) Space Complexity: O(N) as the stack uses N nodes at max

  • Alternative approach includes to use hashmap for searching then the time complexity turns to O(N)

Tree Traversals

4 methods traversal

  1. Inorder - LDR (left,data,right)
  2. Preorder - DLR
  3. Postorder - LRD
  4. Level order
  5. morris traversal
Inorder traversal

LDR is performed from the root node Example: First the left subtree of a node is taken care of and then the root data of that subtree is printed then right subtree is taken of repeat(recursion) until end of the tree is reached

  • Revise recursion before reading the following code
void inorder(root){
if(root==NULL)
	return;
 inorder(root->left);
 cout<<root->val;
 inorder(root->left);
 
}
void preorder(root){
if(root==NULL)
	return;
 cout<<root->val;
 preorder(root->left);
 preorder(root->left);
 	
}
}
void postorder(root){
if(root==NULL)
	return;
 postorder(root->left);
 postorder(root->left);
 cout<<root->val;
 	
}