Before harnessing the power of templates, Alex toiled over two distinct spells: one for integers and another for doubles. Each spell, represented by the functions '**swapIntegers**' and **'swapDoubles'** , was meticulously crafted to perform the act of value exchange.

One day, faced with the daunting task of handling various types of data, Alex decided to summon the power of templates. Instead of crafting separate spells for each data type, templates allowed Alex to create a versatile spell that could adapt to the needs of different magical creatureswhether they were integers, floating-point numbers, or even custom data structures.

With the newfound magic of templates, Alex seamlessly weaved a universal spell (**swapValues**) that could gracefully handle the exchange of values, transcending the need for separate incantations and showcasing the elegance of generic coding in the enchanted world of C++.

Through the application of templates, Alex not only streamlined the enchanted codebase but also embraced the philosophy of code elegance and efficiency. The saga of swapping values transformed into a testament to the wisdom of generic coding, leaving behind a legacy of reusable and versatile spells in the vast and ever-evolving world of C++.

]]>To declare an array in C++, the programmer specifies the type of the elements and the number of elements required by an array as follows

`type arrayName [ arraySize ];`

This is called a single-dimension array. The **arraySize** must be an integer constant greater than zero and **type** can be any valid C++ data type. For example, to declare a 10-element array called balance of type double, use this statement

`double balance[10];`

You can initialize C++ array elements either one by one or using a single statement as follows

`double balance[5] = {1000.0, 2.0, 3.4, 17.0, 50.0};`

If you omit the size of the array, an array just big enough to hold the initialization is created. Therefore, if you write

`double balance[] = {1000.0, 2.0, 3.4, 17.0, 50.0};`

You will create the same array as you did in the previous example.

`#include <iostream>using namespace std;int main() { int n[10]; // Initialize elements of array n for (int i = 0; i < 10; i++) { n[i] = i + 100; } cout << "Element Value" << endl; // Output each array element's value for (int j = 0; j < 10; j++) { cout << " " << j << " " << n[j] << endl; } return 0;}Output:Element Value 0 100 1 101 2 102 3 103 4 104 5 105 6 106 7 107 8 108 9 109`

Array provides a single name for the group of variables of the same type. Therefore, it is easy to remember the names of all the elements of an array.

Traversing an array is a very simple process; we just need to increment the base address of the array in order to visit each element one by one.

Any element in the array can be directly accessed by using the index.

Array is homogenous. It means that the elements with similar data type can be stored in it.

In array, there is static memory allocation that is size of an array cannot be altered.

There will be wastage of memory if we store less number of elements than the declared size.

C++ allows multidimensional arrays. Here is the general form of a multidimensional array declaration

`type name[size1][size2]...[sizeN];`

For example, the following declaration creates a three-dimensional integer array

`int threedim[5][10][4];`

The simplest form of the multidimensional array is the two-dimensional array. A two-dimensional array is, in essence, a list of one-dimensional arrays. To declare a two-dimensional integer array of size x,y, you would write something as follows

`type arrayName [ x ][ y ];`

Where **type** can be any valid C++ data type and **arrayName** will be a valid C++ identifier.

A two-dimensional array can be thought of as a table, which will have x number of rows and y number of columns. A 2-dimensional array **a**, which contains three rows and four columns can be shown below

Thus, every element in array a is identified by an element name of the form **a[ i ][ j ]**, where a is the name of the array, and i and j are the subscripts that uniquely identify each element in a.

Multidimensional arrays may be initialized by specifying bracketed values for each row. Following is an array with 3 rows and each row has 4 columns.

`int a[3][4] = { {0, 1, 2, 3} , /* initializers for row indexed by 0 */ {4, 5, 6, 7} , /* initializers for row indexed by 1 */ {8, 9, 10, 11} /* initializers for row indexed by 2 */};`

The nested braces, which indicate the intended row, are optional. The following initialization is equivalent to the previous example

`int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};`

An element in the 2-dimensional array is accessed by using the subscripts, i.e., row index and column index of the array. For example

`int val = a[2][3];`

The above statement will take 4^{th} element from the 3^{rd} row of the array. You can verify it in the above diagram.

`#include <iostream>using namespace std;int main () { // an array with 5 rows and 2 columns. int a[5][2] = { {0,0}, {1,2}, {2,4}, {3,6},{4,8}}; // output each array element's value for ( int i = 0; i < 5; i++ ) for ( int j = 0; j < 2; j++ ) { cout << "a[" << i << "][" << j << "]: "; cout << a[i][j]<< endl; } return 0;}Output:a[0][0]: 0a[0][1]: 0a[1][0]: 1a[1][1]: 2a[2][0]: 2a[2][1]: 4a[3][0]: 3a[3][1]: 6a[4][0]: 4a[4][1]: 8`

]]>In a priority queue, each element has a priority value associated with it. When you add an element to the queue, it is inserted in a position based on its priority value. For example, if you add an element with a high priority value to a priority queue, it may be inserted near the front of the queue, while an element with a low priority value may be inserted near the back.

*So, a priority Queue is an extension of the**queue**with the following properties.*

Every item has a priority associated with it.

An element with high priority is dequeued before an element with low priority.

If two elements have the same priority, they are served according to their order in the queue.

In the below priority queue, an element with a maximum ASCII value will have the highest priority. The elements with higher priority are served first.

In a priority queue, generally, the value of an element is considered for assigning the priority.

For example, the element with the highest value is assigned the highest priority and the element with the lowest value is assigned the lowest priority. The reverse case can also be used i.e., the element with the lowest value can be assigned the highest priority. Also, the priority can be assigned according to our needs.

A typical priority queue supports the following operations:

When a new element is inserted in a priority queue, it moves to the empty slot from top to bottom and left to right. However, if the element is not in the correct place then it will be compared with the parent node. If the element is not in the correct order, the elements are swapped. The swapping process continues until all the elements are placed in the correct position.

As you know in a max heap, the maximum element is the root node. It will remove the element which has maximum priority first. Thus, you remove the root node from the queue. This removal creates an empty slot, which will be further filled with new insertions. Then, it compares the newly inserted element with all the elements inside the queue to maintain the heap invariant.

This operation helps to return the maximum element from Max Heap or the minimum element from Min Heap without deleting the node from the priority queue.

As the name suggests, in the ascending-order priority queue, the element with a lower priority value is given a higher priority in the priority list. For example, if we have the following elements in a priority queue arranged in ascending order like 4,6,8,9,10. Here, 4 is the smallest number, therefore, it will get the highest priority in a priority queue so when we dequeue from this type of priority queue, 4 will be removed from the queue and dequeue returns 4.

The root node is the maximum element in a max heap, as you may know. It will also remove the element with the highest priority first. As a result, the root node is removed from the queue. This deletion leaves an empty space, which will be filled with fresh insertions in the future. The heap invariant is then maintained by comparing the newly inserted element to all other entries in the queue.

*Types of Priority Queues*

There is no priority attached to elements in a queue, the rule of first-in-first-out(FIFO) is implemented whereas, in a priority queue, the elements have a priority. The elements with higher priority are served first.

It helps to access the elements in a faster way. This is because elements in a priority queue are ordered by priority, one can easily retrieve the highest priority element without having to search through the entire queue.

The ordering of elements in a Priority Queue is done dynamically. Elements in a priority queue can have their priority values updated, which allows the queue to dynamically reorder itself as priorities change.

Efficient algorithms can be implemented. Priority queues are used in many algorithms to improve their efficiency, such as Dijkstras algorithm for finding the shortest path in a graph and the A* search algorithm for pathfinding.

Included in real-time systems. This is because priority queues allow you to quickly retrieve the highest priority element, they are often used in real-time systems where time is of the essence.

High complexity. Priority queues are more complex than simple data structures like arrays and linked lists, and may be more difficult to implement and maintain.

High consumption of memory. Storing the priority value for each element in a priority queue can take up additional memory, which may be a concern in systems with limited resources.

It is not always the most efficient data structure. In some cases, other data structures like heaps or binary search trees may be more efficient for certain operations, such as finding the minimum or maximum element in the queue.

At times it is less predictable: This is because the order of elements in a priority queue is determined by their priority values, the order in which elements are retrieved may be less predictable than with other data structures like stacks or queues, which follow a first-in, first-out (FIFO) or last-in, first-out (LIFO) order.

`#include <iostream>#include <queue>int main() { // Create a priority queue of integers (default is a max-heap) std::priority_queue<int> maxPriorityQueue; // Enqueue elements with different priorities maxPriorityQueue.push(30); // Higher priority maxPriorityQueue.push(10); maxPriorityQueue.push(50); // Highest priority maxPriorityQueue.push(20); // Dequeue elements (highest priority first) while (!maxPriorityQueue.empty()) { std::cout << maxPriorityQueue.top() << " "; // Get the highest priority element maxPriorityQueue.pop(); // Remove the highest priority element } std::cout << std::endl; // Create a priority queue of integers as a min-heap std::priority_queue<int, std::vector<int>, std::greater<int>> minPriorityQueue; // Enqueue elements with different priorities minPriorityQueue.push(30); // Lower priority minPriorityQueue.push(10); minPriorityQueue.push(50); // Lowest priority minPriorityQueue.push(20); // Dequeue elements (lowest priority first) while (!minPriorityQueue.empty()) { std::cout << minPriorityQueue.top() << " "; // Get the lowest priority element minPriorityQueue.pop(); // Remove the lowest priority element } std::cout << std::endl; return 0;}`

]]>We define a queue to be a list in which all additions to the list are made at one end, and all deletions from the list are made at the other end. The element which is first pushed into the order, the delete operation is first performed on that.

When an element is inserted in a queue, then the operation is known as **Enqueue** and when an element is deleted from the queue, then the operation is known as **Dequeue.** It is important to know that we cannot insert an element if the size of the queue is full and cannot delete an element when the queue itself is empty. If we try to insert an element even after the queue is full, then such a condition is known as overflow whereas, if we try to delete an element even after the queue is empty then such a condition is known as underflow.

Queue can handle multiple data.

We can access both ends.

They are fast and flexible.

Like stacks, Queues can also be represented in an array: In this representation, the Queue is implemented using the array. Variables used in this case are

**Queue:**the name of the array storing queue elements.**qfront**: the index where the first element is stored in the array representing the queue.**rear:**the index where the last element is stored in an array representing the queue.size: the size of the array.

`class Queue{ int *arr; int qfront; int rear; int size; public: Queue(){ size = 100001; arr = new int[size]; qfront = 0; rear = 0; } void enqueue(int data) { if (rear == size) { cout << "Queue is full." << endl; } else { arr[rear] = data; rear++; } } int dequeue() { if (qfront == rear) { return -1; } else { int ans = arr[qfront]; arr[qfront] = -1; qfront++; if (qfront == rear) { qfront = 0; rear = 0; } return ans; } } int front() { if (qfront == rear) { return -1; } else { return arr[qfront]; } } bool isEmpty() { if (qfront == rear) { return true; } else { return false; } } };`

A queue can also be represented using the following entities:

Linked-lists,

Pointers, and

Structures.

`struct Node { int data; Node* next; }; // Define a Queue class class Queue { private: Node* front; Node* rear; public: Queue() { front = nullptr; rear = nullptr; } // Function to check if the queue is empty bool isEmpty() { return front == nullptr; } // Function to add an element to the back of the queue void enqueue(int value) { Node* newNode = new Node; newNode->data = value; newNode->next = nullptr; if (isEmpty()) { front = newNode; rear = newNode; } else { rear->next = newNode; rear = newNode; } } // Function to remove and return an element from the front of the queue int dequeue() { if (isEmpty()) { std::cout << "Queue is empty." << std::endl; return -1; // You can choose any value to indicate an error } Node* temp = front; int value = temp->data; front = front->next; delete temp; return value; } };`

Some of the basic operations for Queue in Data Structure are:

**Enqueue()**Adds (or stores) an element to the end of the queue.**Dequeue()**Removal of elements from the queue.**Peek() or front()-**Acquires the data element available at the front node of the queue without deleting it.**rear()**This operation returns the element at the rear end without removing it.**isFull()**Validates if the queue is full.**isNull()**Checks if the queue is empty.

There are a few supporting operations (auxiliary operations):

Enqueue() operation in Queue **adds (or stores) an element to the end of the queue**.

The following steps should be taken to enqueue (insert) data into a queue:

**Step 1:**Check if the queue is full.**Step 2:**If the queue is full, return overflow error and exit.**Step 3:**If the queue is not full, increment the rear pointer to point to the next empty space.**Step 4:**Add the data element to the queue location, where the rear is pointing.**Step 5:**return success.

Removes (or access) the first element from the queue.

The following steps are taken to perform the dequeue operation:

**Step 1:**Check if the queue is empty.**Step 2:**If the queue is empty, return the underflow error and exit.**Step 3:**If the queue is not empty, access the data where the front is pointing.**Step 4:**Increment the front pointer to point to the next available data element.**Step 5:**The Return success.

This operation returns the element at the front end without removing it.

This operation returns the element at the rear end without removing it.

This operation returns a boolean value that indicates whether the queue is empty or not.

This operation returns a boolean value that indicates whether the queue is full or not.

A large amount of data can be managed efficiently with ease.

Operations such as insertion and deletion can be performed with ease as it follow the first in first out rule.

Queues are useful when a particular service is used by multiple consumers.

Queues are fast in speed for data inter-process communication.

Queues can be used in the implementation of other data structures.

The operations such as insertion and deletion of elements from the middle are time-consuming.

Limited Space.

In a classical queue, a new element can only be inserted when the existing elements are deleted from the queue.

Searching for an element takes O(N) time.

The maximum size of a queue must be defined prior.

**Multi programming:**Multi programming means when multiple programs are running in the main memory. It is essential to organize these multiple programs and these multiple programs are organized as queues.**Network:**In a network, a queue is used in devices such as a router or a switch. another application of a queue is a mail queue which is a directory that stores data and controls files for mail messages.**Job Scheduling:**The computer has a task to execute a particular number of jobs that are scheduled to be executed one after another. These jobs are assigned to the processor one by one which is organized using a queue.**Shared resources:**Queues are used as waiting lists for a single shared resource.

ATM Booth Line

Ticket Counter Line

Key press sequence on the keyboard

CPU task scheduling

Waiting time of each customer at call centres.

To perform push operation we will use push(x,m), where x is the value to be inserted and m is the stack number. Say if you have to push 5 in stack 2 that is S2, the push operation will be push(5,2) and to pop we will use pop(m).

But, there is a drawback in this approach, it is not space optimised. So, we will be looking at another approach.

Now, let us look at our second approach

Suppose that we have an array of 6 elements and we want to store 3 stacks in it. We will need two additional things, the first one is top[], which is an array that will store the position of the top elements of the stacks, its size will be the same as that of the number of stacks. And the other one is next[], it will point to the next element after stack top when stack is filled and it point to the next free space when stack is empty.

Now let us look at the algorithm to push an element:

push(10,1) -> this translates to push 10 in stack 1

Find index

int index = freeSpot

update freeSpot

freeSpot = next[index]

freeSpot becomes 1 from 0

insert the element in array

arr[index] = x

update next

next[index] = top[m-1]

update top

top[m-1] = index

The algorithm for pop operation is the reverse of the push.

`// n stacks in an single array#include <iostream>using namespace std;class Nstack{ int *arr; int *top; int *next; int n, s; int freeSpot;public: Nstack(int N, int S) { n = N; s = S; arr = new int[s]; top = new int[n]; next = new int[s]; // top initialise for (int i = 0; i < n; i++) { top[i] = -1; } // next initialise for (int i = 0; i < s; i++) { next[i] = i + 1; } // update last index value to -1 next[s - 1] = -1; // initialise freeSpot freeSpot = 0; } // Pushes 'x' into the Mth stack. Returns true if get pushed bool push(int x, int m) { // Check for overflow if (freeSpot == -1) { return false; } // find index int index = freeSpot; // update freeSpot freeSpot = next[index]; // insert element into array arr[index] = x; // update next next[index] = top[m - 1]; // update top top[m - 1] = index; return true; } // Pops top element from Mth stack. Returns -1 if the stack is empty int pop(int m) { // check underflow if (top[m - 1] == -1) { return -1; } int index = top[m - 1]; top[m - 1] = next[index]; next[index] = freeSpot; freeSpot = index; return arr[index]; }};int main(){ int numberOfStacks = 3; int SizeOfArray = 10; Nstack nStacks(numberOfStacks, SizeOfArray); // Push some elements into the first stack for (int i = 1; i <= 5; i++) { if (nStacks.push(i, 1)) { cout << "Pushed " << i << " to Stack 1" << endl; } else { cout << "Stack 1 is full. Cannot push " << i << endl; } } // Push some elements into the second stack for (int i = 6; i <= 10; i++) { if (nStacks.push(i, 2)) { cout << "Pushed " << i << " to Stack 2" << endl; } else { cout << "Stack 2 is full. Cannot push " << i << endl; } } // Push some elements into the third stack for (int i = 11; i <= 15; i++) { if (nStacks.push(i, 3)) { cout << "Pushed " << i << " to Stack 3" << endl; } else { cout << "Stack 3 is full. Cannot push " << i << endl; } } // Pop elements from the first stack for (int i = 1; i <= 5; i++) { int value = nStacks.pop(1); if (value != -1) { cout << "Popped " << value << " from Stack 1" << endl; } else { cout << "Stack 1 is empty. Cannot pop." << endl; } } // Pop elements from the second stack for (int i = 1; i <= 5; i++) { int value = nStacks.pop(2); if (value != -1) { cout << "Popped " << value << " from Stack 2" << endl; } else { cout << "Stack 2 is empty. Cannot pop." << endl; } } // Pop elements from the third stack for (int i = 1; i <= 5; i++) { int value = nStacks.pop(3); if (value != -1) { cout << "Popped " << value << " from Stack 3" << endl; } else { cout << "Stack 3 is empty. Cannot pop." << endl; } } return 0;}output:Pushed 1 to Stack 1Pushed 2 to Stack 1Pushed 3 to Stack 1Pushed 4 to Stack 1Pushed 5 to Stack 1Pushed 6 to Stack 2Pushed 7 to Stack 2Pushed 8 to Stack 2Pushed 9 to Stack 2Pushed 11 to Stack 3Stack 3 is full. Cannot push 12`

]]>Infix is the day-to-day notation that we use of format A + B type. The general form can be classified as ** (a op b)** where

Example 1 : A + B

Example 2 : A * B + C / D

Postfix is notation that the compiler uses/converts to while reading left to right and is of format ** AB+** type. The general form can be classified as

Example 1 : AB+

Example 2 : AB*CD/+

Prefix is notation that the compiler uses/converts to while reading right to left (some compilers can also read prefix left to right) and is of format +** AB** type. The general form can be classified as

Example 1 : +AB

Example 2 : +*AB/CD

Scan the infix expression

**from left to right**.If the scanned character is an operand, put it in the postfix expression.

Otherwise, do the following

If the precedence and associativity of the scanned operator are greater than the precedence and associativity of the operator in the stack [or the stack is empty or the stack contains a

**(**], then push it in the stack. [**^**operator is right associative and other operators like**+**,,*****and**/**are left-associative].Check especially for a condition when the operator at the top of the stack and the scanned operator both are

**^**. In this condition, the precedence of the scanned operator is higher due to its right associativity. So it will be pushed into the operator stack.In all the other cases when the top of the operator stack is the same as the scanned operator, then pop the operator from the stack because of left associativity due to which the scanned operator has less precedence.

Else, Pop all the operators from the stack which are greater than or equal to in precedence than that of the scanned operator.

- After doing that Push the scanned operator to the stack. (If you encounter parenthesis while popping then stop there and push the scanned operator in the stack.)

If the scanned character is a

**(**, push it to the stack.If the scanned character is a

**)**, pop the stack and output it until a**(**is encountered, and discard both the parenthesis.Repeat steps

**2-5**until the infix expression is scanned.Once the scanning is over, Pop the stack and add the operators in the postfix expression until it is not empty.

Finally, print the postfix expression.

For example:

Consider the infix expression exp = a+b*c+d

and the infix expression is scanned using the iterator i, which is initialized as i = 0.1st Step: Here i = 0 and exp[i] = a i.e., an operand. So add this in the postfix expression. Therefore, postfix = a.

Add a in the postfix

**2nd Step:**Here i = 1 and exp[i] = + i.e., an operator. Push this into the stack.**postfix = a**and**stack = {+}**.Push + in the stack

**3rd Step:**Now i = 2 and exp[i] = b i.e., an operand. So add this in the postfix expression.**postfix = ab**and**stack = {+}**.Add b in the postfix

**4th Step:**Now i = 3 and exp[i] = * i.e., an operator. Push this into the stack.**postfix = ab**and**stack = {+, *}**.Push * in the stack

**5th Step:**Now i = 4 and exp[i] = c i.e., an operand. Add this in the postfix expression.**postfix = abc**and**stack = {+, *}**.Add c in the postfix

**6th Step:**Now i = 5 and exp[i] = + i.e., an operator. The topmost element of the stack has higher precedence. So pop until the stack becomes empty or the top element has less precedence. * is popped and added in postfix. So**postfix = abc***and**stack = {+}**.Pop * and add in postfix

Now the top element is

**+**which also same precedence. Pop it.**postfix = abc*+**.Pop + and add it in postfix

Now the stack is empty. So push

**+**in the stack.**stack = {+}**.Push + in the stack

**7th Step:**Now i = 6 and exp[i] = d i.e., an operand. Add this in the postfix expression.**postfix = abc*+d**.Add d in the postfix

**Final Step:**Now no element is left. So empty the stack and add it in the postfix expression.**postfix = abc*+d+**.Pop + and add it in postfix

Now the implementation of this algorithm is shown below:

`// C++ code to convert infix expression to postfix#include <iostream>#include <stack>using namespace std;// Function to return precedence of operatorsint prec(char c){ if (c == '^') return 3; else if (c == '/' || c == '*') return 2; else if (c == '+' || c == '-') return 1; else return -1;}// The main function to convert infix expression// to postfix expressionvoid infixToPostfix(string s){ stack<char> st; string result; for (int i = 0; i < s.length(); i++) { char c = s[i]; // If the scanned character is // an operand, add it to output string. if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) result += c; // If the scanned character is an // (, push it to the stack. else if (c == '(') st.push('('); // If the scanned character is an ), // pop and add to output string from the stack // until an ( is encountered. else if (c == ')') { while (st.top() != '(') { result += st.top(); st.pop(); } st.pop(); } // If an operator is scanned else { while (!st.empty() && prec(s[i]) <= prec(st.top())) { result += st.top(); st.pop(); } st.push(c); } } // Pop all the remaining elements from the stack while (!st.empty()) { result += st.top(); st.pop(); } cout << result << endl;}// Driver codeint main(){ string exp = "a+b*(c^d-e)^(f+g*h)-i"; // Function call infixToPostfix(exp); return 0;}Output: abcd^e-fgh*+^*+i-`

Step 1:Reverse the infix expression. Note while reversing each ( will become ) and each ) becomes (.

Step 2:Convert the reversed*.*infix expression to postfix expression

While converting to postfix expression, instead of using pop operation to pop operators with greater than or equal precedence, here we will only pop the operators from stack that have greater precedence.

Step 3:Reverse the postfix expression.

The stack is used to convert infix expression to postfix form.

**Illustration:**

See the below image for a clear idea:

*Convert infix expression to prefix expression*

Below is the C++ implementation of the algorithm.

`// C++ program to convert infix to prefix#include <iostream>#include <stack>using namespace std;// Function to check if the character is an operatorbool isOperator(char c){ return (!isalpha(c) && !isdigit(c));}// Function to get the priority of operatorsint getPriority(char C){ if (C == '-' || C == '+') return 1; else if (C == '*' || C == '/') return 2; else if (C == '^') return 3; return 0;}// Function to convert the infix expression to postfixstring infixToPostfix(string infix){ infix = '(' + infix + ')'; int l = infix.size(); stack<char> char_stack; string output; for (int i = 0; i < l; i++) { // If the scanned character is an // operand, add it to output. if (isalpha(infix[i]) || isdigit(infix[i])) output += infix[i]; // If the scanned character is an // (, push it to the stack. else if (infix[i] == '(') char_stack.push('('); // If the scanned character is an // ), pop and output from the stack // until an ( is encountered. else if (infix[i] == ')') { while (char_stack.top() != '(') { output += char_stack.top(); char_stack.pop(); } // Remove '(' from the stack char_stack.pop(); } // Operator found else { if (isOperator(char_stack.top())) { if (infix[i] == '^') { while ( getPriority(infix[i]) <= getPriority(char_stack.top())) { output += char_stack.top(); char_stack.pop(); } } else { while ( getPriority(infix[i]) < getPriority(char_stack.top())) { output += char_stack.top(); char_stack.pop(); } } // Push current Operator on stack char_stack.push(infix[i]); } } } while (!char_stack.empty()) { output += char_stack.top(); char_stack.pop(); } return output;}// Function to convert infix to prefix notationstring infixToPrefix(string infix){ // Reverse String and replace ( with ) and vice versa // Get Postfix // Reverse Postfix int l = infix.size(); // Reverse infix reverse(infix.begin(), infix.end()); // Replace ( with ) and vice versa for (int i = 0; i < l; i++) { if (infix[i] == '(') { infix[i] = ')'; } else if (infix[i] == ')') { infix[i] = '('; } } string prefix = infixToPostfix(infix); // Reverse postfix reverse(prefix.begin(), prefix.end()); return prefix;}// Driver codeint main(){ string s = ("x+y*z/w+u"); // Function call cout << infixToPrefix(s) << std::endl; return 0;}Output: ++x/*yzwu`

We have already discussed **Infix to Postfix**. Below is an algorithm for Postfix to Infix.

**Algorithm**

Create an empty stack to store operands.

Iterate through each symbol in the postfix expression from left to right.

a. If the symbol is an operand (e.g., a number or a variable):

i. Push it onto the stack as a string.

b. If the symbol is an operator:

i. Pop the top two values from the stack as strings, let's call them

`operand1`

and`operand2`

.ii. Create a new string

`result`

by concatenating`operand1`

, the operator symbol, and`operand2`

within parentheses.iii. Push the

`result`

string back onto the stack.After processing all symbols in the postfix expression, the stack should contain a single string, which represents the infix expression.

Pop the string from the stack, and it is your final infix expression.

Here's a C++ implementation based on this algorithm:

`// CPP program to find infix for// a given postfix.#include <iostream>#include <stack>using namespace std;bool isOperand(char x){return (x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z');}// Get Infix for a given postfix// expressionstring getInfix(string exp){ stack<string> s; for (int i=0; exp[i]!='\0'; i++) { // Push operands if (isOperand(exp[i])) { string op(1, exp[i]); s.push(op); } // We assume that input is // a valid postfix and expect // an operator. else { string op1 = s.top(); s.pop(); string op2 = s.top(); s.pop(); s.push("(" + op2 + exp[i] + op1 + ")"); } } // There must be a single element // in stack now which is the required // infix. return s.top();}// Driver codeint main(){ string exp = "ab*c+"; cout << getInfix(exp); return 0;}Output: ((a*b)+c)`

**Algorithm for Postfix to Prefix**:

Read the Postfix expression from left to right

If the symbol is an operand, then push it onto the Stack

If the symbol is an operator, then pop two operands from the Stack

Create a string by concatenating the two operands and the operator before them.string = operator + operand2 + operand1And push the resultant string back to Stack

Repeat the above steps until end of Postfix expression.

`// CPP Program to convert postfix to prefix#include <iostream>#include <stack>using namespace std;// function to check if character is operator or notbool isOperator(char x){ switch (x) { case '+': case '-': case '/': case '*': return true; } return false;}// Convert postfix to Prefix expressionstring postToPre(string post_exp){ stack<string> s; // length of expression int length = post_exp.size(); // reading from left to right for (int i = 0; i < length; i++) { // check if symbol is operator if (isOperator(post_exp[i])) { // pop two operands from stack string op1 = s.top(); s.pop(); string op2 = s.top(); s.pop(); // concat the operands and operator string temp = post_exp[i] + op2 + op1; // Push string temp back to stack s.push(temp); } // if symbol is an operand else { // push the operand to the stack s.push(string(1, post_exp[i])); } } string ans = ""; while (!s.empty()) { ans += s.top(); s.pop(); } return ans;}// Driver Codeint main(){ string post_exp = "ABC/-AK/L-*"; // Function call cout << "Prefix : " << postToPre(post_exp); return 0;}Output: Prefix : *-A/BC-/AKL`

**Algorithm for Prefix to Infix**:

Read the Prefix expression in reverse order (from right to left)

If the symbol is an operand, then push it onto the Stack

If the symbol is an operator, then pop two operands from the Stack

Create a string by concatenating the two operands and the operator between them.

**string = (operand1 + operator + operand2)**

And push the resultant string back to StackRepeat the above steps until the end of Prefix expression.

At the end, stack will have only 1 string i.e. resultant string

`// C++ Program to convert prefix to Infix#include <iostream>#include <stack>using namespace std;// function to check if character is operator or notbool isOperator(char x) {switch (x) {case '+':case '-':case '/':case '*':case '^':case '%': return true;}return false;}// Convert prefix to Infix expressionstring preToInfix(string pre_exp) {stack<string> s;// length of expressionint length = pre_exp.size();// reading from right to leftfor (int i = length - 1; i >= 0; i--) { // check if symbol is operator if (isOperator(pre_exp[i])) { // pop two operands from stack string op1 = s.top(); s.pop(); string op2 = s.top(); s.pop(); // concat the operands and operator string temp = "(" + op1 + pre_exp[i] + op2 + ")"; // Push string temp back to stack s.push(temp); } // if symbol is an operand else { // push the operand to the stack s.push(string(1, pre_exp[i])); }}// Stack now contains the Infix expressionreturn s.top();}// Driver Codeint main() {string pre_exp = "*-A/BC-/AKL";cout << "Infix : " << preToInfix(pre_exp);return 0;}Output: Infix : ((A-(B/C))*((A/K)-L))`

**Algorithm for Prefix to Postfix**:

Read the Prefix expression in reverse order (from right to left)

If the symbol is an operand, then push it onto the Stack

If the symbol is an operator, then pop two operands from the Stack

Create a string by concatenating the two operands and the operator after them.

**string = operand1 + operand2 + operator**

And push the resultant string back to StackRepeat the above steps until end of Prefix expression.

`// CPP Program to convert prefix to postfix#include <iostream>#include <stack>using namespace std;// function to check if character is operator or notbool isOperator(char x){ switch (x) { case '+': case '-': case '/': case '*': return true; } return false;}// Convert prefix to Postfix expressionstring preToPost(string pre_exp){ stack<string> s; // length of expression int length = pre_exp.size(); // reading from right to left for (int i = length - 1; i >= 0; i--) { // check if symbol is operator if (isOperator(pre_exp[i])) { // pop two operands from stack string op1 = s.top(); s.pop(); string op2 = s.top(); s.pop(); // concat the operands and operator string temp = op1 + op2 + pre_exp[i]; // Push string temp back to stack s.push(temp); } // if symbol is an operand else { // push the operand to the stack s.push(string(1, pre_exp[i])); } } // stack contains only the Postfix expression return s.top();}// Driver Codeint main(){ string pre_exp = "*-A/BC-/AKL"; cout << "Postfix : " << preToPost(pre_exp); return 0;}Output: Postfix : ABC/-AK/L-*`

The provided information explains various notations for representing mathematical expressions, such as infix, postfix, and prefix notations. It also presents algorithms and code implementations for converting expressions between these notations in the context of a programming language like C++.

**Infix Notation**:

Infix notation is the common mathematical notation we use daily, like A + B.

The general form is (a op b), where a and b are operands, and op is an operator.

**Postfix Notation (Reverse Polish Notation, RPN)**:

Postfix notation is used by compilers while reading from left to right, e.g., AB+.

The general form is (ab op), where a and b are operands, and op is an operator.

**Prefix Notation (Polish Notation)**:

Prefix notation is used by some compilers while reading from right to left or left to right, e.g., +AB.

The general form is (op ab), where a and b are operands, and op is an operator.

**Conversion from Infix to Postfix**:

Scan the infix expression from left to right.

Use a stack to manage operators.

Apply operator precedence and associativity rules.

Convert to postfix notation, which is more suitable for evaluation.

**Conversion from Infix to Prefix**:

Reverse the infix expression and switch '(' and ')' symbols.

Convert to postfix notation.

Reverse the postfix expression to obtain the prefix notation.

**Conversion from Postfix to Infix**:

Use a stack to manage operands and operators.

Iterate through the postfix expression, combining operands and operators to form an infix expression.

**Conversion from Prefix to Infix**:

Use a stack to manage operands and operators.

Iterate through the prefix expression (in reverse), combining operands and operators to form an infix expression.

**Conversion from Prefix to Postfix**:

Use a stack to manage operands and operators.

Iterate through the prefix expression (in reverse), combining operands and operators to form a postfix expression.

**Code Implementations**:

C++ code examples are provided for each conversion operation.

The code uses stacks to manage operators and operands.

The algorithms handle operator precedence and associativity rules.

They convert expressions between infix, postfix, and prefix notations based on the specified rules.

These algorithms and code implementations are useful in various programming and compiler-related tasks, especially for handling mathematical expressions in different notations.

]]>The LIFO principle makes stacks particularly useful in scenarios where you need to keep track of elements in a specific order, such as function calls in a program (call stack), undo/redo functionality in applications, and parsing expressions, among other applications in computer science and programming.

In day-to-day life, you may encounter situations that can be analogously related to the stack data structure's Last-In, First-Out (LIFO) principle:

**Stacking Dishes:**After washing dishes, you might stack them in a cabinet. The last dish you place on top of the stack will be the first one you use the next time you set the table.**Stacking Chairs:**When you stack chairs for storage, the chair you stack last is the one you'll need to remove first when you want to use them again.**Stacking Firewood:**If you're stacking firewood for a fireplace or wood stove, the last piece of wood you stack is usually the first one you'll grab when you're ready to use it.

These everyday examples demonstrate how the LIFO principle of a stack can be observed in various situations where items are added and removed in a specific order, with the most recently added item being the first to be accessed or used.

To make manipulations in a stack, there are certain operations provided to us.

**push()**to insert an element into the stack**pop()**to remove an element from the stack**peek()**Returns the top element of the stack.**isEmpty()**returns true if the stack is empty else false.**size()**returns the size of the stack.

Adds an item to the stack. If the stack is full, then it is said to be an **Overflow condition.**

**Algorithm for push():**

Start

Store the element to push into the array

If top is equal to (stackSize - 1 > 1), then go to step 4 else go to step 6

Increment top as top = top+1

Add an element to the position arr[top] = num

Print Stack overflow

` void push(int element) { if (size - top > 1) { top++; arr[top] = element; } else { cout << "Stack Overflow" << endl; } }`

Removes an item from the stack. The items are popped in the reversed order in which they are pushed. If the stack is empty, then it is said to be an **Underflow** **condition.**

**Algorithm for pop():**

Start

Check if top >= 0, then go to step 3 else go to step 4

top--

Print Stack underflow

` void pop() { if (top >= 0) { top--; } else { cout << "Stack Underflow" << endl; } }`

Returns the top element of the stack.

**Algorithm for top():**

Start

Check if top > = 0, then go to step 3 else go to step 4

return arr[top]

Print Stack is empty

` int peek() { if (top >= 0) { return arr[top]; } else { cout << "Stack is empty" << endl; return -1; } }`

Returns true if the stack is empty, else false.

**Algorithm for isEmpty()**:

Start

Check if top == -1, then return true

Else return false

` bool isEmpty() { if (top == -1) { return true; } else { return false; } }`

Using C++ STL

`#include <iostream> #include <stack> using namespace std; int main(){ // Create a stack of integers stack<int> myStack; // push() operation myStack.push(2); myStack.push(3); // pop() operation myStack.pop(); // top() operation cout << "Printing top element " << myStack.top() << endl; // empty() operation if (myStack.empty()) { cout << "Stack is empty " << endl; } else { cout << "Stack is not empty " << endl; } return 0; }`

Using Arrays

`#include <iostream> using namespace std; class Stack { // properties public: int *arr; int top; int size; // behaviour Stack(int size) { this->size = size; arr = new int[size]; top = -1; } void push(int element) { if (size - top > 1) { top++; arr[top] = element; } else { cout << "Stack Overflow" << endl; } } void pop() { if (top >= 0) { top--; } else { cout << "Stack Underflow" << endl; } } int peek() { if (top >= 0) { return arr[top]; } else { cout << "Stack is empty" << endl; return -1; } } bool isEmpty() { if (top == -1) { return true; } else { return false; } } }; int main(){ Stack st(5); st.push(22); st.push(33); st.push(44); cout << st.peek() << endl; st.pop(); cout << st.peek() << endl; cout << st.isEmpty() << endl; }`

We can also implement stack using vectors and linked lists, I will cover that in a letter blog.

Using an array to implement a stack has its own set of advantages and disadvantages:

**Advantages:**

**Efficiency**: Array-based stacks provide constant-time (O(1)) complexity for basic stack operations like push and pop. This makes them very efficient for managing elements in a Last-In, First-Out (LIFO) manner.**Simple Implementation**: Implementing a stack using an array is straightforward to understand. It requires minimal code, making it an excellent choice for basic stack operations.**Deterministic Memory Allocation**: Since arrays have a fixed size when created, using an array for a stack allows for deterministic memory allocation. This can be beneficial in scenarios where memory management needs to be predictable.

**Disadvantages:**

**Fixed Size**: One of the most significant limitations of array-based stacks is their fixed size. You need to specify the size of the array when creating it, and it cannot be changed dynamically. If the stack size exceeds the array size, it leads to stack overflow errors.**Wasted Space**: If you allocate a large array to accommodate potential stack growth, you might end up wasting memory when the stack doesn't use its full capacity.**Limited Capacity**: The stack's capacity is limited to the size of the array, and you cannot add more elements once it's full. This limitation can be problematic if the stack's size is unknown in advance or can vary significantly.**Inefficient Dynamic Resizing**: If you want to implement a stack that can grow and shrink dynamically, you'll need to manage this manually by creating a new array with a larger size, copying elements, and deallocating the old array. This process can be computationally expensive.**No Automatic Memory Management**: Arrays don't provide automatic memory management. If you allocate more memory than necessary or forget to deallocate memory when it's no longer needed, it can lead to memory leaks.

In summary, array-based stacks are efficient and simple for basic stack operations but have limitations in terms of fixed size, wasted space, and manual memory management. Depending on your specific use case, these advantages and disadvantages should guide your decision on whether to use an array-based stack or consider other data structures like linked lists for more dynamic stack management.

Redo-undo features at many places like editors and photoshops.

Forward and backward features in web browsers

Used in many algorithms like Tower of Hanoi, tree traversals, stock span problems, and histogram problems.

In Graph Algorithms like Topological Sorting and Strongly Connected Components

In Memory management, any modern computer uses a stack as the primary management for a running purpose. Each program that is running in a computer system has its memory allocations

String reversal is also another application of stack.

**Application of Stack in real life:**

CD/DVD stand.

Stack of books in a bookshop.

Call centre systems.

Undo and Redo mechanism in text editors.

The history of a web browser is stored in the form of a stack.

Call logs, E-mails, and Google photos in any gallery are also stored in the form of a stack.

YouTube downloads and Notifications are also shown in LIFO format(the latest appears first ).

Allocation of memory by an operating system while executing a process.

**Advantages of Stack:**

**Easy implementation:**Stack data structure is easy to implement using arrays or linked lists, and its operations are simple to understand and implement.**Efficient memory utilization**: Stack uses a contiguous block of memory, making it more efficient in memory utilization as compared to other data structures.**Fast access time:**Stack data structure provides fast access time for adding and removing elements as the elements are added and removed from the top of the stack.**Helps in function calls:**Stack data structure is used to store function calls and their states, which helps in the efficient implementation of recursive function calls.**Supports backtracking:**Stack data structure supports backtracking algorithms, which are used in problem-solving to explore all possible solutions by storing the previous states.**Used in Compiler Design:**Stack data structure is used in compiler design for parsing and syntax analysis of programming languages.**Enables undo/redo operations**: Stack data structure is used to enable undo and redo operations in various applications like text editors, graphic design tools, and software development environments.

**Disadvantages of Stack:**

**Limited capacity:**Stack data structure has a limited capacity as it can only hold a fixed number of elements. If the stack becomes full, adding new elements may result in stack overflow, leading to the loss of data.**No random access:**Stack data structure does not allow for random access to its elements, and it only allows for adding and removing elements from the top of the stack. To access an element in the middle of the stack, all the elements above it must be removed.**Memory management:**Stack data structure uses a contiguous block of memory, which can result in memory fragmentation if elements are added and removed frequently.**Not suitable for certain applications:**Stack data structure is not suitable for applications that require accessing elements in the middle of the stack, like searching or sorting algorithms.**Stack overflow and underflow**: Stack data structure can result in stack overflow if too many elements are pushed onto the stack, and it can result in stack underflow if too many elements are popped from the stack.**Recursive function calls limitations:**While stack data structure supports recursive function calls, too many recursive function calls can lead to stack overflow, resulting in the termination of the program.

In this comprehensive exploration of the stack data structure and its various aspects, we've uncovered its fundamental principles, everyday analogies, basic operations, and real-world applications. We've also delved into its implementation using C++ STL , array-based and linked-list approaches. Additionally, we've discussed the advantages and limitations of the stack data structure.

Stacks, with their Last-In, First-Out (LIFO) behaviour, play a crucial role in computer science and programming. They are vital for managing function calls, undo/redo functionality, and parsing expressions, among other applications. As we've seen in everyday examples like stacking dishes and chairs, the LIFO principle is not confined to the digital realm; it mirrors our experiences in the physical world.

The stack's basic operations, including push, pop, peek/top, isEmpty, and size, empower developers to efficiently manage data in a specific order. We've explored their algorithms and their roles in ensuring proper stack manipulation.

However, it's important to be mindful of its limitations, such as fixed capacity and lack of random access. Understanding these limitations and choosing the appropriate data structure for specific tasks is key to effective problem-solving.

In summary, stacks are not just abstract data structures; they are a fundamental concept with practical applications in our digital and physical worlds, making them an essential topic for anyone delving into computer science and programming.

]]>In simple we can say that a linked list forms a series of connected nodes, where each node has a data field and reference of the next data field in the sequence that is stored in the next field.

A linked list is assessed through the first node of the list known as the ** Head** Node. The last node in the list points to NULL or nullptr, which indicates the end of the list this node is known as the

In the above diagram, I have used the structure of a ** Single-Linked List**. Apart from a Single-linked List, there are more two types of linked lists namely

Let's find out, why.

** Arrays** store data in contiguous memory locations, which allows faster access to an element at a specific index. On the other hand,

Based on the above information we can conclude that Linked Lists are like flexible containers that can change their size easily. They're great when you want to add or remove things without knowing how many you'll have ahead of time. Adding or removing stuff from the middle of a linked list is quick and doesn't mess up the rest. But if you need to find something in a hurry, linked lists might not be as fast as arrays, which are like neatly lined-up boxes where you can grab something instantly. So, use linked lists when you want flexibility and easy changes, but consider arrays when you need fast access to items.

The major difference between these two is as follows:

Linked lists are a popular data structure in computer science, and have several advantages over other data structures, such as arrays. Some of the key advantages of linked lists are:

Dynamic size: Linked lists do not have a fixed size, so you can add or remove elements as needed, without having to worry about the size of the list. This makes linked lists a great choice when you need to work with a collection of items whose size can change dynamically.

Efficient Insertion and Deletion: Inserting or deleting elements in a linked list is fast and efficient, as you only need to modify the reference of the next node, which is an O(1) operation.

Memory Efficiency: Linked lists use only as much memory as they need, so they are more efficient with memory compared to arrays, which have a fixed size and can waste memory if not all elements are used.

Easy to Implement: Linked lists are relatively simple to implement and understand compared to other data structures like trees and graphs.

Flexibility: Linked lists can be used to implement various abstract data types, such as stacks, queues, and associative arrays.

Easy to navigate: Linked lists can be easily traversed, making it easier to find specific elements or perform operations on the list.

In conclusion, linked lists are a powerful and flexible data structure that has several advantages over other data structures, making them a great choice for solving many data structure and algorithm problems.

Linked lists are a popular data structure in computer science, but like any other data structure, they have certain disadvantages as well. Some of the key disadvantages of linked lists are:

Slow Access Time: Accessing elements in a linked list can be slow, as you need to traverse the linked list to find the element you are looking for, which is an O(n) operation. This makes linked lists a poor choice for situations where you need to access elements quickly.

Pointers: Linked lists use pointers to reference the next node, which can make them more complex to understand and use compared to arrays. This complexity can make linked lists more difficult to debug and maintain.

Higher overhead: Linked lists have a higher overhead compared to arrays, as each node in a linked list requires extra memory to store the reference to the next node.

Cache Inefficiency: Linked lists are cache-inefficient because the memory is not contiguous. This means that when you traverse a linked list, you are not likely to get the data you need in the cache, leading to cache misses and slow performance.

Extra memory required: Linked lists require an extra pointer for each node, which takes up extra memory. This can be a problem when you are working with large data sets, as the extra memory required for the pointers can quickly add up.

Linked Lists are used to implement stacks and queues.

It is used for the various representations of trees and graphs.

It is used in

**dynamic memory allocation**( linked list of free blocks).It is used for representing

**sparse matrices**.It is used for the manipulation of polynomials.

It is also used for performing arithmetic operations on long integers.

It is used for finding paths in networks.

In operating systems, they can be used in Memory management, process scheduling and file systems.

Linked lists can be used to improve the performance of algorithms that need to frequently insert or delete items from large collections of data.

Implementing algorithms such as the LRU cache, which uses a linked list to keep track of the most recently used items in a cache.

The list of songs in the music player is linked to the previous and next songs.

In a web browser, previous and next web page URLs are linked through the previous and next buttons.

In the image viewer, the previous and next images are linked with the help of the previous and next buttons.

Switching between two applications is carried out by using alt+tab in Windows and cmd+tab in mac book. It requires the functionality of a circular linked list.

On mobile phones, we save the contacts of people. The newly entered contact details will be placed in the correct alphabetical order. This can be achieved by a linked list to set contact at the correct alphabetical position.

The modifications that we make in documents are created as nodes in the doubly linked list. We can simply use the undo option by pressing Ctrl+Z to modify the contents. It is done by the functionality of a linked list.

Now let us continue with the types of Linked Lists.

As I mentioned earlier there are three types of Linked Lists, well it has other variations as well, these are listed below:

Singly Linked List:

It is the simplest type of linked list in which every node contains some data and a pointer to the next node of the same data type.

The node contains a pointer to the next node means that the node stores the address of the next node in the sequence. A single linked list allows the traversal of data only in one way. Below is the image for the same:

Below is the structure of a singly linked list in C++

`// Node of a singly linked listclass Node {public: int data; // Pointer to next node in LL Node* next;};`

`// C++ program to illustrate creation// and traversal of Singly Linked List#include <bits/stdc++.h>using namespace std;// Structure of Nodeclass Node {public: int data; Node* next;};// Function to print the content of// linked list starting from the// given nodevoid printList(Node* n){ // Iterate till n reaches NULL while (n != NULL) { // Print the data cout << n->data << " "; n = n->next; }}// Driver Codeint main(){ Node* head = NULL; Node* second = NULL; Node* third = NULL; // Allocate 3 nodes in the heap head = new Node(); second = new Node(); third = new Node(); // Assign data in first node head->data = 1; // Link first node with second head->next = second; // Assign data to second node second->data = 2; second->next = third; // Assign data to third node third->data = 3; third->next = NULL; printList(head); return 0;}`

**Output**

`1 2 3`

**Time Complexity:** O(N)

Each node holds a single value and a reference to the next node in the list.

The list has a head, which is a reference to the first node in the list, and a tail, which is a reference to the last node in the list.

The nodes are not stored in a contiguous block of memory, but instead, each node holds the address of the next node in the list.

Accessing elements in a singly linked list requires traversing the list from the head to the desired node, as there is no direct access to a specific node in memory.

**Memory management:**Singly linked lists can be used to implement memory pools, in which memory is allocated and deallocated as needed.**Database indexing**: Singly linked lists can be used to implement linked lists in databases, allowing for fast insertion and deletion operations.**Representing polynomials and sparse matrices:**Singly linked lists can be used to efficiently represent polynomials and sparse matrices, where most elements are zero.**Operating systems:**Singly linked lists are used in operating systems for tasks such as scheduling processes and managing system resources.

**Dynamic memory allocation**: Singly linked lists allow for dynamic memory allocation, meaning that the size of the list can change at runtime as elements are added or removed.**Cache friendliness:**Singly linked lists can be cache-friendly as nodes can be stored in separate cache lines, reducing cache misses and improving performance.**Space-efficient:**Singly linked lists are space-efficient, as they only need to store a reference to the next node in each element, rather than a large block of contiguous memory.

**Poor random access performance**: Accessing an element in a singly linked list requires traversing the list from the head to the desired node, making it slow for random access operations compared to arrays.**Increased memory overhead:**Singly linked lists require additional memory for storing the pointers to the next node in each element, resulting in increased memory overhead compared to arrays.**Vulnerability to data loss:**Singly linked lists are vulnerable to data loss if a nodes next pointer is lost or corrupted, as there is no way to traverse the list and access other elements.**Not suitable for parallel processing:**Singly linked lists are not suitable for parallel processing, as updating a node requires exclusive access to its next pointer, which cannot be easily done in a parallel environment.**Backward traversing not possible:**In singly linked list does not support backward traversing.

Doubly Linked List

A doubly linked list or a two-way linked list is a more complex type of linked list that contains a pointer to the next as well as the previous node in the sequence.

Therefore, it contains three parts of data, a pointer to the next node, and a pointer to the previous node. This would enable us to traverse the list in the backward direction as well. Below is the image for the same:

`// Node of a doubly linked liststruct Node { int data; // Pointer to next node in DLL struct Node* next; // Pointer to the previous node in DLL struct Node* prev;};`

`// C++ program to illustrate creation// and traversal of Doubly Linked List#include <bits/stdc++.h>using namespace std;// Doubly linked list nodeclass Node {public: int data; Node* next; Node* prev;};// Function to push a new element in// the Doubly Linked Listvoid push(Node** head_ref, int new_data){ // Allocate node Node* new_node = new Node(); // Put in the data new_node->data = new_data; // Make next of new node as // head and previous as NULL new_node->next = (*head_ref); new_node->prev = NULL; // Change prev of head node to // the new node if ((*head_ref) != NULL) (*head_ref)->prev = new_node; // Move the head to point to // the new node (*head_ref) = new_node;}// Function to traverse the Doubly LL// in the forward & backward directionvoid printList(Node* node){ Node* last; cout << "\nTraversal in forward" << " direction \n"; while (node != NULL) { // Print the data cout << " " << node->data << " "; last = node; node = node->next; } cout << "\nTraversal in reverse" << " direction \n"; while (last != NULL) { // Print the data cout << " " << last->data << " "; last = last->prev; }}// Driver Codeint main(){ // Start with the empty list Node* head = NULL; // Insert 6. // So linked list becomes 6->NULL push(&head, 6); // Insert 7 at the beginning. So // linked list becomes 7->6->NULL push(&head, 7); // Insert 1 at the beginning. So // linked list becomes 1->7->6->NULL push(&head, 1); cout << "Created DLL is: "; printList(head); return 0;}`

**Output**

`Created DLL is: Traversal in forward direction 1 7 6 Traversal in reverse direction 6 7 1`

**Time Complexity**:

The time complexity of the push() function is O(1) as it performs constant-time operations to insert a new node at the beginning of the doubly linked list. The time complexity of the printList() function is O(n) where n is the number of nodes in the doubly linked list. This is because it traverses the entire list twice, once in the forward direction and once in the backward direction. Therefore, the overall time complexity of the program is O(n).

It is used by web browsers for backward and forward navigation of web pages

LRU ( Least Recently Used ) / MRU ( Most Recently Used ) Cache are constructed using Doubly Linked Lists.

Used by various applications to maintain undo and redo functionalities.

In Operating Systems, a doubly linked list is maintained by thread scheduler to keep track of processes that are being executed at that time.

A DLL can be traversed in both forward and backward directions.

The delete operation in DLL is more efficient if a pointer to the node to be deleted is given.

We can quickly insert a new node before a given node.

In a singly linked list, to delete a node, a pointer to the previous node is needed. To get this previous node, sometimes the list is traversed. In DLL, we can get the previous node using the previous pointer.

Every node of DLL Requires extra space for a previous pointer. It is possible to implement DLL with a single pointer though.

All operations require an extra pointer previous to be maintained. For example, in insertion, we need to modify previous pointers together with the next pointers.

Circular Linked List:

A circular linked list is that in which the last node contains the pointer to the first node of the list.

While traversing a circular linked list, we can begin at any node and traverse the list in any direction forward and backward until we reach the same node we started. Thus, a circular linked list has no beginning and no end. Below is the image for the same:

Below is the structure of the Circular Linked List:

`// Structure for a nodeclass Node {public: int data; // Pointer to next node in CLL Node* next;};`

`// C++ program to illustrate creation// and traversal of Circular LL#include <bits/stdc++.h>using namespace std;// Structure for a nodeclass Node {public: int data; Node* next;};// Function to insert a node at the// beginning of Circular LLvoid push(Node** head_ref, int data){ Node* ptr1 = new Node(); Node* temp = *head_ref; ptr1->data = data; ptr1->next = *head_ref; // If linked list is not NULL then // set the next of last node if (*head_ref != NULL) { while (temp->next != *head_ref) { temp = temp->next; } temp->next = ptr1; } // For the first node else ptr1->next = ptr1; *head_ref = ptr1;}// Function to print nodes in the// Circular Linked Listvoid printList(Node* head){ Node* temp = head; if (head != NULL) { do { // Print the data cout << temp->data << " "; temp = temp->next; } while (temp != head); }}// Driver Codeint main(){ // Initialize list as empty Node* head = NULL; // Created linked list will // be 11->2->56->12 push(&head, 12); push(&head, 56); push(&head, 2); push(&head, 11); cout << "Contents of Circular" << " Linked List\n "; // Function call printList(head); return 0;}`

**Output**

`Contents of Circular Linked List 11 2 56 12`

**Time Complexity:**

Insertion at the beginning of the circular linked list takes O(1) time complexity.

Traversing and printing all nodes in the circular linked list takes O(n) time complexity where n is the number of nodes in the linked list.

Therefore, the overall time complexity of the program is O(n).

Multiplayer games use this to give each player a chance to play.

A circular linked list can be used to organize multiple running applications on an operating system. These applications are iterated over by the OS.

Circular linked lists can be used in resource allocation problems.

Circular linked lists are commonly used to implement circular buffers,

Circular linked lists can be used in simulation and gaming.

Advantages of Circular Linked Lists:**

Any node can be a starting point. We can traverse the whole list by starting from any point. We just need to stop when the first visited node is visited again.

Useful for implementation of a queue.

Circular lists are useful in applications to repeatedly go around the list. For example, when multiple applications are running on a PC, it is common for the operating system to put the running applications on a list and then cycle through them, giving each of them a slice of time to execute, and then making them wait while the CPU is given to another application. It is convenient for the operating system to use a circular list so that when it reaches the end of the list it can cycle around to the front of the list.

Circular Doubly Linked Lists are used for the implementation of advanced data structures like the

**Fibonacci Heap**.Implementing a circular linked list can be relatively easy compared to other more complex data structures like trees or graphs.

Compared to singly linked lists, circular lists are more complex.

Reversing a circular list is more complicated than singly or doubly reversing a circular list.

It is possible for the code to go into an infinite loop if it is not handled carefully.

It is harder to find the end of the list and control the loop.

Although circular linked lists can be efficient in certain applications, their performance can be slower than other data structures in certain cases, such as when the list needs to be sorted or searched.

Circular linked lists dont provide direct access to individual nodes

Doubly Circular linked list

A Doubly Circular linked list or a circular two-way linked list is a more complex type of linked list that contains a pointer to the next as well as the previous node in the sequence. The difference between the doubly linked and circular doubly list is the same as that between a singly linked list and a circular linked list. The circular doubly linked list does not contain null in the previous field of the first node. Below is the image for the same:

Below is the structure of the Doubly Circular Linked List:

`// Node of doubly circular linked liststruct Node { int data; // Pointer to next node in DCLL struct Node* next; // Pointer to the previous node in DCLL struct Node* prev;};`

`// C++ program to illustrate creation// & traversal of Doubly Circular LL#include <bits/stdc++.h>using namespace std;// Structure of a Nodestruct Node { int data; struct Node* next; struct Node* prev;};// Function to insert Node at// the beginning of the Listvoid insertBegin(struct Node** start, int value){ // If the list is empty if (*start == NULL) { struct Node* new_node = new Node; new_node->data = value; new_node->next = new_node->prev = new_node; *start = new_node; return; } // Pointer points to last Node struct Node* last = (*start)->prev; struct Node* new_node = new Node; // Inserting the data new_node->data = value; // Update the previous and // next of new node new_node->next = *start; new_node->prev = last; // Update next and previous // pointers of start & last last->next = (*start)->prev = new_node; // Update start pointer *start = new_node;}// Function to traverse the circular// doubly linked listvoid display(struct Node* start){ struct Node* temp = start; printf("\nTraversal in" " forward direction \n"); while (temp->next != start) { printf("%d ", temp->data); temp = temp->next; } printf("%d ", temp->data); printf("\nTraversal in " "reverse direction \n"); Node* last = start->prev; temp = last; while (temp->prev != last) { // Print the data printf("%d ", temp->data); temp = temp->prev; } printf("%d ", temp->data);}// Driver Codeint main(){ // Start with the empty list struct Node* start = NULL; // Insert 5 // So linked list becomes 5->NULL insertBegin(&start, 5); // Insert 4 at the beginning // So linked list becomes 4->5 insertBegin(&start, 4); // Insert 7 at the end // So linked list becomes 7->4->5 insertBegin(&start, 7); printf("Created circular doubly" " linked list is: "); display(start); return 0;}`

**Output**

`Created circular doubly linked list is: Traversal in forward direction 7 4 5 Traversal in reverse direction 5 4 7`

**Time Complexity:**

Insertion at the beginning of a doubly circular linked list takes O(1) time complexity.

Traversing the entire doubly circular linked list takes O(n) time complexity, where n is the number of nodes in the linked list.

Therefore, the overall time complexity of the program is O(n).

Circular doubly linked lists are used in a variety of applications, some of which include:

**Implementation of Circular Data Structures:**Circular doubly linked lists are extremely helpful in the construction of circular data structures like circular queues and circular buffers, which are both circular in nature.**Implementing Undo-Redo Operations**: Text editors and other software programs can use circular doubly linked lists to implement undo-redo operations.**Music Player Playlist**: Playlists in music players are frequently implemented using circular doubly linked lists. Each song is kept as a node in the list in this scenario, and the list can be circled to play the songs in the order they are listed.**Cache Memory Management**: To maintain track of the most recently used cache blocks, circular doubly linked lists are employed in cache memory management.

**Efficient traversal**: Circular doubly linked lists allow efficient traversal in both forward and backward directions. This makes them useful for applications where bidirectional traversal is required, such as in music playlists or video players.**Constant time insertion and deletion:**Insertion and deletion operations can be performed in constant time at any position in the list, unlike singly linked lists which require traversing the list to find the previous node before insertion or deletion.**More efficient memory allocation**: In a circular doubly linked list, each node contains pointers to both the next and previous nodes, as well as the data element. This means that only a single memory allocation is required for each node, rather than two separate allocations required by singly linked lists.**Useful for implementing data structures:**Circular doubly linked lists are useful for implementing other data structures, such as queues, deques, and hash tables. They also provide a good foundation for implementing algorithms such as graph traversal and sorting.**Can be used for circular data structures:**Circular doubly linked lists are well-suited for circular data structures, where the last element is connected to the first element. Examples of circular data structures include circular buffers and circular queues.

**More complex implementation:**Implementing circular doubly linked lists is more complex than implementing other types of linked lists. This is because each node has to maintain pointers to both the next and previous nodes, as well as handle circularity properly.**More memory overhead:**Each node in a circular doubly linked list contains pointers to both the next and previous nodes, which requires more memory overhead compared to singly linked lists.**More difficult to debug:**Circular doubly linked lists can be more difficult to debug than other types of linked lists. This is because they have more complex pointer relationships and circularity, which can make it harder to trace errors and debug issues.**Not suitable for all applications:**While circular doubly linked lists are useful for many applications, they may not be the best choice for all scenarios. For example, if bidirectional traversal is not required, a singly linked list may be a better choice. Additionally, if memory usage is a concern, a dynamic array or other data structure may be more efficient.**Extra care is needed when modifying the list:**Modifying a circular doubly linked list requires extra care to ensure that the circularity is maintained. If the circularity is broken, it can cause errors and unexpected behavior in the code that uses the list.

Header Linked List:

A header linked list is a special type of linked list that contains a header node at the beginning of the list.

So, in a header linked list

**START**will not point to the first node of the list but**START**will contain the address of the header node. Below is the image for Grounded Header Linked List:

Below is the Structure of the Grounded Header Linked List:

`// Structure of the liststruct link { int info; // Pointer to the next node struct link* next;};`

`// C++ program to illustrate creation// and traversal of Header Linked List#include <bits/stdc++.h>// Structure of the liststruct link { int info; struct link* next;};// Empty Liststruct link* start = NULL;// Function to create header of the// header linked liststruct link* create_header_list(int data){ // Create a new node struct link *new_node, *node; new_node = (struct link*)malloc(sizeof(struct link)); new_node->info = data; new_node->next = NULL; // If it is the first node if (start == NULL) { // Initialize the start start = (struct link*)malloc(sizeof(struct link)); start->next = new_node; } else { // Insert the node in the end node = start; while (node->next != NULL) { node = node->next; } node->next = new_node; } return start;}// Function to display the// header linked liststruct link* display(){ struct link* node; node = start; node = node->next; // Traverse until node is // not NULL while (node != NULL) { // Print the data printf("%d ", node->info); node = node->next; } printf("\n"); // Return the start pointer return start;}// Driver Codeint main(){ // Create the list create_header_list(11); create_header_list(12); create_header_list(13); // Print the list printf("List After inserting" " 3 elements:\n"); display(); create_header_list(14); create_header_list(15); // Print the list printf("List After inserting" " 2 more elements:\n"); display(); return 0;}`

**Output**

`List After inserting 3 elements:11 12 13 List After inserting 2 more elements:11 12 13 14 15`

**Time Complexity:**

The time complexity of creating a new node and inserting it at the end of the linked list is O(1).

The time complexity of traversing the linked list to display its contents is O(n), where n is the number of nodes in the list.

Therefore, the overall time complexity of creating and traversing a header linked list is O(n).

The header linked lists are frequently used to maintain the polynomials in memory. The

*header node*is used to represent the*zero polynomial*.Suppose we have

**F(x) = 5x**^{5}3x^{3}+ 2x^{2}+ x^{1}+10x^{0}From the polynomial represented by F(x) it is clear that this polynomial has two parts,

**coefficient**and**exponent**, where,**x**is**formal parameter**. Hence, we can say that a polynomial is sum of terms, each of which consists of a coefficient and an exponent.The computer implementation requires implementing polynomials as a

*list of pair of coefficient and exponent*. Each of these pairs will constitute a structure, so a polynomial will be represented as a list of structures.If one wants to represent F(x) with help of linked list then the list will contain 5 nodes. When we link each node we get a linked list structure that represents polynomial

**F(x)**.*Addition of polynomials*To add two polynomials, we need to scan them once.

If we find terms with the same exponent in the two polynomials, then we add the coefficients, otherwise, we copy the term of larger exponent into the sum and go on.

When we reach at the end of one of the polynomial, then remaining part of the other is copied into the sum.

Suppose we have two polynomials as illustrated and we have to perform addition of these polynomials.

When we scan first node of the two polynomials, we find that exponential power of first node in the second polynomial is greater than that of first node of the first polynomial.

Here the exponent of the first node of the second polynomial is greater hence we have to copy first node of the second polynomial into the sum.

Then we consider the first node of the first polynomial and once again first node value of first polynomial is compared with the second node value of the second polynomial.

Here the first node exponent value of the first polynomial is greater than the second node exponent value of the second polynomial. We copy the first node of the first polynomial into the sum.

Now consider the second node of the first polynomial and compare it with the second node of the second polynomial.

Here the exponent value of the second node of the second polynomial is greater than the second node of the first polynomial, hence we copy the second node of the second list into the sum.

Now we consider the third node exponent of the second polynomial and compare it with second node exponent value of the first polynomial. We find that both are equal, hence perform addition of their coefficient and copy in to the sum.

This process continues till all the nodes of both the polynomial are exhausted. For example after adding the above two polynomials, we get the following

*resultant polynomial*as shown.

That's all about Linked List I will further cover insertion and deletion into Linked List in my upcoming blogs. So make sure to subscribe me.

Some resources on Linked List for you all:

]]>

Javascript also provides advanced features such as Spread and Rest operators, which can be used to manipulate arrays and objects more efficiently.

Moreover destructuring can also be achieved using Set and Map objects in Javascript. Set and Map are not directly considered as destructuring, they do use similar syntax to the spread operator, which is a useful tool for working with iterable data types.

The above four properties are discussed below :

The spread(**...**) syntax allows an iterable, such as an array or string, to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected. In an object literal, the spread syntax enumerates the properties of an object and adds the key-value pairs to the object being created.

It can be understood with the help of the code below :

`const person {name: "Alex", age: 30};const address {city: "New York", country: "USA"};`

Let's create a new object that contains both the properties of "person" and "address"

`const mergedObj = { ...person, ...address };// spreading the properties of both "person" and "address" into a new object "mergedObj"`

This results in

`{ name: "Alex", age: 30, city: "New York", country: "USA"}`

Spread syntax can be used when all elements from an object or array need to be included in a new array or object, or should be applied one by one in a function call's arguments list. Three distinct places that accept the spread syntax are:

Function arguments list (myFunction(a, ...iterableObj, b))

Array literals ([1, ...iterableObj, "five", 4])

Object literals ({...obj, key: "value"})

Spread can be used in various ways to manipulate arrays :

- Concatenation: Concatenation of arrays is generally achieved by the "concat()" method, but with it can also be achieved with the help of spread.

`const arr1 = [1, 2, 3];const arr2 = [4, 5, 6];const newArr = [...arr1, ...arr2];console.log(newArr); // Output: [1, 2, 3, 4, 5, 6]`

Cloning arrays: "slice()" method is used for shallow copying of arrays but with the help of spread it looks something like this:

`const arr1 = [1, 2, 3]; const arr2 = [...arr1]; console.log(arr2); // Output: [1, 2, 3]`

Merging objects: "Object.assign()" method is used to merge objects into a new object. We can also achieve the same result using spread.

`const obj1 = { name: 'Alex', age: 30 };const obj2 = { city: 'New York', country: 'USA' };const mergedObj = { ...obj1, ...obj2 };console.log(mergedObj); // Output: { name: 'Alex', age: 30, city: 'New York', country: 'USA' }`

Overall the spread syntax provides a convenient and concise way to manipulate arrays.

The rest parameter syntax is a way of representing vardiac functions(indefinite arguments as an array) in Javascript.

Syntax :

`function f(a, b, ...theArgs) { // }`

Note: A function definition can have only one rest parameter and it should be placed at last.

`function myFun(a, b, ...manyMoreArgs) { console.log("a", a); console.log("b", b); console.log("manyMoreArgs", manyMoreArgs);}myFun("one", "two", "three", "four", "five", "six");// a, "one"// b, "two"// manyMoreArgs, ["three", "four", "five", "six"] <-- an array`

...manyMoreArgs is the Rest parameter here.

Argument length :

`function fun1(...theArgs) { console.log(theArgs.length);}fun1(); // 0fun1(5); // 1fun1(5, 6, 7); // 3`

Using parameters in combination with an ordinary array :

`function multiply(multiplier, ...theArgs) { return theArgs.map((element) => multiplier * element);}const arr = multiply(2, 15, 25, 42);console.log(arr); // [30, 50, 84]`

Here the first element of the array is used to multiply the rest element.

From arguments to array :

Array methods can be used in rest parameters but not on argument objects.

`function sortRestArgs(...theArgs) { const sortedArgs = theArgs.sort(); return sortedArgs;}console.log(sortRestArgs(5, 3, 7, 1)); // 1, 3, 5, 7function sortArguments() { const sortedArgs = arguments.sort(); return sortedArgs; // this will never happen}console.log(sortArguments(5, 3, 7, 1));// throws a TypeError (arguments.sort is not a function)`

Rest parameters are useful when you want to create a function that can accept a variable number of arguments without having to explicitly define each argument. This makes your code more flexible and easier to maintain.

One common use case for rest parameters is when you want to create a function that can operate on an arbitrary number of elements in an array. For example, suppose you have an array of numbers and you want to write a function that can calculate the sum of all the numbers in the array:

`function sumArray(numbers) { return numbers.reduce((acc, curr) => acc + curr, 0);}console.log(sumArray([1, 2, 3, 4])); // Output: 10`

But what if we get an array of unknown sizes:

`function sumArray() { var numbers = Array.prototype.slice.call(arguments); return numbers.reduce((acc, curr) => acc + curr, 0);}console.log(sumArray.apply(null, [1, 2, 3, 4])); // Output: 10`

"apply()" method is used to tackle the situation.

With rest, it becomes easy to read and is less cumbersome.

`function sumArray(...numbers) { return numbers.reduce((acc, curr) => acc + curr, 0);}console.log(sumArray(1, 2, 3, 4)); // Output: 10`

The Set objects let us store any type whether it is primitive values or object references.

The "Set()" constructor is used to create a new set. Alternatively, we can pass any iterable such as an array to the "Set()" constructor.

`const mySet = new Set([1,2,3]);`

add(value): Add elements to a Set.

delete(value): Remove elements from a Set.

has(value): It is used to check if a particular element is present in a Set and returns a boolean value.

keys(): Returns a new iterator object that contains the keys for each element in the Set.

entries(): Returns a new iterator object that contains an array of [value, value] for each element in the Set (since Set objects don't have keys).

clear(): Removes the entire element from the Set.

values(): Returns a new iterator object that contains the values of each element in the Set.

forEach(callbackFn, thisArg): Calls the given function for each element in the Set, in insertion order. The second argument "thisArg" is optional and sets the value of this within the callback function.

size(): Return the number of elements in the Set.

`const mySet = new Set();// add(value)mySet.add('apple');mySet.add('banana');mySet.add('cherry');console.log(mySet); // Set { 'apple', 'banana', 'cherry' }// clear()mySet.clear();console.log(mySet); // Set {}// add(value) againmySet.add('apple');mySet.add('banana');mySet.add('cherry');// delete(value)mySet.delete('banana');console.log(mySet); // Set { 'apple', 'cherry' }// has(value)console.log(mySet.has('cherry')); // trueconsole.log(mySet.has('orange')); // false// sizeconsole.log(mySet.size); // 2// keys()const keysIterator = mySet.keys();console.log(keysIterator.next()); // { value: 'apple', done: false }console.log(keysIterator.next()); // { value: 'cherry', done: false }console.log(keysIterator.next()); // { value: undefined, done: true }// values()const valuesIterator = mySet.values();console.log(valuesIterator.next()); // { value: 'apple', done: false }console.log(valuesIterator.next()); // { value: 'cherry', done: false }console.log(valuesIterator.next()); // { value: undefined, done: true }// entries()const entriesIterator = mySet.entries();console.log(entriesIterator.next()); // { value: [ 'apple', 'apple' ], done: false }console.log(entriesIterator.next()); // { value: [ 'cherry', 'cherry' ], done: false }console.log(entriesIterator.next()); // { value: undefined, done: true }// forEach(callbackFn, thisArg)mySet.forEach((value, key, set) => { console.log(value, key, set);});// apple apple Set { 'apple', 'cherry' }// cherry cherry Set { 'apple', 'cherry' }`

For "keys", "values" and "entries" methods, we can also use the Spread operator (...) to convert Set into an array.

`const mySet = new Set(['apple', 'banana', 'cherry']);// Using the spread operator to convert the Set to an arrayconst myArray = [...mySet];console.log(myArray); // [ 'apple', 'banana', 'cherry' ]`

In Javascript, we can iterate over the elements of a Set using various iteration methods ("keys", "values" and "entries")

One way to do this using "for...of" loop or the "next()" method.

`const mySet = new Set(['apple', 'banana', 'cherry']);// Using for...of loop to iterate over Setfor (const fruit of mySet) { console.log(fruit);}//////const mySet = new Set(['apple', 'banana', 'cherry']);// Using the keys() methodconst keysIterator = mySet.keys();for (const key of keysIterator) { console.log(key); // apple, banana, cherry}// Using the values() methodconst valuesIterator = mySet.values();for (const value of valuesIterator) { console.log(value); // apple, banana, cherry}// Using the entries() methodconst entriesIterator = mySet.entries();for (const entry of entriesIterator) { console.log(entry); // [ 'apple', 'apple' ], [ 'banana', 'banana' ], [ 'cherry', 'cherry' ]}////////using next()const mySet = new Set(['apple', 'banana', 'cherry']);// Using entries() method to get iterator for Setconst entries = mySet.entries();// Using while loop to iterate over the Setlet result = entries.next();while (!result.done) { console.log(result.value); result = entries.next();}`

`// BASIC SET OPERATIONfunction isSuperset(set, subset) { for (const elem of subset) { if (!set.has(elem)) { return false; } } return true;}function union(setA, setB) { const _union = new Set(setA); for (const elem of setB) { _union.add(elem); } return _union;}function intersection(setA, setB) { const _intersection = new Set(); for (const elem of setB) { if (setA.has(elem)) { _intersection.add(elem); } } return _intersection;}function symmetricDifference(setA, setB) { const _difference = new Set(setA); for (const elem of setB) { if (_difference.has(elem)) { _difference.delete(elem); } else { _difference.add(elem); } } return _difference;}function difference(setA, setB) { const _difference = new Set(setA); for (const elem of setB) { _difference.delete(elem); } return _difference;}// Examplesconst setA = new Set([1, 2, 3, 4]);const setB = new Set([2, 3]);const setC = new Set([3, 4, 5, 6]);isSuperset(setA, setB); // returns trueunion(setA, setC); // returns Set {1, 2, 3, 4, 5, 6}intersection(setA, setC); // returns Set {3, 4}symmetricDifference(setA, setC); // returns Set {1, 2, 5, 6}difference(setA, setC); // returns Set {1, 2}////////RELATION TO ARRAYSconst myArray = ["value1", "value2", "value3"];// Use the regular Set constructor to transform an Array into a Setconst mySet = new Set(myArray);mySet.has("value1"); // returns true// Use the spread syntax to transform a set into an Array.console.log([...mySet]); // Will show you exactly the same Array as myArray////////Use to remove duplicate elements from an arrayconst numbers = [2, 3, 4, 4, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 5, 32, 3, 4, 5];console.log([...new Set(numbers)]);// [2, 3, 4, 5, 6, 7, 32]`

In conclusion, a Set in JavaScript is a collection of unique values. It provides various methods to add, delete, and manipulate values. The Set object is iterable, which means you can use a loop to iterate through its elements. Additionally, the keys, values, and entries methods can be used to extract specific information from a Set. Overall, the Set is a useful tool for managing collections of data in JavaScript.

The "map()" in JavaScript is a built-in method that is available on arrays. It creates a new array by executing a provided function on every element of the original array. The resulting array has the same length as the original array, with each element replaced by the result of the provided function.

The syntax is as follows:

`// Arrow functionmap((element) => { /* */ })map((element, index) => { /* */ })map((element, index, array) => { /* */ })// Callback functionmap(callbackFn)map(callbackFn, thisArg)// Inline callback functionmap(function (element) { /* */ })map(function (element, index) { /* */ })map(function (element, index, array) { /* */ })map(function (element, index, array) { /* */ }, thisArg)`

`const numbers = [1, 4, 9];const roots = numbers.map((num) => Math.sqrt(num));// roots is now [1, 2, 3]// numbers is still [1, 4, 9]//////const kvArray = [ { key: 1, value: 10 }, { key: 2, value: 20 }, { key: 3, value: 30 },];const reformattedArray = kvArray.map(({ key, value }) => ({ [key]: value }));console.log(reformattedArray); // [{ 1: 10 }, { 2: 20 }, { 3: 30 }]console.log(kvArray);// [// { key: 1, value: 10 },// { key: 2, value: 20 },// { key: 3, value: 30 }// ]`

]]>