{"id":18013227,"url":"https://github.com/tajpouria/algorithms-and-data-structures-cheat-sheet","last_synced_at":"2026-02-17T19:34:15.335Z","repository":{"id":37906698,"uuid":"206094912","full_name":"tajpouria/algorithms-and-data-structures-cheat-sheet","owner":"tajpouria","description":"Providing a summary of important algorithms and data structures along with their key characteristics and use cases. It can be used as a quick reference for solving programming problems or refreshing knowledge of foundational computer science concepts.","archived":false,"fork":false,"pushed_at":"2025-12-01T21:27:26.000Z","size":1476,"stargazers_count":763,"open_issues_count":0,"forks_count":144,"subscribers_count":9,"default_branch":"master","last_synced_at":"2026-02-06T13:48:08.781Z","etag":null,"topics":["algorithms","datastructures","javascript","typescript"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tajpouria.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2019-09-03T14:16:40.000Z","updated_at":"2026-02-06T05:45:13.000Z","dependencies_parsed_at":"2023-02-07T23:16:28.485Z","dependency_job_id":"f94d43d7-9717-4042-b2d0-19b01521f30f","html_url":"https://github.com/tajpouria/algorithms-and-data-structures-cheat-sheet","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/tajpouria/algorithms-and-data-structures-cheat-sheet","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tajpouria%2Falgorithms-and-data-structures-cheat-sheet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tajpouria%2Falgorithms-and-data-structures-cheat-sheet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tajpouria%2Falgorithms-and-data-structures-cheat-sheet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tajpouria%2Falgorithms-and-data-structures-cheat-sheet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tajpouria","download_url":"https://codeload.github.com/tajpouria/algorithms-and-data-structures-cheat-sheet/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tajpouria%2Falgorithms-and-data-structures-cheat-sheet/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29555346,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-17T18:16:07.221Z","status":"ssl_error","status_checked_at":"2026-02-17T18:16:04.782Z","response_time":100,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["algorithms","datastructures","javascript","typescript"],"created_at":"2024-10-30T03:20:31.123Z","updated_at":"2026-02-17T19:34:10.326Z","avatar_url":"https://github.com/tajpouria.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003c!-- TOC start (generated with https://github.com/derlin/bitdowntoc) --\u003e\n\n- [Algorithms and Data Structures Cheat Sheet](#algorithms-and-data-structures-cheat-sheet)\n  * [Big O Notation](#big-o-notation)\n  * [Big O Notation for some of the Objects and Arrays methods](#big-o-notation-for-some-of-the-objects-and-arrays-methods)\n  * [Space Complexity](#space-complexity)\n  * [Common Problem-Solving Patterns](#common-problem-solving-patterns)\n    + [Frequency Counter](#frequency-counter)\n  * [Multiple Pointers](#multiple-pointers)\n    + [Sliding Window](#sliding-window)\n    + [Divide-and-Conquer](#divide-and-conquer)\n    + [Recursion](#recursion)\n      - [Understanding the Call Stack](#understanding-the-call-stack)\n  * [Searching Algorithms](#searching-algorithms)\n    + [Linear Search](#linear-search)\n  * [Sorting Algorithms](#sorting-algorithms)\n    + [Bubble Sort](#bubble-sort)\n    + [Selection Sort](#selection-sort)\n    + [Insertion Sort](#insertion-sort)\n    + [Merge Sort](#merge-sort)\n    + [Quick Sort](#quick-sort)\n    + [Radix Sort](#radix-sort)\n  * [Data Structure](#data-structure)\n    + [complexity comparison](#complexity-comparison)\n  * [Singly Linked list](#singly-linked-list)\n  * [Doubly Linked List](#doubly-linked-list)\n  * [Stacks](#stacks)\n  * [Queue](#queue)\n  * [Tree](#tree)\n    + [terminology](#terminology)\n    + [binary search tree](#binary-search-tree)\n    + [tree traversal](#tree-traversal)\n    + [traversal comparison](#traversal-comparison)\n  * [Binary heaps](#binary-heaps)\n    + [terminology](#terminology-1)\n    + [binary heap parent and child relations](#binary-heap-parent-and-child-relations)\n  * [Priority Queue](#priority-queue)\n  * [Hash Tables](#hash-tables)\n    + [collisions](#collisions)\n  * [Graphs](#graphs)\n    + [terminology](#terminology-2)\n    + [adjacency matrix](#adjacency-matrix)\n  * [adjacency list](#adjacency-list)\n  * [adjacency list vs adjacency matrix](#adjacency-list-vs-adjacency-matrix)\n    + [graph(adjacency list)](#graphadjacency-list)\n  * [Graph Traversal](#graph-traversal)\n    + [depth first traversal and breadth-first traversal in the the graph](#depth-first-traversal-and-breadth-first-traversal-in-the-the-graph)\n  * [Dijkstra's Shortest path first Algorithms](#dijkstras-shortest-path-first-algorithms)\n  * [Dynamic Programming (light introduction)](#dynamic-programming-light-introduction)\n    + [example Fibonacci sequence](#example-fibonacci-sequence)\n    + [memorization](#memorization)\n    + [tabulation](#tabulation)\n  * [Interesting Stuff](#interesting-stuff)\n  * [String](#string)\n    + [string pattern matching](#string-pattern-matching)\n  * [Array](#array)\n    + [Object](#object)\n    + [Map](#map)\n  * [Math](#math)\n\n\u003c!-- TOC end --\u003e\n\n\u003c!-- TOC --\u003e\u003ca name=\"algorithms-and-data-structures-cheat-sheet\"\u003e\u003c/a\u003e\n# Algorithms and Data Structures Cheat Sheet\n\nAn algorithm is a set of steps for solving a specific problem, while a data structure is a method for organizing and storing data in a computer so that it can be accessed and modified efficiently. This cheat sheet provides a summary of key concepts and techniques in algorithms and data structures, including big O notation, common data structures such as arrays, linked lists, and hash tables, and popular algorithms such as search and sorting algorithms. Understanding these concepts is essential for designing and implementing efficient software solutions.\n\n\u003c!-- TOC --\u003e\u003ca name=\"big-o-notation\"\u003e\u003c/a\u003e\n## Big O Notation\n\nBig O notation is a way to describe the efficiency or complexity of an algorithm. It provides a rough estimate of how long an algorithm will take to run, based on the size of the input data.\n\nIn computer science, the \"O\" in Big O notation is used to describe the upper bound of an algorithm's running time. For example, if an algorithm has a running time of O(n), it means that the algorithm's running time grows most linearly with the size of the input data. This means that if the input size doubles, the running time of the algorithm will approximately double as well.\n\nSeveral common complexities are described using Big O notation, including:\n\n-   `O(1)`: Constant time. The running time of the algorithm is independent of the size of the input data.\n\n-   `O(log n)`: Logarithmic time. The running time increases logarithmically with the size of the input data.\n\n-   `O(n)`: Linear time. The running time increases linearly with the size of the input data.\n\n-   `O(n log n)`: Log-linear time. The running time increases logarithmically with the size of the input data, but with a smaller coefficient than `O(log n)`.\n\n-   `O(n^2)`: Quadratic time. The running time increases as the square of the size of the input data.\n\n-   `O(n^3)`: Cubic time. The running time increases as the cube of the size of the input data.\n\n-   `O(2^n)`: Exponential time. The running time increases exponentially with the size of the input data.\n\nBig O notation is useful for comparing the efficiency of different algorithms, as well as for predicting the performance of an algorithm on larger inputs. However, it is important to note that Big O notation only provides an upper bound on an algorithm's running time, and actual running times may be faster in practice.\n\nThe following chart is a comparison of the common complexities, from fastest to slowest:\n\n![Big-O Complexity Comparison](./assets/bigo.png)\n\nHere are some examples of Big O complexities:\n\n`O(n)`:\n\n```ts\nfunction addUpToSimple(n: number): void {\n    let total = 0;\n    for (let i = 0; i \u003c n; i++) {\n        total += i;\n    }\n\n    return total;\n}\n```\n\nThe time complexity of the `addUpToSimple` function is O(n). This is because the function has a loop that iterates over all the values from 0 to n, and the time it takes to complete the function grows linearly with the value of n.\n\n`O(1)`:\n\n```ts\nfunction addUpComplex(n: number): void {\n    return (n * (n + 1)) / 2;\n}\n```\n\nThe time complexity of the `addUpComplex` function is O(1). This is because the function does not have any loops, and the time it takes to complete the function does not depend on the value of n.\n\n`O(n)`:\n\n```ts\nfunction printUpAndDown(n: number): void {\n    console.log(\"Going up\");\n    for (let i = 0; i \u003c n; i++) {\n        console.log(i);\n    }\n\n    console.log(\"Going down\");\n    for (let j = n - 1; j \u003e 0; j--) {\n        console.log(j);\n    }\n}\n```\n\nThe time complexity of the `printUpAndDown` function is O(n). This is because the function has two loops that each iterate over all the values from 0 to n, and the time it takes to complete the function grows linearly with the value of n.\nIt's possible to think of the complexity as O(2n), but it's important to remember that Big O notation is a way of expressing the general trend of the time complexity of a function, rather than a precise measure. In other words, we are not concerned with the exact number of operations that the function performs, but rather the general trend of how the time complexity increases as the input size grow.\n\n`O(n^2)`:\n\n```ts\nfunction printAllPairs(n: number): void {\n    for (let i = 0; i \u003c n; i++) {\n        console.log(i);\n        for (let j = 0; j \u003c n; j++) {\n            console.log(j);\n        }\n    }\n}\n```\n\nThe time complexity of the `printAllPairs` function is O(n^2). This is because the function has a nested loop, with the inner loop iterating over all the values from 0 to n for each iteration of the outer loop. The time it takes to complete the function grows quadratically with the value of n.\n\n`O(n)`:\n\n```ts\nfunction logAtLeastFive(n: number): void {\n    for (let i = 0; i \u003c= Math.max(5, n); i++) {\n        console.log(i);\n    }\n}\n```\n\nThe time complexity of the `logAtLeastFive` function is O(n). This is because the function has a loop that iterates over all the values from 0 to n, and the time it takes to complete the function grows linearly with the value of n.\n\n`O(1)`:\n\n```ts\nfunction logAtMostFive(n: number): void {\n    for (let i = 0; i \u003c= Math.min(5, n); i++) {\n        console.log(i);\n    }\n}\n```\n\nThe time complexity of the `logAtMostFive` function is O(1). This is because the function has a loop that iterates over a maximum of 5 values, regardless of the value of n. The time it takes to complete the function does not depend on the value of n.\n\n\u003c!-- TOC --\u003e\u003ca name=\"big-o-notation-for-some-of-the-objects-and-arrays-methods\"\u003e\u003c/a\u003e\n## Big O Notation for some of the Objects and Arrays methods\n\nFor the Objects, the `Object.keys`, `Object.values`, and `Object.entries` methods are used to retrieve the keys, values, and key-value pairs, respectively, of the object. These methods have a space complexity of O(n) because they iterate over all the properties of the object and create a new array that is the same size as the number of properties in the object.\n\nThe `hasOwnProperty` method, on the other hand, has a space complexity of O(1), because it only performs a single operation (checking whether the object has a property with a specific name). The size of the object does not affect the amount of memory required by the method, so the space complexity is constant.\n\n```ts\nconst person = { name: \"John\", age: 22, hobbies: [\"reading\", \"sleeping\"] };\n\nObject.keys(person); // [\"name\", \"age\", \"hobbies\"]\nObject.values(person); // [\"John\", 22, [\"reading\", \"sleeping\"]]\nObject.entries(person); // [[\"name\", \"John\"], [\"age\", 22], [\"hobbies\", [\"reading\", \"sleeping\"]]]\nperson.hasOwnProperty(\"name\"); // true\n```\n\nIn terms of arrays, the `push` and `pop` methods are generally faster than the `unshift` and `shift` methods when inserting or removing elements from the beginning of an array. This is because inserting or removing elements from the beginning of an array requires re-indexing all the elements in the array, which can be time-consuming. The push and pop methods, on the other hand, only require re-indexing the last element in the array, which is generally faster. However, the exact performance difference between these methods will depend on the specific implementation and the size of the array.\n\n```ts\nconst array = [1, 2, 3, 4, 5];\n\nconsole.time(\"push\");\narray.push(6);\nconsole.timeEnd(\"push\"); // takes a very small amount of time\n\nconsole.time(\"unshift\");\narray.unshift(0);\nconsole.timeEnd(\"unshift\"); // takes a longer amount of time\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"space-complexity\"\u003e\u003c/a\u003e\n## Space Complexity\n\nIn computer science, space complexity refers to the amount of memory that an algorithm requires to run to completion. It is a measure of the resources that an algorithm consumes, and it is typically expressed in terms of the size of the input to the algorithm.\n\nFor example, if an algorithm has a space complexity of O(1), this means that it requires a constant amount of memory, regardless of the size of the input. On the other hand, if an algorithm has a space complexity of O(n), this means that it requires a larger amount of memory as the size of the input increases.\n\nMost primitive data types (booleans, numbers, undefined, and null) are considered to have a constant space complexity. This means that they do not consume more memory as the size of the input increases.\n\nOn the other hand, strings and reference types like objects and arrays are considered to have a space complexity of O(n). This means that they consume more memory as the size of the input increases. For strings, the size of the input is the length of the string. For objects and arrays, the size of the input is the number of keys or elements in the object or array.\n\nIt is important to note that space complexity is a measure of the resources that an algorithm consumes, and it does not take into account the resources that are required to store the input data. For example, if an algorithm has a space complexity of O(1) but the input data has a space complexity of O(n), the overall space complexity of the algorithm will still be O(n).\n\nHere are some examples of space complexity:\n\n`O(1)`:\n\n```ts\nfunction sum(arr: number[]): number[] {\n    let total = 0;\n    for (let i = 0; i \u003c arr.length; i++) {\n        total += arr[i];\n    }\n}\n```\n\nThe space complexity of the function, `sum`, is O(1) because it only uses a single variable (`total`) to store the result of the computation. The size of the input (the length of the array arr) does not affect the amount of memory required by the function, so the space complexity is constant.\n\n`O(n)`:\n\n```ts\nfunction double(arr: number[]): number[] {\n    const newArr = [];\n    for (let i = 0; i \u003c arr.length; i++) {\n        newArr.push(arr[i] * 2);\n    }\n\n    return newArr;\n}\n```\n\nThe space complexity of the function, `double`, is O(n), because it creates a new array (`newArr`) and stores one element in the array for each element in the input array `arr`. The size of the input (the length of the array `arr`) directly determines the number of elements that are stored in the new array, so the space complexity is proportional to the size of the input.\n\n\u003c!-- TOC --\u003e\u003ca name=\"common-problem-solving-patterns\"\u003e\u003c/a\u003e\n## Common Problem-Solving Patterns\n\nSome common problem-solving patterns are:\n\n-   Frequency Counter\n-   Multiple Pointers\n-   Sliding Window\n-   Divide and Conquer\n-   Recursion\n\nThese patterns involve creating and manipulating data structures and algorithms to solve problems more efficiently and effectively. They are often used in interviews and technical assessments as a way to test a candidate's problem-solving skills.\n\n\u003c!-- TOC --\u003e\u003ca name=\"frequency-counter\"\u003e\u003c/a\u003e\n### Frequency Counter\n\nThe frequency counter is a technique used in algorithm design to count the frequency of elements in a data structure. It is often used to optimize the performance of an algorithm by avoiding the use of costly operations such as searching or sorting.\n\nTo implement a frequency counter, you can create an object or map to store the frequencies of the elements in the data structure. You can then iterate through the data structure and increment the count for each element in the object or map.\n\nFor example, consider the following array:\n\n```json\n[1, 2, 3, 2, 3, 1, 3]\n```\n\nTo implement a frequency counter for this array, you can create an object with keys representing the elements in the array and values representing their frequencies:\n\n```json\n{\n    \"1\": 2,\n    \"2\": 2,\n    \"3\": 3\n}\n```\n\nYou can then use this object to quickly look up the frequency of any element in the array without having to iterate through the entire array. This can be particularly useful when the array is large or when you need to perform multiple lookups.\n\nFrequency counters are often used in conjunction with other techniques such as multiple pointers or sliding windows to solve problems efficiently.\n\nHere's a problem that could be solved using the frequency counter pattern:\n\nWrite a function `same` that takes in two arrays of numbers `arrOne` and `arrTwo`. The function should return a boolean indicating whether or not the elements in `arrOne` are the squares of the elements in `arrTwo`.\n\n**Without** Frequency Counter:\n\n```ts\nfunction same(arrOne: number[], arrTwo: number[]): boolean {\n    // Return false if the arrays have different lengths\n    if (arrOne.length !== arrTwo.length) {\n        return false;\n    }\n\n    // Iterate through each element in arrOne\n    for (let element of arrOne) {\n        // Return false if the square of the element is not in arrTwo\n        if (!arrTwo.includes(element ** 2)) {\n            return false;\n        }\n        // Remove the element from arrTwo if it is present\n        arrTwo.splice(arrTwo.indexOf(element ** 2), 1);\n    }\n\n    // If all elements are present and the lengths match, return true\n    return true;\n}\n```\n\nThe function has a time complexity of O(n^2). This is because it includes two nested loops. The outer loop iterates through each element of arrOne, and the inner loop searches for the corresponding element in `arrTwo` using the includes method, which has a time complexity of O(n). The splice method, which is also used in the inner loop, has a time complexity of O(n) as well. Therefore, the overall time complexity of the first function is O(n^2).\n\n**With** Frequency Counter:\n\n```ts\nfunction same(arr1: number[], arr2: number[]): boolean {\n    // Return false if the arrays have different lengths\n    if (arr1.length !== arr2.length) {\n        return false;\n    }\n\n    // Initialize empty frequency counter objects for arr1 and arr2\n    const frequencyCounter1 = {};\n    const frequencyCounter2 = {};\n\n    // Populate frequencyCounter1 with the frequency of each element in arr1\n    for (let val of arr1) {\n        frequencyCounter1[val] = (frequencyCounter1[val] || 0) + 1;\n    }\n    // Populate frequencyCounter2 with the frequency of each element in arr2\n    for (let val of arr2) {\n        frequencyCounter2[val] = (frequencyCounter2[val] || 0) + 1;\n    }\n\n    // Iterate through the keys in frequencyCounter1\n    for (let key in frequencyCounter1) {\n        // Calculate the square of the key\n        const sqrtKey = parseInt(key, 10) ** 2;\n        // Return false if the square of the key is not in frequencyCounter2 or if the frequencies do not match\n        if (\n            !(sqrtKey in frequencyCounter2) ||\n            frequencyCounter2[sqrtKey] !== frequencyCounter1[key]\n        ) {\n            return false;\n        }\n    }\n\n    // If all checks pass, return true\n    return true;\n}\n```\n\nThe function has a time complexity of O(n). This is because it only includes a single loop through each of the arrays. The `frequencyCounter1` and `frequencyCounter2` objects are built in O(n) time by iterating through `arr1` and `arr2` respectively and adding each element to the corresponding object. Then, the function iterates through the keys in `frequencyCounter1` and checks the corresponding values in `frequencyCounter2`. Since there are a constant number of keys in frequencyCounter1, the time complexity of this step is O(1). Therefore, the overall time complexity of the second function is O(n).\n\nAnother example:\n\nWrite a function `validAnagram` that takes in two strings, `str1` and `str2`, and returns a boolean indicating whether or not `str1` is an anagram of `str2`. An anagram is a word or phrase formed by rearranging the letters of a different word or phrase, typically using all the original letters exactly once.\n\n```ts\nfunction validAnagram(str1: string, str2: string): boolean {\n    // Return false if the strings have different lengths\n    if (str1.length !== str2.length) {\n        return false;\n    }\n\n    // Initialize empty frequency count objects for str1 and str2\n    const frequencyCount1 = {};\n    const frequencyCount2 = {};\n\n    // Populate frequencyCount1 with the frequency of each character in str1\n    for (let value of str1) {\n        frequencyCount1[value] = (frequencyCount1[value] || 0) + 1;\n    }\n    // Populate frequencyCount2 with the frequency of each character in str2\n    for (let value of str2) {\n        frequencyCount2[value] = (frequencyCount2[value] || 0) + 1;\n    }\n\n    // Iterate through each key in frequencyCount1\n    for (let key in frequencyCount1) {\n        // Return false if the value of the key is different in frequency counters\n        if (frequencyCount1[key] !== frequencyCount2[key]) {\n            return false;\n        }\n    }\n\n    // If all characters in str2 are present in the frequency count object and the counts match, return true\n    return true;\n}\n```\n\nThe function has a time complexity of O(n). This is because it includes two loops that each iterate through the characters in the strings. The `frequencyCount1` and `frequencyCount2` objects are built in O(n) time by iterating through `str1` and `str2` respectively and adding each character to the corresponding object. Then, the function iterates through the keys in `frequencyCount1` and checks the corresponding values in `frequencyCount2`. Since there are a constant number of keys in `frequencyCount1`, the time complexity of this step is O(1). Therefore, the overall time complexity of the first function is O(n).\n\nAnother approach to solving this problem:\n\n```ts\nfunction validAnagram(str1: string, str2: string): boolean {\n    // Return false if the strings have different lengths\n    if (str1.length !== str2.length) {\n        return false;\n    }\n\n    // Initialize an empty frequency count object\n    const frequencyCount = {};\n\n    // Iterate through each character in str1\n    for (let i = 0; i \u003c str1.length; i++) {\n        const currentElement = str1[i];\n        // Increment the frequency count for the current character in the frequency count object\n        frequencyCount[currentElement] = (frequencyCount[currentElement] || 0) + 1;\n    }\n\n    // Iterate through each character in str2\n    for (let i = 0; i \u003c str2.length; i++) {\n        const currentElement = str2[i];\n        // Return false if the current character is not in the frequency count object\n        if (!frequencyCount[currentElement]) {\n            return false;\n        }\n        // Decrement the frequency count for the current character in the frequency count object\n        else {\n            frequencyCount[currentElement] -= 1;\n        }\n    }\n\n    // If all characters in str2 are present in the frequency count object and the counts match, return true\n    return true;\n}\n```\n\nThe function also has a time complexity of O(n). This is because it includes two loops that each iterate through the characters in the strings. The `frequencyCount` object is built in O(n) time by iterating through `str1` and adding each character to the object. Then, the function iterates through `str2` and decrements the count for each character in the `frequencyCount` object. Since the function only iterates through the characters in `str2`, the time complexity is O(n).\n\n\u003c!-- TOC --\u003e\u003ca name=\"multiple-pointers\"\u003e\u003c/a\u003e\n## Multiple Pointers\n\nThe multiple pointers pattern involves using two or more pointers to solve a problem by traversing a data structure, such as an array or a linked list. The pointers typically move toward each other or towards the middle of the data structure, and the algorithm performs some operation on the values at the pointers' current positions.\n\nHere's an example of how the multiple pointers pattern can be used to solve a problem:\n\nSuppose we want to find the first pair of elements in an array that sums to a target value. We could use the multiple pointers pattern to solve this problem as follows:\n\n1. Initialize two pointers, `left` and `right`, to the first and last elements of the array, respectively.\n2. While `left` is less than or equal to `right`, do the following:\n    1. If the sum of the elements at `left` and `right` is less than the target value, increment `left`.\n    2. If the sum of the elements at `left` and `right` is greater than the target value, decrement `right`.\n    3. If the sum of the elements at `left` and `right` is equal to the target value, return the pair (`left`, `right`).\n3. If the loop terminates without finding a pair that sums to the target value, return null or some other sentinel value indicating that no such pair was found.\n   Using the multiple pointers pattern can be an efficient way to solve certain problems, as it allows us to traverse the data structure in a single pass, rather than needing to perform multiple passes or use nested loops. It can also make the solution more readable and easier to understand, as it clearly defines the roles of the different pointers and the logic of the algorithm.\n\nOne of the key benefits of the multiple-pointers pattern is that it can help reduce the time complexity of a problem, as it allows for a linear or logarithmic time complexity solution in some cases. However, this pattern is only effective when the array is sorted, as the pointers rely on the ordering of the elements to work correctly.\n\nIf the array is not sorted, the pointers may not be able to move through the array in the correct order, leading to incorrect results or an infinite loop. Therefore, it is important to ensure that the input array is sorted before using the multiple-pointers pattern to solve a problem.\n\nFor example, consider the following array of integers:\n\n```json\n[3, 7, 1, 5, 2, 4]\n```\n\nIf we want to find a pair of elements that sum to a target value using the multiple pointers pattern, we would first need to sort the array in ascending order:\n\n```json\n[1, 2, 3, 4, 5, 7]\n```\n\nOnly then can we use the multiple pointers pattern to find a pair of elements that sum to the target value. Without sorting the array first, the pointers may not be able to find the correct pair of elements, even if they exist in the array.\n\nHere's a problem that could be solved using the multiple pointers pattern:\n\nWrite a function `sumZero` that takes in a **sorted** array of integers `arr` and returns the first pair of elements that sum to zero, if one exists. If no such pair is found, the function should return an empty array.\n\n**Without** Multiple Pointers:\n\n```ts\nfunction sumZero(arr: number[]): number[] {\n    // Iterate over each element in the array\n    for (let i = 0; i \u003c arr.length; i++) {\n        // Iterate over the remaining elements in the array, starting at the element after i\n        for (let j = i + 1; j \u003c arr.length; j++) {\n            // If the sum of the current elements is 0, return the pair\n            if (arr[i] + arr[j] === 0) {\n                return [arr[i], arr[j]];\n            }\n        }\n    }\n\n    // If no pair was found, return an empty array\n    return [];\n}\n```\n\nThe time complexity of the `sumZero` function is O(n^2), or quadratic. This is because the function contains a nested loop, and the inner loop iterates over all elements of the array for each iteration of the outer loop. This means that the total number of iterations of the inner loop is equal to the length of the array multiplied by itself, or n \\* n.\n\nFor example, if the length of the array is 5, the inner loop will be executed 5 \\* 5 = 25 times. As the length of the array increases, the time complexity of the function will increase significantly, making it less efficient for larger inputs.\n\nTo improve the time complexity of this function, we could try using a different algorithm or data structure, such as the multiple pointers pattern, which has a time complexity of O(n) (linear) for this particular problem.\n\n**With** Multiple Pointers:\n\n```ts\nfunction sumZero(arr: number[]): number[] {\n    // Initialize left pointer to the first element of the array\n    let left = 0;\n    // Initialize right pointer to the last element of the array\n    let right = arr.length - 1;\n\n    // While the left pointer is less than the right pointer, do the following:\n    while (left \u003c right) {\n        // Calculate the sum of the elements at the left and right pointers\n        const sum = arr[left] + arr[right];\n        // If the sum is 0, return the pair\n        if (sum === 0) {\n            return [arr[left], arr[right]];\n        }\n        // If the sum is greater than 0, decrement the right pointer\n        else if (sum \u003e 0) {\n            right--;\n        }\n        // If the sum is less than 0, increment the left pointer\n        else {\n            left++;\n        }\n    }\n\n    // If no pair was found, return an empty array\n    return [];\n}\n```\n\nThe time complexity of the `sumZero` function is O(n), or linear. This is because the function traverses the input array only once, using two pointers that move towards each other until they meet or pass each other. The number of iterations of the loop is directly proportional to the length of the array, so the time complexity is linear.\n\nThis is a significant improvement over the previous version of the `sumZero` function, which had a time complexity of O(n^2) (quadratic) due to the nested loop. Using the multiple-pointers pattern allowed us to solve the problem with a single pass through the array, making the solution more efficient for larger inputs.\n\nImplement a function called `countUniqueValues` that takes in a single parameter, `arr`, which is a **sorted** array of integers. The function should return the number of unique values in the array.\n\nAnother example:\n\n```ts\nfunction countUniqueValues(arr: number[]): number {\n    // Return 0 if the array is empty.\n    if (arr.length === 0) {\n        return 0;\n    }\n\n    // Initialize a counter and a pointer to the first element of the array.\n    let i = 0;\n    // Iterate through the array, starting from the second element.\n    for (let j = 1; j \u003c arr.length; j++) {\n        // If the current element is different from the element at the pointer,\n        // increment the counter and update the value at the pointer.\n        if (arr[i] !== arr[j]) {\n            i++;\n            arr[i] = arr[j];\n        }\n    }\n\n    // Return the final value of the counter, plus 1.\n    return i + 1;\n}\n```\n\nThe time complexity of the `countUniqueValues` function is O(n), where n is the length of the input array.\n\nThe function performs a single loop through the array, and the time taken to iterate through the array is directly proportional to the size of the array. Therefore, the time complexity of the function is linear with respect to the size of the input array.\n\n\u003c!-- TOC --\u003e\u003ca name=\"sliding-window\"\u003e\u003c/a\u003e\n### Sliding Window\n\nThe sliding window pattern is a technique that involves iterating through an array and maintaining a \"window\" of elements that meet certain conditions. The window is typically defined by two pointers, one at the start of the window and one at the end.\n\nThe sliding window pattern is often used to solve problems that involve finding a subarray or subsequence of elements that meet certain conditions, such as having a maximum or minimum sum, length, or average.\n\nTo use the sliding window pattern, we first initialize the start and end pointers to the beginning of the array. Then, we iterate through the array and update the window by moving the end pointer forward until the window meets the desired conditions. Once the window is valid, we can perform any necessary operations on the elements within the window, such as calculating the sum or finding the minimum element.\n\nAfter performing these operations, we can then move the start pointer forward to \"slide\" the window along the array and repeat the process until we have covered the entire array.\n\nHere is an example of using the sliding window pattern to find the maximum sum subarray of a given array:\n\n```ts\nfunction maxSumSubarray(arr: number[]): number {\n    // Initialize the start and end pointers to the beginning of the array.\n    let start = 0;\n    let end = 0;\n    // Initialize a variable to store the maximum sum.\n    let maxSum = 0;\n    // Initialize a variable to store the current sum.\n    let currSum = 0;\n\n    // Iterate through the array.\n    while (end \u003c arr.length) {\n        // Add the current element to the current sum.\n        currSum += arr[end];\n        // Update the maximum sum if necessary.\n        maxSum = Math.max(maxSum, currSum);\n        // If the current sum is negative, reset it to 0 and move the start pointer to the next element.\n        if (currSum \u003c 0) {\n            currSum = 0;\n            start = end + 1;\n        }\n        // Move the end pointer to the next element.\n        end++;\n    }\n\n    // Return the maximum sum.\n    return maxSum;\n}\n```\n\nThe time complexity of the sliding window pattern is typically O(n), where n is the length of the input array. This makes it a relatively efficient solution for finding subarrays or subsequences that meet certain conditions.\n\nHere is another example of using the sliding window pattern to find the longest contiguous subarray of a given array that has a maximum average value.\nThe function takes in an array of integer `arr`, and an integer `k` as input and returns the longest contiguous subarray of the array that has a maximum average value among all subarrays of length `k`.\n\n```ts\nfunction maxAvgSubarray(arr: number[], k: number): number[] {\n    // Initialize the start and end pointers to the beginning of the array.\n    let start = 0;\n    let end = 0;\n    // Initialize a variable to store the maximum average.\n    let maxAvg = -Infinity;\n    // Initialize a variable to store the current sum.\n    let currSum = 0;\n    // Initialize variables to store the start and end indices of the maximum average subarray.\n    let maxStart = 0;\n    let maxEnd = 0;\n\n    // Iterate through the array.\n    while (end \u003c arr.length) {\n        // Add the current element to the current sum.\n        currSum += arr[end];\n        // If the current window is at least k elements long, update the maximum average if necessary.\n        if (end - start + 1 \u003e= k) {\n            let avg = currSum / (end - start + 1);\n            if (avg \u003e maxAvg) {\n                maxAvg = avg;\n                maxStart = start;\n                maxEnd = end;\n            }\n        }\n        // If the current sum is negative, reset it to 0 and move the start pointer to the next element.\n        if (currSum \u003c 0) {\n            currSum = 0;\n            start = end + 1;\n        }\n        // Move the end pointer to the next element.\n        end++;\n    }\n\n    // Return the maximum average subarray.\n    return arr.slice(maxStart, maxEnd + 1);\n}\n```\n\nThe time complexity of the `maxAvgSubarray` function is O(n), where n is the length of the input array.\n\nThe function performs a single loop through the array, and the time taken to iterate through the array is directly proportional to the size of the array. The time taken to update the maximum average and the start and end indices of the maximum average subarray is a constant time operation, as it does not depend on the size of the array.\n\nTherefore, the time complexity of the function is linear with respect to the size of the input array.\n\n\u003c!-- TOC --\u003e\u003ca name=\"divide-and-conquer\"\u003e\u003c/a\u003e\n### Divide-and-Conquer\n\nThe divide-and-conquer pattern is a common algorithmic technique used to solve problems by dividing them into smaller subproblems, solving those subproblems, and then combining the solutions to the subproblems to solve the original problem.\n\nThis pattern involves dividing the problem into smaller subproblems, solving each of those subproblems recursively, and then combining the solutions to the subproblems to solve the original problem.\n\nThere are several benefits to using the divide-and-conquer pattern:\n\nIt can lead to more efficient algorithms, as the time complexity of many divide-and-conquer algorithms is often much better than other algorithms that solve the same problem.\n\nIt can be easier to design and implement divide-and-conquer algorithms, as the subproblems can often be solved independently and in parallel.\n\nThe divide-and-conquer pattern is well-suited to problems that can be naturally divided into smaller subproblems, such as sorting and searching algorithms.\n\nSome common examples of divide-and-conquer algorithms include merge sort, quick sort, and binary search.\n\nHere's a problem that could be solved using the divide-and-conquer pattern:\n\nImplement a function `search` that takes in a **sorted** array of integers `sortedArr` and a value `value` and returns the index of `value` in the array, or `-1` if it is not present.\n\n**Without** divide-and-conquer:\n\n```typescript\nfunction linearSearch(sortedArr: number[], value: number): number {\n    for (let i = 0; i \u003c arr.length; i++) {\n        if (arr[i] === val) {\n            return i;\n        }\n    }\n\n    return -1;\n}\n```\n\nThe complexity of the `linearSearch` function is O(n), where n is the length of the array `sortedArr`. This is because the function performs a linear search through the array, meaning it will take longer to run as the size of the array increases.\nIn the worst-case scenario, where the value being searched for is not present in the array, the function will have to iterate through the entire array to determine that it is not present. This means that the running time of the function will be directly proportional to the size of the array.\n\n**With** divide-and-conquer (Binary Search):\n\nThe binary search algorithm is an efficient algorithm for searching for a specific value in a sorted array. It works by repeatedly dividing the search space in half until the value being searched for is found or it is determined that the value is not present in the array.\n\nThe function begins by setting the variables `min` and `max` to the first and last indices of the array, respectively. It then enters a loop that continues as long as `min` is less than or equal to `max`.\n\nInside the loop, the function calculates the `middle` index of the current search space by taking the floor of the average of `min` and `max`. It then checks if the value at the `middle` index is less than, greater than, or equal to the value being searched for:\n\n-   If it is less than the value, the search space is updated to the indices after the `middle` index, by setting `min` to `middle` + 1.\n-   If it is greater than the value, the search space is updated to the indices before the `middle` index, by setting `max` to `middle` - 1.\n-   If it is equal to the value, the function returns the index.\n\n```ts\nfunction binarySearch(sortedArr: number[], value: number): number {\n    // Set the initial search space to the entire array\n    let min = 0;\n    let max = sortedArr.length - 1;\n\n    // Continue searching as long as the search space is not empty\n    while (min \u003c= max) {\n        // Calculate the middle index of the current search space\n        let middle = Math.floor((min + max) / 2);\n\n        // Check if the value at the middle index is less than, greater than, or equal to the value being searched for\n        if (sortedArr[middle] \u003c value) {\n            // Update the search space to the indices after the middle index\n            min = middle + 1;\n        } else if (sortedArr[middle] \u003e value) {\n            // Update the search space to the indices before the middle index\n            max = middle - 1;\n        } else {\n            // Return the index if the value is found\n            return middle;\n        }\n    }\n\n    // Return -1 if the value is not found\n    return -1;\n}\n```\n\nThe complexity of the `binarySearch` function is O(log n), where n is the length of the array `sortedArr`. This means that the running time of the function increases logarithmically with the size of the array.\n\nThis is because the function reduces the search space by half with each iteration. For example, if the array has 8 elements, the first iteration will check the middle element, which reduces the search space to either the first 4 elements or the last 4 elements. The next iteration will check the middle element of this reduced search space, which reduces the search space to either the first 2 elements or the last 2 elements. This process continues until the value is found or it is determined that the value is not present in the array.\n\nIn the worst-case scenario, where the value being searched for is not present in the array, the function will have to perform log n iterations to determine this. This is much more efficient than a linear search, which would require n iterations in the worst-case scenario.\n\nOverall, the binary search algorithm is a very efficient algorithm for searching through sorted arrays and is much faster than a linear search for larger arrays.\n\n\u003c!-- TOC --\u003e\u003ca name=\"recursion\"\u003e\u003c/a\u003e\n### Recursion\n\nRecursion is a technique in which a function calls itself repeatedly until a certain condition is met. It can be a useful way to solve problems that can be divided into smaller subproblems, or that involve repeating a process with slightly different inputs each time.\n\nThe key to understanding recursion is to identify the base case, which is the point at which the recursion stops. The base case is often a simple condition that can be checked directly, such as the input being equal to a certain value.\n\nFor example, consider a function that calculates the factorial of a number. The factorial of a number is the product of all the integers from 1 up to that number. For example, the factorial of 5 is 1 \\* 2 \\* 3 \\* 4 \\* 5 = 120.\n\nHere is a recursive function that calculates the factorial of a number:\n\n```ts\nfunction factorial(n: number): number {\n    if (n == 1) {\n        return 1; // base case\n    }\n\n    return n * factorial(n - 1); // recursive case\n}\n```\n\nIn this function, the base case is the condition n == 1, which is checked at the beginning of the function. If n is equal to 1, the function returns 1. Otherwise, the function calls itself with an input of n - 1, and the result of this recursive call is multiplied by n and returned.\n\nThe recursion continues until the base case is reached, at which point the function stops calling itself and the result is returned up the chain of recursive calls.\n\nFor example, if we call `factorial(5)`, the function will call itself with the input 4, then with the input 3, then 2, and finally 1. When the input is 1, the base case is reached and the function returns 1, which is then multiplied by 2 and returned, which is then multiplied by 3 and returned, and so on, until the final result of 120 is returned.\n\n\u003c!-- TOC --\u003e\u003ca name=\"understanding-the-call-stack\"\u003e\u003c/a\u003e\n#### Understanding the Call Stack\n\nThe call stack is a data structure that keeps track of the functions that are currently executing. It is used to store the execution context of each function, which includes the local variables and the current position in the code.\n\nWhen a function is called, its execution context is pushed onto the top of the call stack. When the function returns, its execution context is popped off the top of the call stack. This process continues as the program executes, with the call stack growing and shrinking as functions are called and returned from.\n\nWhen a recursive function is called, a new execution context is added to the top of the call stack for each recursive call. This can lead to the call stack growing very large, especially if the recursion is not properly controlled.\n\nConsider the following example:\n\n```ts\nfunction wakeUp() {\n    takeShower();\n    eatBreakfast();\n    console.log(\"Ready to go ... \");\n}\n\nfunction takeShower() {\n    console.log(\"taking shower\");\n}\n\nfunction eatBreakfast() {\n    const meal = cookBreakFast();\n    console.log(`eating ${meal}`);\n}\n\nfunction cookBreakFast() {\n    const meals = [\"Cheese\", \"Protein Shake\", \"Coffee\"];\n    return meals[Math.floor(Math.random() * meals.length)];\n}\n\nwakeUp();\n```\n\nHere's how the call stack works:\n\nWhen the `wakeUp` function is called, it is added to the call stack, which is a data structure that keeps track of the functions that are currently executing. The call stack grows and shrinks as functions are called and returned from.\n\nAs the `wakeUp` function executes, it calls the `takeShower` and `eatBreakfast` functions. These functions are added to the call stack on top of the `wakeUp` function, so the call stack now looks like this:\n\n```txt\n[wakeUp][(takeShower, wakeUp)]\n```\n\nWhen the `takeShower` function finishes executing, it is removed from the call stack, and the call stack now looks like this:\n\n```txt\n[wakeUp][(eatBreakfast, wakeUp)]\n```\n\nThe `eatBreakfast` function then calls the `cookBreakfast` function, which is added to the call stack on top of the `eatBreakfast` and `wakeUp` functions:\n\n```txt\n[wakeUp][(eatBreakfast, wakeUp)][(cookBreakfast, eatBreakfast, wakeUp)]\n```\n\nWhen the `cookBreakfast` function finishes executing, it is removed from the call stack, leaving the call stack looking like this:\n\n```txt\n[wakeUp][(eatBreakfast, wakeUp)]\n```\n\nFinally, when the `eatBreakfast` and `wakeUp` functions finish executing, they are also removed from the call stack, leaving the call stack empty.\n\nHere is how `factorial` function calculates the factorial of a number:\n\nIn the `factorial` function, the base case is the condition n == 1, which is checked at the beginning of the function. If n is equal to 1, the function returns 1. Otherwise, the function calls itself with an input of n - 1, and the result of this recursive call is multiplied by n and returned.\n\nEach time the `factorial` function is called, a new execution context is added to the top of the call stack. If the recursion is not properly controlled, the call stack can grow very large, potentially leading to a stack overflow error. To avoid this, it is important to ensure that the base case is reached and the recursion terminates.\n\nWrite a function called `collectOdd` that takes in an array of numbers and returns a new array containing only the odd numbers from the input array. The function should use recursion to achieve this, and should not modify the original input array.\n\n```ts\nfunction collectOdd(arr: number[], result: number[] = []): number[] {\n    // base case: if the input array is empty, return the accumulated result array\n    if (!arr.length) {\n        return result;\n    }\n\n    // if the first element in the array is odd, add it to the result array\n    if (arr[0] % 2 !== 0) {\n        result.push(arr[0]);\n    }\n\n    // recursive case: call the function with the rest of the input array and the accumulated result\n    return collectOdd(arr.slice(1), result);\n}\n```\n\nThis function has a time complexity of O(n), where n is the length of the input array. This is because the function processes each element of the array once, and the time taken to do so is constant. The function also makes one recursive call for each element in the array, but since the size of the input array decreases by 1 on each call, the total number of recursive calls is also O(n). Therefore, the overall time complexity of the function is O(n).\n\n\u003c!-- TOC --\u003e\u003ca name=\"searching-algorithms\"\u003e\u003c/a\u003e\n## Searching Algorithms\n\nSearching algorithms are techniques for finding a particular item in a collection of items. They are an important part of computer science and are used to perform a wide variety of tasks, such as finding a specific file on a computer, searching for information on the internet, or locating a particular piece of data in a database.\n\nThere are several different types of search algorithms, including linear search, binary search, and hash table search. The linear search involves searching through a list of items one by one until the desired item is found. Binary search is a more efficient method that involves dividing a list in half and repeatedly narrowing down the search to a smaller and smaller portion of the list until the desired item is found. Hash table search uses a data structure called a hash table to quickly locate the desired item.\n\nThe efficiency of a search algorithm depends on the structure of the data being searched and the specific search method being used. For example, linear search is less efficient than binary search when searching through a large list of items, but it may be more efficient when searching through a small list or when the items are not in any particular order.\n\n\u003c!-- TOC --\u003e\u003ca name=\"linear-search\"\u003e\u003c/a\u003e\n### Linear Search\n\n```ts\nfunction linearSearch(arr: number[], value: number): number {\n    for (let i = 0; i \u003c arr.length; i++) {\n        if (arr[i] === value) {\n            return i;\n        }\n        return -1;\n    }\n}\n```\n\nThe complexity of the function `linearSearch` is O(n), or linear time. This means that the time it takes for the function to complete is directly proportional to the size of the input array.\n\nIn this function, the time complexity is determined by the for loop, which will run once for each element in the array. If the array has n elements, the loop will run n times. As a result, the time taken to complete the function will increase linearly with the size of the input array.\n\nIn general, linear search algorithms have a time complexity of O(n), meaning that they are less efficient than some other types of search algorithms, such as binary search, which has a time complexity of O(log n). However, linear search is often used when the array is small or when the elements are not in a particular order, as it is relatively simple to implement and does not require any additional data structures.\n\nThe `indexOf()` method in JavaScript is used to search for an element in an array and returns its index. If the element is not found, it returns -1. This method uses a linear search algorithm to search for the element.\n\n```ts\nconst fruits = [\"apple\", \"banana\", \"mango\", \"orange\"];\n\nconsole.log(fruits.indexOf(\"banana\")); // Output: 1\nconsole.log(fruits.indexOf(\"grapes\")); // Output: -1\n```\n\nThe `includes()` method in JavaScript is similar to the `indexOf()` method and is used to check if an element is present in an array. It returns a Boolean value (true or false) indicating whether the element was found or not. This method also uses a linear search algorithm to search for the element.\n\n```ts\nconst fruits = [\"apple\", \"banana\", \"mango\", \"orange\"];\n\nconsole.log(fruits.includes(\"banana\")); // Output: true\nconsole.log(fruits.includes(\"grapes\")); // Output: false\n```\n\nThe `find()` and `findIndex()` methods in JavaScript are used to search for an element in an array that satisfies a given condition. They use a linear search algorithm to search for the element.\n\nThe `find()` method returns the value of the first element in the array that satisfies the given condition, or undefined if no element is found.\n\nThe `findIndex()` method returns the index of the first element in the array that satisfies the given condition, or -1 if no element is found.\n\nHere is an example of using the `find()` and `findIndex()` methods to search for an element in an array:\n\n```ts\nconst numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];\n\n// Find the first even number\nconst evenNumber = numbers.find((num) =\u003e num % 2 === 0);\nconsole.log(evenNumber); // Output: 2\n\n// Find the index of the first even number\nconst evenNumberIndex = numbers.findIndex((num) =\u003e num % 2 === 0);\nconsole.log(evenNumberIndex); // Output: 1\n```\n\nThe `find()` and `findIndex()` methods take a callback function as an argument, which is used to specify the condition that the element must satisfy. In the example above, the callback function checks if the element is even by checking if it is divisible by 2.\n\nHere's an example of a problem that could be solved using linear search:\n\nWrite a function `naiveStringSearch` that takes in two strings, `long` and `pattern`, and returns the number of occurrences of pattern in long. The function should use a naive string search algorithm to search for patterns in `long`.\n\n```ts\nfunction naiveStringSearch(long: string, pattern: string): number {\n    // Initialize a counter to keep track of the number of occurrences of pattern in long\n    let count = 0;\n\n    // Loop through each character in long\n    for (let i = 0; i \u003c long.length; i++) {\n        // Loop through each character in pattern\n        for (let j = 0; j \u003c pattern.length; j++) {\n            // If the characters at the current indices in long and pattern do not match, break out of the inner loop\n            if (pattern[j] !== long[i + j]) {\n                break;\n            }\n            // If we have reached the last character in pattern and all characters have matched, increment the counter\n            if (j === pattern.length - 1) {\n                count++;\n            }\n        }\n    }\n\n    // Return the final count\n    return count;\n}\n```\n\nThe time complexity of this function is O(n^2), as it uses two nested loops to search for the pattern in the long string. The outer loop iterates through each character in the long string, and the inner loop compares each character in the pattern to the corresponding character in the long string. This means that the time taken to search for the pattern increases quadratically with the length of the long string and the length of the pattern. This makes the function less efficient for larger strings compared to other string search algorithms such as the KMP algorithm. However, it is a simple algorithm that can be useful for small strings or for searching for patterns that are not too long.\n\nOne way to improve the efficiency of the `naiveStringSearch` function is to use the Knuth-Morris-Pratt (KMP) algorithm, which has a time complexity of O(n + m) where n is the length of the long string and m is the length of the pattern.\n\n```ts\nfunction kmpStringSearch(long: string, pattern: string): number {\n    // Initialize a counter to keep track of the number of occurrences of pattern in long\n    let count = 0;\n\n    // Calculate the prefix table for the pattern\n    const prefixTable = computePrefixTable(pattern);\n\n    // Initialize the variables for the main loop\n    let i = 0;\n    let j = 0;\n\n    // Main loop: continue until we have reached the end of the long string\n    while (i \u003c long.length) {\n        // If the characters at the current indices in long and pattern match, move to the next character in both strings\n        if (long[i] === pattern[j]) {\n            i++;\n            j++;\n        }\n\n        // If we have reached the end of the pattern, increment the counter and reset the pattern index to the value in the prefix table\n        if (j === pattern.length) {\n            count++;\n            j = prefixTable[j - 1];\n        }\n        // If the characters do not match, reset the pattern index to the value in the prefix table\n        else if (i \u003c long.length \u0026\u0026 long[i] !== pattern[j]) {\n            if (j !== 0) {\n                j = prefixTable[j - 1];\n            }\n            // If the pattern index is already 0, move to the next character in the long string\n            else {\n                i++;\n            }\n        }\n    }\n\n    // Return the final count\n    return count;\n}\n\n// Helper function to compute the prefix table for the KMP algorithm\nfunction computePrefixTable(pattern: string): number[] {\n    const prefixTable = [0];\n    let j = 0;\n\n    for (let i = 1; i \u003c pattern.length; i++) {\n        if (pattern[i] === pattern[j]) {\n            prefixTable[i] = j + 1;\n            j++;\n        } else {\n            while (j \u003e 0 \u0026\u0026 pattern[i] !== pattern[j]) {\n                j = prefixTable[j - 1];\n            }\n            if (pattern[i] === pattern[j]) {\n                prefixTable[i] = j + 1;\n                j++;\n            } else {\n                prefixTable[i] = 0;\n            }\n        }\n    }\n\n    return prefixTable;\n}\n```\n\nThe KMP algorithm uses a prefix table to keep track of the longest proper prefix that is also a suffix of the pattern. This allows it to efficiently skip over portions of the pattern when searching for a match in the long string, which reduces the time complexity from O(n \\* m) to O(n + m).\n\n\u003c!-- TOC --\u003e\u003ca name=\"sorting-algorithms\"\u003e\u003c/a\u003e\n## Sorting Algorithms\n\nSorting algorithms are a set of instructions that take in a list of items and arrange them in a particular order. The order can be ascending (smallest to largest), descending (largest to smallest), or some other predetermined order. Sorting algorithms are used in many different contexts, including data analysis, computer science, and everyday life. Some common examples of sorting algorithms include bubble sort, insertion sort, selection sort, merge sort, and quick sort. These algorithms differ in terms of their efficiency and the amount of work they do, and they have different use cases depending on the needs of the situation. In general, sorting algorithms are an important tool for organizing and making sense of data.\n\nThere are several ways to categorize sorting algorithms based on their time complexity, or the amount of time it takes for the algorithm to run. One way to categorize them is by the number of comparisons they make:\n\n-   O(n) sorting algorithms: These algorithms have a time complexity of O(n), meaning that the running time grows linearly with the size of the input. Examples of O(n) sorting algorithms include **radix sort**.\n\n-   O(nlogn) sorting algorithms: These algorithms have a time complexity of O(nlogn), meaning that the running time grows at a rate of n\\*log(n) with the size of the input. Examples of O(nlogn) sorting algorithms include **merge sort** and **quick sort**.\n\n-   O(n^2) sorting algorithms: These algorithms have a time complexity of O(n^2), meaning that the running time grows at a rate of n\\*n with the size of the input. Examples of O(n^2) sorting algorithms include **bubble sort**, **insertion sort**, and **selection sort**.\n\n-   O(n^3): and higher sorting algorithms: These algorithms have a time complexity of O(n^3) or higher, meaning that the running time grows at a rate of nnn or higher with the size of the input. These algorithms are generally less efficient than the other categories and are not commonly used.\n\n[visualgo.net](https://visualgo.net/en/sorting) is a great resource for visualizing sorting algorithms and their time complexities.\n\nFollowing is a table of the most common sorting algorithms and their time complexities:\n\n|   Algorithm    | Time Complexity (Best) | Time Complexity (Average) | Time Complexity (Worst) | Space Complexity (Worst) |\n| :------------: | :--------------------: | :-----------------------: | :---------------------: | :----------------------: |\n|  Bubble Sort   |          O(n)          |          O(n^2)           |         O(n^2)          |           O(1)           |\n| Selection Sort |         O(n^2)         |          O(n^2)           |         O(n^2)          |           O(1)           |\n| Insertion Sort |          O(n)          |          O(n^2)           |         O(n^2)          |           O(1)           |\n|   Merge Sort   |       O(n Log n)       |        O(n Log n)         |       O(n Log n)        |           O(n)           |\n|   Quick Sort   |       O(n Log n)       |        O(n Log n)         |         O(n^2)          |         O(Log n)         |\n|   Radix Sort   |         O(nk)          |           O(nk)           |          O(nk)          |         O(n + k)         |\n\nIt's worth noting that these time complexities are just rough estimates and can vary depending on the specific implementation of the algorithm. Additionally, other factors can affect the running time of a sorting algorithm, such as the speed of the computer it is running on and the specific characteristics of the input data.\n\n\u003c!-- TOC --\u003e\u003ca name=\"bubble-sort\"\u003e\u003c/a\u003e\n### Bubble Sort\n\nBubble sort is a simple sorting algorithm that repeatedly iterates through a list of items, compares adjacent items, and swaps them if they are in the wrong order.\n\n![Bubble Sort GIF](./assets/bubblesort.gif)\n\nHere is an example of bubble sort implemented in TypeScript:\n\n```ts\nfunction bubbleSort(arr: number[]) {\n    for (let i = 0; i \u003c arr.length; i++) {\n        for (let j = 0; j \u003c arr.length - i - 1; j++) {\n            if (arr[j] \u003e arr[j + 1]) {\n                // Swap elements\n                let temp = arr[j];\n                arr[j] = arr[j + 1];\n                arr[j + 1] = temp;\n            }\n        }\n    }\n    return arr;\n}\n\n// Test the function\nconsole.log(bubbleSort([5, 2, 1, 3, 4])); // Output: [1, 2, 3, 4, 5]\n```\n\nIn this implementation, the function takes in an array of numbers and uses two nested for loops to iterate through the array. The outer loop starts at the beginning of the array and moves from left to right, while the inner loop starts at the second element and moves from left to right, comparing each element to its left neighbor. If the left neighbor is larger, the two elements are swapped. This process is repeated until the array is sorted.\n\nBubble sort has a time complexity of O(n^2), which means that it is not very efficient for large lists. However, it is simple to implement and can be a good choice for small lists.\n\n\u003c!-- TOC --\u003e\u003ca name=\"selection-sort\"\u003e\u003c/a\u003e\n### Selection Sort\n\nSelection sort is a sorting algorithm that works by repeatedly finding the minimum element in a list and swapping it with the first element in the unsorted portion of the list.\n\n![Selection Sort GIF](./assets/selectionsort.gif)\n\nHere is an example of selection sort implemented in TypeScript:\n\n```ts\nfunction selectionSort(arr: number[]) {\n    for (let i = 0; i \u003c arr.length - 1; i++) {\n        let minIndex = i;\n        for (let j = i + 1; j \u003c arr.length; j++) {\n            if (arr[j] \u003c arr[minIndex]) {\n                minIndex = j;\n            }\n        }\n        // Swap elements\n        let temp = arr[i];\n        arr[i] = arr[minIndex];\n        arr[minIndex] = temp;\n    }\n    return arr;\n}\n\n// Test the function\nconsole.log(selectionSort([5, 2, 1, 3, 4])); // Output: [1, 2, 3, 4, 5]\n```\n\nIn this implementation, the function takes in an array of numbers and uses two nested for loops to iterate through the array. The outer loop starts at the beginning of the array and moves from left to right, while the inner loop starts at the second element and moves from left to right, searching for the minimum element. When the minimum element is found, it is swapped with the element at the current position of the outer loop.\n\nSelection sort has a time complexity of O(n^2), which means that it is not very efficient for large lists. However, it is relatively simple to implement and can be a good choice for small lists or lists that are almost sorted.\n\n\u003c!-- TOC --\u003e\u003ca name=\"insertion-sort\"\u003e\u003c/a\u003e\n### Insertion Sort\n\nInsertion sort is a sorting algorithm that works by iterating through a list of items, taking each element in turn and inserting it into its correct position in the list.\n\n![Insertion Sort Gif](./assets/insertionsort.gif)\n\nHere is an example of insertion sort implemented in TypeScript:\n\n```ts\nfunction insertionSort(arr: number[]) {\n    for (let i = 1; i \u003c arr.length; i++) {\n        let current = arr[i];\n        let j = i - 1;\n        while (j \u003e= 0 \u0026\u0026 arr[j] \u003e current) {\n            arr[j + 1] = arr[j];\n            j--;\n        }\n        arr[j + 1] = current;\n    }\n    return arr;\n}\n\n// Test the function\nconsole.log(insertionSort([5, 2, 1, 3, 4])); // Output: [1, 2, 3, 4, 5]\n```\n\nIn this implementation, the function takes in an array of numbers and uses a for loop to iterate through the array. On each iteration, the current element is stored in a variable and the loop compares it to the elements on its left, shifting them to the right if they are larger. When the correct position for the current element is found, it is inserted into the list.\n\nInsertion sort has a time complexity of O(n^2), which means that it is not very efficient for large lists. However, it is relatively simple to implement and can be a good choice for small lists or lists that are almost sorted.\n\n\u003c!-- TOC --\u003e\u003ca name=\"merge-sort\"\u003e\u003c/a\u003e\n### Merge Sort\n\nMerge sort is a sorting algorithm that works by dividing a list of items into smaller sublists, sorting the sublists, and then merging the sorted sublists back together to form a sorted list.\n\n![Merge Sort GIF](./assets/mergesort.gif)\n\nHere is an example of merge sort implemented in TypeScript:\n\n```ts\nfunction mergeSort(arr: number[]) {\n    if (arr.length \u003c= 1) return arr;\n\n    const middle = Math.floor(arr.length / 2);\n    const left = arr.slice(0, middle);\n    const right = arr.slice(middle);\n\n    return merge(mergeSort(left), mergeSort(right));\n}\n\nfunction merge(left: number[], right: number[]) {\n    const result = [];\n    let i = 0;\n    let j = 0;\n\n    while (i \u003c left.length \u0026\u0026 j \u003c right.length) {\n        if (left[i] \u003c right[j]) {\n            result.push(left[i]);\n            i++;\n        } else {\n            result.push(right[j]);\n            j++;\n        }\n    }\n\n    return result.concat(left.slice(i)).concat(right.slice(j));\n}\n\n// Test the function\nconsole.log(mergeSort([5, 2, 1, 3, 4])); // Output: [1, 2, 3, 4, 5]\n```\n\nIn this implementation, the `mergeSort` function takes in an array of numbers and divides it into smaller sublists using recursion. The `merge` function is then used to merge the sorted sublists back together. The `merge` function works by comparing the first element of each sublist and adding the smaller element to the result array. This process is repeated until one of the sublists is empty, at which point the remaining elements of the other sublist are added to the result.\n\nMerge sort has a time complexity of O(nlogn), which means that it is more efficient than some other sorting algorithms for large lists. It is also relatively simple to implement and is a good choice for many different situations.\n\n\u003c!-- TOC --\u003e\u003ca name=\"quick-sort\"\u003e\u003c/a\u003e\n### Quick Sort\n\nQuick sort is a sorting algorithm that works by selecting a \"pivot\" element from the list and partitioning the other elements into two sublists based on whether they are less than or greater than the pivot. The sublists are then sorted recursively and the results are merged back together to form a sorted list.\n\n![Quick Search GIF](./assets/quick_sort.gif)\n\nHere is an example of quick sort implemented in TypeScript:\n\n```ts\nfunction quickSort(\n    arr: number[],\n    left: number = 0,\n    right: number = arr.length - 1\n) {\n    if (left \u003c right) {\n        const pivotIndex = partition(arr, left, right);\n        quickSort(arr, left, pivotIndex - 1);\n        quickSort(arr, pivotIndex + 1, right);\n    }\n    return arr;\n}\n\nfunction partition(arr: number[], left: number, right: number) {\n    const pivot = arr[right];\n    let i = left;\n    for (let j = left; j \u003c right; j++) {\n        if (arr[j] \u003c pivot) {\n            // Swap elements\n            const temp = arr[i];\n            arr[i] = arr[j];\n            arr[j] = temp;\n            i++;\n        }\n    }\n    // Swap pivot into correct position\n    arr[right] = arr[i];\n    arr[i] = pivot;\n    return i;\n}\n\n// Test the function\nconsole.log(quickSort([5, 2, 1, 3, 4])); // Output: [1, 2, 3, 4, 5]\n```\n\nIn this implementation, the `quickSort` function takes in an array of numbers and uses recursion to sort the sublists. The `partition` function is used to divide the list into sublists based on the pivot element. It works by iterating through the list and swapping elements that are less than the pivot into the left side of the list. The pivot is then swapped into its correct position and the index of the pivot is returned.\n\nQuick sort has a time complexity of O(nlogn) on average, which means that it is generally more efficient than some other sorting algorithms for large lists. However, it can have a time complexity of O(n^2) in the worst case, which makes it less efficient than some other algorithms in certain situations.\n\n\u003c!-- TOC --\u003e\u003ca name=\"radix-sort\"\u003e\u003c/a\u003e\n### Radix Sort\n\nRadix sort is a sorting algorithm that works by sorting the elements of a list based on the digits of their numeric keys, starting with the least significant digit and working toward the most significant digit.\n\n![Radix sort GIF](./assets/redixsort.gif)\n\nHere is an example of radix sort implemented in TypeScript:\n\n```typescript\nfunction radixSort(arr: number[]) {\n    const max = Math.max(...arr);\n    let maxLength = max.toString().length;\n    for (let i = 0; i \u003c maxLength; i++) {\n        let buckets = Array.from({ length: 10 }, () =\u003e []);\n        for (let j = 0; j \u003c arr.length; j++) {\n            let digit = getDigit(arr[j], i);\n            buckets[digit].push(arr[j]);\n        }\n        arr = [].concat(...buckets);\n    }\n    return arr;\n}\n\nfunction getDigit(num: number, place: number) {\n    return Math.floor(Math.abs(num) / Math.pow(10, place)) % 10;\n}\n\n// Test the function\nconsole.log(radixSort([5, 2, 1, 3, 4])); // Output: [1, 2, 3, 4, 5]\n```\n\nIn this implementation, the `radixSort` function takes in an array of numbers and uses a for loop to iterate through each digit place, starting with the least significant digit. The `getDigit` function is used to extract the digit at the current place for each element. The elements are then placed into \"buckets\" based on their digits and the buckets are concatenated back into the array. This process is repeated until all digits have been considered.\n\nRadix sort has a time complexity of O(kn) where k is the number of digits in the largest element and n is the number of elements in the list. This means that it can be more efficient than some other sorting algorithms for large lists with a small range of values. However, it is not suitable for lists with elements that have a large number of digits.\n\n\u003c!-- TOC --\u003e\u003ca name=\"data-structure\"\u003e\u003c/a\u003e\n## Data Structure\n\n\u003c!-- TOC --\u003e\u003ca name=\"complexity-comparison\"\u003e\u003c/a\u003e\n### complexity comparison\n\n|   DataStructure    | Insertion |                         Removal                          |                 Searching                 | Access |\n| :----------------: | :-------: | :------------------------------------------------------: | :---------------------------------------: | :----: |\n| Singly Linked List |   O(1)    | bestCase(very beginning): O(1) worstCase(very end): O(n) |                   O(n)                    |  O(n)  |\n| Doubly Linked List |   O(1)    |                           O(1)                           | O(n) it is faster than Singly Linked List |  O(n)  |\n|       Stack        |   O(1)    |                           O(1)                           |                   O(n)                    |  O(n)  |\n|       Queue        |   O(1)    |                           O(1)                           |                   O(n)                    |  O(n)  |\n| Binary Search Tree | O( Log n) |                            -                             |                 O(Log n)                  |   -    |\n|    Binary Heap     | O( Log n) |                        O( Log n)                         |                  O( n )                   |   -    |\n|    Hash Tables     |  O( 1 )   |                          O( 1 )                          |                     -                     | O( 1 ) |\n\n\u003c!-- TOC --\u003e\u003ca name=\"singly-linked-list\"\u003e\u003c/a\u003e\n## Singly Linked list\n\n```typescript\nclass _Node {\n    constructor(public value: any) {}\n    public next: _Node | null = null;\n}\n\nclass SinglyLinkedList {\n    private _length: number = 0;\n    private head: _Node | null = null;\n    private tail: _Node | null = null;\n\n    get length() {\n        return this._length;\n    }\n\n    get print(): null | _Node[] {\n        if (!this._length) return null;\n\n        const arr = [];\n        let currentNode = this.head;\n        while (currentNode) {\n            arr.push(currentNode.value);\n            currentNode = currentNode.next;\n        }\n        return arr;\n    }\n\n    public push(value: any): SinglyLinkedList {\n        const node = new _Node(value);\n\n        if (!this.head || !this.tail) {\n            this.head = node;\n            this.tail = this.head;\n        } else {\n            this.tail.next = node;\n            this.tail = node;\n        }\n        this._length += 1;\n\n        return this;\n    }\n\n    public pop(): _Node | null {\n        if (!this.head) return null;\n\n        let currentNode = this.head;\n\n        if (!currentNode.next) {\n            this.head = null;\n            this.tail = null;\n            this._length -= 1;\n            return currentNode;\n        }\n        while (currentNode.next \u0026\u0026 currentNode.next.next) {\n            currentNode = currentNode.next;\n        }\n        this.tail = currentNode;\n        this.tail.next = null;\n        this._length -= 1;\n        return currentNode.next as _Node;\n    }\n\n    public unShift(value: any): SinglyLinkedList {\n        const currentHead = this.head;\n\n        this.head = new _Node(value);\n\n        if (currentHead) {\n            this.head.next = currentHead;\n        } else {\n            this.tail = this.head;\n        }\n        this._length += 1;\n        return this;\n    }\n\n    public shift(): _Node | null {\n        if (!this.head) return null;\n\n        const currentHead = this.head;\n        this.head = currentHead.next;\n        this._length -= 1;\n\n        if (currentHead === this.tail) this.tail = null;\n\n        return currentHead;\n    }\n\n    public get(index: number): _Node | null {\n        if (index \u003c 0 || index \u003e= this._length) return null;\n\n        let currentNode = this.head;\n        for (let j = 0; j \u003c index; j++) {\n            if (currentNode \u0026\u0026 currentNode.next) {\n                currentNode = currentNode.next;\n            }\n        }\n        return currentNode;\n    }\n\n    public set(index: number, value: any): _Node | null {\n        const node = this.get(index);\n        if (node) {\n            node.value = value;\n        }\n        return node;\n    }\n\n    public insert(index: number, value: any): SinglyLinkedList | null {\n        if (index \u003c 0 || index \u003e= this._length) {\n            return null;\n        } else if (index === 0) {\n            return this.unShift(value);\n        } else if (index === this._length) {\n            return this.push(value);\n        } else {\n            const prevNode = this.get(index - 1);\n\n            if (prevNode) {\n                const newNode = new _Node(value);\n                newNode.next = prevNode.next;\n                prevNode.next = newNode;\n                this._length += 1;\n\n                return this;\n            }\n            return prevNode;\n        }\n    }\n\n    public remove(index: number): _Node | null {\n        if (index === 0) {\n            return this.shift();\n        } else if (index === this._length - 1) {\n            return this.pop();\n        } else {\n            const prevNode = this.get(index - 1);\n            const currentNode = this.get(index);\n            if (prevNode \u0026\u0026 currentNode) {\n                prevNode.next = currentNode.next;\n                this._length -= 1;\n            }\n            return currentNode;\n        }\n    }\n\n    public reverse(): SinglyLinkedList | false {\n        if (this._length \u003c= 1) return false;\n\n        let node = this.head;\n        this.head = this.tail;\n        this.tail = node;\n\n        let next: _Node | null;\n        let prev: _Node | null = null;\n        for (let i = 0; i \u003c this._length; i++) {\n            if (node) {\n                next = node.next;\n                node.next = prev;\n                prev = node;\n                node = next;\n            }\n        }\n        return this;\n    }\n}\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"doubly-linked-list\"\u003e\u003c/a\u003e\n## Doubly Linked List\n\n```typescript\nclass _Node {\n    public next: _Node | null = null;\n    public prev: _Node | null = null;\n\n    constructor(public value: any) {}\n}\n\nclass DoublyLinkedList {\n    private head: _Node | null = null;\n    private tail: _Node | null = null;\n\n    private _length = 0;\n\n    get length() {\n        return this._length;\n    }\n\n    get print(): null | _Node[] {\n        if (!this._length) return null;\n\n        const arr = [];\n        let currentNode = this.head;\n        while (currentNode) {\n            arr.push(currentNode.value);\n            currentNode = currentNode.next;\n        }\n        return arr;\n    }\n\n    public push(value: any): DoublyLinkedList {\n        const node = new _Node(value);\n\n        if (!this.tail) {\n            this.head = node;\n        } else {\n            this.tail.next = node;\n            node.prev = this.tail;\n        }\n        this._length += 1;\n        this.tail = node;\n\n        return this;\n    }\n\n    public pop(): _Node | null {\n        if (!this.tail) {\n            return null;\n        }\n\n        const currentTail = this.tail;\n        if (currentTail.prev) {\n            this.tail = currentTail.prev;\n            this.tail.next = null;\n            currentTail.prev = null;\n        } else {\n            this.head = null;\n            this.tail = null;\n        }\n\n        this._length -= 1;\n        return currentTail;\n    }\n\n    public shift(): null | _Node {\n        if (!this.head) {\n            return null;\n        }\n\n        const currentHead = this.head;\n        if (currentHead.next) {\n            this.head = currentHead.next;\n            this.head.prev = null;\n            currentHead.next = null;\n        } else {\n            return this.pop();\n        }\n\n        this._length -= 1;\n        return currentHead;\n    }\n\n    public unshift(value: any): DoublyLinkedList {\n        if (!this.head) {\n            return this.push(value);\n        }\n\n        const node = new _Node(value);\n        const currentHead = this.head;\n\n        this.head = node;\n        this.head.next = currentHead;\n        currentHead.prev = this.head;\n\n        this._length += 1;\n        return this;\n    }\n\n    public get(index: number): null | _Node {\n        if (index \u003c 0 || index \u003e= this._length) return null;\n\n        let currentNode: _Node | null = null;\n\n        if (index \u003c Math.floor(this._length / 2)) {\n            // iterate from head to tail\n\n            currentNode = this.head;\n            for (let i = 0; i \u003c index; i++) {\n                if (currentNode \u0026\u0026 currentNode.next) {\n                    currentNode = currentNode.next;\n                }\n            }\n        } else {\n            // iterate from tail to head\n\n            currentNode = this.tail;\n            for (let i = this._length - 1; i \u003e index; i--) {\n                if (currentNode \u0026\u0026 currentNode.prev) {\n                    currentNode = currentNode.prev;\n                }\n                return currentNode;\n            }\n        }\n\n        return currentNode;\n    }\n\n    public set(index: number, value: any): _Node | null {\n        const node = this.get(index);\n        if (node) {\n            node.value = value;\n        }\n        return node;\n    }\n\n    public insert(index: number, value: any): DoublyLinkedList | null {\n        if (index \u003c 0 || index \u003e this._length) {\n            return null;\n        } else if (index === 0) {\n            return this.unshift(value);\n        } else if (index === this._length) {\n            return this.push(value);\n        } else {\n            const prevNode = this.get(index - 1);\n            const nextNode = this.get(index);\n\n            if (prevNode \u0026\u0026 nextNode) {\n                const newNode = new _Node(value);\n\n                prevNode.next = newNode;\n                (newNode.prev = prevNode), (newNode.next = nextNode);\n                nextNode.prev = newNode;\n            }\n        }\n        this._length += 1;\n        return this;\n    }\n\n    public remove(index: number): DoublyLinkedList | null {\n        if (index \u003c 0 || index \u003e this._length) {\n            return null;\n        } else if (index === 0) {\n            this.shift();\n        } else if (index === this._length - 1) {\n            this.pop();\n        } else {\n            const node = this.get(index);\n\n            if (node \u0026\u0026 node.prev \u0026\u0026 node.next) {\n                (node.prev.next = node.next), (node.next.prev = node.prev);\n                (node.next = null), (node.prev = null);\n            }\n            this._length -= 1;\n        }\n        return this;\n    }\n}\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"stacks\"\u003e\u003c/a\u003e\n## Stacks\n\nLIFO\nlast in first out\n\n```typescript\n// implement stack using array\nconst stack = [1, 2, 3];\nstack.push(4); // [1,2,3,4]\nstack.pop(); // [1,2,3]\n// stacks just have push and pop\nstack.unshift(0); // [0,1,2,3]\nstack.shift(); // [1,2,3]\n```\n\n```typescript\n// implementing stack using singly linked list\nclass _Node {\n    public next: _Node | null = null;\n\n    constructor(public value: any) {}\n}\n\nclass Stack {\n    private first: _Node | null = null;\n    private last: _Node | null = null;\n\n    private _length = 0;\n    get length(): number {\n        return this._length;\n    }\n\n    push(value: any): Stack {\n        const node = new _Node(value);\n        const currentFirst = this.first;\n\n        (this.first = node), (this.first.next = currentFirst);\n\n        if (!currentFirst) {\n            this.last = node;\n        }\n\n        this._length += 1;\n        return this;\n    }\n\n    pop(): _Node | null {\n        const currentFirst = this.first;\n        if (currentFirst) {\n            if (this.first === this.last) this.last = currentFirst.next;\n            this.first = currentFirst.next;\n            this._length -= 1;\n        }\n        return currentFirst;\n    }\n}\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"queue\"\u003e\u003c/a\u003e\n## Queue\n\nFIFO\nfirst in first out\n\n```typescript\n// implementing queue using array\nconst q = [];\nq.push(1);\nq.push(2);\nq.shift(1); // out first items first\n// or\nq.shift(1);\nq.shift(2);\nq.pop(); // out first items first\n```\n\n```typescript\n// implementing queue using singly linked list\nclass _Node {\n    public next: _Node | null = null;\n\n    constructor(public value: any) {}\n}\n\nclass Queue {\n    private first: _Node | null = null;\n    private last: _Node | null = null;\n\n    private _length = 0;\n    get length(): number {\n        return this._length;\n    }\n\n    enqueue(value: any): Queue {\n        const node = new _Node(value);\n        if (!this.last) {\n            (this.first = node), (this.last = node);\n        } else {\n            this.last.next = node;\n            this.last = node;\n        }\n\n        this._length += 1;\n        return this;\n    }\n\n    dequeue(): _Node | null {\n        const currentFirst = this.first;\n        if (currentFirst) {\n            if (this.first === this.last) this.last = null;\n            this.first = currentFirst.next;\n            this._length -= 1;\n        }\n\n        return currentFirst;\n    }\n}\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"tree\"\u003e\u003c/a\u003e\n## Tree\n\n\u003c!-- TOC --\u003e\u003ca name=\"terminology\"\u003e\u003c/a\u003e\n### terminology\n\n-   root: top node of the tree\n-   child: a node directly connected to another node when moving away from the root\n-   parent: the converse notion of a child\n-   sibling: a group of nodes with the same parent\n-   leaf: a child with no children\n-   edge: connection from two-node\n\n\u003c!-- TOC --\u003e\u003ca name=\"binary-search-tree\"\u003e\u003c/a\u003e\n### binary search tree\n\n-   every parent node has at most **two** children\n-   every node to the **left** of the parent node is always **less** than the **parent**\n-   every node to the **right** of the parent node is always **greater** than the **parent**\n\n```typescript\nclass _Node {\n    constructor(public value: number) {}\n\n    public left: _Node | null = null;\n    public right: _Node | null = null;\n}\nclass BinarySearchTree {\n    public root: _Node | null = null;\n\n    public insert(value: number): BinarySearchTree | null {\n        const node = new _Node(value);\n        if (!this.root) {\n            this.root = node;\n        } else {\n            let currentNode: _Node = this.root;\n            do {\n                if (value === currentNode.value) return null;\n\n                if (value \u003c currentNode.value) {\n                    if (currentNode.left) {\n                        currentNode = currentNode.left;\n                    } else {\n                        currentNode.left = node;\n                        break;\n                    }\n                } else {\n                    if (currentNode.right) {\n                        currentNode = currentNode.right;\n                    } else {\n                        currentNode.right = node;\n                        break;\n                    }\n                }\n            } while (currentNode);\n        }\n        return this;\n    }\n\n    public have(value: number): boolean {\n        let currentNode = this.root;\n        while (currentNode) {\n            if (value === currentNode.value) {\n                return true;\n            } else {\n                if (value \u003c currentNode.value) {\n                    if (currentNode.left) {\n                        currentNode = currentNode.left;\n                        continue;\n                    }\n                    break;\n                } else {\n                    if (currentNode.right) {\n                        currentNode = currentNode.right;\n                        continue;\n                    }\n                    break;\n                }\n            }\n        }\n        return false;\n    }\n}\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"tree-traversal\"\u003e\u003c/a\u003e\n### tree traversal\n\nthere are two main strategies to traversal a tree: **Breadth-first-search** and **Depth-first-search**\n\n```typescript\nclass _Node {\n    constructor(public value: number) {}\n\n    public left: _Node | null = null;\n    public right: _Node | null = null;\n}\nclass BinarySearchTree {\n    public root: _Node | null = null;\n\n    public insert(value: number): BinarySearchTree | null {\n        const node = new _Node(value);\n        if (!this.root) {\n            this.root = node;\n        } else {\n            let currentNode: _Node = this.root;\n            do {\n                if (value === currentNode.value) return null;\n\n                if (value \u003c currentNode.value) {\n                    if (currentNode.left) {\n                        currentNode = currentNode.left;\n                    } else {\n                        currentNode.left = node;\n                        break;\n                    }\n                } else {\n                    if (currentNode.right) {\n                        currentNode = currentNode.right;\n                    } else {\n                        currentNode.right = node;\n                        break;\n                    }\n                }\n            } while (currentNode);\n        }\n        return this;\n    }\n\n    public have(value: number): boolean {\n        let currentNode = this.root;\n        while (currentNode) {\n            if (value === currentNode.value) {\n                return true;\n            } else {\n                if (value \u003c currentNode.value) {\n                    if (currentNode.left) {\n                        currentNode = currentNode.left;\n                    }\n                    break;\n                } else {\n                    if (currentNode.right) {\n                        currentNode = currentNode.right;\n                        continue;\n                    }\n                    break;\n                }\n            }\n        }\n        return false;\n    }\n    /* \n    breadth first search (bfs) : traverse tree horizontally\n*/\n    public bfs(): _Node[] {\n        const visited: _Node[] = [];\n        if (this.root) {\n            const q: _Node[] = [this.root];\n            while (q.length) {\n                if (q[0].left) q.push(q[0].left);\n                if (q[0].right) q.push(q[0].right);\n\n                visited.push(q[0]), q.shift();\n            }\n        }\n        return visited;\n    }\n    /*\n    depth first search (dfs) : traverse tree vertically\n    following contains three dfs searching methods:\n    1. preOrder : add node =\u003e going to left and add left =\u003e going to right and add right \n    2. postOrder : going to left and add left =\u003e going to right and add right =\u003e going to node and add node \n    3. inOrder : going to the left and add left =\u003e add node =\u003e going to the right and add right\n     */\n    public dfsPreOrder(): _Node[] {\n        const visited: _Node[] = [];\n        if (this.root) {\n            (function traverse(node: _Node): void {\n                visited.push(node);\n\n                if (node.left) {\n                    traverse(node.left);\n                }\n                if (node.right) {\n                    traverse(node.right);\n                }\n            })(this.root);\n        }\n\n        return visited;\n    }\n\n    public dfsPostOrder(): _Node[] {\n        const visited: _Node[] = [];\n\n        if (this.root) {\n            (function traverse(node: _Node): void {\n                if (node.left) {\n                    traverse(node.left);\n                }\n                if (node.right) {\n                    traverse(node.right);\n                }\n\n                visited.push(node);\n            })(this.root);\n        }\n        return visited;\n    }\n\n    dfsInOrder(): _Node[] {\n        const visited: _Node[] = [];\n\n        if (this.root) {\n            (function traverse(node: _Node) {\n                if (node.left) {\n                    traverse(node.left);\n                }\n\n                visited.push(node);\n                f;\n\n                if (node.right) {\n                    traverse(node.right);\n                }\n            })(this.root);\n        }\n\n        return visited;\n    }\n}\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"traversal-comparison\"\u003e\u003c/a\u003e\n### traversal comparison\n\n**depth-first** _vs_ **breadth-first** : they both **timeComplexity is same** but **spaceComplexity is different** if we got **a wide tree** like this:\n\n![](./assets/Z20M5iE.png)\n\n**breadth-first take up more space.** cuz we adding more element to queue.\n\nif we got **a depth long tree** like this:\n\n![](./assets/Binary-search-tree-insertion-When-a-sequence-of-data-f1-3-4-6-5-7-9-8-2-g.png)\n\n**depth-first take up more space.**\n\n\u003chr/\u003e\n\n**potentially use cases for dfs variants (_preOder postOrder inOrder_)**\npreOrder is useful when we want a clone of the tree.\ninOrder is useful when we want data so that it's stored in the tree.\n\n\u003c!-- TOC --\u003e\u003ca name=\"binary-heaps\"\u003e\u003c/a\u003e\n## Binary heaps\n\n\u003c!-- TOC --\u003e\u003ca name=\"terminology-1\"\u003e\u003c/a\u003e\n### terminology\n\n-   a binary heap is as compact as possible (all the children of each node are as full as they can be and left children and filled out first)\n-   each parent has at most two children\n\n**Max Binary Heap**:\n\n-   **parent** nodes are always greater than **child** nodes but there are no guarantees between sibling\n\n**Min Binary Heap**:\n\n-   **child** nodes are always greater than **parent** nodes but there are no guarantees between sibling\n\n\u003c!-- TOC --\u003e\u003ca name=\"binary-heap-parent-and-child-relations\"\u003e\u003c/a\u003e\n### binary heap parent and child relations\n\n![](./assets/binaryHeapsParentAndChildRelation.jpg)\n\n```typescript\nclass MaxBinaryHeap {\n    private _values: number[] = [];\n    get values(): number[] {\n        return this._values;\n    }\n\n    private sinkingUp(value: number): void {\n        let valueIndex = this._values.length - 1;\n        while (valueIndex \u003e 0) {\n            const parentIndex = Math.floor((valueIndex - 1) / 2);\n            const parent = this._values[parentIndex];\n\n            if (value \u003c= parent) break;\n\n            this._values[parentIndex] = value;\n            this._values[valueIndex] = parent;\n\n            valueIndex = parentIndex;\n        }\n    }\n    private sinkingDown(): void {\n        let targetIndex = 0;\n        while (true) {\n            let leftChildIndex = targetIndex * 2 + 1,\n                rightChildIndex = targetIndex * 2 + 2;\n\n            let target = this._values[targetIndex],\n                leftChild = this._values[leftChildIndex],\n                rightChild = this._values[rightChildIndex];\n\n            if (target \u003c leftChild \u0026\u0026 target \u003c rightChild) {\n                if (rightChild \u003e leftChild) {\n                    [this._values[targetIndex], this._values[rightChildIndex]] =\n                        [\n                            this._values[rightChildIndex],\n                            this._values[targetIndex],\n                        ];\n\n                    targetIndex = rightChildIndex;\n                } else {\n                    [this._values[targetIndex], this._values[leftChildIndex]] =\n                        [\n                            this._values[leftChildIndex],\n                            this._values[targetIndex],\n                        ];\n\n                    targetIndex = leftChildIndex;\n                }\n\n                continue;\n            } else if (rightChild \u003e= target) {\n                [this._values[targetIndex], this._values[rightChildIndex]] = [\n                    this._values[rightChildIndex],\n                    this._values[targetIndex],\n                ];\n\n                targetIndex = leftChildIndex;\n\n                continue;\n            } else if (leftChild \u003e= target) {\n                [this._values[targetIndex], this._values[leftChildIndex]] = [\n                    this._values[leftChildIndex],\n                    this._values[targetIndex],\n                ];\n\n                targetIndex = leftChildIndex;\n\n                continue;\n            }\n\n            break;\n        }\n    }\n\n    public insert(value: number): number[] {\n        this._values.push(value);\n        this.sinkingUp(value);\n        return this._values;\n    }\n\n    public extractMax(): number | null {\n        if (!this._values.length) {\n            return null;\n        }\n        const root = this._values[0];\n        this._values[0] = this._values[this._values.length - 1];\n        this._values.pop();\n        this.sinkingDown();\n\n        return root;\n    }\n}\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"priority-queue\"\u003e\u003c/a\u003e\n## Priority Queue\n\nA data structure which every element has a priority.\nElements with higher priorities are served before elements with lower priorities.\n\n**In the following example, we implemented a priority queue using minBinaryHeap but you should know binaryHeaps and priority queue is two different concepts and we just use abstract of it**\n\n```typescript\ninterface INode {\n    value: any;\n    priority: number;\n}\n\nclass _Node implements INode {\n    constructor(public value: any, public priority: number = 0) {}\n}\n\nclass PriorityQueue {\n    private _values: INode[] = [];\n    get values(): INode[] {\n        return this._values;\n    }\n\n    private sinkingUp(node: INode): void {\n        let valueIndex = this._values.length - 1;\n        while (valueIndex \u003e 0) {\n            const parentIndex = Math.floor((valueIndex - 1) / 2);\n            const parent = this._values[parentIndex];\n\n            if (node.priority \u003e= parent.priority) break;\n\n            this._values[parentIndex] = node;\n            this._values[valueIndex] = parent;\n\n            valueIndex = parentIndex;\n        }\n    }\n    private sinkingDown(): void {\n        let targetIndex = 0;\n        while (true) {\n            let leftChildIndex = targetIndex * 2 + 1,\n                rightChildIndex = targetIndex * 2 + 2;\n\n            let target = this._values[targetIndex],\n                leftChild = this._values[leftChildIndex],\n                rightChild = this._values[rightChildIndex];\n\n            if (\n                leftChild \u0026\u0026\n                rightChild \u0026\u0026\n                target.priority \u003e leftChild.priority \u0026\u0026\n                target.priority \u003e rightChild.priority\n            ) {\n                if (rightChild.priority \u003c leftChild.priority) {\n                    [this._values[targetIndex], this._values[rightChildIndex]] =\n                        [\n                            this._values[rightChildIndex],\n                            this._values[targetIndex],\n                        ];\n\n                    targetIndex = rightChildIndex;\n                } else {\n                    [this._values[targetIndex], this._values[leftChildIndex]] =\n                        [\n                            this._values[leftChildIndex],\n                            this._values[targetIndex],\n                        ];\n\n                    targetIndex = leftChildIndex;\n                }\n\n                continue;\n            } else if (rightChild \u0026\u0026 rightChild.priority \u003c= target.priority) {\n                [this._values[targetIndex], this._values[rightChildIndex]] = [\n                    this._values[rightChildIndex],\n                    this._values[targetIndex],\n                ];\n\n                targetIndex = leftChildIndex;\n\n                continue;\n            } else if (leftChild \u0026\u0026 leftChild.priority \u003c= target.priority) {\n                [this._values[targetIndex], this._values[leftChildIndex]] = [\n                    this._values[leftChildIndex],\n                    this._values[targetIndex],\n                ];\n\n                targetIndex = leftChildIndex;\n\n                continue;\n            }\n\n            break;\n        }\n    }\n\n    public enqueue({ value, priority }: INode): _Node[] {\n        const node = new _Node(value, priority);\n        this._values.push(node);\n        this.sinkingUp(node);\n        return this._values;\n    }\n\n    public dequeue(): _Node | null {\n        if (!this._values.length) {\n            return null;\n        }\n        const root = this._values[0];\n        this._values[0] = this._values[this._values.length - 1];\n        this._values.pop();\n        this.sinkingDown();\n\n        return root;\n    }\n}\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"hash-tables\"\u003e\u003c/a\u003e\n## Hash Tables\n\nHash tables are a collection of key-value pairs\n\n\u003c!-- TOC --\u003e\u003ca name=\"collisions\"\u003e\u003c/a\u003e\n### collisions\n\nThere is a possibility for handle collisions in hash tables :\n\n-   Separate chaining ( e.g. using nested arrays of key values _implemented in following hash tables_ )\n-   linear probing ( if index filled place {key, value} in next position )\n\n```typescript\ntype El = [string, any];\nclass HashTable {\n    private keyMap: El[][];\n    constructor(size: number = 53) {\n        this.keyMap = new Array(size);\n    }\n\n    public _hash(key: string): number {\n        let total = 0;\n        const WEIRD_PRIME = 31;\n\n        for (let i = 0; i \u003c key.length; i++) {\n            const characterCode = key.charCodeAt(i) - 96;\n            total = (total + characterCode * WEIRD_PRIME) % this.keyMap.length;\n        }\n        return total;\n    }\n\n    set(key: string, value: any): El[][] {\n        const index = this._hash(key);\n        if (!this.keyMap[index]) {\n            this.keyMap[index] = [];\n        }\n\n        this.keyMap[index].push([key, value]);\n\n        return this.keyMap;\n    }\n\n    get(key: string): El | undefined {\n        const index = this._hash(key);\n\n        const elements = this.keyMap[index];\n\n        if (elements) {\n            for (let value of elements) {\n                if (value[0] === key) return value[1];\n            }\n        }\n\n        return undefined;\n    }\n\n    get keys(): string[] {\n        const keys: string[] = [];\n        for (let value of this.keyMap) {\n            if (value) {\n                for (let _value of value) {\n                    keys.push(_value[0]);\n                }\n            }\n        }\n        return keys;\n    }\n\n    get values(): any[] {\n        const values = new Set\u003cany\u003e();\n\n        for (let value of this.keyMap) {\n            if (value) {\n                for (let _value of value) {\n                    values.add(value[1]);\n                }\n            }\n        }\n\n        return [...values];\n    }\n}\n```\n\n\u003c!-- TOC --\u003e\u003ca name=\"graphs\"\u003e\u003c/a\u003e\n## Graphs\n\nA graph data structure consists of a finite (and possibly mutable) set of vertices or nodes or points, together with a set of unordered pairs of these vertices for an undirected graph or a set of ordered pairs for a directed graph.\n\n\u003c!-- TOC --\u003e\u003ca name=\"terminology-2\"\u003e\u003c/a\u003e\n### terminology\n\n-   vertex :node\n\n-   edge: the connection between nodes\n\n-   directed/ undirected graph:\n    in the directed graph there is a direction assigned to vertices and in undirected, no direction is assigned.\n    ![](./assets/three-node-networks.jpg)\n\n-   weighted/ unweighted graph:\n    in a weighted graph,depth-first there is a weight associated with edges but in an unweighted graph no weight assigned to edges\n    ![](./assets/3.-Weithened-Graph.png)\n\n\u003c!-- TOC --\u003e\u003ca name=\"adjacency-matrix\"\u003e\u003c/a\u003e\n### adjacency matrix\n\n![](./assets/GahiR.jpg)\n\n\u003c!-- TOC --\u003e\u003ca name=\"adjacency-list\"\u003e\u003c/a\u003e\n## adjacency list\n\n![](./assets/268857bd-bb32-4fa5-88c9-66d7787952e9.png)\n\n\u003c!-- TOC --\u003e\u003ca name=\"adjacency-list-vs-adjacency-matrix\"\u003e\u003c/a\u003e\n## adjacency list vs adjacency matrix\n\n|   Operation   | Adjacency List | Adjacency Matrix |\n| :-----------: | :------------: | :--------------: |\n|  Add vertex   |      O(1)      |      O(V^2)      |\n|   Add Edge    |      O(1)      |       O(1)       |\n| Remove vertex |     O(V+E)     |      O(V^2)      |\n|  Remove Edge  |      O(E)      |       O(1)       |\n|     Query     |     O(V+E)     |       O(1)       |\n|    Storage    |     O(V+E)     |      O(V^2)      |\n\n-   |V| : number of Vertices\n-   |E| : number of Edges\n\n\u003chr/\u003e\n\n-   **Adjacency List** take **less space** in sparse graph( when we have a few edges ).\n-   **Adjacency List** are **faster to iterate** over edges.\n-   **Adjacency Matrix** are **faster to** finding a specific edge.\n\n\u003c!-- TOC --\u003e\u003ca name=\"graphadjacency-list\"\u003e\u003c/a\u003e\n### graph(adjacency list)\n\n```typescript\ninterface AdjacencyList {\n    [vertex: string]: string[];\n}\n\nclass Graph {\n    private _adjacencyList: AdjacencyList = {};\n    public get adjacencyList(): AdjacencyList {\n        return this._adjacencyList;\n    }\n    public set adjacencyList(value: AdjacencyList) {\n        this._adjacencyList = value;\n    }\n\n    public addVertex(vertex: string): AdjacencyList {\n        this._adjacencyList[vertex] = [];\n        return this._adjacencyList;\n    }\n\n    public addEdge(vertex1: string, vertex2: string): boolean {\n        if (this._adjacencyList[","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftajpouria%2Falgorithms-and-data-structures-cheat-sheet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftajpouria%2Falgorithms-and-data-structures-cheat-sheet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftajpouria%2Falgorithms-and-data-structures-cheat-sheet/lists"}