[data structure and algorithm] JavaScript data structure and algorithm reading notes

Posted by angershallreign on Thu, 13 Jan 2022 16:09:07 +0100

1. Introduction to JavaScript

2. ECMAScript and TypeScript overview

2.2.7 object oriented programming using classes

class Book {
    constructor(title, pages, isbn) {
        this.title = title
        this.pages = pages
        this.isbn = isbn
    }
    printTitle() {
        console.log(this.title)
    }
}

let book = new Book('title', 'pag')
book.title // title
book.pages // pag

1. Succession

About super keyword:

ES6 requires that the constructor of a subclass must execute the super function once, otherwise an error will be reported.

The keyword super can be used as either a function or an object. In these two cases, its usage is completely different;

  1. Use as a function

    The super method must be called in the constructor, because the subclass does not have its own this object, but inherits and processes the this object of the parent class, and super represents the constructor of the parent class. Although super represents the constructor of parent class A, it returns an instance of subclass B, that is, this inside super refers to B. therefore, super() is equivalent to a.prototype constructor. call(this, props)

  2. Use as object

    In normal methods, point to the prototype object of the parent class; In a static method, point to the parent class;

class ITBook extends Book {
	constructor(title, pages, isbn, technology) {
        super(title, pages, isbn) 
        this.technology = technology
    }
    printTechnology () {
        console.log(this.technology)
    }
}

let jsBook = new ITBook('study js algorithm', '210', '123456', 'JavaScript')
jsBook.title // 'learning js algorithm'
jsBook.printTechnology // 'JavaScript'

2. Use attribute accessor

class Person {
    constructor(name) {
        this._name = name
    }
    get name () {
        return this._name
    }
    set name(value) {
        this._name = value
    }
}

2.2.8 power operator

const area = 3.14 * r * r

const area = 3.14 * Math.pow(r, 2)

const area = 3.14 * (r ** 2)

// The above three methods are the same

3. Array

shift pop

unshift push

3.5 add or delete elements anywhere

number.splice(5, 3)  // Starting with index 5, three elements of length 3 are deleted

number.splice(5, 0, 2, 3, 4) // Starting with index 5, insert 2,3,4

number.splice(5, 3, 2, 3, 4) // Starting from index 5, three elements of length 3 are deleted and three elements of 2, 3 and 4 are inserted at the same time

3.6 2D and multidimensional arrays

Matrix (two-dimensional array, or array of arrays)

// To see the output of the matrix, you can create a general function
function printMatrix(myMatrix) {
    for (let i = 0; i < myMatrix.length; i++) {
        for (let j = 0l j < myMatrix[i].length; j++) {
            console.log(myMatrix[i][j])
        }
    }
}

3.7 array method reference of JavaScript

3.7.1 array merging

const zero = 0;
const positiveNumbers = [1, 2, 3];
const negativeNumbers = [-3, -2, -1];

let numbers = negativeNumbers.concat(zero, positiveNumbers) // [-3, -2, -1, 0, 1, 2, 3]
    

3.7.2 iterator function

4. Stack

4.2 stack data structure

An ordered set of last in, first out elements, newly added or to be deleted, are stored at the same end of the stack, called the top of the stack, and the other end is called the bottom of the stack;

4.2.1 create an array based stack

// Use array
class Stack {
    constructor() {
        this.items = []
    }
    push(item) {
        this.item.push(item)
    }
    pop() {
        return this.items.pop()
    }
    peek() {
        return this.items[this.item.length - 1]
    }
    isEmpty() {
        return this.items.length === 0
    }
    size() {
        return this.items.length
    }
    clear() {
        this.items = []
    }
}

4.3 create a Stack class based on JavaScript objects

// Use object
class ObjStack {
    constructor() {
        this.count = 0
        this.items = {}
    }
    push(item) {
        this.items[this.count] = item
        this.count++
    }
    pop() {
        if (this.isEmpty()) {
            return undefined
        }
        this.count--
        const result = this.items[this.count]
        delete this.items[this.count]
        return result
    }
    peek() {
        if (this.isEmpty()) {
            return undefined
        }
        return this.items[this.count - 1]
    }
    clear() {
        this.items = {}
        this.count = 0
    }
    toString() {
        if (this.isEmpty()) {
            return ''
        }
        let str = `${this.items[0]}`
        for (let i = 1; i < this.count; i++) {
            str = `${str},${this.items[i]}`
        }
        return str
    }
}

4.4 protecting internal elements of data structure

Although prototype based classes can save memory space and are better than function based classes in expansion, they cannot declare private properties or methods, which will make the data structure very dangerous, because the data structure can be changed by modifying properties or methods;

4.4.1 underline naming convention

Some developers like to use underline naming convention in js to mark an attribute as private;

However, this is only an agreement and cannot protect data. It can only rely on the common sense of developers;

4.4.2 implementing classes with Symbol

const _items = Symbol('stackItems')
class Stack {
	constructor() {
		this[_items] = []
	}
}

This method creates a fake private property because object Getownpropertysymbols method can obtain all Symbols properties declared in the class;

const stack = new Stack()
stack.push(5)
stack.push(8)
let objectSymbols = Object.getOwnPropertySymbols(stack)
stack[objectSymbols[0]].push(1)
console.log(stack) // 5,8,1

As you can see from the above code, you can access stack[objectSymbols[0]]_ items, and_ items is an array, which can be used for any array operation;

4.4.3 implementing classes with WeakMap

const items = new WeakMap()

class Stack {
    constructor() {
        items.set(this, [])
    }
    push(item) {
        const s = item.get(this)
        s.push(item)
    }
    pop() {
        const s = item.get(this)
        const r = s.pop()
        return r
    }
}

items is the real private attribute in the Stack class; Using this method, the readability of the code is not strong, and the private attributes cannot be inherited when extending the class;

4.4.4 ECMAScript attribute proposal

class Stack {
    #count = 0
    #items = 0
}

TS provides a private modifier for class attributes and methods. Then, the modifier is only useful at compile time. After the code is transferred, the attributes are also exposed;

A proposal to add private attributes to js classes; Use # as a prefix to declare private attributes;

4.5 solving problems with stack

4.5.1 from decimal to binary

function decimalToBinary(decNumber) {
    const remStack = new ObjStack()
    let number = decNumber
    let rem;
    let binaryString = ''
    while (number > 0) {
        rem = Math.floor(number % 2)
        remStack.push(rem)
        number = Math.floor(number / 2)
    }
    while (!remStack.isEmpty()) {
        binaryString += remStack.pop().toString
    }
    return binaryString
}

4.5.2 stack containing min

class MinStack {
    constructor () {
        // stackA is used to store data
        this.stackA = []
        this.countA = 0
        // Stackbis used to store data in descending order
        this.stackB = []
        this.countB = 0
    }
    push (item) {
        this.stackA[this.countA++] = item
        if (this.countB === 0 || item < this.min()) {
            this.stackB[this.countB++] = item
        }
    }
    min () {
        return this.stackB[this.countB - 1]
    }
    top () {
        return this.stackA[this.countA - 1]
    }
    pop () {
        if (this.top() === this.min()) {
            delete this.stackB[--this.countB]
        }
        delete this.stackA[--this.countA]
    }
}

4.5.3 leetCode739. Daily temperature

// Input the daily temperature and output the temperature of that day. How many days will it rise
// Input [73,74,75,71,69,72,76,73]
// Output [1, 1, 4, 2, 1, 1, 0, 0]
// Daily temperature problem
var dailyTemperatures = function(T) {
    const stack = new Stack()
    const arr = new Array(T.length).fill(0)
    for (let i = 0; i < T.length; i++) {
        let temp = T[i]
        while(stack.size() && temp > T[stack.peek()]) {
            let index = stack.pop()
            arr[index] = i - index
        }
        stack.push(i)
    }
    return arr
}
console.log(dailyTemperatures([73,74,75,71,69,72,76,73]))

5. Queue and double ended queue

A queue is an ordered set of items that follow the first in first out principle; Add a new element at the end of the queue and remove the element from the top. The newly added element must be at the end of the queue;

5.1 queue data structure

Because we want to remove the element from the head, we need a variable to track the head element

For the length of the queue, it becomes the distance from lowestCount to count;

// queue
class Queue {
    constructor() {
        this.count = 0 // Total number of queues
        this.lowestCount = 0 // Trace the sequence of the first element
        this.items = {}
    }
    // Add element to queue tail
    enqueue(element) {
        this.item[this.count] = element
        this.count++
    }
    // Remove header element from queue
    dequeue() {
        if (this.isEmpty()) {
            return undefined
        }
        const result = this.items[this.lowestCount]
        delete this.item[this.lowestCount]
        this.lowestCount++
        return result
    }
    // Return header element
    peek() {
        if (this.isEmpty()) {
            return undefined
        }
        return this.items[this.lowestCount]
    }
    isEmpty() {
        return this.size() === 0
    }
    size() {
        return this.count - this.lowestCount
    }
    // Empty queue
    clear() {
        this.items = {}
        this.count = 0
        this.lowestCount = 0
    }
    toString() {
        if (this.isEmpty()) {
            return ''
        }
        let str = this.items[this.lowestCount]
        for (let i = this.lowestCount + 1; i < this.count; i++) {
            str +=',' + this.items[i]
        }
        return str
        // return Object.value(this.items).join(',')
    }
}

5.2 double ended queue data structure

A double ended queue is a special queue that allows us to add and remove elements from both the front end and the back end;

Because the double ended queue abides by the principles of first in first out and last in first out at the same time, it can be said that it is a data structure combining queue and stack;

For the length of the double ended queue, it becomes the distance from lowestCount to count;

Because both the front end and the back end will be changed, pointers are required at the head and tail of the double ended queue;

Here you just need to focus on the logic of the addFront method

There are three situations:

  1. The double ended queue is empty;

    Directly call the addBack method

  2. Elements have been removed from the front end of the double ended queue, when lowestcount > 0;

    lowestCount needs to be subtracted by 1 and a new element is added at lowestCount

  3. No element has been removed from the front end, when lowestCount === 0;

    All elements need to move backward one position to make room for lowestCount (0 at this time) to add new elements;

// Double ended queue
class Deque {
    constructor() {
        this.count = 0 // Serial number at the end of the queue
        this.lowestCount = 0 // Trace the sequence of the first element
        this.items = {}
    }
    /*
            *   New addFront addBack removeFront removeBack peekFront peekBack method
            */
    // Adds an element to the front end of a two ended queue
    addFront(element) {
        if (this.isEmpty()) {
            this.addBack(element)
            // An element has been removed from the front end
        } else if (this.lowestCount > 0) {
            this.lowestCount--
            this.items[this.lowestCount] = element
            // No elements were removed
        } else {
            // All elements are moved back one bit to make room for the first position
            for (let i = this.count; i > 0; i--) {
                this.items[i] = this.items[i - 1]
            }
            this.count++
            this.lowestCount = 0 // This step can be considered not because it is 0
            this.item[0] = element
        }
    }
    // Same as enqueue method
    addBack(element) {
        this.item[this.count] = element
        this.count++
    }
    // Same as dequeue method
    removeFront() {
        if (this.isEmpty()) {
            return undefined
        }
        const result = this.items[this.lowestCount]
        delete this.item[this.lowestCount]
        this.lowestCount++
        return result
    }
    // The same as the pop method in the Stack class
    removeFront() {
        if (this.isEmpty()) {
            return undefined
        }
        this.count--
        const result = this.items[this.count]
        delete this.items[this.count]
        return result
    }
    // Same as peek method
    peekFront() {
        if (this.isEmpty()) {
            return undefined
        }
        return this.items[this.lowestCount]
    }
    // Same as peek method in Stack class
    peekBack() {
        if (this.isEmpty()) {
            return undefined
        }
        return this.items[this.count - 1]
    }
}

5.2.1 leetCode59. Maximum number of queues

// Please define a queue and implement the function max_value gets the maximum value in the queue
var MaxQueue = function () {
    // Store queue data
    this.queue = {}
    // Double ended queue maintenance maximum
    this.deque = {}
    // Prepare queue related data
    this.countQ = this.countD = this.headQ = this.headD = 0
}
MaxQueue.prototype.push_back = function(value) {
    this.queue[this.countQ++] = value
    // Detects whether data can be added to a dual ended queue
    // If the added value is greater than the end of the queue value, the end of the queue will be out of the queue and the new value will be in the queue
    // ?  What if you just leave the maximum value?
    // no way!! If deleted in this way, the queue will become null
    // At this time, it is necessary to ensure that the double ended queue is a monotonically decreasing queue, and the head of the queue is the maximum value

    // 10 9 11 
    // 10

    // if (!this.isEmptyDeque()) {
    //     this.deque[0] = value
    // }
    // if (value > this.deque[0]) {
    //     this.deque[0] = value
    // }
    while (!this.isEmptyDeque() && value > this.deque[this.countD - 1]) {
        delete this.deque[--this.countD]
    }
    this.deque[this.countD++] = value
}
MaxQueue.prototype.pop_front = function() {
    if (this.isEmptyQueue()) {
        return -1
    }
    // Compare the first value of deque and queue. If they are the same, deque will be out of the queue, otherwise deque will not operate
    if (this.queue[this.headQ] === this.deque[this.headD]) {
        delete this.deque[this.headD++]
    }
    const frontData = this.queue[this.headQ]
    delete this.queue[this.headQ++]
    return frontData
}
MaxQueue.prototype.max_value = function () {
    return this.deque[this.headD]
}

MaxQueue.prototype.isEmptyDeque = function () {
    return !(this.countD - this.headD)
}

MaxQueue.prototype.isEmptyQueue = function () {
    return !(this.countQ - this.headQ)
}

Summary:

Double ended queues can be maintained as monotonically increasing or decreasing; In this way, the team leader is the minimum or maximum value;

For special cases, if the deleted number is just the head of the queue, you need to remove the head of the queue;

5.2.2 leetCode59. sliding window

// sliding window
var maxSlidingWindow = function(nums, k) {
    if (k <= 1) {
        return nums
    }
    const result = []
    const deque = []
    deque.push(nums[0])
    let i = 1
    for (; i < k; i++) {
        while(deque.length && nums[i] > deque[deque.length - 1]) {
            deque.pop()
        }
        // It means that in fact, every number should join the team, but it depends on whether the number at the end of the team can get out of the team
        deque.push(nums[i])
    }
    result.push(deque[0])

    // Traverse subsequent data
    const len = nums.length
    for (; i < len; i++) {
        while(deque.length && nums[i] > deque[deque.length - 1]) {
            deque.pop()
        }
        deque.push(nums[i])
        // The most critical step
        // When the maximum value is outside the window, remove the head of the team 
        // Detects whether the current maximum value is outside the window
        if (deque[0] === nums[i - k]) {
            deque.shift()
        }
        result.push(deque[0])
    }
    return result
}

Summary:

The first k-length queue and k-nums Queues of length need to be traversed separately

Directly check the maximum value at the front k end

When querying at the back len end, pay attention to special cases. If the length of the queue is greater than K (when the maximum value is outside the window, ensure that the length of the queue cannot be greater than k), take the value at the head of the queue out of the queue and continue to enter the subsequent values (note here that when the value of the queue is greater than k, deque [0] = = nums [I - k] must be true)

5.3 practice - circular queue and palindrome check

5.4 summary

  1. Learned to add and remove elements through enqueue method and dequeue method and follow the first in first out principle
  2. You learned about the data structure of double ended queues, how to add elements to the front and back ends of double ended queues, and how to remove elements from the front and back ends of double ended queues
  3. Loop queue and palindrome check

6. Linked list

A linked list stores an ordered set of elements, but unlike an array, the elements in the linked list are not placed continuously in memory. Each element is composed of a node storing the element itself and a reference to the next element (also known as pointer and link);

If you want to access an element in the middle of the linked list, you need to iterate the linked list from the starting point to the required element;

6.1 one way linked list

push method

To add an element to the end of the linked list, we must find the last element. Remember, we only have the reference of the first element! Therefore, we need to access the list circularly until the last item is found. Therefore, we need a variable pointing to the current item in the linked list;

removeAt method

The current variable always refers to the current element of the looped list. We also need a reference to the previous element of the current element;

// Linked list
class LinkedList {
    constructor(equalsFn = defaultEquals) {
        this.count = 0 // Number of elements in the linked list
        this.head = undefined // Reference to the first element
        this.equalsFn = equalsFn // Compare elements for equality
    }
    // Add a new element to the tail
    push(element) {
        const node = new Node(element)
        let current;
        if (this.head == null) {
            this.head = node
        } else {
            // It is well explained that if you want to access an element in the middle of the linked list, you need to iterate the linked list from the starting point to the required element;
            current = this.head
            // The last element was found. The next of the last element is null
            while (current.next != null) {
                current = current.next
            }
            current.next = node
        }
        this.count++
    }
    // Inserts a new element into the specified location
    insert(element, index) {
        if(index >= 0 && index <= this.count) {
            const node = new Node(element)
            let current = this.head
            // Add in the first position
            if (index === 0) {
                node.next = current
                this.head = node
            } else {
                // Add in the middle
                let previous = this.getElementAt(index - 1)
                current = previous.next
                node.next = current
                previous.next = node
            }
        }
        return false
    }
    // Remove an element
    remove(element) {
        const index = this.indexOf(element)
        return this.removeAt(index)
    }
    // Returns the index of the element in the linked list
    indexOf(element) {
        let current = this.head
        for (let i = 0; i < this.count && current != null; i++) {
            if (this.equalsFn(element, current.element)) {
                return i
            }
            current = current.next
        }
        return -1
    }
    // Removes the element at the specified location
    removeAt(index) {
        // Check out of bounds
        if (index >= 0 && index < this.count) {
            let previous; // Previous item
            let current = this.head // Current item
            // The first item may be removed
            if (index === 0) {
                this.head = current.next
            } else {
                // Is one of them
                for (let i = 0; i < index; i++) {
                    // When looping once, previous is head, current is the first item, and current Next is the second item
                    previous = current // head
                    current = current.next // Is the second item
                }
                // Removes a previous item by connecting it to the next item in the current item, skipping the current item
                previous.next = current.next
            }
            this.count--
            // Returns the deleted element
            return current.element
        }
        return undefined
    }
    // Refactoring the removeAt method using getElementAt
    newRemoveAt(index) {
        // Check out of bounds
        if (index >= 0 && index < this.count) {
            let previous; // Previous item
            let current = this.head // Current item
            // The first item may be removed
            if (index === 0) {
                this.head = current.next
            } else {
                previous = this.getElementAt(index - 1)
                current = previous.next
                previous.next = current.next
            }
            this.count--
            // Returns the deleted element
            return current.element
        }
        return undefined
    }
    // Pull out the method and iterate over the linked list until the target location index returns the current element
    // Returns the element at a specific location. If it does not exist, it returns undefined
    getElementAt(index) {
        if (index >= 0 && index < this.count) {
            let current = this.head
            for (let i = 0; i < index && node != null; i++) {
                current = current.next
            }
            return current
        }
        return undefined
    }
    isEmpty() {
        return this.size() === 0
    }
    size() {
        return this.count
    }
    toString() {
        if (this.isEmpty()) {
            return ''
        }
        let current = this.head.next
        let str = this.head.element
        for (let i = 1; i < this.count && current != null; i++) {
            str += ',' + current.element
            current = current.next
        }
        return str
    }
    getHead() {
        return this.head
    }
}
// Helper class Node
class Node {
    constructor(element) {
        this.element = element
        this.next = null
    }
}

function defaultEquals (a, b) {
    return a === b
}

const demo = new LinkedList()
demo.push(3)

console.log(demo) // LinkedList {count: 1, head: Node, equalsFn: ƒ}count: 1equalsFn: ƒ defaultEquals(a, b)head: Node {element: 3, next: null}[[Prototype]]: Object

6.2 bidirectional linked list

The difference between a two-way linked list and a one-way linked list is that the links in a two-way linked list are two-way. One chain goes down one element and one chain goes forward one element;

Bidirectional linked list provides two iterative methods: from beginning to end and from end to end;

In a one-way linked list, if you miss the element you want to find during the iteration, you need to go back to the starting point and restart the iteration; This is an advantage of two-way linked list;

// Bidirectional linked list
class DoublyLinkedList extends LinkedList {
    constructor(equalsFn = defaultEquals) {
        super(equalsFn)
        this.tail = null
    }
    // An algorithm for inserting a new element anywhere
    insert(element, index) {
        // Within the legal scope
        if (index >= 0 && index <= this.count) {
            const node = new DoublyNode(element)
            let current = this.head
            // If you add in the header
            if (index === 0) {
                // Empty linked list
                if (current == null) {
                    this.head = node
                    this.tail = node
                } else {
                    node.next = current
                    current.prev = node
                    this.head = node
                }
                // Add in the last item
            } else if (index === this.count) {
                current = this.tail
                current.next = node
                node.prev = current
                this.tail = node
                // Add a new item in the middle
            } else {
                let previous = this.getElementAt(index - 1)
                current = this.getElementAt(index)
                previous.next = node
                node.prev = previous
                node.next = current
                current.prev = node
            }
            this.count++
            return true
        }
        return false
    }
    // Deletes the element at the specified location
    removeAt(index) {
        // Safety range
        if (index >= 0 && index < this.count) {
            let current = this.head
            // Deleted is the header
            if (index = 0) {
                this.head = current.next
                // If there is only one item
                if (this.count == 1) {
                    this.tail = undefined
                    // If there are multiple
                } else {
                    this.head.prev = undefined
                }
                // The tail is deleted
            } else if (index === this.count - 1) {
                current = this.tail
                previous = current.prev
                previous.next = undefined
                // The middle element is deleted
            } else {
                current = this.getElementAt(index)
                let previous = current.prev
                previous.next = current.next
                current.next.prev = previous // The prev of the next element is the previous element
            }
            this.count--
            return current.element
        }
        return undefined
    }
}
// Helper 
class DoublyNode extends Node {
    constructor(element, next, prev) {
        super(element, next)
        this.prev = prev
    }
}

6.3 circular linked list

A circular linked list can have only one-way references like a linked list, or two-way references like a two-way linked list;

The only difference between a circular linked list and a linked list is that the pointer (tail.next) of the last element to the next element does not refer to undefined, but points to the first element (this.head)

// Circular linked list -- unidirectional linked list
class CircularLinkedList extends LinkedList {
    constructor(equalsFn = defaultEquals) {
        super(equalsFn)
    }
    // Insert a new element anywhere
    insert(element, index) {
        // boundary condition 
        if (index > 0 && index <= this.count) {
            const node = new Node(element)
            let current = this.head
            // Insert in head
            if (index === 0) {
                if (current == null) {
                    this.head = node
                    node.next = this.head
                } else {
                    node.next = current
                    current = this.getElementAt(this.size())
                    this.head = node
                    current.next = node
                }
            } else {
                const previous = this.getElementAt(index)
                current = previous.next
                node.next = current
                previous.next = node
            }
            this.count++
            return true
        }
        return false
    }
    // Remove element anywhere
    removeAt(index) {
        if (index > 0 && index < this.count) {
            let current = this.head
            if (index === 0) {
                if (this.size() === 1) {
                    this.head = undefined
                } else {
                    current = this.getElementAt(this.size())
                    current.next = this.head.next
                    current = this.head
                }
            } else {
                const previous = this.getElementAt(index - 1)
                current = previous.next
                previous.next = current.next
            }
            this.count--
            return current.element
        }
        return undefined
    }
}

*6.4 with sequence table

Ordered linked list is a linked list structure in which values keep elements in order;

In addition to using the sorting algorithm, you can also insert elements into the correct position to ensure the order of the linked list;

// Ordered linked list
const Compare = {
    LESS_THAN: -1,
    BIGGER_THAN: 1
}

function defaultCompare(a, b) {
    if (a === b) {
        return 0
    }
    // If a is smaller, return - 1. If a is larger, return 1
    return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN
}

class SortedLinkedList extends LinkedList {
    constructor(equalsFn = defaultEquals, compareFn = defaultCompare) {
        super(equalsFn)
        this.compareFn = compareFn
    }
    insert(element, index = 0) {
        if (this.isEmpty()) {
            return super.insert(element, 0)
        }
        // Obtain appropriate sequence points
        const pos = this.getIndexNextSortedElement(element)
        return super.insert(element, pos)
    }
    getIndexNextSortedElement(element) {
        let current = this.head
        let i = 0
        for (; i < this.size() && current; i++) {
            const comp = this.compareFn(element, current.element)
            if (comp === Compare.LESS_THAN) {
                return i
            }
            current = current.next
        }
        return i
    }
}

6.5 using linked list to create other data structures - stack

// Implementation stack
class StackLinkedList {
    constructor() {
        // Use a two-way linked list
        this.items = new DoublyLinkedList()
    }
    push(element) {
        this.items.push(element)
    }
    pop() {
        if(this.isEmpty()) {
            return undefined
        } else {
            return this.items.removeAt(this.size() - 1)
        }
    }
}

The reason for using a two-way linked list is that for the stack, we will add elements to the tail and remove elements from the tail of the linked list;

The bidirectional linked list has a reference to the last element of the list, which can be obtained without iterating over the elements of the whole linked list;

The two-way linked list can directly obtain the head and tail elements and reduce the process consumption. Its time complexity is the same as that of the original Stack implementation, which is O(1);

Save a tail element:

	// Return tail element
    peek() {
        if(this.items.isEmpty()) {
            return undefined
        } else {
            return this.items.getElementAt(this.size() - 1).element
        }
    }
    isEmpty() {
        return this.items.isEmpty()
    }
    size() {
        return this.items.size()
    }
    clear() {
        return this.items.clear()
    }
    toString() {
        return this.items.toString()
    }

6.6 chain related algorithm problems

6.6.1 leetCode206 reverse linked list

Reverse the linked list recursively

The first newHead is assigned to head. Note the reference data type of Js;

follow-up

node.next.next = node
node.next = null

The value of newHead is modified

// Reverse linked list
var reverseList = function(node) {
    // let prev = null
    // let current = head
    // while(current) {
    //     const next = current.next
    //     current.next = prev
    //     //Every time I go down
    //     prev = current
    //     current = next
    // }
    if (node === null || node.next === null) {
        return node
    }
    const newHead = reverseList(node.next)
    // The node that can execute here for the first time is the penultimate node
    node.next.next = node
    node.next = null
    return newHead
}

6.6.2 leetCode02.08 loop detection

6.7 summary

  1. The most important advantage of the linked list data structure over the array is that you can easily add and remove elements without moving the elements in the linked list; Therefore, when you need to add and remove many elements, the best choice is a linked list rather than an array;
  2. Learned that if you use an internal linked list to store elements to create a stack, instead of using arrays and objects;
  3. Understand the benefits of reusing operations available in other data structures rather than rewriting all logical code;

7. Assembly

A set consists of a set of unordered and unique (i.e. non repeatable) items; The data structure uses the same mathematical concepts as finite sets;

7.1 creating collection classes

Use object prototype. hasOwnproperty. The purpose of call () is to prevent the loss of the hasOwnproperty method on the object;

// aggregate
class Set {
    constructor() {
        this.items = {}
    }
    has(element) {
        // return element in this.items
        return Object.prototype.hasOwnProperty.call(this.items, element)
    }
    add(element) {
        if (!this.has(element)) {
            this.items[element] = element
            return true
        }
        return false
    }
    delete(element) {
        if (this.has(element)) {
            delete this.items[element]
            return true
        }
        return false
    }
    clear() {
        this.items = {}
    }
    size() {
        return Object.keys(this.items).length
    }
    // Optimized version of size
    sizeLegacy() {
        let count = 0;
        for(let key in this.items) {
            // At this point, the key must exist in this In items, consider removing the has judgment
            if (this.has(key)) {
                count++
            }
        }
        return count
    }
    values() {
        return Object.values(this.items)
    }
    // Improved values method
    valuesLegacy() {
        let values = []
        for(let key in this.items) {
            // At this point, the key must exist in this In items, consider removing the has judgment
            if (this.has(key)) {
                values.push(key)
            }
        }
        return values
    }
}

7.2 set operation

We can calculate the set as follows:

Union: a new set containing all elements in two sets;

Intersection: a new set containing elements common to two sets;

Difference set: a new set containing all elements that exist in the first set and do not exist with the second set; (in addition to the elements of the given set, the set of remaining elements)

Subset: verify whether a given set is a subset of another set;

7.2.1 Union

union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union union Union;

Cram both sets into a new set;

// Union
union(otherSet) {
    // Define a new collection
    const unionSet = new Set()
    this.values.forEach(item => unionSet.add(item))
    otherSet.values().forEach(item => unionSet.add(item))
    return unionSet
}

7.2.2 intersection

Iterate a set, judge whether another set contains the elements of the iterated set, and if so, put them into a new set;

At this time, the set we iterate over should be a set with fewer elements, so we should judge the set with fewer elements first;

// intersection
intersection(otherSet) {
    const intersectionSet = new Set()
    const values = this.values()
    for (let i = 0; i < values.length; i++) {
        if (otherSet.has(values[i])) {
            intersectionSet.add(values[i])
        }
    }
    return intersectionSet
}
// Improved intersection
nextIntersection(otherSet) {
    const intersectionSet = new Set()
    const values = this.values()
    const otherValues = otherSet.values()
    // Suppose the new collection has fewer elements
    let biggerSet = values;
    let smallSet = otherValues;
    // The new set has more elements than the meta set. Exchange variables
    if (values.length < otherValues.length) {
        smallSet = values
        biggerSet = otherValues
    }
    // Iterate over a collection with fewer elements
    for (let i = 0; i < smallSet.length; i++) {
        if (biggerSet.includes(smallSet[i])) {
            intersectionSet.add(smallSet[i])
        }
    }
    return intersectionSet
}

7.2.3 difference set

In addition to the elements of the given set, the set of remaining elements;

// Difference set
difference(otherSet) {
    const differenceSet = new Set()
    this.values.forEach(item => {
        if (!otherSet.has(item)) {
            differenceSet.add(item)
        }
    })
    return differenceSet
}

7.2.4 subset

Determining whether the set is a subset of a given set;

The use of every method is considered for performance. When one returns false, the iteration will stop;

// subset
isSubsetOf(otherSet) {
    // If the elements of the current set are more than those of the given set, it is certainly not a subset
    if (this.size() > otherSet.size) {
        return false
    }
    // Whether a record is a subset variable. By default, it is a subset of a given set
    let isSubset = true
    // for (let i = 0; i < this.size(); i++) {
    //     if(!otherSet.has(this.values()[i])) {
    //         isSubset = false
    //     }
    // }
    this.values().every(item => {
        if (!otherSet.has(item)) {
            isSubset = false
            return false
        }
        return true
    })
    return isSubset
}

7.3 ECMAScript2015 - Set class

The native Set class is somewhat different from our Set;

The values method of set returns Iterator, and size is an attribute of set;

7.3.1 analog set operation

Analog union operation

const union = (setA, setB) => {
    const unionAb = new Set()
    setA.forEach(value => unionAb.add(value))
    setB.forEach(value => unionAb.add(value))
    return unionAb
}

Simulated intersection operation

const intersection = (setA, setB) => {
    const intersectionSet = new Set()
    setA.forEach(item => {
        if (setB.has(item)) {
            intersectionSet.add(value)
        }
    })
    return intersectionSet
}

Analog difference set operation

const difference = (setA, setB) => {
    const differentSet = new Set()
    setA.forEach(item => {
        if (!setB.has(item)) {
            differentSet.add(item)
        }
    })
    return differentSet
}

7.3.2 using extension operators

// Union
new Set([...setA, ...setB])
// intersection
new Set([...setA].filter(item => setB.has(item)))
// Difference set
new Set([...setA].filter(item => !setB.has(item)))

7.4 summary

In advance, we introduced a set class similar to the set class defined in ECMAScript2015, and also introduced some uncommon methods of set data structure, such as union, intersection, difference and subset;

8. Dictionary and hash table

In this chapter, we continue to learn the data structure of using dictionary and hash table to store unique values (non repeated values);

In a set, we are interested in each value itself and regard it as the main element;

In the dictionary, we store data in the form of [key, value] pairs;

The same is true in the hash table, but each key in the dictionary can only have one value;

8.1 dictionary

A dictionary is very similar to a set. A set stores elements in the form of [value, value], and a dictionary stores elements in the form of [key, value];

Dictionaries are also called maps, symbol tables, or associative arrays;

// Dictionaries and hash tables
class Dictionary {
    constructor(toStrFn = defaultToString) {
        this.toStrFn = toStrFn
        this.table = {}
    }
    // Check if a key exists in the dictionary
    hasKey(key) {
        return this.table[this.toStrFn(key)] != null
    }
    // Like adding a new element to a dictionary
    set(key, value) {
        // Both key and value are legal
        if(key != null && value != null) {
            this.table[this.toStrFn(key)] = new ValuePair(key, value)
            return true
        }
        return false                
    }
    // Remove a value from the dictionary
    remove(key) {
        if (this.hasKey(key)) {
            delete this.table[this.toStrFn(key)]
            return true
        }
        return false
    }
    // Retrieve a value from the dictionary
    get(key) {
        // if (this.hasKey(key)) {
        //     return this.table[this.toStrFn(key)]
        // }
        // return undefined
        const valuePair = this.table[this.toStrFn(key)]
        return valuePair == null ? undefined : valuePair.value
    }
    // Returns all valuePair objects in the dictionary as an array
    keyValues() {
        // return Object.values(this.table)
        const valuePairs = []
        // At this time, k is already the string processed by toStrFn
        for (const k in this.table) {
            if (this.hasKey(k)) {
                valuePairs.push(this.table[k])
            }
        }
        return valuePairs
    }
    // Returns all the original key names in the class that are used to identify the value
    keys() {
        // return this.keyValues.map(valuePair => valuePair.key)
        const keys = []
        const valuePairs = this.keyValues()
        for (let i = 0; i < valuePairs.length; i++) {
            keys.push(valuePairs[i].key)
        }
        return key
    }
    // Return all values
    values() {
        return this.keyValues.map(valuePair => valuePair.value)
    }
    // Use forEach to iterate over each key value pair in the dictionary
    forEach(callback) {
        const valuePairs = this.keyValues()
        for (let i = 0; i < valuePairs.length; i++) {
            const valuePair = valuePairs[i]
            const result = callback(valuePair.key, valuePair.value)
            // return false is to jump out of the loop
            if (result === false) {
                break
            }
        }
    }
    size() {
        return Object.keys(this.table).length
    }
    isEmpty() {
        return this.size() === 0
    }
    clear() {
        this.table = {}
    }
    toString() {
        if(this.isEmpty()) {
            return ''
        }
        const valuePairs = this.keyValues()
        let str = `${valuePairs[0].toString()}`
        for (let i = 1; i < valuePairs.length; i++) {
            str = `${str},${valuePairs[i].toString()}`
        }
        return str
    }
}
// The value stored in the dictionary contains the key and value attributes
class ValuePair {
    constructor(key, value) {
        this.key = key
        this.value = value
    }
    toString() {
        return `[#${this.key}: ${this.value}]`
    }
}
// The default function is to convert key to string
function defaultToString(item) {
    if (item === null) {
        return 'NULL'
    } else if (item === undefined) {
        return 'UNDEFINED'
    } else if (typeof item === 'string' || item instanceof String) {
        // If it is a string, it returns itself
        return `${item}`
    }
    return item.toString()
}

const demo = new Dictionary()
demo.set(111, 123)
demo.set(222, 123)

8.2 hash table

8.2.1 create hash table

class HashTable {
    constructor(toStrFn = defaultToString) {
        this.toStrFn = toStrFn
        this.table = {}
    }
    // Hash function
    loseloseHashCode(key) {
        if (typeof key === 'number') {
            return key
        }
        const tableKey = this.toStrFn(key)
        let hash = 0
        for (let i = 0; i < tableKey.length; i++) {
            hash += tableKey.charCodeAt(i)
        }
        return hash % 37
    }
    // Get hashCode value
    hashCode (key) {
        return this.loseloseHashCode(key)
    }
    //Add keys and values to the hash table
    put(key, value) {
        if (key != null && value != null) {
            const position = this.hashCode(key)
            this.table[position] = new ValuePair(key, value)
            return true
        }
    }
    // Gets a value from the hash table
    get(key) {
        const valuePair = this.table[this.hashCode(key)]
        return valuePair == null ? undefined : valuePairs.value
    }
    // Removes a value from the hash table
    remove(key) {
        const hashCode = this.hashCode(key)
        if (this.table[hashCode]) {
            delete this.table[hash]
            return true
        }
        return false
    }
}

8.2.3 Hash list and hash set

The difference between a hash set and a Hash list is that instead of adding key value pairs, only values are inserted without keys;

8.2.4 handling conflicts in hash tables

Sometimes, some keys have the same hash value;

When different values correspond to the same position in the hash table, we call it conflict;

Have the same hashCode and multiple different elements;

The methods to deal with conflicts are: link separation, linear exploration and double hash;

8.2.4.1 separate links

The separate link method includes creating a linked list for each position of the hash table and storing the elements in it; It is the simplest way to resolve conflicts, but additional storage space is required in addition to the HashTable instance;

// The method of separating links is used to solve the conflict
class HashTableSeparateChaining {
    constructor(toStrFn = defaultToString) {
        this.toStrFn = toStrFn
        this.table = {}
    }
    put (key, value) {
        // Ensure that both key and value exist
        if (key != null && value != null) {
            const position = this.hashCode(key)
            if (this.table[position] == null) {
                this.table[position] = new LinkedList()
            }
            this.table[position].push(new ValuePair(key, value))
            return true
        }
        return false
    }
    get (key) {
        const position = this.hashCode(key)
        const LinkedList = this.table[position]
        if (LinkedList != null && !LinkedList.isEmpty()) {
            let current = LinkedList.getHead()
            while(current != null) {
                if (current.element.key === key) {
                    return current.element.value
                }
                current = current.next
            }
        }
        return undefined
    }
    remove(key) {
        const position = this.hashCode(key)
        const LinkedList = this.table[position]
        if (LinkedList != null && !LinkedList.isEmpty()) {
            let current = LinkedList.getHead()
            while(current != null) {
                if (current.element.key === key) {
                    LinkedList.remove(current.element)
                    if (LinkedList.isEmpty()) {
                        delete this.table[position]
                    }
                    return true
                }
                current = current.next
            }
        }
        return false
    }
}

8.2.4.1 linear detection

Store elements directly in a table rather than in a separate data structure;

We search the hashCode location in the table. If not, we add the value to the correct location. If occupied, we iterate the hash table until we find a free location;

  • When we remove a key value pair from the hash table, it is not enough to remove only the elements of the position implemented by the data structure before this chapter; If we only remove elements, we may find an empty location when looking for other elements with the same hashCode, which will lead to problems in the algorithm;

There are two techniques for linear exploration:

The first method is soft deletion;

The second is to check whether it is necessary to move one or more elements to the previous position;

// Override put method
put (key, value) {
    // Ensure that both key and value exist
    if (key != null && value != null) {
        const position = this.hashCode(key)
        if (this.table[position] == null) {
            this.table[position] = new LinkedList()
        } else {
            // Key point: when there are elements in the current hashCode, continue iterative search until the next null location
            let index = position + 1
            while(this.table[index] != null) {
                index++
            }
            // Null location found
            this.table[index] = new ValuePair(key, value)
        }
        this.table[position].push(new ValuePair(key, value))
        return true
    }
    return false
}
// Override get method
get(key) {
    const position = this.hasCode(key)
    let current = this.table[position]
    // Current hashCode is not empty
    if (current != null) {
        // The key corresponding to the current hashCode position is exactly the key to be found
        if (current.key == key) {
            return current.value
        } else {
            let index = position + 1
            // Start the iteration from the current hasCode position. If the current position is null, it returns undefined
            while(this.table[index].key != key && this.table[index] != null) {
                index++
            }
            // If the current position is not null and the key values are equal
            if (this.table[index] != null && this.table[index].key === key) {
                return this.table[index].value
            }

        }
    }
    return undefined
}
// Override delete method
remove(key) {
    const position = this.hasCode(key)
    let current = this.table[position]
    // Current hashCode is not empty
    if (current != null) {
        // The key corresponding to the current hashCode position is exactly the key to be found
        if (current.key == key) {
            delete this.table[position]
            this.verifyRemoveSideEffect(key, position)
            return true
        } else {
            let index = position + 1
            // Start the iteration from the current hasCode position. If the current position is null, it returns undefined
            while(this.table[index].key != key && this.table[index] != null) {
                index++
            }
            // If the current position is not null and the key values are equal
            if (this.table[index] != null && this.table[index].key === key) {
                delete this.table[index]
                this.verifyRemoveSideEffect(key, index)
                return true
            }

        }
    }
    return false
}
// The hasCode location of the incoming key and the deleted key
verifyRemoveSideEffect(key, removePosition) {
    // Initial hashCode position corresponding to the current key
    const hash = this.hashCode(key)
    // The next position of the hashCode corresponding to the current key
    const index = removePosition + 1
    // The safe location cannot be reached until the iteration location is empty
    while(this.table[index] != null) {
        // The initial hasCode value corresponding to the key value of the iterated position
        const posHash = this.hashCode(this.table[index].key)
        if (posHash <= hash || posHash <= removePosition) {
            this.table[removePosition] = this.table[index]
            delete this.table[index]
            removePosition = index
        }
        index++
    }
}

8.3 ES2015Map

A Map is essentially a collection of key value pairs;

Unlike the Map and Dictionary classes, the values and keys methods both return iterators instead of arrays of values or keys;

Another difference is that our previous size method returns the number of values stored in the dictionary, while the Map class of ES2015 has a size attribute;

8.4 ES2015 WeakMap and WeakSet classes

Weakening methods belonging to Set and Map;

  • The WeakSet or WeakMap class has no entries, keys and values methods;

  • Only objects can be used as keys;

  • These two classes are created mainly for performance. The objects referenced by the key name of WeakMap are weak references, that is, the garbage collection mechanism does not take this reference into account. Therefore, as long as other references of the referenced object are cleared, the garbage collection mechanism will free the memory occupied by the object. That is, once it is no longer needed, the key name object and the corresponding key value pair in WeakMap will disappear automatically without manually deleting the reference;

8.5 summary

Learned the relevant knowledge of the dictionary, and learned how to add, remove and obtain elements and other methods;

Also learned the differences between dictionaries and collections;

Learned hash calculation, how to create a hash data structure, how to add, remove and obtain elements, and how to create hash functions;

Learned to use different methods to solve the conflict problem in hash table;

9. Recursion

ECMAScript2015 tail call Optimization:

If the last operation in the function is to call the function, which means that the function has been basically completed, it does not need to create a new frame stack when calling the last function, but can reuse the existing function frame stack, which is not only fast, but also saves memory;

10. Trees

We learned that the first non sequential data structure is hash table. Next, we will learn another non sequential data structure tree, which is very useful for storing data that needs to be searched quickly;

10.2 related terms

Root node, internal node, external node, subtree;

Binary tree complete binary tree full binary tree

10.3 binary tree and binary search tree

A node in a binary tree can only have two child nodes at most: one is the left child node and the other is the right child node;

Binary search tree is a kind of binary tree, but you only store smaller values in the left node (than the parent node) and larger values in the right node (than the parent node);

// Binary search tree

// node
class TreeNode {
    constructor(key) {
        this.key = key
        this.left = null
        this.right = null
    }
}
class BinarySearchTree {
    // The comparison function here is similar to the ordered linked list
    constructor(compareFn = defaultCompare) {
        this.compareFn = compareFn
        this.root = null
    }
    // Insert a new key into the tree
    insert(key) {
        if (this.root == null) {
            this.root = new TreeNode(key)
        } else {
            this.insertNode(this.root, key)
        }
    }
    insertNode(node, key) {
        // New key is better than node When I was younger
        if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
            // When the smaller tree is empty
            if (node.left == null) {
                node.left = new TreeNode(key)
            } else {
                // When the smaller tree is not empty, check it again
                this.insertNode(node.left, key)
            }
        } else {
            if (node.right == null) {
                node.right = new TreeNode(key)
            } else {
                this.insertNode(node.right, key)
            }
        }
    }
}

10.4 tree traversal

The process of accessing each node of the tree and performing some operation on them;

10.4.1 middle order traversal

Middle order traversal is a way to access all nodes of the BST in the order of more than one row, that is, access all nodes in the order from the smallest to the largest;

Left subtree = root node = right subtree

// Medium order traversal
inOrderTraverse(callback) {
    this.inOrderTraverseNode(this.root, callback)
}
inOrderTraverseNode(node, callback) {
    if (node != null) {
        this.inOrderTraverseNode(node.left, callback)
        callback(node.key)
        this.inOrderTraverseNode(node.right, callback)
    }
}

10.4.2 preorder traversal

Preorder traversal accesses each node with priority over the descendant nodes. One application of preorder traversal is to print a structured document;

Root node = left subtree = right subtree

// Preorder traversal 
preOrderTraverse(callback) {
    this.preOrderTraverseNode(this.root, callback)
}
preOrderTraverseNode(node, callback) {
    if (node != null) {
        callback(node.key)
        this.preOrderTraverseNode(node.left, callback)
        this.preOrderTraverseNode(node.right, callback)
    }
}

10.4.2 post order traversal

Postorder traversal is to access the descendants of the node first, and then the node itself. One application of postorder traversal is to calculate the space occupied by all files in a directory and its letters;

Left subtree = number of right words = root node

// Postorder traversal
postOrderTraverse(callback) {
    this.postOrderTraverseNode(this.root, callback)
}
postOrderTraverseNode(node, callback) {
    if (node != null) {
        callback(node.key)
        this.postOrderTraverseNode(node.left, callback)
        this.postOrderTraverseNode(node.right, callback)
    }
}

10.5 values in the search tree

10.5.1 search for maximum and minimum values

The minimum value is on the left and the maximum value is on the right;

For finding the minimum value, always follow the left side of the tree; For finding the maximum value, always follow the right side of the tree;

// Search for maximum and minimum values
min() {
    return this.minNode(this.root)
}
minNode(node) {
    let current = node
    while(current != null && current.left != null) {
        current = node.left
    }
    return current
}

max() {
    return this.maxNode(this.root)
}
maxNode(node) {
    let current = node
    while(current != null && current.right != null) {
        current = node.right
    }
    return current
}

10.5.2 search for a specific value

true is returned when found, false is returned when not found

// Search for a specific value
search(key) {
    this.searchNode(this.root, key)
}
searchNode(node, key) {
    if (node == null) {
        return false
    }
    // Small, small
    if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
        return this.searchNode(node.left, key)
    } else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
        // Big big
        return this.searchNode(node.right, ket)
    } else {
        // It's him
        return true
    }
}

10.5.3 removing a node

To remove a node, first find the node;

root is assigned the return value of the removeNode method;

The removeNode method returns the tree structure after the specified node is deleted;

// remove
remove(key) {
    this.root = this.removeNode(this.root, key)
}
removeNode(node, key) {
    if(node === null) {
        return null
    }
    if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
        node.left = this.removeNode(node.left, key)
        return node
    } else if (this.compareFn(key, node.key === Compare.BIGGER_THAN)) {
        node.right = this.removeNode(node.right, key)
        return node
    } else {
        // The first case has no child nodes
        if (node.left == null && node.right == null) {
            node = null
            return node
        }
        // In the second case, there is only a single child node
        if (node.left == null) {
            node = node.right
            return node
        } else if (node.right == null) {
            node = node.left
            return node
        }
        // The third case has two child nodes
        // You need to find the smallest node in the right subtree
        // Then use the smallest node to update the value of this node. At this time, the node has been removed
        // Remove the smallest node on the right
        // Returns an updated node reference to its parent node
        const aux = this.minNode(node.right)
        node.key = aux.key
        node.right = this.removeNode(node.right, aux.key)
        return node
    }
}

10.6 self balancing tree

There is a problem with BST: depending on the number of nodes you add, one edge of the tree may be very deep, that is, one branch of the tree will have many layers, while other branches have only a few layers;

This will cause some performance problems, so there is a tree called AVL tree; AVL tree is a self balanced binary search tree, which means that the height difference between the left and right subtrees of any node is at most 1;

10.6.1 AVL tree

Topics: Javascript Algorithm data structure