1. Realize the stack structure (Stack)
I. Introduction
1.1. Understand what is a data structure?
A data structure is the way in which data is stored and organized in a computer.
The main things to consider: insertion and lookup.
Common data structures:
Array (Aarray)
Stack
Linked List
Graph
Hash table (Hash)
Queue
Tree
Heap
1.2. What is an algorithm?
Algorithms:
A limited instruction set, the description of each instruction does not depend on the language;
Receive some input (in some cases no input is needed);
generate input;
must terminate after a finite number of steps;
Popular understanding: the method/step logic to solve the problem.
2. Stack structure (Stack)
2.1. Introduction
An array is a linear structure, and elements can be inserted and deleted at any position in the array. Stacks and queues are more common restricted linear structures. As shown below:
The stack is characterized by first in last out, last in first out (LIFO: last in first out).
The stack structure in the program:
Function call stack: A(B(C(D()))): That is, A function calls B, B calls C, and C calls D; during the execution of A, A will be pushed onto the stack, and then when B is executed, B It is also pushed onto the stack, and functions C and D are also pushed onto the stack when they are executed. So the order of the current stack is: A->B->C->D (stack top); after the function D is executed, the stack will be popped and released, and the order of the pop-up stack is D->C->B->A;
Recursion: Why does recursion without a stop condition cause stack overflow? For example, function A is a recursive function, which keeps calling itself (because the function has not been executed yet, the function will not be popped off the stack), and keeps pushing the same function A onto the stack, eventually causing a stack overflow (Stack Overfloat).
Common operations on stacks:
- push(element): add a new element to the top position of the stack;
- pop(): Remove the element at the top of the stack and return the removed element at the same time;
- peek(): returns the element at the top of the stack without making any changes to the stack (this method does not remove the element at the top of the stack, just returns it);
- isEmpty(): Return true if there is no element in the stack, otherwise return false;
- size(): returns the number of elements in the stack. This method is similar to the length property of an array;
- toString(): Return the contents of the stack structure as a string.
2.2. Encapsulation stack class
// encapsulation stack class
function Stack(){
// properties on the stack
this.items =[]
// Stack related operations
// 1.push(): Push elements onto the stack
//Method 1 (not recommended): Add methods to objects, other objects cannot be reused
// this.push = () => { }
//Method 2 (recommended): Add methods to the Stack class, which can be reused by multiple objects
Stack.prototype.push = function(element) {
this.items.push(element)
}
// 2.pop(): Remove elements from the stack
Stack.prototype.pop = () => {
return this.items.pop()
}
// 3.peek(): Look at the top element of the stack
Stack.prototype.peek = () => {
return this.items[this.items.length - 1]
}
// 4.isEmpty(): Determine whether the stack is empty
Stack.prototype.isEmpty = () => {
// Not this.length (not the length of the Stack object, the Stack class has no length attribute), but the array items defined in the Stack class have the length attribute
return this.items.length == 0
}
// 5.size(): Get the number of elements in the stack
Stack.prototype.size = () => {
return this.items.length
}
// 6.toString(): output the data in the stack as a string
Stack.prototype.toString = () => {
//The desired output format: 20 10 12 8 7
let resultString = ''
for (let i of this.items){
resultString += i + ' '
}
return resultString
}
}
Two, JavaScript realizes the queue structure (Queue)
1. Introduction to Queue
Implementation of the queue class:
The implementation of the queue is the same as that of the stack. There are two options:
Based on array implementation;
Implementation based on linked list;
Common operations on queues:
- enqueue(element): Add one (or more) new items to the end of the queue;
- dequeue(): Remove the first (that is, the front of the queue) item of the queue and return the removed element;
- front(): Returns the first element in the queue - the first to be added and will be the first to be removed. The queue does not make any changes (the elements are not removed, and only the element information is returned, which is very similar to the peek method of the Stack class);
- isEmpty(): If the queue does not contain any elements, return true, otherwise return false;
- size(): returns the number of elements contained in the queue, similar to the length attribute of the array;
- toString(): convert the content in the queue into a string form;
Two, encapsulation queue class
// Based on array encapsulation queue class
function Queue() {
// Attributes
this.items = []
// method
// 1.enqueue(): Add elements to the queue
Queue.prototype.enqueue = element => {
this.items.push(element)
}
// 2.dequeue(): delete the front element from the queue
Queue.prototype.dequeue = () => {
return this.items.shift()
}
// 3.front(): View the front-end elements
Queue.prototype.front = () => {
return this.items[0]
}
// 4.isEmpty: Check if the queue is empty
Queue.prototype.isEmpty = () => {
return this.items.length == 0;
}
// 5.size(): View the number of elements in the queue
Queue.prototype.size = () => {
return this.items.length
}
// 6.toString(): Output the elements in the queue as strings
Queue.prototype.toString = () => {
let resultString = ''
for (let i of this.items){
resultString += i + ' '
}
return resultString
}
}
// Queue application: interview questions: drumming and passing flowers
let passGame = (nameList, num) => { //1. Create a queue structure let queue = new Queue()//2. Add everyone to the queue one by one
// This is the for loop writing method of ES6, i is equivalent to nameList[i]
for(let i of nameList){ queue.enqueue(i) }// 3. Start counting
while(queue.size() > 1){//There is only one person left in the queue and stop counting
//When it is not num, re-join the end of the queue
//When it is num, add it Delete from the queue
// The people before 3.1.num numbers are put back into the end of the queue (add those deleted from the front of the queue to the end of the queue)
for(let i = 0; i<num-1; i++ ){ queue.enqueue( queue.dequeue()) } // 3.2.num corresponds to this person, delete it directly from the queue /* The idea is this, because the queue does not have an array-like subscript value and cannot directly fetch a certain element, so use, put The num-1 elements in front of num are deleted first and then added to the end of the queue, so that the numth element is placed at the front of the queue, and can be deleted directly by using the dequeue method */ queue.dequeue ( ) }//4. Get the remaining person
console.log(queue.size()); //104
let endName = queue.front()
console.log('The final remaining person:' + endName); // 106
return nameList. indexOf(endName);
}//5. Test drum pass
let names = ['lily', 'lucy', 'Tom', 'Lilei', 'Tony']
console.log(passGame(names, 3)); //113
The form of the push method of the array in the array, stack and queue:
3. JavaScript implements collections and dictionaries
1. Collection structure
1.1. Introduction
- Elements in sets often referred to in mathematics can be repeated, but elements in sets in computers cannot be repeated.
- The special thing is that the elements inside have no order and cannot be repeated .
-
No order means that it cannot be accessed by subscript value , and no duplication means that there will only be one copy of the same object in the collection .
- The Set class in ES6 is a collection class. Here we repackage a Set class to understand the underlying implementation of the collection.
- The key in the Object class in JavaScript is a collection, which can be used to encapsulate the collection class Set.
- add(value): add a new item to the collection;
- remove(value): remove a value from the collection;
- has(value): returns true if the value is in the collection, otherwise returns false;
- clear(): remove all items in the collection;
- size(): Returns the number of elements contained in the collection, similar to the length property of the array;
- values(): returns an array containing all the values in the collection;
- Other methods...
1.2. Code implementation
//encapsulation collection class
function Set() {
//Attributes
this.items = {}
//method
//One.has method
Set.prototype.has = value => {
return this.items.hasOwnProperty(value)
}
//two.add method
Set.prototype.add = value => {
// Check if the element is already contained in the set
if (this.has(value)) {
return false
}
// add the element to the collection
this.items[value] = value//Indicates that the attribute key and value are both value
return true//indicates that the addition is successful
}
//3.remove method
Set.prototype.remove = (value) => {
//1. Determine whether the element is contained in the collection
if (!this.has(value)) {
return false
}
//2. Remove the element from the attribute
delete this.items[value]
return true
}
//four.clear method
Set.prototype.clear = () => {
//The original object has no reference point and will be automatically recycled
this.items = {}
}
//5.size method
Set.prototype.size = () => {
return Object.keys(this.items).length
}
// get all the values in the collection
//Six.values method
Set.prototype.values = function() {
return Object.keys(this.items)
}
}
1.3. Operations between collections
- Union : Given two collections, returns a new collection containing all the elements in the two collections;
- Intersection : Given two sets, return a new set containing elements common to both sets;
- Difference : Given two sets, returns a new set containing all elements that exist in the first set and do not exist in the second set;
- Subset : Verify whether a given set is a subset of another set;
Implementation of union :
Implementation idea: Create set C to represent the union of set A and set B, first add all elements in set A to set C, and then traverse set B, if it is an element that set C does not have, add it to set C middle.
Set.prototype.union = otherSet => {
// this: collection object A
// otherSet: collection object B
//1. Create a new collection
let unionSet = new Set()
//2. Add all elements in the A collection to the new collection
let values = this.values()
// for(let i of values){
// unionSet.add(i)
// }
for(let i = 0;i < values.length;i++){
unionSet.add(values[i])
}
//3. Take out the elements in the B collection and judge whether it needs to be added to the new collection
values = otherSet.values()
// for(let i of values){
// //Since the add method of the collection has judged the repeated elements, it can be added directly here
// unionSet.add(i)
// }
for(let i = 0;i < values.length;i++){
unionSet.add(values[i])
}
return unionSet
}
Implementation of intersection :
Implementation idea: traverse collection A, and when the obtained element also exists in collection B, add the element to another collection C.
Set.prototype.intersection = otherSet => {
// this: collection A
// otherSet: Set B
//1. Create a new collection
let intersectionSet = new Set()
//2. Take an element from A, judge whether it exists in set B at the same time, and put it into a new set if it is
let values = this.values()
for(let i =0 ; i < values.length; i++){
let item = values[i]
if (otherSet.has(item)) {
intersectionSet.add(item)
}
}
return intersectionSet
}
Implementation of difference set :
Implementation idea: traverse collection A, and when the obtained element does not exist in collection B, add the element to another collection C.
Set.prototype.diffrence = otherSet => {
//this: collection A
//otherSet: Set B
//1. Create a new collection
var diffrenceSet = new Set()
//2. Take out each element in the A collection, judge whether it exists in B at the same time, add it to the new collection if it does not exist
var values = this.values()
for(var i = 0;i < values.length; i++){
var item = values[i]
if (!otherSet.has(item)) {
diffrenceSet.add(item)
}
}
return diffrenceSet
}
Subset implementation :
Implementation idea: Traversing set A, when one of the obtained elements does not exist in set B, it means that set A is not a subset of set B, and returns false.
Set.prototype.subset = otherSet => {
//this: collection A
//otherSet: Set B
//Traverse all the elements in collection A, if you find that the elements in collection A do not exist in collection B, then return false, if you traverse the entire collection A without returning false, return true
let values = this.values()
for(let i = 0; i < values.length; i++){
let item = values[i]
if(!otherSet.has(item)){
return false
}
}
return true
}
2. Dictionary structure
2.1. Introduction
- The dictionary stores key-value pairs, and its main feature is one-to-one correspondence;
- In the dictionary, keys cannot be repeated and are unordered , while Values can be repeated .
- Some programming languages call this mapping relationship a dictionary , such as Dictonary in Swift and dict in Python;
- In some programming languages, this mapping relationship is called Map , such as HashMap&TreeMap in Java, etc.;
Common operations of dictionary class:
- set(key,value): Add new elements to the dictionary.
- remove(key): Remove the data value corresponding to the key value from the dictionary by using the key value.
- has(key): If a key value exists in this dictionary, it returns true, otherwise it returns false.
- get(key): Find a specific value through the key value and return it.
- clear(): Delete all elements in this dictionary.
- size(): Returns the number of elements contained in the dictionary. Similar to the length property of an array.
- keys(): Return all the key names contained in the dictionary as an array.
- values(): Returns all the values contained in the dictionary as an array.
2.2. Package dictionary
//Encapsulate dictionary classfunction Dictionary(){//dictionary attributesthis.items = {}//Dictionary operation method//1. Add key-value pairs to the dictionaryDictionary.prototype.set = function(key, value){this.items[key] = value}//2. Determine whether there is a key in the dictionaryDictionary.prototype.has = function(key){return this.items.hasOwnProperty(key)}//Three. Remove elements from the dictionaryDictionary.prototype.remove = function(key){//1. Determine whether the key exists in the dictionaryif(!this.has(key)) return false//2. Delete the key from the dictionarydelete this.items[key]return true}//4. Get the value according to the keyDictionary.prototype.get = function(key){return this.has(key) ? this.items[key] : undefined}//5. Get all keysDictionary.prototype.keys = function(){return Object.keys(this.items)}//6.size methodDictionary.prototype.keys = function(){return this.keys().length}//Seven.clear methodDictionary.prototype.clear = function(){this.items = {}}}
Fourth, JavaScript implements a hash table
1. Introduction to Hash Table
1.1. Know the hash table
- Hash tables can provide very fast insert-delete-find operations ;
- No matter how much data there is, inserting and deleting values only takes a very short time, that is, O(1) time class. In fact, it only takes a few machine instructions to do it;
- The speed of the hash table is faster than that of the tree , and the desired element can be found almost instantly. But it's much simpler to encode than a tree .
- The data in the hash table is out of order , so the elements in it cannot be traversed in a fixed way (such as from small to large).
- Usually, the key in the hash table is not allowed to be repeated , and the same key cannot be placed to store different elements.
- Hash tables are not easy to understand. Unlike arrays, linked lists, and trees, their structures and principles can be represented graphically.
- The structure of the hash table is an array , but its magic lies in a transformation of the subscript value . This transformation can be called a hash function , and the HashCode can be obtained through the hash function .
The hash table is finally implemented based on data, but the hash table can convert the string into the corresponding subscript value through the hash function, and establish the corresponding relationship between the string and the subscript value .
1.2. Hash method
Some concepts of hash table :
- Hashing : The process of converting large numbers into subscripts within the range of the array is called hashing;
- Hash function : We usually convert words into large numbers , and put the code implementation of hashing large numbers in a function, which is called a hash function;
- Hash table : Encapsulate the entire structure of the array into which the final data is inserted , and the result is a hash table.
Issues that still need to be resolved:
- Subscripts after hashing may still be repeated . How to solve this problem? This situation is called conflict , conflict is inevitable , we can only resolve conflict.
1.3. Methods for resolving conflicts
- Option 1: chain address method (zipper method) ;
In this way, the entire array or linked list can be obtained according to the subscript value, and then continue to search in the array or linked list. Moreover, there are generally not too many conflicting elements.
Summary : The method of chain address method to resolve conflicts is that each array unit stores no single data , but a chain . The data structure commonly used in this chain is an array or a linked list , and the efficiency of searching for the two data structures is equivalent ( Because the elements of the chain are generally not too many).
- Solution 2: open address method ;
The main way the open address method works is to find empty cells to place conflicting data items.
According to the different ways of detecting the position of blank cells, it can be divided into three methods: linear detection, secondary detection, and rehashing
1.4. Ways to find blank cells
- After hashing (modulo 10), the subscript value index=3 is obtained, but data 33 has already been placed in this position. The linear detection is to start from the index position + 1 to find a suitable position to place 13 one by one . The so-called suitable position refers to the empty position . For example, the position of index=4 in the above figure is the suitable position.
- First, 13 is hashed to get index=3. If the data stored at index=3 is the same as the data 13 to be queried, it will be returned directly;
- If they are not the same, then search linearly, starting from the index+1 position to search for data 13 one by one;
- The entire hash table will not be traversed during the query process, as long as the query finds an empty position, it will stop , because when inserting 13, the empty position will not be skipped to insert other positions.
- The deletion operation is similar to the above two cases, but it should be noted that when deleting a data item, the content of the subscript of the position cannot be set to null , otherwise it will affect other subsequent query operations , because once a null position is encountered will stop searching.
- Usually, when deleting a data item at a location, we can perform special processing on it (for example, set it to -1), so that when we encounter -1 during the search, we know to continue the search.
- There is a serious problem in linear detection, which is aggregation ;
- For example, when no element has been inserted into the hash table, insert 23, 24, 25, 26, and 27, which means that the positions with subscript values of 3, 4, 5, 6, and 7 are all placed with data. The filling unit is called aggregation ;
- Aggregation will affect the performance of the hash table , whether it is insertion/query/deletion;
- For example, when you insert 13, you will find that the continuous units 3~7 are not allowed to insert data, and you need to experience this situation many times during the insertion process. The quadratic probing method can solve this problem.
secondary detection
The problems with the linear detection mentioned above :
- If the previous data is inserted continuously , then a newly inserted data may need to detect a long distance ;
- The secondary detection is optimized on the basis of linear detection :
- Linear detection : We can regard it as a detection with a step size of 1. For example, starting from the value x in the table below, then the linear detection is to detect in sequence according to the subscript values: x+1, x+2, x+3, etc.;
- Secondary detection : The step size is optimized, for example, starting from the subscript value x: x+12, x+22, x+33. In this way, a relatively long distance is detected at one time , which avoids the impact of data aggregation.
Problems with secondary detection :
- When inserting a group of data with large data distribution, such as: 13-163-63-3-213, this situation will cause a kind of aggregation with different step sizes (although the probability of this situation is higher than that of linear detection Aggregation should be small), which will also affect performance.
rehashing
The best solution for finding blank cells in open addressing is rehashing:
- The step size of the secondary detection is fixed: 1, 4, 9, 16 and so on;
- Now we need a method: generate a detection sequence that depends on keywords (data) , rather than the detection step of each keyword is the same;
- In this way, different keywords can use different detection sequences even if they map to the same array subscript ;
- The method of rehashing is: use another hash function for the keyword, perform hashing again , and use the result of this hashing as the step size of the keyword ;
The second hashing needs to meet the following two points:
- Different from the first hash function , otherwise the result after hashing is still in the original position;
- The output cannot be 0 , otherwise each detection will be an endless loop of standing still;
Excellent hash function:
- stepSize = constant - (key % constant);
- Where constant is a prime number and is less than the capacity of the array;
- For example: stepSize = 5 - (key % 5), meet the requirements, and the result cannot be 0;
Efficiency of hashing
Performing insertion and search operations in a hash table is very efficient.
- If there are no conflicts , then the efficiency will be higher;
- If a conflict occurs , the access time depends on the subsequent probe length;
- The average probe length and average access time depend on the fill factor , and as the fill factor becomes larger, the probe length will become longer and longer.
Understanding the concept fill factor:
- The filling factor represents the ratio of the data items already contained in the current hash table to the length of the entire hash table ;
- Fill factor = total data items / hash table length ;
- The filling factor of the open address method is at most 1 , because only blank cells can be filled with elements;
- The filling factor of the chain address method can be greater than 1 , because as long as you want, the zipper method can be extended indefinitely;
1.5. Performance comparison of different detection methods
- Linear probing :
- Performance of secondary probing and rehashing :
Quadratic probing is comparable to rehashing, and they perform slightly better than linear probing. It can be seen from the figure below that as the filling factor increases, the average detection length increases exponentially, and the number of detections required also increases exponentially, and the performance is not high.
- The performance of the chain address method :
It can be seen that as the filling factor increases, the average detection length increases linearly, which is relatively gentle. There are many chain address methods used in development. For example, the chain address method is used in HashMap in Java.
1.6. Excellent hash function
- fast calculation ;
- uniform distribution ;
When calculating the value of a polynomial, first calculate the value of the first-degree polynomial in the innermost bracket, and then calculate the value of the first-degree polynomial layer by layer from the inside to the outside. This algorithm converts the value of polynomial f(x) of degree n into the value of polynomial of degree n.
Before transformation :
- Multiplication times: n(n+1)/2 times;
- Number of additions: n times;
After transformation :
- Number of multiplications: n times;
- Number of additions: n times;
If you use big O to represent the time complexity, it is directly reduced from O(N^2) before the transformation to O(N).
Evenly distributed
In order to ensure that the data is evenly distributed in the hash table , when we need to use constants , try to use prime numbers ; for example: the length of the hash table, the base number of the power of N, etc.
HashMap in Java uses the chain address method, and the formula for hashing is: index = HashCode (key) & (Length-1)
That is to convert the data into binary and perform the AND operation instead of the remainder operation. In this way, the computer directly operates binary data, which is more efficient. However, JavaScript will have problems when performing AND operations called big data, so the remainder operation is still used when using JavaScript to implement hashing.
2. Preliminary package hash table
- put (key, value): insert or modify operation;
- get(key): Get the element at a specific position in the hash table;
- remove(key): delete the element at a specific position in the hash table;
- isEmpty(): If the hash table does not contain any elements, return trun, if the length of the hash table is greater than 0, return false;
- size(): returns the number of elements contained in the hash table;
- resize(value): expand the hash table;
2.1. Simple implementation of hash function
First, use Horner's rule to calculate the value of hashCode, and implement hashing through the remainder operation. Here, simply specify the size of the array.
//Design the hash function
//1. Convert the string to a relatively large number: hashCede
//2. Compress the large number hasCode into the range (size) of the array
function hashFunc(str, size){
//1. Define the hashCode variable
let hashCode = 0
//2. Horner's rule, calculate the value of hashCode
//cats -> Unicode encoding
for(let i = 0 ;i < str.length; i++){
// str.charCodeAt(i)//Get the unicode encoding corresponding to a character
hashCode = 37 * hashCode + str.charCodeAt(i)
}
//3. Remainder operation
let index = hashCode % size
return index
}
2.2. Create a hash table
First create the hash table class HashTable, and add the necessary attributes and the hash function implemented above, and then implement other methods.
//Encapsulate the hash table class
function HashTable() {
//Attributes
this.storage = []
this.count = 0//Calculate the number of stored elements
//Filling factor: when loadFactor > 0.75, capacity expansion is required; when loadFactor < 0.25, capacity needs to be reduced
this.limit = 7//initial length
//method
//hash function
HashTable.prototype.hashFunc = function(str, size){
//1. Define the hashCode variable
let hashCode = 0
//2. Horner's rule, calculate the value of hashCode
//cats -> Unicode encoding
for(let i = 0 ;i < str.length; i++){
// str.charCodeAt(i)//Get the unicode encoding corresponding to a character
hashCode = 37 * hashCode + str.charCodeAt(i)
}
//3. Remainder operation
let index = hashCode % size
return index
}
2.3.put(key,value)
Implementation ideas :
- First, get the index value index according to the key, the purpose is to insert the data into the corresponding location of the storage;
- Then, take out the bucket according to the index value. If the bucket does not exist, create the bucket first, and then place it at the position of the index value;
- Then, judge whether to add or modify the original value. If there is already a value, modify the value; if not, perform subsequent operations.
- Finally, perform the new data operation.
Code implementation :
//insert & modify operations
HashTable.prototype.put = function (key, value){
//1. Obtain the corresponding index according to the key
let index = this.hashFunc(key, this.limit)
//2. Take out the corresponding bucket according to the index
let bucket = this.storage[index]
//3. Determine whether the bucket is null
if (bucket == null) {
bucket = []
this.storage[index] = bucket
}
//4. Determine whether to modify data
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i];
if (tuple[0] == key) {
tuple[1] = value
return//No return value
}
}
//5. Add operation
bucket.push([key, value])
this.count += 1
}
2.4.get(key)
- First, obtain its corresponding index value index in the storage through the hash function according to the key;
- Then, get the corresponding bucket according to the index value;
- Next, judge whether the acquired bucket is null, if it is null, directly return null;
- Then, linearly traverse whether each key in the bucket is equal to the incoming key. If equal, return the corresponding value directly;
- Finally, after traversing the bucket, if the corresponding key is still not found, just return null.
//get operation
HashTable.prototype.get = function(key){
//1. Obtain the corresponding index according to the key
let index = this.hashFunc(key, this.limit)
//2. Obtain the corresponding bucket according to the index
let bucket = this.storage[index]
//3. Determine whether the bucket is equal to null
if (bucket == null) {
return null
}
//4. If there is a bucket, then perform a linear search
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i];
if (tuple[0] == key) {//tuple[0] stores key, tuple[1] stores value
return tuple[1]
}
}
//5. Still not found, then return null
return null
}
2.5.remove(key)
- First, obtain its corresponding index value index in the storage through the hash function according to the key;
- Then, get the corresponding bucket according to the index value;
- Next, judge whether the acquired bucket is null, if it is null, directly return null;
- Then, search the bucket linearly, find the corresponding data, and delete it;
- Finally, still not found, return null;
Code:
//delete operation
HashTable.prototype.remove = function(key){
//1. Obtain the corresponding index according to the key
let index = this.hashFunc(key, this.limit)
//2. Obtain the corresponding bucket according to the index
let bucket = this.storage[index]
//3. Determine whether the bucket is null
if (bucket == null) {
return null
}
//4. If there is a bucket, then perform a linear search and delete it
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i]
if (tuple[0] == key) {
bucket.splice(i,1)
this.count -= 1
return tuple[1]
}
}
//5. Still not found, return null
return null
}
2.6. Implementation of other methods
/ / Determine whether the hash table is nullHashTable.prototype.isEmpty = function(){return this.count == 0}// Get the number of elements in the hash tableHashTable.prototype.size = function(){return this.count}
3. Expansion of the hash table
- We used an array with a length of 7 in the hash table earlier. Since the chain address method is used , the load factor (loadFactor) can be greater than 1, so this hash table can insert new data without limit.
- However, as the amount of data increases , the bucket array (linked list) corresponding to each index in the storage will become longer and longer, which will reduce the efficiency of the hash table
- The common situation is to expand the capacity when loadFactor > 0.75 ;
- Simple expansion can be doubled directly (about prime numbers, discussed later);
- After expansion, all data items must be modified synchronously;
Implementation ideas :
- First, define a variable, such as oldStorage pointing to the original storage;
- Then, create a new array with a larger capacity and let this.storage point to it;
- Finally, take out each data in each bucket in oldStorage and add them to the new array pointed to by this.storage in turn;
//Hash table expansion
HashTable.prototype.resize = function(newLimit){
//1. Save the old storage array content
let oldStorage = this.storage
//2. Reset all properties
this.storage = []
this.count = 0
this.limit = newLimit
//3. Traverse all buckets in oldStorage
for (let i = 0; i < oldStorage.length; i++) {
//3.1. Take out the corresponding bucket
const bucket = oldStorage[i];
//3.2. Determine whether the bucket is null
if (bucket == null) {
continue
}
//3.3. If there is data in the bucket, take out the data and insert it again
for (let j = 0; j < bucket.length; j++) {
const tuple = bucket[j];
this.put(tuple[0], tuple[1])//key and value of inserted data
}
}
}
The resize method of the hash table defined above can not only realize the expansion of the hash table, but also realize the compression of the capacity of the hash table.
Loading factor = data in the hash table / length of the hash table , that is, loadFactor = count / HashTable.length.
- Usually, when the filling factor laodFactor > 0.75 , the hash table is expanded. Add the following code to the add method (push method) in the hash table:
//Determine whether expansion operation is required
if(this.count > this.limit * 0.75){
this.resize(this.limit * 2)
}
- When the filling factor laodFactor < 0.25 , the hash table capacity is compressed. Add the following code to the delete method (remove method) in the hash table:
//Reduce capacity
if (this.limit > 7 && this.count < this.limit * 0.25) {
this.resize(Math.floor(this.limit / 2))
}
3.2. Choose a prime number as capacity
- Step 1: First, you need to add the isPrime method for judging the prime number and the getPrime method for obtaining the prime number to the HashTable class:
/ / Determine whether the incoming num is a prime number
HashTable.prototype.isPrime = function(num){
if (num <= 1) {
return false
}
//1. Get the square root of num: Math.sqrt(num)
//2. Loop judgment
for(var i = 2; i<= Math.sqrt(num); i++ ){
if(num % i == 0){
return false;
}
}
return true;
}
// method to get prime number
HashTable.prototype.getPrime = function(num){
//7*2=14,+1=15,+1=16,+1=17(prime number)
while (!this.isPrime(num)) {
num++
}
return num
}
- Step 2: Modify the operations related to array expansion in the put method of adding elements and the remove method of deleting elements:
Add the following code to the put method:
//Determine whether expansion operation is required
if(this.count > this.limit * 0.75){
let newSize = this.limit * 2
let newPrime = this.getPrime(newSize)
this.resize(newPrime)
}
Add the following code to the remove method:
//Reduce capacity
if (this.limit > 7 && this.count < this.limit * 0.25) {
let newSize = Math.floor(this.limit / 2)
let newPrime = this.getPrime(newSize)
this.resize(newPrime)
}
Fourth, the complete implementation of the hash table
//Encapsulate the hash table class
function HashTable() {
//Attributes
this.storage = []
this.count = 0//Calculate the number of stored elements
//Filling factor: when loadFactor > 0.75, capacity expansion is required; when loadFactor < 0.25, capacity needs to be reduced
this.limit = 7//initial length
//method
//hash function
HashTable.prototype.hashFunc = function(str, size){
//1. Define the hashCode variable
let hashCode = 0
//2. Horner's rule, calculate the value of hashCode
//cats -> Unicode encoding
for(let i = 0 ;i < str.length; i++){
// str.charCodeAt(i)//Get the unicode encoding corresponding to a character
hashCode = 37 * hashCode + str.charCodeAt(i)
}
//3. Remainder operation
let index = hashCode % size
return index
}
//1. Insert & modify operation
HashTable.prototype.put = function (key, value){
//1. Obtain the corresponding index according to the key
let index = this.hashFunc(key, this.limit)
//2. Take out the corresponding bucket according to the index
let bucket = this.storage[index]
//3. Determine whether the bucket is null
if (bucket == null) {
bucket = []
this.storage[index] = bucket
}
//4. Determine whether to modify data
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i];
if (tuple[0] == key) {
tuple[1] = value
return//No return value
}
}
//5. Add operation
bucket.push([key, value])
this.count += 1
//6. Determine whether expansion operation is required
if(this.count > this.limit * 0.75){
let newSize = this.limit * 2
let newPrime = this.getPrime(newSize)
this.resize(newPrime)
}
}
//2. Get operation
HashTable.prototype.get = function(key){
//1. Obtain the corresponding index according to the key
let index = this.hashFunc(key, this.limit)
//2. Obtain the corresponding bucket according to the index
let bucket = this.storage[index]
//3. Determine whether the bucket is equal to null
if (bucket == null) {
return null
}
//4. If there is a bucket, then perform a linear search
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i];
if (tuple[0] == key) {//tuple[0] stores key, tuple[1] stores value
return tuple[1]
}
}
//5. Still not found, then return null
return null
}
//3. Delete operation
HashTable.prototype.remove = function(key){
//1. Obtain the corresponding index according to the key
let index = this.hashFunc(key, this.limit)
//2. Obtain the corresponding bucket according to the index
let bucket = this.storage[index]
//3. Determine whether the bucket is null
if (bucket == null) {
return null
}
//4. If there is a bucket, then perform a linear search and delete it
for (let i = 0; i < bucket.length; i++) {
let tuple = bucket[i]
if (tuple[0] == key) {
bucket.splice(i,1)
this.count -= 1
return tuple[1]
//6. Reduce capacity
if (this.limit > 7 && this.count < this.limit * 0.25) {
let newSize = Math.floor(this.limit / 2)
let newPrime = this.getPrime(newSize)
this.resize(newPrime)
}
}
}
//5. Still not found, return null
return null
}
/*------------------Other methods--------------------*/
/ / Determine whether the hash table is null
HashTable.prototype.isEmpty = function(){
return this.count == 0
}
// Get the number of elements in the hash table
HashTable.prototype.size = function(){
return this.count
}
//Hash table expansion
HashTable.prototype.resize = function(newLimit){
//1. Save the old storage array content
let oldStorage = this.storage
//2. Reset all properties
this.storage = []
this.count = 0
this.limit = newLimit
//3. Traverse all buckets in oldStorage
for (let i = 0; i < oldStorage.length; i++) {
//3.1. Take out the corresponding bucket
const bucket = oldStorage[i];
//3.2. Determine whether the bucket is null
if (bucket == null) {
continue
}
//3.3. If there is data in the bucket, take out the data and insert it again
for (let j = 0; j < bucket.length; j++) {
const tuple = bucket[j];
this.put(tuple[0], tuple[1])//key and value of inserted data
}
}
}
/ / Determine whether the incoming num is a prime number
HashTable.prototype.isPrime = function(num){
if (num <= 1) {
return false
}
//1. Get the square root of num: Math.sqrt(num)
//2. Loop judgment
for(var i = 2; i<= Math.sqrt(num); i++ ){
if(num % i == 0){
return false;
}
}
return true;
}
// method to get prime number
HashTable.prototype.getPrime = function(num){
//7*2=14,+1=15,+1=16,+1=17(prime number)
while (!this.isPrime(num)) {
num++
}
return num
}
}
Five, JavaScript implements one-way linked list
1. Introduction to one-way linked list
Linked lists, like arrays, can be used to store a series of elements, but the implementation mechanisms of linked lists and arrays are completely different. Each element of the linked list consists of a node that stores the element itself and a reference (some languages call it a pointer or connection) pointing to the next element.
- The head attribute points to the first node of the linked list;
- The last node in the linked list points to null;
- When there is no node in the linked list, head directly points to null;
Disadvantages of arrays:
- The creation of an array usually needs to apply for a continuous memory space (a whole block of memory), and the size is fixed. Therefore, when the original array cannot meet the capacity requirements , it needs to be expanded (generally, apply for a larger array, such as 2 times, and then copy the elements in the original array).
- Inserting data at the beginning or middle of an array is expensive and requires a large number of element shifts.
Advantages of linked list:
- The elements in the linked list do not need to be a continuous space in the memory , and the memory of the computer can be fully utilized to realize flexible dynamic memory management .
- The linked list does not have to determine the size when it is created , and the size can be extended indefinitely .
- When the linked list inserts and deletes data, the time complexity can reach O(1), which is much more efficient than the array.
Disadvantages of linked list:
- When the linked list accesses any element at any position, it needs to be accessed from the beginning (the first element cannot be skipped to access any element).
- Elements cannot be directly accessed through the subscript value, and need to be accessed one by one from the beginning until the corresponding element is found.
- While it is easy to get to the next node, it is difficult to go back to the previous node .
Common operations in linked lists:
- append(element): Add a new item to the end of the linked list;
- insert(position, element): Insert a new item into a specific position of the linked list;
- get(position): Get the element at the corresponding position;
- indexOf(element): Returns the index of the element in the linked list. If there is no element in the linked list, return -1;
- update(position, element): modify an element at a certain position;
- removeAt(position): Remove an item from a specific position in the linked list;
- remove(element): remove an item from the linked list;
- isEmpty(): If the linked list does not contain any elements, return trun, if the length of the linked list is greater than 0, return false;
- size(): Returns the number of elements contained in the linked list, similar to the length attribute of the array;
- toString(): Since the linked list item uses the Node class, it is necessary to rewrite the default toString method inherited from the JavaScript object, so that it only outputs the value of the element;
2. Encapsulate the one-way linked list class
2.0. Create a one-way linked list class
First create the one-way linked list class Linklist, and add basic attributes, and then implement the common methods of one-way linked list:
// Encapsulate linked list class
function LinkList(){
// Encapsulate an inner class: node class
function Node(data){
this.data = data;
this.next = null;
}
// Attributes
// The attribute head points to the first node of the linked list
this.head = null;
this.length = 0;
// 1. Implement the append method
LinkList.prototype.append = data => {
//1. Create a new node
let newNode = new Node(data)
//2. Add a new node
//Case 1: When there is only one node
if(this.length == 0){
this.head = newNode
//Case 2: The number of nodes is greater than 1, add a new node at the end of the linked list
}else {
//Let the variable current point to the first node
let current = this.head
//When current.next (the next node is not empty) is not empty, keep looping until current points to the last node
while (current.next){
current = current.next
}
// The next of the last node points to the new node
current.next = newNode
}
//3. length+1 after adding new nodes
this.length += 1
}
// Two. Implement the toString method
LinkList.prototype.toString = () => {
// 1. Define variables
let current = this.head
let listString = ""
// 2. Loop to get nodes one by one
while(current){
listString += current.data + " "
current = current.next//Don't forget to make current point to the next node after splicing the data of a node
}
return listString
}
// 3. Implement the insert method
LinkList.prototype.insert = (position, data) => {
//Understand the meaning of positionon: position=0 means that the new boundary point will become the first node after insertion, position=2 means that the new boundary point will become the third node after insertion
//1. Make an out-of-bounds judgment on the position: it is required that the incoming position cannot be negative and cannot exceed the length of the LinkList
if(position < 0 || position > this.length){
return false
}
//2. Create newNode based on data
let newNode = new Node(data)
//3. Insert a new node
//Case 1: insert position position=0
if(position == 0){
// Make the new node point to the first node
newNode.next = this.head
// Let head point to the new node
this.head = newNode
//Case 2: Insertion position position>0 (this case includes position=length)
} else{
let index = 0
let previous = null
let current = this.head
//Step 1: Use the while loop to make the variable current point to the next node of the position position (note the writing of the while loop)
while(index++ < position){
//Step 2: Before current points to the next node, let previous point to the node currently pointed to by current
previous = current
current = current.next
}
// Step 3: Make newNode point to the next node of the position through the variable current (the current has already pointed to the next node of the position position at this time)
newNode.next = current
//Step 4: Use the variable previous to make the previous node at position point to newNode
previous.next = newNode
//We can't directly operate the nodes in the linked list, but we can point to these nodes through variables to indirectly operate the nodes;
}
//4. Length+1 is required after the new node is inserted
this.length += 1;
return true
}
//Four. Implement the get method
LinkList.prototype.get = (position) => {
//1. Out of bounds judgment
// When position = length, get null so 0 =< position < length
if(position < 0 || position >= this.length){
return null
}
//2. Get the data of the next node at the specified positon position
// Also use a variable to indirectly manipulate nodes
let current = this.head
let index = 0
while(index++ < position){
current = current.next
}
return current.data
}
//5. Implement the indexOf method
LinkList.prototype.indexOf = data => {
//1. Define variables
let current = this.head
let index = 0
//2. Start searching: as long as current does not point to null, it will keep looping
while(current){
if(current.data == data){
return index
}
current = current.next
index += 1
}
//3. After traversing the linked list, if it is not found, return -1
return -1
}
//6. Implement the update method
LinkList.prototype.update = (position, newData) => {
//1. Out of bounds judgment
//Because the modified node cannot be null, position cannot be equal to length
if(position < 0 || position >= this.length){
return false
}
//2. Find the correct node
let current = this.head
let index = 0
while(index++ < position){
current = current.next
}
//3. Change the data of the next node at position to newData
current.data = newData
//Return true to indicate the modification is successful
return true
}
// Seven. Implement the removeAt method
LinkList.prototype.removeAt = position => {
//1. Out of bounds judgment
if (position < 0 || position >= this.length) {
return null
}
//2. Delete element
//Case 1: When position = 0 (delete the first node)
let current = this.head
if (position ==0 ) {
//Case 2: position > 0
this.head = this.head.next
}else{
let index = 0
let previous = null
while (index++ < position) {
previous = current
current = current.next
}
//After the loop ends, current points to the node after position, and previous points to the node before current
//Make the next of the previous node point to the next of the current
previous.next = current.next
}
//3,length-1
this.length -= 1
//Return the data of the deleted node, for which current is defined at the top
return current.data
}
/*-------------Implementation of other methods--------------*/
//8. Implement the remove method
LinkList.prototype.remove = (data) => {
//1. Get the position of data in the list
let position = this.indexOf(data)
//2. According to the location information, delete the node
return this.removeAt(position)
}
//9. Implement the isEmpty method
LinkList.prototype.isEmpty = () => {
return this.length == 0
}
//10. Implement the size method
LinkList.prototype.size = () => {
return this.length
}
}
Six, JavaScript realizes the doubly linked list
1. Introduction to doubly linked list
Doubly linked list: It can be traversed from the beginning to the end , and from the end to the head . That is to say, the process of linked list connection is bidirectional, and its realization principle is: a node has both forward-connection reference and backward-connection reference .
Disadvantages of doubly linked list:
- Every time a node is inserted or deleted, four references need to be processed instead of two, which will be more difficult to implement;
- Compared with the one-way linked list, it occupies a larger memory space;
- However, these disadvantages are trivial compared to the convenience of doubly linked lists.
The structure of the doubly linked list:
- The doubly linked list not only has a head pointer pointing to the first node, but also has a tail pointer pointing to the last node;
- Each node consists of three parts: item stores data, prev points to the previous node, and next points to the next node;
- The prev of the first node of the doubly linked list points to null ;
- The next of the last node of the doubly linked list points to null ;
Common operations (methods) for doubly linked lists:
- append(element): Add a new item to the end of the linked list;
- inset(position, element): Insert a new item into a specific position of the linked list;
- get(element): Get the element at the corresponding position;
- indexOf(element): Returns the index of the element in the linked list, if there is no element in the linked list, it returns -1;
- update(position, element): modify an element at a certain position;
- removeAt(position): Remove an item from a specific position in the linked list;
- isEmpty(): If the linked list does not contain any elements, return trun, if the length of the linked list is greater than 0, return false;
- size(): Returns the number of elements contained in the linked list, similar to the length attribute of the array;
- toString(): Since the linked list item uses the Node class, it is necessary to rewrite the default toString method inherited from the JavaScript object, so that it only outputs the value of the element;
- forwardString(): returns the string form of forward traversal nodes;
- backwordString(): returns the string form of the node traversed in reverse;
2. Encapsulating the doubly linked list class
2.0. Create a doubly linked list class
First create a doubly linked list class DoubleLinklist, and add basic attributes, and then implement the common method of doubly linked list:
//Encapsulate the doubly linked list class
function DoubleLinklist(){
//Encapsulate inner class: node class
function Node(data){
this.data = data
this.prev = null
this.next = null
}
//Attributes
this.head = null
this.tail ==null
this.length = 0
}
2.1.append(element)
Code:
//append method
DoubleLinklist.prototype.append = data => {
//1. Create a new node based on data
let newNode = new Node(data)
//2. Add node
//Case 1: The first node is added
if (this.length == 0) {
this.tail = newNode
this.head = newNode
//Case 2: Adding is not the first node
}else {
newNode.prev = this.tail
this.tail.next = newNode
this.tail = newNode
}
//3.length+1
this.length += 1
}
2.2.toString() summary
//Convert the linked list into a string format
//one.toString method
DoubleLinklist.prototype.toString = () => { return this.backwardString() }//2.forwardString method
DoubleLinklist.prototype.forwardString = () => { //1. Define variable let current =this.tail let resultString = ""//2. Traverse forward sequentially to get each node
while (current) { resultString += current.data + "--" current = current.prev } return resultString }//Three. backwardString method
DoubleLinklist.prototype.backwardString = () => { //1. Define variable let current = this.head let resultString = ""//2. Traverse backwards one by one to get each node
while (current) { resultString += current.data + "--" current = current.next } return resultString }
2.3.insert(position,element)
//insert method
DoubleLinklist.prototype.insert = (position, data) => { //1. Cross-border judgment if (position < 0 || position > this.length) return false//2. Create a new node based on data
let newNode = new Node(data)//3. Insert a new node
//The original linked list is empty
//Case 1: The inserted newNode is the first nodeif (this.length == 0) { this.head = newNode this.tail = newNode //The original linked list is not empty }else {
//情况2:position == 0
if (position == 0) {
this.head.prev = newNode
newNode.next = this.head
this.head = newNode
//情况3:position == this.length
} else if(position == this.length){
this.tail.next = newNode
newNode.prev = this.tail
this.tail = newNode
//Case 4: 0 < position < this.length
}else{ let current = this.head let index = 0 while(index++ < position){ current = current.next } //Modify the node variable before and after the position of pos to point to newNode. next = current newNode.prev = current.prev current.prev.next = newNode current.prev = newNode } } //4.length+1 this.length += 1 return true//Return true to indicate successful insertion }
2.4.get(position)
//get method
DoubleLinklist.prototype.get = position => { //1. Out of bounds judgment if (position < 0 || position >= this.length) {//position cannot be equal to length return null when getting elements//2. Get element
let current = null
let index = 0
//this.length / 2 > position: traverse from the beginning
if ((this.length / 2) > position) { current = this.head while(index++ < position ){ current = current.next } //this.length / 2 =< position: traverse from the tail }else{ current = this.tail index = this.length - 1 while(index-- > position){ current = current .prev } } return current.data }
Be sure to use this.length to obtain the number of nodes in the linked list, otherwise an error will be reported.
- When this.length / 2 > position: start traversing from the head (head);
- When this.length / 2 < position: start traversing from the tail (tail);
2.5.indexOf(element)
//indexOf methodDoubleLinklist.prototype.indexOf = data => {//1. Define variableslet current = this.headlet index = 0//2. Traverse the linked list to find the same node as datawhile(current){if (current.data == data) {return index}current = current.nextindex += 1}return -1}
2.7.update(position,element)
//update methodDoubleLinklist.prototype.update = (position, newData) => {//1. Out of bounds judgmentif (position < 0 || position >= this.length) {return false}//2. Find the correct nodelet current = this.headlet index = 0//this.length / 2 > position: traverse from the beginningif (this.length / 2 > position) {while(index++ < position){current = current.next}//this.length / 2 =< position: traverse from the end}else{current = this.tailindex = this.length - 1while (index -- > position) {current = current.prev}}//3. Modify the data of the found nodecurrent.data = newDatareturn true//indicates successful modification}
2.8.removeAt(position)
Code:
//removeAt method
DoubleLinklist.prototype.removeAt = position => {
//1. Out of bounds judgment
if (position < 0 || position >= this.length) {
return null
}
//2. Delete node
//When length == 1 in the linked list
//Case 1: There is only one node in the linked list
let current = this.head//Defined at the top to facilitate the return of current.data in the following situations
if (this.length == 1) {
this.head = null
this.tail = null
//When length > 1 in the linked list
} else{
//case 2: delete the first node
if (position == 0) {
this.head.next.prev = null
this.head = this.head.next
//case 3: delete the last node
}else if(position == this.length - 1){
current = this.tail//In this case, return the last node that was deleted
this.tail.prev.next = null
this.tail = this.tail.prev
}else{
//Case 4: Delete the node in the middle of the linked list
let index = 0
while(index++ < position){
current = current.next
}
current.next.prev = current.prev
current.prev.next = current.next
}
}
//3.length -= 1
this.length -= 1
return current.data//return the data of the deleted node
}
Detailed process:
There are several situations when a node is deleted:
When the length of the linked list = 1:
- Case 1: Delete all nodes in the linked list: just make the head and tail of the linked list point to null.
When the length of the linked list > 1:
- Case 2: Delete the first node in the linked list:
Pass: this.head.next.prev = null, change to 1;
Pass: this.head = this.head.next, change to point to 2;
Although Node1 has references to other nodes, but no references to Node1, Node1 will be automatically recycled.
- Case 3: Delete the last node in the linked list:
Pass: this.tail.prev.next = null, modify to point to 1;
Pass: this.tail = this.tail.prev, modify to point to 2;
- Case 4: Delete the node in the middle of the linked list:
find the node to be deleted through the while loop, such as position = x, then the node to be deleted is Node(x+1), as shown in the following figure:
Pass: current.next.prev = current.prev, modify to point to 1;
Pass: current.prev.next = current.next, modify to point to 2;
In this way, there is no reference pointing to Node(x+1) (current is pointing to Node(x+1), but current is a temporary variable, which will be destroyed after the method is executed), and then Node(x+1) will be automatically delete.
2.9. Other methods
//8.remove method
DoubleLinklist.prototype.remove = data => {
//1. Get the subscript value according to data
let index = this.indexOf(data)
//2. Delete the node at the corresponding position according to the index
return this.removeAt(index)
}
//nine.isEmpty method
DoubleLinklist.prototype.isEmpty = () => {
return this.length == 0
}
//10.size method
DoubleLinklist.prototype.size = () => {
return this.length
}
//11.getHead method: Get the first element of the linked list
DoubleLinklist.prototype.getHead = () => {
return this.head.data
}
//12.getTail method: Get the last element of the linked list
DoubleLinklist.prototype.getTail = () => {
return this.tail.data
}
3. Summary of linked list structure
3.1. Notes
- In the linked list, current = current.next can be seen from left to right, as current --> current.next, that is, current points to the next node of current.
- The principle of deleting a node: as long as there is no reference to the object, no matter whether the object has references to other objects, the object will be recycled (deleted).
- Any position in the parameter must be judged out of bounds.
3.2. Addition, deletion, modification and query of linked list
- Situation 1: You only need two variables of head and tail to obtain the variables that need to be operated (here refers to being able to obtain them easily, of course you want to obtain them through head.next.next... or tail.prev.prev... The desired node is also ok), in this case the length of the linked list is length: 0 <= length <= 2.
- Situation 2: When you cannot rely on tail and head to obtain the variables that need to be operated, you can use the while loop to traverse to find the nodes that need to be operated:
3.3. Modify the linked list reference point
- Case 1: When the node that needs to be operated can be obtained through head and tail references, finally change the pointer of the head or tail variable.
- Case 2: When using current to obtain the node that needs to be operated, change the pointing of curren.next or current.prev at last.
3.4. Traversing the linked list
- Get the next node and index value at the specified position = x position:
After the loop ends index = position = x, the variable current points to Node(x+1), and the value of the variable index is the index value x of Node(x+1).
- Traverse all nodes in the linked list: